From d4725a51f21c0b132dc3a5717112372ed579fde2 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Sun, 14 May 2017 12:02:07 -0700 Subject: [PATCH] Add Experimental Text Node Implementation (#259) * Add experimental text node implementation, based on YYText * Fix warnings and alert when unimplemented experimental features are used. * Address feedback from review * Extend the cthulog * Update license headers --- AsyncDisplayKit.xcodeproj/project.pbxproj | 112 + CHANGELOG.md | 1 + Source/ASTextNode+Beta.h | 25 + Source/ASTextNode.mm | 73 +- Source/Private/ASTextNode2.h | 228 ++ Source/Private/ASTextNode2.mm | 1088 ++++++ .../Component/ASTextDebugOption.h | 95 + .../Component/ASTextDebugOption.m | 139 + .../TextExperiment/Component/ASTextInput.h | 87 + .../TextExperiment/Component/ASTextInput.m | 152 + .../TextExperiment/Component/ASTextLayout.h | 548 +++ .../TextExperiment/Component/ASTextLayout.m | 3370 +++++++++++++++++ .../TextExperiment/Component/ASTextLine.h | 79 + .../TextExperiment/Component/ASTextLine.m | 167 + .../TextExperiment/String/ASTextAttribute.h | 347 ++ .../TextExperiment/String/ASTextAttribute.m | 485 +++ .../TextExperiment/String/ASTextRunDelegate.h | 68 + .../TextExperiment/String/ASTextRunDelegate.m | 71 + .../TextExperiment/Utility/ASTextUtilities.h | 319 ++ .../TextExperiment/Utility/ASTextUtilities.m | 146 + .../Utility/NSAttributedString+ASText.h | 1393 +++++++ .../Utility/NSAttributedString+ASText.m | 1248 ++++++ .../Utility/NSParagraphStyle+ASText.h | 37 + .../Utility/NSParagraphStyle+ASText.m | 218 ++ examples/Kittens/Sample/AppDelegate.m | 2 + examples/Kittens/Sample/KittenNode.mm | 13 + 26 files changed, 10503 insertions(+), 8 deletions(-) create mode 100644 Source/Private/ASTextNode2.h create mode 100644 Source/Private/ASTextNode2.mm create mode 100755 Source/Private/TextExperiment/Component/ASTextDebugOption.h create mode 100755 Source/Private/TextExperiment/Component/ASTextDebugOption.m create mode 100755 Source/Private/TextExperiment/Component/ASTextInput.h create mode 100755 Source/Private/TextExperiment/Component/ASTextInput.m create mode 100755 Source/Private/TextExperiment/Component/ASTextLayout.h create mode 100755 Source/Private/TextExperiment/Component/ASTextLayout.m create mode 100755 Source/Private/TextExperiment/Component/ASTextLine.h create mode 100755 Source/Private/TextExperiment/Component/ASTextLine.m create mode 100755 Source/Private/TextExperiment/String/ASTextAttribute.h create mode 100755 Source/Private/TextExperiment/String/ASTextAttribute.m create mode 100755 Source/Private/TextExperiment/String/ASTextRunDelegate.h create mode 100755 Source/Private/TextExperiment/String/ASTextRunDelegate.m create mode 100755 Source/Private/TextExperiment/Utility/ASTextUtilities.h create mode 100755 Source/Private/TextExperiment/Utility/ASTextUtilities.m create mode 100755 Source/Private/TextExperiment/Utility/NSAttributedString+ASText.h create mode 100755 Source/Private/TextExperiment/Utility/NSAttributedString+ASText.m create mode 100755 Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.h create mode 100755 Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 537e0e204c..06e5fbe4f0 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -365,6 +365,26 @@ CCA282D11E9EBF6C0037E8B7 /* ASTipsWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA282CF1E9EBF6C0037E8B7 /* ASTipsWindow.m */; }; CCB2F34D1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m */; }; CCBBBF5D1EB161760069AA91 /* ASRangeManagingNode.h in Headers */ = {isa = PBXBuildFile; fileRef = CCBBBF5C1EB161760069AA91 /* ASRangeManagingNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCCCCCD51EC3EF060087FE10 /* ASTextDebugOption.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCC31EC3EF060087FE10 /* ASTextDebugOption.h */; }; + CCCCCCD61EC3EF060087FE10 /* ASTextDebugOption.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCC41EC3EF060087FE10 /* ASTextDebugOption.m */; }; + CCCCCCD71EC3EF060087FE10 /* ASTextInput.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCC51EC3EF060087FE10 /* ASTextInput.h */; }; + CCCCCCD81EC3EF060087FE10 /* ASTextInput.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCC61EC3EF060087FE10 /* ASTextInput.m */; }; + CCCCCCD91EC3EF060087FE10 /* ASTextLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCC71EC3EF060087FE10 /* ASTextLayout.h */; }; + CCCCCCDA1EC3EF060087FE10 /* ASTextLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCC81EC3EF060087FE10 /* ASTextLayout.m */; }; + CCCCCCDB1EC3EF060087FE10 /* ASTextLine.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCC91EC3EF060087FE10 /* ASTextLine.h */; }; + CCCCCCDC1EC3EF060087FE10 /* ASTextLine.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCCA1EC3EF060087FE10 /* ASTextLine.m */; }; + CCCCCCDD1EC3EF060087FE10 /* ASTextAttribute.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCCC1EC3EF060087FE10 /* ASTextAttribute.h */; }; + CCCCCCDE1EC3EF060087FE10 /* ASTextAttribute.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCCD1EC3EF060087FE10 /* ASTextAttribute.m */; }; + CCCCCCDF1EC3EF060087FE10 /* ASTextRunDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCCE1EC3EF060087FE10 /* ASTextRunDelegate.h */; }; + CCCCCCE01EC3EF060087FE10 /* ASTextRunDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCCF1EC3EF060087FE10 /* ASTextRunDelegate.m */; }; + CCCCCCE11EC3EF060087FE10 /* ASTextUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCD11EC3EF060087FE10 /* ASTextUtilities.h */; }; + CCCCCCE21EC3EF060087FE10 /* ASTextUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCD21EC3EF060087FE10 /* ASTextUtilities.m */; }; + CCCCCCE31EC3EF060087FE10 /* NSParagraphStyle+ASText.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCD31EC3EF060087FE10 /* NSParagraphStyle+ASText.h */; }; + CCCCCCE41EC3EF060087FE10 /* NSParagraphStyle+ASText.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCD41EC3EF060087FE10 /* NSParagraphStyle+ASText.m */; }; + CCCCCCE71EC3F0FC0087FE10 /* NSAttributedString+ASText.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCE51EC3F0FC0087FE10 /* NSAttributedString+ASText.h */; }; + CCCCCCE81EC3F0FC0087FE10 /* NSAttributedString+ASText.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCE61EC3F0FC0087FE10 /* NSAttributedString+ASText.m */; }; + CCD523111EBD658C001F2191 /* ASTextNode2.h in Headers */ = {isa = PBXBuildFile; fileRef = CCD5230F1EBD658C001F2191 /* ASTextNode2.h */; }; + CCD523121EBD658C001F2191 /* ASTextNode2.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCD523101EBD658C001F2191 /* ASTextNode2.mm */; }; CCF18FF41D2575E300DF5895 /* NSIndexSet+ASHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */; settings = {ATTRIBUTES = (Private, ); }; }; DB55C2671C641AE4004EDCF5 /* ASContextTransitioning.h in Headers */ = {isa = PBXBuildFile; fileRef = DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */; settings = {ATTRIBUTES = (Public, ); }; }; DB7121BCD50849C498C886FB /* libPods-AsyncDisplayKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */; }; @@ -796,6 +816,26 @@ CCBBBF5C1EB161760069AA91 /* ASRangeManagingNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRangeManagingNode.h; sourceTree = ""; }; CCBD05DE1E4147B000D18509 /* ASIGListAdapterBasedDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASIGListAdapterBasedDataSource.m; sourceTree = ""; }; CCBD05DF1E4147B000D18509 /* ASIGListAdapterBasedDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASIGListAdapterBasedDataSource.h; sourceTree = ""; }; + CCCCCCC31EC3EF060087FE10 /* ASTextDebugOption.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextDebugOption.h; sourceTree = ""; }; + CCCCCCC41EC3EF060087FE10 /* ASTextDebugOption.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTextDebugOption.m; sourceTree = ""; }; + CCCCCCC51EC3EF060087FE10 /* ASTextInput.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextInput.h; sourceTree = ""; }; + CCCCCCC61EC3EF060087FE10 /* ASTextInput.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTextInput.m; sourceTree = ""; }; + CCCCCCC71EC3EF060087FE10 /* ASTextLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextLayout.h; sourceTree = ""; }; + CCCCCCC81EC3EF060087FE10 /* ASTextLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTextLayout.m; sourceTree = ""; }; + CCCCCCC91EC3EF060087FE10 /* ASTextLine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextLine.h; sourceTree = ""; }; + CCCCCCCA1EC3EF060087FE10 /* ASTextLine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTextLine.m; sourceTree = ""; }; + CCCCCCCC1EC3EF060087FE10 /* ASTextAttribute.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextAttribute.h; sourceTree = ""; }; + CCCCCCCD1EC3EF060087FE10 /* ASTextAttribute.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTextAttribute.m; sourceTree = ""; }; + CCCCCCCE1EC3EF060087FE10 /* ASTextRunDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextRunDelegate.h; sourceTree = ""; }; + CCCCCCCF1EC3EF060087FE10 /* ASTextRunDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTextRunDelegate.m; sourceTree = ""; }; + CCCCCCD11EC3EF060087FE10 /* ASTextUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextUtilities.h; sourceTree = ""; }; + CCCCCCD21EC3EF060087FE10 /* ASTextUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTextUtilities.m; sourceTree = ""; }; + CCCCCCD31EC3EF060087FE10 /* NSParagraphStyle+ASText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSParagraphStyle+ASText.h"; sourceTree = ""; }; + CCCCCCD41EC3EF060087FE10 /* NSParagraphStyle+ASText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSParagraphStyle+ASText.m"; sourceTree = ""; }; + CCCCCCE51EC3F0FC0087FE10 /* NSAttributedString+ASText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSAttributedString+ASText.h"; sourceTree = ""; }; + CCCCCCE61EC3F0FC0087FE10 /* NSAttributedString+ASText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSAttributedString+ASText.m"; sourceTree = ""; }; + CCD5230F1EBD658C001F2191 /* ASTextNode2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextNode2.h; sourceTree = ""; }; + CCD523101EBD658C001F2191 /* ASTextNode2.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextNode2.mm; sourceTree = ""; }; CCE04B1E1E313EA7006AEBBB /* ASSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASSectionController.h; sourceTree = ""; }; CCE04B201E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "IGListAdapter+AsyncDisplayKit.h"; sourceTree = ""; }; CCE04B211E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "IGListAdapter+AsyncDisplayKit.m"; sourceTree = ""; }; @@ -1220,6 +1260,8 @@ 058D0A01195D050800B7D73C /* Private */ = { isa = PBXGroup; children = ( + CCD5230F1EBD658C001F2191 /* ASTextNode2.h */, + CCD523101EBD658C001F2191 /* ASTextNode2.mm */, CCA282CE1E9EBF6C0037E8B7 /* ASTipsWindow.h */, CCA282CF1E9EBF6C0037E8B7 /* ASTipsWindow.m */, CCA282C61E9EB64B0037E8B7 /* ASDisplayNodeTipState.h */, @@ -1294,6 +1336,7 @@ CC512B841DAC45C60054848E /* ASTableView+Undeprecated.h */, 83A7D9581D44542100BF333E /* ASWeakMap.h */, 83A7D9591D44542100BF333E /* ASWeakMap.m */, + CCCCCCC11EC3EF060087FE10 /* TextExperiment */, ); path = Private; sourceTree = ""; @@ -1432,6 +1475,55 @@ path = Layout; sourceTree = ""; }; + CCCCCCC11EC3EF060087FE10 /* TextExperiment */ = { + isa = PBXGroup; + children = ( + CCCCCCC21EC3EF060087FE10 /* Component */, + CCCCCCCB1EC3EF060087FE10 /* String */, + CCCCCCD01EC3EF060087FE10 /* Utility */, + ); + path = TextExperiment; + sourceTree = ""; + }; + CCCCCCC21EC3EF060087FE10 /* Component */ = { + isa = PBXGroup; + children = ( + CCCCCCC31EC3EF060087FE10 /* ASTextDebugOption.h */, + CCCCCCC41EC3EF060087FE10 /* ASTextDebugOption.m */, + CCCCCCC51EC3EF060087FE10 /* ASTextInput.h */, + CCCCCCC61EC3EF060087FE10 /* ASTextInput.m */, + CCCCCCC71EC3EF060087FE10 /* ASTextLayout.h */, + CCCCCCC81EC3EF060087FE10 /* ASTextLayout.m */, + CCCCCCC91EC3EF060087FE10 /* ASTextLine.h */, + CCCCCCCA1EC3EF060087FE10 /* ASTextLine.m */, + ); + path = Component; + sourceTree = ""; + }; + CCCCCCCB1EC3EF060087FE10 /* String */ = { + isa = PBXGroup; + children = ( + CCCCCCCC1EC3EF060087FE10 /* ASTextAttribute.h */, + CCCCCCCD1EC3EF060087FE10 /* ASTextAttribute.m */, + CCCCCCCE1EC3EF060087FE10 /* ASTextRunDelegate.h */, + CCCCCCCF1EC3EF060087FE10 /* ASTextRunDelegate.m */, + ); + path = String; + sourceTree = ""; + }; + CCCCCCD01EC3EF060087FE10 /* Utility */ = { + isa = PBXGroup; + children = ( + CCCCCCD11EC3EF060087FE10 /* ASTextUtilities.h */, + CCCCCCD21EC3EF060087FE10 /* ASTextUtilities.m */, + CCCCCCE51EC3F0FC0087FE10 /* NSAttributedString+ASText.h */, + CCCCCCE61EC3F0FC0087FE10 /* NSAttributedString+ASText.m */, + CCCCCCD31EC3EF060087FE10 /* NSParagraphStyle+ASText.h */, + CCCCCCD41EC3EF060087FE10 /* NSParagraphStyle+ASText.m */, + ); + path = Utility; + sourceTree = ""; + }; CCE04B1D1E313E99006AEBBB /* Collection Data Adapter */ = { isa = PBXGroup; children = ( @@ -1509,6 +1601,7 @@ E58E9E461E941D74004CFC59 /* ASCollectionLayoutDelegate.h in Headers */, E5E281741E71C833006B67C2 /* ASCollectionLayoutState.h in Headers */, E5B077FF1E69F4EB00C24B5B /* ASElementMap.h in Headers */, + CCCCCCE31EC3EF060087FE10 /* NSParagraphStyle+ASText.h in Headers */, E58E9E441E941D74004CFC59 /* ASCollectionLayoutContext.h in Headers */, E58E9E421E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h in Headers */, 696F01EC1DD2AF450049FBD5 /* ASEventLog.h in Headers */, @@ -1528,7 +1621,9 @@ B13CA1011C52004900E031AB /* ASCollectionNode+Beta.h in Headers */, 68C215581DE10D330019C4BC /* ASCollectionViewLayoutInspector.h in Headers */, B35062411B010EFD0018CF92 /* _ASAsyncTransactionGroup.h in Headers */, + CCD523111EBD658C001F2191 /* ASTextNode2.h in Headers */, B350620F1B010EFD0018CF92 /* _ASDisplayLayer.h in Headers */, + CCCCCCD71EC3EF060087FE10 /* ASTextInput.h in Headers */, B35062111B010EFD0018CF92 /* _ASDisplayView.h in Headers */, 9C55866C1BD54A3000B50E3A /* ASAsciiArtBoxCreator.h in Headers */, 509E68611B3AEDA0009B9150 /* ASAbstractLayoutController.h in Headers */, @@ -1558,6 +1653,7 @@ B35062171B010EFD0018CF92 /* ASDataController.h in Headers */, 34EFC75B1B701BAF00AD841F /* ASDimension.h in Headers */, 68FC85EA1CE29C7D00EDD713 /* ASVisibilityProtocols.h in Headers */, + CCCCCCD91EC3EF060087FE10 /* ASTextLayout.h in Headers */, A37320101C571B740011FC94 /* ASTextNode+Beta.h in Headers */, 9C70F2061CDA4F0C007D6C76 /* ASTraitCollection.h in Headers */, CC6AA2DA1E9F03B900978E87 /* ASDisplayNode+Ancestry.h in Headers */, @@ -1581,10 +1677,12 @@ 698DFF471E36B7E9002891F1 /* ASLayoutSpecUtilities.h in Headers */, 9C70F20D1CDBE9CB007D6C76 /* ASDefaultPlayButton.h in Headers */, DE7EF4F81DFF77720082B84A /* ASDisplayNode+FrameworkSubclasses.h in Headers */, + CCCCCCD51EC3EF060087FE10 /* ASTextDebugOption.h in Headers */, CC034A091E60BEB400626263 /* ASDisplayNode+Convenience.h in Headers */, 254C6B7E1BF94DF4003EC431 /* ASTextKitTailTruncater.h in Headers */, B35062491B010EFD0018CF92 /* _ASCoreAnimationExtras.h in Headers */, 68EE0DBE1C1B4ED300BA1B99 /* ASMainSerialQueue.h in Headers */, + CCCCCCE11EC3EF060087FE10 /* ASTextUtilities.h in Headers */, B350624B1B010EFD0018CF92 /* _ASPendingState.h in Headers */, CC54A81C1D70079800296A24 /* ASDispatch.h in Headers */, B350624D1B010EFD0018CF92 /* _ASScopeTimer.h in Headers */, @@ -1598,6 +1696,7 @@ 69CB62AC1CB8165900024920 /* _ASDisplayViewAccessiblity.h in Headers */, 254C6B7C1BF94DF4003EC431 /* ASTextKitRenderer+TextChecking.h in Headers */, 68AF37DB1CBEF4D80077BF76 /* ASImageNode+AnimatedImagePrivate.h in Headers */, + CCCCCCDD1EC3EF060087FE10 /* ASTextAttribute.h in Headers */, B35062461B010EFD0018CF92 /* ASBasicImageDownloaderInternal.h in Headers */, 044285081BAA63FE00D16268 /* ASBatchFetching.h in Headers */, AC026B701BD57DBF00BBC17E /* _ASHierarchyChangeSet.h in Headers */, @@ -1673,7 +1772,10 @@ B35062081B010EFD0018CF92 /* ASScrollNode.h in Headers */, CCA282CC1E9EB73E0037E8B7 /* ASTipNode.h in Headers */, 25E327571C16819500A2170C /* ASPagerNode.h in Headers */, + CCCCCCDB1EC3EF060087FE10 /* ASTextLine.h in Headers */, 9C70F20E1CDBE9E5007D6C76 /* NSArray+Diffing.h in Headers */, + CCCCCCE71EC3F0FC0087FE10 /* NSAttributedString+ASText.h in Headers */, + CCCCCCDF1EC3EF060087FE10 /* ASTextRunDelegate.h in Headers */, 9C49C3701B853961000B0DD5 /* ASStackLayoutElement.h in Headers */, 34EFC7701B701CFA00AD841F /* ASStackLayoutDefines.h in Headers */, CC0F885C1E42807F00576FED /* ASCollectionViewFlowLayoutInspector.h in Headers */, @@ -1957,6 +2059,7 @@ 9C70F2091CDABA36007D6C76 /* ASViewController.mm in Sources */, 3917EBD51E9C2FC400D04A01 /* _ASCollectionReusableView.m in Sources */, CCA282D11E9EBF6C0037E8B7 /* ASTipsWindow.m in Sources */, + CCCCCCE41EC3EF060087FE10 /* NSParagraphStyle+ASText.m in Sources */, 8BBBAB8D1CEBAF1E00107FC6 /* ASDefaultPlaybackButton.m in Sources */, B30BF6541C59D889004FCD53 /* ASLayoutManager.m in Sources */, 690C35671E0567C600069B91 /* ASDimensionDeprecated.mm in Sources */, @@ -1986,6 +2089,7 @@ E5B078001E69F4EB00C24B5B /* ASElementMap.m in Sources */, 9C8898BC1C738BA800D6B02E /* ASTextKitFontSizeAdjuster.mm in Sources */, 690ED59B1E36D118000627C0 /* ASImageNode+tvOS.m in Sources */, + CCCCCCD81EC3EF060087FE10 /* ASTextInput.m in Sources */, 34EFC7621B701CA400AD841F /* ASBackgroundLayoutSpec.mm in Sources */, DE8BEAC41C2DF3FC00D57C12 /* ASDelegateProxy.m in Sources */, B35062141B010EFD0018CF92 /* ASBasicImageDownloader.mm in Sources */, @@ -2007,10 +2111,12 @@ 8021EC1F1D2B00B100799119 /* UIImage+ASConvenience.m in Sources */, B35062181B010EFD0018CF92 /* ASDataController.mm in Sources */, 767E7F8E1C90191D0066C000 /* AsyncDisplayKit+Debug.m in Sources */, + CCCCCCD61EC3EF060087FE10 /* ASTextDebugOption.m in Sources */, 34EFC75C1B701BD200AD841F /* ASDimension.mm in Sources */, B350624E1B010EFD0018CF92 /* ASDisplayNode+AsyncDisplay.mm in Sources */, 25E327591C16819500A2170C /* ASPagerNode.m in Sources */, 636EA1A41C7FF4EC00EE152F /* NSArray+Diffing.m in Sources */, + CCD523121EBD658C001F2191 /* ASTextNode2.mm in Sources */, B35062501B010EFD0018CF92 /* ASDisplayNode+DebugTiming.mm in Sources */, DEC146B91C37A16A004A0EE7 /* ASCollectionInternal.m in Sources */, 254C6B891BF94F8A003EC431 /* ASTextKitRenderer+Positioning.mm in Sources */, @@ -2039,6 +2145,8 @@ 34EFC75E1B701BF000AD841F /* ASInternalHelpers.m in Sources */, 34EFC7681B701CDE00AD841F /* ASLayout.mm in Sources */, DECBD6EA1BE56E1900CF4905 /* ASButtonNode.mm in Sources */, + CCCCCCE01EC3EF060087FE10 /* ASTextRunDelegate.m in Sources */, + CCCCCCDA1EC3EF060087FE10 /* ASTextLayout.m in Sources */, 254C6B841BF94F8A003EC431 /* ASTextNodeWordKerner.m in Sources */, 34EFC76B1B701CEB00AD841F /* ASLayoutSpec.mm in Sources */, CC3B20861C3F76D600798563 /* ASPendingStateController.mm in Sources */, @@ -2049,6 +2157,7 @@ B35062071B010EFD0018CF92 /* ASNetworkImageNode.mm in Sources */, 34EFC76D1B701CF100AD841F /* ASOverlayLayoutSpec.mm in Sources */, 044285101BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.m in Sources */, + CCCCCCDE1EC3EF060087FE10 /* ASTextAttribute.m in Sources */, CCA282B51E9EA7310037E8B7 /* ASTipsController.m in Sources */, B35062271B010EFD0018CF92 /* ASRangeController.mm in Sources */, 0442850A1BAA63FE00D16268 /* ASBatchFetching.m in Sources */, @@ -2073,7 +2182,9 @@ E58E9E431E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m in Sources */, DE84918E1C8FFF9F003D89E9 /* ASRunLoopQueue.mm in Sources */, 68FC85E51CE29B7E00EDD713 /* ASTabBarController.m in Sources */, + CCCCCCDC1EC3EF060087FE10 /* ASTextLine.m in Sources */, 34EFC7741B701D0A00AD841F /* ASAbsoluteLayoutSpec.mm in Sources */, + CCCCCCE81EC3F0FC0087FE10 /* NSAttributedString+ASText.m in Sources */, 690C35621E055C5D00069B91 /* ASDimensionInternal.mm in Sources */, 68C2155A1DE10D330019C4BC /* ASCollectionViewLayoutInspector.m in Sources */, DB78412E1C6BCE1600A9E2B4 /* _ASTransitionContext.m in Sources */, @@ -2086,6 +2197,7 @@ 254C6B871BF94F8A003EC431 /* ASTextKitEntityAttribute.m in Sources */, 34566CB31BC1213700715E6B /* ASPhotosFrameworkImageRequest.m in Sources */, 254C6B831BF94F8A003EC431 /* ASTextKitCoreTextAdditions.m in Sources */, + CCCCCCE21EC3EF060087FE10 /* ASTextUtilities.m in Sources */, CC55A70E1E529FA200594372 /* UIResponder+AsyncDisplayKit.m in Sources */, 697796611D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.mm in Sources */, B350623B1B010EFD0018CF92 /* NSMutableAttributedString+TextKitAdditions.m in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index cbb9523bf3..0e467856fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,5 +18,6 @@ - [ASDisplayNode] Pass drawParameter in rendering context callbacks [Michael Schneider](https://github.com/maicki)[#248](https://github.com/TextureGroup/Texture/pull/248) - [ASTextNode] Move to class method of drawRect:withParameters:isCancelled:isRasterizing: for drawing [Michael Schneider] (https://github.com/maicki)[#232](https://github.com/TextureGroup/Texture/pull/232) - [ASDisplayNode] Remove instance:-drawRect:withParameters:isCancelled:isRasterizing: (https://github.com/maicki)[#232](https://github.com/TextureGroup/Texture/pull/232) +- [ASTextNode] Add an experimental new implementation. See `+[ASTextNode setExperimentOptions:]`. [Adlai Holler](https://github.com/Adlai-Holler)[#259](https://github.com/TextureGroup/Texture/pull/259) - [ASVideoNode] Added error reporing to ASVideoNode and it's delegate [#260](https://github.com/TextureGroup/Texture/pull/260) - [ASCollectionNode] Fixed conversion of item index paths between node & view. [Adlai Holler](https://github.com/Adlai-Holler) [#262](https://github.com/TextureGroup/Texture/pull/262) diff --git a/Source/ASTextNode+Beta.h b/Source/ASTextNode+Beta.h index 4361445768..c296af0cbc 100644 --- a/Source/ASTextNode+Beta.h +++ b/Source/ASTextNode+Beta.h @@ -21,6 +21,18 @@ NS_ASSUME_NONNULL_BEGIN +typedef NS_OPTIONS(NSUInteger, ASTextNodeExperimentOptions) { + // All subclass instances use the experimental implementation. + ASTextNodeExperimentSubclasses = 1 << 0, + // Random instances of ASTextNode (50% chance) (not subclasses) use experimental impl. + // Useful for profiling with apps that have no custom text node subclasses. + ASTextNodeExperimentRandomInstances = 1 << 1, + // All instances of ASTextNode itself use experimental implementation. Supersedes `.randomInstances`. + ASTextNodeExperimentAllInstances = 1 << 2, + // Add highlighting etc. for debugging. + ASTextNodeExperimentDebugging = 1 << 3 +}; + @interface ASTextNode () /** @@ -38,6 +50,19 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) UIEdgeInsets textContainerInset; +/** + * Opt in to an experimental implementation of text node. The implementation may improve performance and correctness, + * but may not support all features and has not been thoroughly tested in production. + * + * @precondition You may not call this after allocating any text nodes. You may only call this once. + */ ++ (void)setExperimentOptions:(ASTextNodeExperimentOptions)options; + +/** + * Returns YES if this node is using the experimental implementation. NO otherwise. Will not change. + */ +@property (atomic, readonly) BOOL usingExperiment; + @end NS_ASSUME_NONNULL_END diff --git a/Source/ASTextNode.mm b/Source/ASTextNode.mm index 7f2220e3fa..d1334b21bb 100644 --- a/Source/ASTextNode.mm +++ b/Source/ASTextNode.mm @@ -16,6 +16,7 @@ // #import +#import #import #include @@ -263,14 +264,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; #pragma mark - ASDisplayNode -- (void)clearContents -{ - // We discard the backing store and renderer to prevent the very large - // memory overhead of maintaining these for all text nodes. They can be - // regenerated when layout is necessary. - [super clearContents]; // ASDisplayNode will set layer.contents = nil -} - - (void)didLoad { [super didLoad]; @@ -1386,6 +1379,70 @@ static NSAttributedString *DefaultTruncationAttributedString() } #endif +static ASDN::Mutex _experimentLock; +static ASTextNodeExperimentOptions _experimentOptions; +static BOOL _hasAllocatedNode; + ++ (void)setExperimentOptions:(ASTextNodeExperimentOptions)options +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ASDN::MutexLocker lock(_experimentLock); + + // They must call this before allocating any text nodes. + ASDisplayNodeAssertFalse(_hasAllocatedNode); + + _experimentOptions = options; + + // Set superclass of all subclasses to ASTextNode2 + if (options & ASTextNodeExperimentSubclasses) { + unsigned int classCount; + Class originalClass = [ASTextNode class]; + Class newClass = [ASTextNode2 class]; + Class *classes = objc_copyClassList(&classCount); + for (int i = 0; i < classCount; i++) { + Class c = classes[i]; + if (class_getSuperclass(c) == originalClass) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + class_setSuperclass(c, newClass); +#pragma clang diagnostic pop + } + } + free(classes); + } + + if (options & ASTextNodeExperimentDebugging) { + [ASTextNode2 enableDebugging]; + } + }); +} + ++ (id)allocWithZone:(struct _NSZone *)zone +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ASDN::MutexLocker lock(_experimentLock); + _hasAllocatedNode = YES; + }); + + // All instances || (random instances && rand() != 0) + BOOL useExperiment = (_experimentOptions & ASTextNodeExperimentAllInstances) + || ((_experimentOptions & ASTextNodeExperimentRandomInstances) + && (arc4random_uniform(2) != 0)); + + if (useExperiment) { + return (ASTextNode *)[ASTextNode2 allocWithZone:zone]; + } else { + return [super allocWithZone:zone]; + } +} + +- (BOOL)usingExperiment +{ + return NO; +} + @end @implementation ASTextNode (Deprecated) diff --git a/Source/Private/ASTextNode2.h b/Source/Private/ASTextNode2.h new file mode 100644 index 0000000000..81cb0837ad --- /dev/null +++ b/Source/Private/ASTextNode2.h @@ -0,0 +1,228 @@ +// +// ASTextNode2.h +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +// Import this to get ASTextNodeHighlightStyle +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + @abstract Draws interactive rich text. + @discussion Backed by the code in TextExperiment folder, on top of CoreText. + */ +@interface ASTextNode2 : ASControlNode + +/** + @abstract The styled text displayed by the node. + @discussion Defaults to nil, no text is shown. + For inline image attachments, add an attribute of key NSAttachmentAttributeName, with a value of an NSTextAttachment. + */ +@property (nullable, nonatomic, copy) NSAttributedString *attributedText; + +#pragma mark - Truncation + +/** + @abstract The attributedText to use when the text must be truncated. + @discussion Defaults to a localized ellipsis character. + */ +@property (nullable, nonatomic, copy) NSAttributedString *truncationAttributedText; + +/** + @summary The second attributed string appended for truncation. + @discussion This string will be highlighted on touches. + @default nil + */ +@property (nullable, nonatomic, copy) NSAttributedString *additionalTruncationMessage; + +/** + @abstract Determines how the text is truncated to fit within the receiver's maximum size. + @discussion Defaults to NSLineBreakByWordWrapping. + @note Setting a truncationMode in attributedString will override the truncation mode set here. + */ +@property (nonatomic, assign) NSLineBreakMode truncationMode; + +/** + @abstract If the text node is truncated. Text must have been sized first. + */ +@property (nonatomic, readonly, assign, getter=isTruncated) BOOL truncated; + +/** + @abstract The maximum number of lines to render of the text before truncation. + @default 0 (No limit) + */ +@property (nonatomic, assign) NSUInteger maximumNumberOfLines; + +/** + @abstract The number of lines in the text. Text must have been sized first. + */ +@property (nonatomic, readonly, assign) NSUInteger lineCount; + +/** + * An array of path objects representing the regions where text should not be displayed. + * + * @discussion The default value of this property is an empty array. You can + * assign an array of UIBezierPath objects to exclude text from one or more regions in + * the text node's bounds. You can use this property to have text wrap around images, + * shapes or other text like a fancy magazine. + */ +@property (nullable, nonatomic, strong) NSArray *exclusionPaths; + +#pragma mark - Placeholders + +/** + * @abstract ASTextNode has a special placeholder behavior when placeholderEnabled is YES. + * + * @discussion Defaults to NO. When YES, it draws rectangles for each line of text, + * following the true shape of the text's wrapping. This visually mirrors the overall + * shape and weight of paragraphs, making the appearance of the finished text less jarring. + */ +@property (nonatomic, assign) BOOL placeholderEnabled; + +/** + @abstract The placeholder color. + */ +@property (nullable, nonatomic, strong) UIColor *placeholderColor; + +/** + @abstract Inset each line of the placeholder. + */ +@property (nonatomic, assign) UIEdgeInsets placeholderInsets; + +#pragma mark - Shadow + +/** + @abstract When you set these ASDisplayNode properties, they are composited into the bitmap instead of being applied by CA. + + @property (nonatomic, assign) CGColorRef shadowColor; + @property (nonatomic, assign) CGFloat shadowOpacity; + @property (nonatomic, assign) CGSize shadowOffset; + @property (nonatomic, assign) CGFloat shadowRadius; + */ + +/** + @abstract The number of pixels used for shadow padding on each side of the receiver. + @discussion Each inset will be less than or equal to zero, so that applying + UIEdgeInsetsRect(boundingRectForText, shadowPadding) + will return a CGRect large enough to fit both the text and the appropriate shadow padding. + */ +@property (nonatomic, readonly, assign) UIEdgeInsets shadowPadding; + +#pragma mark - Positioning + +/** + @abstract Returns an array of rects bounding the characters in a given text range. + @param textRange A range of text. Must be valid for the receiver's string. + @discussion Use this method to detect all the different rectangles a given range of text occupies. + The rects returned are not guaranteed to be contiguous (for example, if the given text range spans + a line break, the rects returned will be on opposite sides and different lines). The rects returned + are in the coordinate system of the receiver. + */ +- (NSArray *)rectsForTextRange:(NSRange)textRange AS_WARN_UNUSED_RESULT; + +/** + @abstract Returns an array of rects used for highlighting the characters in a given text range. + @param textRange A range of text. Must be valid for the receiver's string. + @discussion Use this method to detect all the different rectangles the highlights of a given range of text occupies. + The rects returned are not guaranteed to be contiguous (for example, if the given text range spans + a line break, the rects returned will be on opposite sides and different lines). The rects returned + are in the coordinate system of the receiver. This method is useful for visual coordination with a + highlighted range of text. + */ +- (NSArray *)highlightRectsForTextRange:(NSRange)textRange AS_WARN_UNUSED_RESULT; + +/** + @abstract Returns a bounding rect for the given text range. + @param textRange A range of text. Must be valid for the receiver's string. + @discussion The height of the frame returned is that of the receiver's line-height; adjustment for + cap-height and descenders is not performed. This method raises an exception if textRange is not + a valid substring range of the receiver's string. + */ +- (CGRect)frameForTextRange:(NSRange)textRange AS_WARN_UNUSED_RESULT; + +/** + @abstract Returns the trailing rectangle of space in the receiver, after the final character. + @discussion Use this method to detect which portion of the receiver is not occupied by characters. + The rect returned is in the coordinate system of the receiver. + */ +- (CGRect)trailingRect AS_WARN_UNUSED_RESULT; + + +#pragma mark - Actions + +/** + @abstract The set of attribute names to consider links. Defaults to NSLinkAttributeName. + */ +@property (nonatomic, copy) NSArray *linkAttributeNames; + +/** + @abstract Indicates whether the receiver has an entity at a given point. + @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. + */ +- (nullable id)linkAttributeValueAtPoint:(CGPoint)point attributeName:(out NSString * _Nullable * _Nullable)attributeNameOut range:(out NSRange * _Nullable)rangeOut AS_WARN_UNUSED_RESULT; + +/** + @abstract The style to use when highlighting text. + */ +@property (nonatomic, assign) ASTextNodeHighlightStyle highlightStyle; + +/** + @abstract The range of text highlighted by the receiver. Changes to this property are not animated by default. + */ +@property (nonatomic, assign) NSRange highlightRange; + +/** + @abstract Set the range of text to highlight, with optional animation. + + @param highlightRange The range of text to highlight. + + @param animated Whether the text should be highlighted with an animation. + */ +- (void)setHighlightRange:(NSRange)highlightRange animated:(BOOL)animated; + +/** + @abstract Responds to actions from links in the text node. + @discussion The delegate must be set before the node is loaded, and implement + textNode:longPressedLinkAttribute:value:atPoint:textRange: in order for + the long press gesture recognizer to be installed. + */ +@property (nonatomic, weak) id delegate; + +/** + @abstract If YES and a long press is recognized, touches are cancelled. Default is NO + */ +@property (nonatomic, assign) BOOL longPressCancelsTouches; + +/** + @abstract if YES will not intercept touches for non-link areas of the text. Default is NO. + */ +@property (nonatomic, assign) BOOL passthroughNonlinkTouches; + ++ (void)enableDebugging; + +@end + +@interface ASTextNode2 (Unavailable) + +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock __unavailable; + +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock __unavailable; + +@end + +NS_ASSUME_NONNULL_END + + diff --git a/Source/Private/ASTextNode2.mm b/Source/Private/ASTextNode2.mm new file mode 100644 index 0000000000..379e126b0a --- /dev/null +++ b/Source/Private/ASTextNode2.mm @@ -0,0 +1,1088 @@ +// +// ASTextNode2.mm +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +#import +#import +#import +#import + +#import +#import +#import + +#import + +#import +#import +#import + +@interface ASTextCacheValue : NSObject { + @package + ASDN::Mutex _m; + std::deque> _layouts; +} +@end +@implementation ASTextCacheValue +@end + +/** + * If set, we will record all values set to attributedText into an array + * and once we get 2000, we'll write them all out into a plist file. + * + * This is useful for gathering realistic text data sets from apps for performance + * testing. + */ +#define AS_TEXTNODE2_RECORD_ATTRIBUTED_STRINGS 0 + +#define AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE() { \ + static dispatch_once_t onceToken; \ + dispatch_once(&onceToken, ^{ \ + NSLog(@"[Texture] Warning: Feature %@ is unimplemented in the experimental text node.", NSStringFromSelector(_cmd)); \ + });\ +} + +static const CGFloat ASTextNodeHighlightLightOpacity = 0.11; +static const CGFloat ASTextNodeHighlightDarkOpacity = 0.22; +static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncationAttribute"; + +@interface ASTextNode2 () + +@end + +@implementation ASTextNode2 { + ASTextContainer *_textContainer; + + CGSize _shadowOffset; + CGColorRef _shadowColor; + CGFloat _shadowOpacity; + CGFloat _shadowRadius; + + NSAttributedString *_attributedText; + NSAttributedString *_composedTruncationText; + NSArray *_pointSizeScaleFactors; + + NSString *_highlightedLinkAttributeName; + id _highlightedLinkAttributeValue; + ASTextNodeHighlightStyle _highlightStyle; + NSRange _highlightRange; + ASHighlightOverlayLayer *_activeHighlightLayer; + + UILongPressGestureRecognizer *_longPressGestureRecognizer; +} +@dynamic placeholderEnabled; + +static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; + +- (instancetype)init +{ + if (self = [super init]) { + _textContainer = [[ASTextContainer alloc] init]; + // Load default values from superclass. + _shadowOffset = [super shadowOffset]; + _shadowColor = CGColorRetain([super shadowColor]); + _shadowOpacity = [super shadowOpacity]; + _shadowRadius = [super shadowRadius]; + + // Disable user interaction for text node by default. + self.userInteractionEnabled = NO; + self.needsDisplayOnBoundsChange = YES; + + _textContainer.truncationType = ASTextTruncationTypeEnd; + + // The common case is for a text node to be non-opaque and blended over some background. + self.opaque = NO; + self.backgroundColor = [UIColor clearColor]; + + self.linkAttributeNames = DefaultLinkAttributeNames; + + // Accessibility + self.isAccessibilityElement = YES; + self.accessibilityTraits = UIAccessibilityTraitStaticText; + + // Placeholders + // Disabled by default in ASDisplayNode, but add a few options for those who toggle + // on the special placeholder behavior of ASTextNode. + _placeholderColor = ASDisplayNodeDefaultPlaceholderColor(); + _placeholderInsets = UIEdgeInsetsMake(1.0, 0.0, 1.0, 0.0); + } + + return self; +} + +- (void)dealloc +{ + CGColorRelease(_shadowColor); + + if (_longPressGestureRecognizer) { + _longPressGestureRecognizer.delegate = nil; + [_longPressGestureRecognizer removeTarget:nil action:NULL]; + [self.view removeGestureRecognizer:_longPressGestureRecognizer]; + } +} + +#pragma mark - Description + +- (NSString *)_plainStringForDescription +{ + NSString *plainString = [[self.attributedText string] stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]; + if (plainString.length > 50) { + plainString = [[plainString substringToIndex:50] stringByAppendingString:@"โ€ฆ"]; + } + return plainString; +} + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [super propertiesForDescription]; + NSString *plainString = [self _plainStringForDescription]; + if (plainString.length > 0) { + [result insertObject:@{ @"text" : ASStringWithQuotesIfMultiword(plainString) } atIndex:0]; + } + return result; +} + +- (NSMutableArray *)propertiesForDebugDescription +{ + NSMutableArray *result = [super propertiesForDebugDescription]; + NSString *plainString = [self _plainStringForDescription]; + if (plainString.length > 0) { + [result insertObject:@{ @"text" : ASStringWithQuotesIfMultiword(plainString) } atIndex:0]; + } + return result; +} + +#pragma mark - ASDisplayNode + +- (void)didLoad +{ + [super didLoad]; + + // If we are view-backed and the delegate cares, support the long-press callback. + SEL longPressCallback = @selector(textNode:longPressedLinkAttribute:value:atPoint:textRange:); + if (!self.isLayerBacked && [_delegate respondsToSelector:longPressCallback]) { + _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(_handleLongPress:)]; + _longPressGestureRecognizer.cancelsTouchesInView = self.longPressCancelsTouches; + _longPressGestureRecognizer.delegate = self; + [self.view addGestureRecognizer:_longPressGestureRecognizer]; + } +} + +- (BOOL)supportsLayerBacking +{ + if (!super.supportsLayerBacking) { + return NO; + } + + // If the text contains any links, return NO. + NSAttributedString *attributedText = self.attributedText; + NSRange range = NSMakeRange(0, attributedText.length); + for (NSString *linkAttributeName in _linkAttributeNames) { + __block BOOL hasLink = NO; + [attributedText enumerateAttribute:linkAttributeName inRange:range options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { + hasLink = (value != nil); + *stop = YES; + }]; + if (hasLink) { + return NO; + } + } + return YES; +} + +#pragma mark - Layout and Sizing + +- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset +{ + BOOL needsUpdate = !UIEdgeInsetsEqualToEdgeInsets(_textContainer.insets, textContainerInset); + _textContainer.insets = textContainerInset; + + if (needsUpdate) { + [self setNeedsLayout]; + } +} + +- (UIEdgeInsets)textContainerInset +{ + return _textContainer.insets; +} + +- (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); + + ASTextContainer *container = [_textContainer copy]; + NSAttributedString *attributedText = self.attributedText; + container.size = constrainedSize; + [self _ensureTruncationText]; + + NSMutableAttributedString *mutableText = [attributedText mutableCopy]; + [self prepareAttributedStringForDrawing:mutableText]; + ASTextLayout *layout = [ASTextNode2 compatibleLayoutWithContainer:container text:mutableText]; + + [self setNeedsDisplay]; + + return layout.textBoundingSize; +} + +#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. ++ (CGFloat)ascenderWithAttributedString:(NSAttributedString *)attributedString +{ + UIFont *font = [attributedString attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL]; + NSParagraphStyle *paragraphStyle = [attributedString attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:NULL]; + if (!paragraphStyle) { + return font.ascender; + } + CGFloat lineHeight = MAX(font.lineHeight, paragraphStyle.minimumLineHeight); + if (paragraphStyle.maximumLineHeight > 0) { + lineHeight = MIN(lineHeight, paragraphStyle.maximumLineHeight); + } + return lineHeight + font.descender; +} + +- (NSAttributedString *)attributedText +{ + ASDN::MutexLocker l(__instanceLock__); + return _attributedText; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText +{ + + if (attributedText == nil) { + attributedText = [[NSAttributedString alloc] initWithString:@"" attributes:nil]; + } + + // Don't hold textLock for too long. + { + ASDN::MutexLocker l(__instanceLock__); + if (ASObjectIsEqual(attributedText, _attributedText)) { + return; + } + + _attributedText = attributedText; +#if AS_TEXTNODE2_RECORD_ATTRIBUTED_STRINGS + [ASTextNode _registerAttributedText:_attributedText]; +#endif + } + + // Since truncation text matches style of attributedText, invalidate it now. + [self _invalidateTruncationText]; + + NSUInteger length = attributedText.length; + if (length > 0) { + self.style.ascender = [[self class] ascenderWithAttributedString:attributedText]; + self.style.descender = [[attributedText attribute:NSFontAttributeName atIndex:attributedText.length - 1 effectiveRange:NULL] descender]; + } + + // Tell the display node superclasses that the cached layout is incorrect now + [self setNeedsLayout]; + + // Force display to create renderer with new size and redisplay with new string + [self setNeedsDisplay]; + + + // Accessiblity + self.accessibilityLabel = attributedText.string; + self.isAccessibilityElement = (length != 0); // We're an accessibility element by default if there is a string. +} + +#pragma mark - Text Layout + +- (void)setExclusionPaths:(NSArray *)exclusionPaths +{ + _textContainer.exclusionPaths = exclusionPaths; + + [self setNeedsLayout]; + [self setNeedsDisplay]; +} + +- (NSArray *)exclusionPaths +{ + return _textContainer.exclusionPaths; +} + +- (void)prepareAttributedStringForDrawing:(NSMutableAttributedString *)attributedString +{ + ASDN::MutexLocker lock(__instanceLock__); + + // Apply paragraph style if needed + [attributedString enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, attributedString.length) options:kNilOptions usingBlock:^(NSParagraphStyle *style, NSRange range, BOOL * _Nonnull stop) { + if (style == nil || style.lineBreakMode == _truncationMode) { + return; + } + + NSMutableParagraphStyle *paragraphStyle = [style mutableCopy] ?: [[NSMutableParagraphStyle alloc] init]; + paragraphStyle.lineBreakMode = _truncationMode; + [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; + }]; + + // Apply background color if needed + UIColor *backgroundColor = self.backgroundColor; + if (CGColorGetAlpha(backgroundColor.CGColor) > 0) { + [attributedString addAttribute:NSBackgroundColorAttributeName value:backgroundColor range:NSMakeRange(0, attributedString.length)]; + } + + // Apply shadow if needed + if (_shadowOpacity > 0 && (_shadowRadius != 0 || !CGSizeEqualToSize(_shadowOffset, CGSizeZero)) && CGColorGetAlpha(_shadowColor) > 0) { + NSShadow *shadow = [[NSShadow alloc] init]; + if (_shadowOpacity != 1) { + shadow.shadowColor = [UIColor colorWithCGColor:CGColorCreateCopyWithAlpha(_shadowColor, _shadowOpacity * CGColorGetAlpha(_shadowColor))]; + } else { + shadow.shadowColor = [UIColor colorWithCGColor:_shadowColor]; + } + shadow.shadowOffset = _shadowOffset; + shadow.shadowBlurRadius = _shadowRadius; + [attributedString addAttribute:NSShadowAttributeName value:shadow range:NSMakeRange(0, attributedString.length)]; + } +} + +#pragma mark - Drawing + +- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer +{ + [self _ensureTruncationText]; + ASTextContainer *copiedContainer = [_textContainer copy]; + copiedContainer.size = self.bounds.size; + NSMutableAttributedString *mutableText = [self.attributedText mutableCopy] ?: [[NSMutableAttributedString alloc] init]; + [self prepareAttributedStringForDrawing:mutableText]; + return @{ + @"container": copiedContainer, + @"text": mutableText + }; +} + +/** + * If it can't find a compatible layout, this method creates one. + */ ++ (ASTextLayout *)compatibleLayoutWithContainer:(ASTextContainer *)container + text:(NSAttributedString *)text + +{ + static ASDN::Mutex layoutCacheLock; + static NSCache *textLayoutCache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + textLayoutCache = [[NSCache alloc] init]; + }); + + ASTextCacheValue *cacheValue = ({ + ASDN::MutexLocker lock(layoutCacheLock); + cacheValue = [textLayoutCache objectForKey:text]; + if (cacheValue == nil) { + cacheValue = [[ASTextCacheValue alloc] init]; + [textLayoutCache setObject:cacheValue forKey:text]; + } + cacheValue; + }); + + CGRect containerBounds = (CGRect){ .size = container.size }; + { + ASDN::MutexLocker lock(cacheValue->_m); + for (auto &t : cacheValue->_layouts) { + CGSize constrainedSize = std::get<0>(t); + ASTextLayout *layout = std::get<1>(t); + + CGSize layoutSize = layout.textBoundingSize; + // 1. CoreText can return frames that are narrower than the constrained width, for obvious reasons. + // 2. CoreText can return frames that are slightly wider than the constrained width, for some reason. + // We have to trust that somehow it's OK to try and draw within our size constraint, despite the return value. + // 3. Thus, those two values (constrained width & returned width) form a range, where + // intermediate values in that range will be snapped. Thus, we can use a given layout as long as our + // width is in that range, between the min and max of those two values. + CGRect minRect = CGRectMake(0, 0, MIN(layoutSize.width, constrainedSize.width), MIN(layoutSize.height, constrainedSize.height)); + if (!CGRectContainsRect(containerBounds, minRect)) { + continue; + } + CGRect maxRect = CGRectMake(0, 0, MAX(layoutSize.width, constrainedSize.width), MAX(layoutSize.height, constrainedSize.height)); + if (!CGRectContainsRect(maxRect, containerBounds)) { + continue; + } + + // Now check container params. + ASTextContainer *otherContainer = layout.container; + if (!UIEdgeInsetsEqualToEdgeInsets(container.insets, otherContainer.insets)) { + continue; + } + if (!ASObjectIsEqual(container.exclusionPaths, otherContainer.exclusionPaths)) { + continue; + } + if (container.maximumNumberOfRows != otherContainer.maximumNumberOfRows) { + continue; + } + if (container.truncationType != otherContainer.truncationType) { + continue; + } + if (!ASObjectIsEqual(container.truncationToken, otherContainer.truncationToken)) { + continue; + } + // TODO: When we get a cache hit, move this entry to the front (LRU). + return layout; + } + } + + // Cache Miss. + + // Compute the text layout. + ASTextLayout *layout = [ASTextLayout layoutWithContainer:container text:text]; + + // Store the result in the cache. + { + ASDN::MutexLocker lock(cacheValue->_m); + cacheValue->_layouts.push_front(std::make_tuple(container.size, layout)); + if (cacheValue->_layouts.size() > 3) { + cacheValue->_layouts.pop_back(); + } + } + + return layout; +} + ++ (void)drawRect:(CGRect)bounds withParameters:(NSDictionary *)layoutDict isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing; +{ + ASTextContainer *container = layoutDict[@"container"]; + NSAttributedString *text = layoutDict[@"text"]; + ASTextLayout *layout = [self compatibleLayoutWithContainer:container text:text]; + + if (isCancelledBlock()) { + return; + } + CGContextRef context = UIGraphicsGetCurrentContext(); + ASDisplayNodeAssert(context, @"This is no good without a context."); + + [layout drawInContext:context size:bounds.size point:bounds.origin view:nil layer:nil debug:[ASTextDebugOption sharedDebugOption] cancel:isCancelledBlock]; +} + +#pragma mark - Attributes + +- (id)linkAttributeValueAtPoint:(CGPoint)point + attributeName:(out NSString **)attributeNameOut + range:(out NSRange *)rangeOut +{ + return [self _linkAttributeValueAtPoint:point + attributeName:attributeNameOut + range:rangeOut + inAdditionalTruncationMessage:NULL + forHighlighting:NO]; +} + +- (id)_linkAttributeValueAtPoint:(CGPoint)point + attributeName:(out NSString **)attributeNameOut + range:(out NSRange *)rangeOut + inAdditionalTruncationMessage:(out BOOL *)inAdditionalTruncationMessageOut + forHighlighting:(BOOL)highlighting +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return nil; +} + +#pragma mark - UIGestureRecognizerDelegate + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + ASDisplayNodeAssertMainThread(); + + if (gestureRecognizer == _longPressGestureRecognizer) { + // Don't allow long press on truncation message + if ([self _pendingTruncationTap]) { + return NO; + } + + // Ask our delegate if a long-press on an attribute is relevant + if ([_delegate respondsToSelector:@selector(textNode:shouldLongPressLinkAttribute:value:atPoint:)]) { + return [_delegate textNode:(ASTextNode *)self + shouldLongPressLinkAttribute:_highlightedLinkAttributeName + value:_highlightedLinkAttributeValue + atPoint:[gestureRecognizer locationInView:self.view]]; + } + + // Otherwise we are good to go. + return YES; + } + + if (([self _pendingLinkTap] || [self _pendingTruncationTap]) + && [gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] + && CGRectContainsPoint(self.threadSafeBounds, [gestureRecognizer locationInView:self.view])) { + return NO; + } + + return [super gestureRecognizerShouldBegin:gestureRecognizer]; +} + +#pragma mark - Highlighting + +- (ASTextNodeHighlightStyle)highlightStyle +{ + ASDN::MutexLocker l(__instanceLock__); + + return _highlightStyle; +} + +- (void)setHighlightStyle:(ASTextNodeHighlightStyle)highlightStyle +{ + ASDN::MutexLocker l(__instanceLock__); + + _highlightStyle = highlightStyle; +} + +- (NSRange)highlightRange +{ + ASDisplayNodeAssertMainThread(); + + return _highlightRange; +} + +- (void)setHighlightRange:(NSRange)highlightRange +{ + [self setHighlightRange:highlightRange animated:NO]; +} + +- (void)setHighlightRange:(NSRange)highlightRange animated:(BOOL)animated +{ + [self _setHighlightRange:highlightRange forAttributeName:nil value:nil animated:animated]; +} + +- (void)_setHighlightRange:(NSRange)highlightRange forAttributeName:(NSString *)highlightedAttributeName value:(id)highlightedAttributeValue animated:(BOOL)animated +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + // Much of the code from original ASTextNode is probably usable here. + return; +} + +- (void)_clearHighlightIfNecessary +{ + ASDisplayNodeAssertMainThread(); + + if ([self _pendingLinkTap] || [self _pendingTruncationTap]) { + [self setHighlightRange:NSMakeRange(0, 0) animated:YES]; + } +} + ++ (CGColorRef)_highlightColorForStyle:(ASTextNodeHighlightStyle)style +{ + return [UIColor colorWithWhite:(style == ASTextNodeHighlightStyleLight ? 0.0 : 1.0) alpha:1.0].CGColor; +} + ++ (CGFloat)_highlightOpacityForStyle:(ASTextNodeHighlightStyle)style +{ + return (style == ASTextNodeHighlightStyleLight) ? ASTextNodeHighlightLightOpacity : ASTextNodeHighlightDarkOpacity; +} + +#pragma mark - Text rects + +- (NSArray *)rectsForTextRange:(NSRange)textRange +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return @[]; +} + +- (NSArray *)highlightRectsForTextRange:(NSRange)textRange +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return @[]; +} + +- (CGRect)trailingRect +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return CGRectZero; +} + +- (CGRect)frameForTextRange:(NSRange)textRange +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return CGRectZero; +} + +#pragma mark - Placeholders + +- (void)setPlaceholderColor:(UIColor *)placeholderColor +{ + ASDN::MutexLocker l(__instanceLock__); + + _placeholderColor = placeholderColor; + + // prevent placeholders if we don't have a color + self.placeholderEnabled = placeholderColor != nil; +} + +- (UIImage *)placeholderImage +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return nil; +} + +#pragma mark - Touch Handling + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + + if (!_passthroughNonlinkTouches) { + return [super pointInside:point withEvent:event]; + } + + NSRange range = NSMakeRange(0, 0); + NSString *linkAttributeName = nil; + BOOL inAdditionalTruncationMessage = NO; + + id linkAttributeValue = [self _linkAttributeValueAtPoint:point + attributeName:&linkAttributeName + range:&range + inAdditionalTruncationMessage:&inAdditionalTruncationMessage + forHighlighting:YES]; + + NSUInteger lastCharIndex = NSIntegerMax; + BOOL linkCrossesVisibleRange = (lastCharIndex > range.location) && (lastCharIndex < NSMaxRange(range) - 1); + + if (inAdditionalTruncationMessage) { + return YES; + } else if (range.length && !linkCrossesVisibleRange && linkAttributeValue != nil && linkAttributeName != nil) { + return YES; + } else { + return NO; + } +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + + [super touchesBegan:touches withEvent:event]; + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return; +} + + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + [super touchesCancelled:touches withEvent:event]; + + [self _clearHighlightIfNecessary]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + [super touchesEnded:touches withEvent:event]; + + if ([self _pendingLinkTap] && [_delegate respondsToSelector:@selector(textNode:tappedLinkAttribute:value:atPoint:textRange:)]) { + CGPoint point = [[touches anyObject] locationInView:self.view]; + [_delegate textNode:(ASTextNode *)self tappedLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue atPoint:point textRange:_highlightRange]; + } + + if ([self _pendingTruncationTap]) { + if ([_delegate respondsToSelector:@selector(textNodeTappedTruncationToken:)]) { + [_delegate textNodeTappedTruncationToken:(ASTextNode *)self]; + } + } + + [self _clearHighlightIfNecessary]; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + [super touchesMoved:touches withEvent:event]; + + 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: + if (CGPointEqualToPoint([touch previousLocationInView:self.view], locationInView)) + return; + + // If touch has moved out of the current highlight range, clear the highlight. + if (_highlightRange.length > 0) { + NSRange range = NSMakeRange(0, 0); + [self _linkAttributeValueAtPoint:locationInView + attributeName:NULL + range:&range + inAdditionalTruncationMessage:NULL + forHighlighting:YES]; + + if (!NSEqualRanges(_highlightRange, range)) { + [self _clearHighlightIfNecessary]; + } + } +} + +- (void)_handleLongPress:(UILongPressGestureRecognizer *)longPressRecognizer +{ + ASDisplayNodeAssertMainThread(); + + // Respond to long-press when it begins, not when it ends. + if (longPressRecognizer.state == UIGestureRecognizerStateBegan) { + if ([_delegate respondsToSelector:@selector(textNode:longPressedLinkAttribute:value:atPoint:textRange:)]) { + CGPoint touchPoint = [_longPressGestureRecognizer locationInView:self.view]; + [_delegate textNode:(ASTextNode *)self longPressedLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue atPoint:touchPoint textRange:_highlightRange]; + } + } +} + +- (BOOL)_pendingLinkTap +{ + ASDN::MutexLocker l(__instanceLock__); + + return (_highlightedLinkAttributeValue != nil && ![self _pendingTruncationTap]) && _delegate != nil; +} + +- (BOOL)_pendingTruncationTap +{ + ASDN::MutexLocker l(__instanceLock__); + + return [_highlightedLinkAttributeName isEqualToString:ASTextNodeTruncationTokenAttributeName]; +} + +#pragma mark - Shadow Properties + +/** + * Note about shadowed text: + * + * Shadowed text is pretty rare, and we are a framework that targets serious developers. + * We should probably ignore these properties and tell developers to set the shadow into their attributed text instead. + */ +- (CGColorRef)shadowColor +{ + ASDN::MutexLocker l(__instanceLock__); + + return _shadowColor; +} + +- (void)setShadowColor:(CGColorRef)shadowColor +{ + __instanceLock__.lock(); + + if (_shadowColor != shadowColor && CGColorEqualToColor(shadowColor, _shadowColor) == NO) { + CGColorRelease(_shadowColor); + _shadowColor = CGColorRetain(shadowColor); + __instanceLock__.unlock(); + + [self setNeedsDisplay]; + return; + } + + __instanceLock__.unlock(); +} + +- (CGSize)shadowOffset +{ + ASDN::MutexLocker l(__instanceLock__); + + return _shadowOffset; +} + +- (void)setShadowOffset:(CGSize)shadowOffset +{ + { + ASDN::MutexLocker l(__instanceLock__); + + if (CGSizeEqualToSize(_shadowOffset, shadowOffset)) { + return; + } + _shadowOffset = shadowOffset; + } + + [self setNeedsDisplay]; +} + +- (CGFloat)shadowOpacity +{ + ASDN::MutexLocker l(__instanceLock__); + + return _shadowOpacity; +} + +- (void)setShadowOpacity:(CGFloat)shadowOpacity +{ + { + ASDN::MutexLocker l(__instanceLock__); + + if (_shadowOpacity == shadowOpacity) { + return; + } + + _shadowOpacity = shadowOpacity; + } + + [self setNeedsDisplay]; +} + +- (CGFloat)shadowRadius +{ + ASDN::MutexLocker l(__instanceLock__); + + return _shadowRadius; +} + +- (void)setShadowRadius:(CGFloat)shadowRadius +{ + { + ASDN::MutexLocker l(__instanceLock__); + + if (_shadowRadius == shadowRadius) { + return; + } + + _shadowRadius = shadowRadius; + } + + [self setNeedsDisplay]; +} + +- (UIEdgeInsets)shadowPadding +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return UIEdgeInsetsZero; +} + +- (void)setPointSizeScaleFactors:(NSArray *)scaleFactors +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + _pointSizeScaleFactors = [scaleFactors copy]; +} + +- (NSArray *)pointSizeScaleFactors +{ + return _pointSizeScaleFactors; +} + +#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 +{ + if (_textContainer.truncationToken == nil) { + ASDN::MutexLocker l(__instanceLock__); + _textContainer.truncationToken = [self _locked_composedTruncationText]; + } +} + +- (void)setTruncationAttributedText:(NSAttributedString *)truncationAttributedText +{ + { + ASDN::MutexLocker l(__instanceLock__); + + if (ASObjectIsEqual(_truncationAttributedText, truncationAttributedText)) { + return; + } + + _truncationAttributedText = [truncationAttributedText copy]; + } + + [self _invalidateTruncationText]; +} + +- (void)setAdditionalTruncationMessage:(NSAttributedString *)additionalTruncationMessage +{ + { + ASDN::MutexLocker l(__instanceLock__); + + if (ASObjectIsEqual(_additionalTruncationMessage, additionalTruncationMessage)) { + return; + } + + _additionalTruncationMessage = [additionalTruncationMessage copy]; + } + + [self _invalidateTruncationText]; +} + +- (void)setTruncationMode:(NSLineBreakMode)truncationMode +{ + ASDN::MutexLocker lock(__instanceLock__); + if (_truncationMode == truncationMode) { + return; + } + _truncationMode = truncationMode; + + ASTextTruncationType truncationType; + switch (truncationMode) { + case NSLineBreakByTruncatingHead: + truncationType = ASTextTruncationTypeStart; + break; + case NSLineBreakByTruncatingTail: + truncationType = ASTextTruncationTypeEnd; + break; + case NSLineBreakByTruncatingMiddle: + truncationType = ASTextTruncationTypeMiddle; + break; + default: + truncationType = ASTextTruncationTypeNone; + } + + _textContainer.truncationType = truncationType; + + [self setNeedsDisplay]; +} + +- (BOOL)isTruncated +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return NO; +} + +- (NSUInteger)maximumNumberOfLines +{ + return _textContainer.maximumNumberOfRows; +} + +- (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines +{ + if (_textContainer.maximumNumberOfRows == maximumNumberOfLines) { + return; + } + _textContainer.maximumNumberOfRows = maximumNumberOfLines; + + [self setNeedsDisplay]; +} + +- (NSUInteger)lineCount +{ + ASDN::MutexLocker l(__instanceLock__); + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return 0; +} + +#pragma mark - Truncation Message + +- (void)_invalidateTruncationText +{ + _textContainer.truncationToken = nil; + [self setNeedsDisplay]; +} + +/** + * @return the additional truncation message range within the as-rendered text. + * Must be called from main thread + */ +- (NSRange)_additionalTruncationMessageRangeWithVisibleRange:(NSRange)visibleRange +{ + ASDN::MutexLocker l(__instanceLock__); + + // Check if we even have an additional truncation message. + if (!_additionalTruncationMessage) { + return NSMakeRange(NSNotFound, 0); + } + + // Character location of the unicode ellipsis (the first index after the visible range) + NSInteger truncationTokenIndex = NSMaxRange(visibleRange); + + NSUInteger additionalTruncationMessageLength = _additionalTruncationMessage.length; + // We get the location of the truncation token, then add the length of the + // truncation attributed string +1 for the space between. + return NSMakeRange(truncationTokenIndex + _truncationAttributedText.length + 1, additionalTruncationMessageLength); +} + +/** + * @return the truncation message for the string. If there are both an + * additional truncation message and a truncation attributed string, they will + * be properly composed. + */ +- (NSAttributedString *)_locked_composedTruncationText +{ + if (_composedTruncationText == nil) { + if (_truncationAttributedText != nil && _additionalTruncationMessage != nil) { + NSMutableAttributedString *newComposedTruncationString = [[NSMutableAttributedString alloc] initWithAttributedString:_truncationAttributedText]; + [newComposedTruncationString.mutableString appendString:@" "]; + [newComposedTruncationString appendAttributedString:_additionalTruncationMessage]; + _composedTruncationText = newComposedTruncationString; + } else if (_truncationAttributedText != nil) { + _composedTruncationText = _truncationAttributedText; + } else if (_additionalTruncationMessage != nil) { + _composedTruncationText = _additionalTruncationMessage; + } else { + _composedTruncationText = DefaultTruncationAttributedString(); + } + _composedTruncationText = [self _locked_prepareTruncationStringForDrawing:_composedTruncationText]; + } + return _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 +{ + 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 = [NSMutableDictionary dictionaryWithDictionary:originalStringAttributes]; + [futureTruncationAttributes addEntriesFromDictionary:attributes]; + [truncationMutableString setAttributes:futureTruncationAttributes range:range]; + }]; + } + return truncationMutableString; +} + +#if AS_TEXTNODE2_RECORD_ATTRIBUTED_STRINGS ++ (void)_registerAttributedText:(NSAttributedString *)str +{ + static NSMutableArray *array; + static NSLock *lock; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + lock = [NSLock new]; + array = [NSMutableArray new]; + }); + [lock lock]; + [array addObject:str]; + if (array.count % 20 == 0) { + NSLog(@"Got %d strings", (int)array.count); + } + if (array.count == 2000) { + NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"AttributedStrings.plist"]; + NSAssert([NSKeyedArchiver archiveRootObject:array toFile:path], nil); + NSLog(@"Saved to %@", path); + } + [lock unlock]; +} +#endif + ++ (void)enableDebugging +{ + ASTextDebugOption *debugOption = [[ASTextDebugOption alloc] init]; + debugOption.CTLineFillColor = [UIColor colorWithRed:0 green:0.3 blue:1 alpha:0.1]; + [ASTextDebugOption setSharedDebugOption:debugOption]; +} + +- (BOOL)usingExperiment +{ + return YES; +} + +@end diff --git a/Source/Private/TextExperiment/Component/ASTextDebugOption.h b/Source/Private/TextExperiment/Component/ASTextDebugOption.h new file mode 100755 index 0000000000..1ef4ddd8be --- /dev/null +++ b/Source/Private/TextExperiment/Component/ASTextDebugOption.h @@ -0,0 +1,95 @@ +// +// ASTextDebugOption.h +// Modified from YYText +// +// Created by ibireme on 15/4/8. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import + +@class ASTextDebugOption; + +NS_ASSUME_NONNULL_BEGIN + +/** + The ASTextDebugTarget protocol defines the method a debug target should implement. + A debug target can be add to the global container to receive the shared debug + option changed notification. + */ +@protocol ASTextDebugTarget + +@required +/** + When the shared debug option changed, this method would be called on main thread. + It should return as quickly as possible. The option's property should not be changed + in this method. + + @param option The shared debug option. + */ +- (void)setDebugOption:(nullable ASTextDebugOption *)option; +@end + + + +/** + The debug option for ASText. + */ +@interface ASTextDebugOption : NSObject +@property (nullable, nonatomic, strong) UIColor *baselineColor; ///< baseline color +@property (nullable, nonatomic, strong) UIColor *CTFrameBorderColor; ///< CTFrame path border color +@property (nullable, nonatomic, strong) UIColor *CTFrameFillColor; ///< CTFrame path fill color +@property (nullable, nonatomic, strong) UIColor *CTLineBorderColor; ///< CTLine bounds border color +@property (nullable, nonatomic, strong) UIColor *CTLineFillColor; ///< CTLine bounds fill color +@property (nullable, nonatomic, strong) UIColor *CTLineNumberColor; ///< CTLine line number color +@property (nullable, nonatomic, strong) UIColor *CTRunBorderColor; ///< CTRun bounds border color +@property (nullable, nonatomic, strong) UIColor *CTRunFillColor; ///< CTRun bounds fill color +@property (nullable, nonatomic, strong) UIColor *CTRunNumberColor; ///< CTRun number color +@property (nullable, nonatomic, strong) UIColor *CGGlyphBorderColor; ///< CGGlyph bounds border color +@property (nullable, nonatomic, strong) UIColor *CGGlyphFillColor; ///< CGGlyph bounds fill color + +- (BOOL)needDrawDebug; ///< `YES`: at least one debug color is visible. `NO`: all debug color is invisible/nil. +- (void)clear; ///< Set all debug color to nil. + +/** + Add a debug target. + + @discussion When `setSharedDebugOption:` is called, all added debug target will + receive `setDebugOption:` in main thread. It maintains an unsafe_unretained + reference to this target. The target must to removed before dealloc. + + @param target A debug target. + */ ++ (void)addDebugTarget:(id)target; + +/** + Remove a debug target which is added by `addDebugTarget:`. + + @param target A debug target. + */ ++ (void)removeDebugTarget:(id)target; + +/** + Returns the shared debug option. + + @return The shared debug option, default is nil. + */ ++ (nullable ASTextDebugOption *)sharedDebugOption; + +/** + Set a debug option as shared debug option. + This method must be called on main thread. + + @discussion When call this method, the new option will set to all debug target + which is added by `addDebugTarget:`. + + @param option A new debug option (nil is valid). + */ ++ (void)setSharedDebugOption:(nullable ASTextDebugOption *)option; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/TextExperiment/Component/ASTextDebugOption.m b/Source/Private/TextExperiment/Component/ASTextDebugOption.m new file mode 100755 index 0000000000..fe36f250ed --- /dev/null +++ b/Source/Private/TextExperiment/Component/ASTextDebugOption.m @@ -0,0 +1,139 @@ +// +// ASTextDebugOption.m +// Modified from YYText +// +// Created by ibireme on 15/4/8. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import "ASTextDebugOption.h" +#import +#import + +static pthread_mutex_t _sharedDebugLock; +static CFMutableSetRef _sharedDebugTargets = nil; +static ASTextDebugOption *_sharedDebugOption = nil; + +static const void* _sharedDebugSetRetain(CFAllocatorRef allocator, const void *value) { + return value; +} + +static void _sharedDebugSetRelease(CFAllocatorRef allocator, const void *value) { +} + +void _sharedDebugSetFunction(const void *value, void *context) { + id target = (__bridge id)(value); + [target setDebugOption:_sharedDebugOption]; +} + +static void _initSharedDebug() { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + pthread_mutex_init(&_sharedDebugLock, NULL); + CFSetCallBacks callbacks = kCFTypeSetCallBacks; + callbacks.retain = _sharedDebugSetRetain; + callbacks.release = _sharedDebugSetRelease; + _sharedDebugTargets = CFSetCreateMutable(CFAllocatorGetDefault(), 0, &callbacks); + }); +} + +static void _setSharedDebugOption(ASTextDebugOption *option) { + _initSharedDebug(); + pthread_mutex_lock(&_sharedDebugLock); + _sharedDebugOption = option.copy; + CFSetApplyFunction(_sharedDebugTargets, _sharedDebugSetFunction, NULL); + pthread_mutex_unlock(&_sharedDebugLock); +} + +static ASTextDebugOption *_getSharedDebugOption() { + _initSharedDebug(); + pthread_mutex_lock(&_sharedDebugLock); + ASTextDebugOption *op = _sharedDebugOption; + pthread_mutex_unlock(&_sharedDebugLock); + return op; +} + +static void _addDebugTarget(id target) { + _initSharedDebug(); + pthread_mutex_lock(&_sharedDebugLock); + CFSetAddValue(_sharedDebugTargets, (__bridge const void *)(target)); + pthread_mutex_unlock(&_sharedDebugLock); +} + +static void _removeDebugTarget(id target) { + _initSharedDebug(); + pthread_mutex_lock(&_sharedDebugLock); + CFSetRemoveValue(_sharedDebugTargets, (__bridge const void *)(target)); + pthread_mutex_unlock(&_sharedDebugLock); +} + + +@implementation ASTextDebugOption + +- (id)copyWithZone:(NSZone *)zone { + ASTextDebugOption *op = [self.class new]; + op.baselineColor = self.baselineColor; + op.CTFrameBorderColor = self.CTFrameBorderColor; + op.CTFrameFillColor = self.CTFrameFillColor; + op.CTLineBorderColor = self.CTLineBorderColor; + op.CTLineFillColor = self.CTLineFillColor; + op.CTLineNumberColor = self.CTLineNumberColor; + op.CTRunBorderColor = self.CTRunBorderColor; + op.CTRunFillColor = self.CTRunFillColor; + op.CTRunNumberColor = self.CTRunNumberColor; + op.CGGlyphBorderColor = self.CGGlyphBorderColor; + op.CGGlyphFillColor = self.CGGlyphFillColor; + return op; +} + +- (BOOL)needDrawDebug { + if (self.baselineColor || + self.CTFrameBorderColor || + self.CTFrameFillColor || + self.CTLineBorderColor || + self.CTLineFillColor || + self.CTLineNumberColor || + self.CTRunBorderColor || + self.CTRunFillColor || + self.CTRunNumberColor || + self.CGGlyphBorderColor || + self.CGGlyphFillColor) return YES; + return NO; +} + +- (void)clear { + self.baselineColor = nil; + self.CTFrameBorderColor = nil; + self.CTFrameFillColor = nil; + self.CTLineBorderColor = nil; + self.CTLineFillColor = nil; + self.CTLineNumberColor = nil; + self.CTRunBorderColor = nil; + self.CTRunFillColor = nil; + self.CTRunNumberColor = nil; + self.CGGlyphBorderColor = nil; + self.CGGlyphFillColor = nil; +} + ++ (void)addDebugTarget:(id)target { + if (target) _addDebugTarget(target); +} + ++ (void)removeDebugTarget:(id)target { + if (target) _removeDebugTarget(target); +} + ++ (ASTextDebugOption *)sharedDebugOption { + return _getSharedDebugOption(); +} + ++ (void)setSharedDebugOption:(ASTextDebugOption *)option { + NSAssert([NSThread isMainThread], @"This method must be called on the main thread"); + _setSharedDebugOption(option); +} + +@end + diff --git a/Source/Private/TextExperiment/Component/ASTextInput.h b/Source/Private/TextExperiment/Component/ASTextInput.h new file mode 100755 index 0000000000..6b2aa79bbc --- /dev/null +++ b/Source/Private/TextExperiment/Component/ASTextInput.h @@ -0,0 +1,87 @@ +// +// ASTextInput.h +// Modified from YYText +// +// Created by ibireme on 15/4/17. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Text position affinity. For example, the offset appears after the last + character on a line is backward affinity, before the first character on + the following line is forward affinity. + */ +typedef NS_ENUM(NSInteger, ASTextAffinity) { + ASTextAffinityForward = 0, ///< offset appears before the character + ASTextAffinityBackward = 1, ///< offset appears after the character +}; + + +/** + A ASTextPosition object represents a position in a text container; in other words, + it is an index into the backing string in a text-displaying view. + + ASTextPosition has the same API as Apple's implementation in UITextView/UITextField, + so you can alse use it to interact with UITextView/UITextField. + */ +@interface ASTextPosition : UITextPosition + +@property (nonatomic, readonly) NSInteger offset; +@property (nonatomic, readonly) ASTextAffinity affinity; + ++ (instancetype)positionWithOffset:(NSInteger)offset; ++ (instancetype)positionWithOffset:(NSInteger)offset affinity:(ASTextAffinity) affinity; + +- (NSComparisonResult)compare:(id)otherPosition; + +@end + + +/** + A ASTextRange object represents a range of characters in a text container; in other words, + it identifies a starting index and an ending index in string backing a text-displaying view. + + ASTextRange has the same API as Apple's implementation in UITextView/UITextField, + so you can alse use it to interact with UITextView/UITextField. + */ +@interface ASTextRange : UITextRange + +@property (nonatomic, readonly) ASTextPosition *start; +@property (nonatomic, readonly) ASTextPosition *end; +@property (nonatomic, readonly, getter=isEmpty) BOOL empty; + ++ (instancetype)rangeWithRange:(NSRange)range; ++ (instancetype)rangeWithRange:(NSRange)range affinity:(ASTextAffinity) affinity; ++ (instancetype)rangeWithStart:(ASTextPosition *)start end:(ASTextPosition *)end; ++ (instancetype)defaultRange; ///< <{0,0} Forward> + +- (NSRange)asRange; + +@end + + +/** + A ASTextSelectionRect object encapsulates information about a selected range of + text in a text-displaying view. + + ASTextSelectionRect has the same API as Apple's implementation in UITextView/UITextField, + so you can alse use it to interact with UITextView/UITextField. + */ +@interface ASTextSelectionRect : UITextSelectionRect + +@property (nonatomic, readwrite) CGRect rect; +@property (nonatomic, readwrite) UITextWritingDirection writingDirection; +@property (nonatomic, readwrite) BOOL containsStart; +@property (nonatomic, readwrite) BOOL containsEnd; +@property (nonatomic, readwrite) BOOL isVertical; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/TextExperiment/Component/ASTextInput.m b/Source/Private/TextExperiment/Component/ASTextInput.m new file mode 100755 index 0000000000..88f355b1f9 --- /dev/null +++ b/Source/Private/TextExperiment/Component/ASTextInput.m @@ -0,0 +1,152 @@ +// +// ASTextInput.m +// Modified from YYText +// +// Created by ibireme on 15/4/17. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import "ASTextInput.h" +#import "ASTextUtilities.h" + + +@implementation ASTextPosition + ++ (instancetype)positionWithOffset:(NSInteger)offset { + return [self positionWithOffset:offset affinity:ASTextAffinityForward]; +} + ++ (instancetype)positionWithOffset:(NSInteger)offset affinity:(ASTextAffinity)affinity { + ASTextPosition *p = [self new]; + p->_offset = offset; + p->_affinity = affinity; + return p; +} + +- (instancetype)copyWithZone:(NSZone *)zone { + return [self.class positionWithOffset:_offset affinity:_affinity]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p> (%@%@)", self.class, self, @(_offset), _affinity == ASTextAffinityForward ? @"F":@"B"]; +} + +- (NSUInteger)hash { + return _offset * 2 + (_affinity == ASTextAffinityForward ? 1 : 0); +} + +- (BOOL)isEqual:(ASTextPosition *)object { + if (!object) return NO; + return _offset == object.offset && _affinity == object.affinity; +} + +- (NSComparisonResult)compare:(ASTextPosition *)otherPosition { + if (!otherPosition) return NSOrderedAscending; + if (_offset < otherPosition.offset) return NSOrderedAscending; + if (_offset > otherPosition.offset) return NSOrderedDescending; + if (_affinity == ASTextAffinityBackward && otherPosition.affinity == ASTextAffinityForward) return NSOrderedAscending; + if (_affinity == ASTextAffinityForward && otherPosition.affinity == ASTextAffinityBackward) return NSOrderedDescending; + return NSOrderedSame; +} + +@end + + + +@implementation ASTextRange { + ASTextPosition *_start; + ASTextPosition *_end; +} + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + _start = [ASTextPosition positionWithOffset:0]; + _end = [ASTextPosition positionWithOffset:0]; + return self; +} + +- (ASTextPosition *)start { + return _start; +} + +- (ASTextPosition *)end { + return _end; +} + +- (BOOL)isEmpty { + return _start.offset == _end.offset; +} + +- (NSRange)asRange { + return NSMakeRange(_start.offset, _end.offset - _start.offset); +} + ++ (instancetype)rangeWithRange:(NSRange)range { + return [self rangeWithRange:range affinity:ASTextAffinityForward]; +} + ++ (instancetype)rangeWithRange:(NSRange)range affinity:(ASTextAffinity)affinity { + ASTextPosition *start = [ASTextPosition positionWithOffset:range.location affinity:affinity]; + ASTextPosition *end = [ASTextPosition positionWithOffset:range.location + range.length affinity:affinity]; + return [self rangeWithStart:start end:end]; +} + ++ (instancetype)rangeWithStart:(ASTextPosition *)start end:(ASTextPosition *)end { + if (!start || !end) return nil; + if ([start compare:end] == NSOrderedDescending) { + ASTEXT_SWAP(start, end); + } + ASTextRange *range = [ASTextRange new]; + range->_start = start; + range->_end = end; + return range; +} + ++ (instancetype)defaultRange { + return [self new]; +} + +- (instancetype)copyWithZone:(NSZone *)zone { + return [self.class rangeWithStart:_start end:_end]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p> (%@, %@)%@", self.class, self, @(_start.offset), @(_end.offset - _start.offset), _end.affinity == ASTextAffinityForward ? @"F":@"B"]; +} + +- (NSUInteger)hash { + return (sizeof(NSUInteger) == 8 ? OSSwapInt64(_start.hash) : OSSwapInt32(_start.hash)) + _end.hash; +} + +- (BOOL)isEqual:(ASTextRange *)object { + if (!object) return NO; + return [_start isEqual:object.start] && [_end isEqual:object.end]; +} + +@end + + + +@implementation ASTextSelectionRect + +@synthesize rect = _rect; +@synthesize writingDirection = _writingDirection; +@synthesize containsStart = _containsStart; +@synthesize containsEnd = _containsEnd; +@synthesize isVertical = _isVertical; + +- (id)copyWithZone:(NSZone *)zone { + ASTextSelectionRect *one = [self.class new]; + one.rect = _rect; + one.writingDirection = _writingDirection; + one.containsStart = _containsStart; + one.containsEnd = _containsEnd; + one.isVertical = _isVertical; + return one; +} + +@end diff --git a/Source/Private/TextExperiment/Component/ASTextLayout.h b/Source/Private/TextExperiment/Component/ASTextLayout.h new file mode 100755 index 0000000000..2051c80050 --- /dev/null +++ b/Source/Private/TextExperiment/Component/ASTextLayout.h @@ -0,0 +1,548 @@ +// +// ASTextLayout.h +// Modified from YYText +// +// Created by ibireme on 15/3/3. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import +#import + +#import "ASTextDebugOption.h" +#import "ASTextLine.h" +#import "ASTextInput.h" + +@protocol ASTextLinePositionModifier; + +NS_ASSUME_NONNULL_BEGIN + +/** + The max text container size in layout. + */ +extern const CGSize ASTextContainerMaxSize; + +/** + The ASTextContainer class defines a region in which text is laid out. + ASTextLayout class uses one or more ASTextContainer objects to generate layouts. + + A ASTextContainer defines rectangular regions (`size` and `insets`) or + nonrectangular shapes (`path`), and you can define exclusion paths inside the + text container's bounding rectangle so that text flows around the exclusion + path as it is laid out. + + All methods in this class is thread-safe. + + Example: + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” <------- container + โ”‚ โ”‚ + โ”‚ asdfasdfasdfasdfasdfa <------------ container insets + โ”‚ asdfasdfa asdfasdfa โ”‚ + โ”‚ asdfas asdasd โ”‚ + โ”‚ asdfa <----------------------- container exclusion path + โ”‚ asdfas adfasd โ”‚ + โ”‚ asdfasdfa asdfasdfa โ”‚ + โ”‚ asdfasdfasdfasdfasdfa โ”‚ + โ”‚ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + */ +@interface ASTextContainer : NSObject + +/// Creates a container with the specified size. @param size The size. ++ (instancetype)containerWithSize:(CGSize)size; + +/// Creates a container with the specified size and insets. @param size The size. @param insets The text insets. ++ (instancetype)containerWithSize:(CGSize)size insets:(UIEdgeInsets)insets; + +/// Creates a container with the specified path. @param size The path. ++ (instancetype)containerWithPath:(nullable UIBezierPath *)path; + +/// The constrained size. (if the size is larger than ASTextContainerMaxSize, it will be clipped) +@property CGSize size; + +/// The insets for constrained size. The inset value should not be negative. Default is UIEdgeInsetsZero. +@property UIEdgeInsets insets; + +/// Custom constrained path. Set this property to ignore `size` and `insets`. Default is nil. +@property (nullable, copy) UIBezierPath *path; + +/// An array of `UIBezierPath` for path exclusion. Default is nil. +@property (nullable, copy) NSArray *exclusionPaths; + +/// Path line width. Default is 0; +@property CGFloat pathLineWidth; + +/// YES:(PathFillEvenOdd) Text is filled in the area that would be painted if the path were given to CGContextEOFillPath. +/// NO: (PathFillWindingNumber) Text is fill in the area that would be painted if the path were given to CGContextFillPath. +/// 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; + +/// The line truncation type, default is none. +@property ASTextTruncationType truncationType; + +/// The truncation token. If nil, the layout will use "โ€ฆ" instead. Default is nil. +@property (nullable, copy) NSAttributedString *truncationToken; + +/// This modifier is applied to the lines before the layout is completed, +/// give you a chance to modify the line position. Default is nil. +@property (nullable, copy) id linePositionModifier; +@end + + +/** + The ASTextLinePositionModifier protocol declares the required method to modify + the line position in text layout progress. See `ASTextLinePositionSimpleModifier` for example. + */ +@protocol ASTextLinePositionModifier +@required +/** + This method will called before layout is completed. The method should be thread-safe. + @param lines An array of ASTextLine. + @param text The full text. + @param container The layout container. + */ +- (void)modifyLines:(NSArray *)lines fromText:(NSAttributedString *)text inContainer:(ASTextContainer *)container; +@end + + +/** + A simple implementation of `ASTextLinePositionModifier`. It can fix each line's position + to a specified value, lets each line of height be the same. + */ +@interface ASTextLinePositionSimpleModifier : NSObject +@property (assign) CGFloat fixedLineHeight; ///< The fixed line height (distance between two baseline). +@end + + + +/** + ASTextLayout class is a readonly class stores text layout result. + All the property in this class is readonly, and should not be changed. + The methods in this class is thread-safe (except some of the draw methods). + + example: (layout with a circle exclusion path) + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” <------ container + โ”‚ [--------Line0--------] โ”‚ <- Row0 + โ”‚ [--------Line1--------] โ”‚ <- Row1 + โ”‚ [-Line2-] [-Line3-] โ”‚ <- Row2 + โ”‚ [-Line4] [Line5-] โ”‚ <- Row3 + โ”‚ [-Line6-] [-Line7-] โ”‚ <- Row4 + โ”‚ [--------Line8--------] โ”‚ <- Row5 + โ”‚ [--------Line9--------] โ”‚ <- Row6 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + */ +@interface ASTextLayout : NSObject + + +#pragma mark - Generate text layout +///============================================================================= +/// @name Generate text layout +///============================================================================= + +/** + Generate a layout with the given container size and text. + + @param size The text container's size + @param text The text (if nil, returns nil). + @return A new layout, or nil when an error occurs. + */ ++ (nullable ASTextLayout *)layoutWithContainerSize:(CGSize)size text:(NSAttributedString *)text; + +/** + Generate a layout with the given container and text. + + @param container The text container (if nil, returns nil). + @param text The text (if nil, returns nil). + @return A new layout, or nil when an error occurs. + */ ++ (nullable ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text; + +/** + Generate a layout with the given container and text. + + @param container The text container (if nil, returns nil). + @param text The text (if nil, returns nil). + @param range The text range (if out of range, returns nil). If the + length of the range is 0, it means the length is no limit. + @return A new layout, or nil when an error occurs. + */ ++ (nullable ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range; + +/** + Generate layouts with the given containers and text. + + @param containers An array of ASTextContainer object (if nil, returns nil). + @param text The text (if nil, returns nil). + @return An array of ASTextLayout object (the count is same as containers), + or nil when an error occurs. + */ ++ (nullable NSArray *)layoutWithContainers:(NSArray *)containers + text:(NSAttributedString *)text; + +/** + Generate layouts with the given containers and text. + + @param containers An array of ASTextContainer object (if nil, returns nil). + @param text The text (if nil, returns nil). + @param range The text range (if out of range, returns nil). If the + length of the range is 0, it means the length is no limit. + @return An array of ASTextLayout object (the count is same as containers), + or nil when an error occurs. + */ ++ (nullable NSArray *)layoutWithContainers:(NSArray *)containers + text:(NSAttributedString *)text + range:(NSRange)range; + +- (instancetype)init UNAVAILABLE_ATTRIBUTE; ++ (instancetype)new UNAVAILABLE_ATTRIBUTE; + + +#pragma mark - Text layout attributes +///============================================================================= +/// @name Text layout attributes +///============================================================================= + +///< The text container +@property (nonatomic, strong, readonly) ASTextContainer *container; +///< The full text +@property (nonatomic, strong, readonly) NSAttributedString *text; +///< The text range in full text +@property (nonatomic, readonly) NSRange range; +///< CTFrameSetter +@property (nonatomic, readonly) CTFramesetterRef frameSetter; +///< CTFrame +@property (nonatomic, readonly) CTFrameRef frame; +///< Array of `ASTextLine`, no truncated +@property (nonatomic, strong, readonly) NSArray *lines; +///< ASTextLine with truncated token, or nil +@property (nullable, nonatomic, strong, readonly) ASTextLine *truncatedLine; +///< Array of `ASTextAttachment` +@property (nullable, nonatomic, strong, readonly) NSArray *attachments; +///< Array of NSRange(wrapped by NSValue) in text +@property (nullable, nonatomic, strong, readonly) NSArray *attachmentRanges; +///< Array of CGRect(wrapped by NSValue) in container +@property (nullable, nonatomic, strong, readonly) NSArray *attachmentRects; +///< Set of Attachment (UIImage/UIView/CALayer) +@property (nullable, nonatomic, strong, readonly) NSSet *attachmentContentsSet; +///< Number of rows +@property (nonatomic, readonly) NSUInteger rowCount; +///< Visible text range +@property (nonatomic, readonly) NSRange visibleRange; +///< Bounding rect (glyphs) +@property (nonatomic, readonly) CGRect textBoundingRect; +///< Bounding size (glyphs and insets, ceil to pixel) +@property (nonatomic, readonly) CGSize textBoundingSize; +///< Has highlight attribute +@property (nonatomic, readonly) BOOL containsHighlight; +///< Has block border attribute +@property (nonatomic, readonly) BOOL needDrawBlockBorder; +///< Has background border attribute +@property (nonatomic, readonly) BOOL needDrawBackgroundBorder; +///< Has shadow attribute +@property (nonatomic, readonly) BOOL needDrawShadow; +///< Has underline attribute +@property (nonatomic, readonly) BOOL needDrawUnderline; +///< Has visible text +@property (nonatomic, readonly) BOOL needDrawText; +///< Has attachment attribute +@property (nonatomic, readonly) BOOL needDrawAttachment; +///< Has inner shadow attribute +@property (nonatomic, readonly) BOOL needDrawInnerShadow; +///< Has strickthrough attribute +@property (nonatomic, readonly) BOOL needDrawStrikethrough; +///< Has border attribute +@property (nonatomic, readonly) BOOL needDrawBorder; + + +#pragma mark - Query information from text layout +///============================================================================= +/// @name Query information from text layout +///============================================================================= + +/** + The first line index for row. + + @param row A row index. + @return The line index, or NSNotFound if not found. + */ +- (NSUInteger)lineIndexForRow:(NSUInteger)row; + +/** + The number of lines for row. + + @param row A row index. + @return The number of lines, or NSNotFound when an error occurs. + */ +- (NSUInteger)lineCountForRow:(NSUInteger)row; + +/** + The row index for line. + + @param line A row index. + + @return The row index, or NSNotFound if not found. + */ +- (NSUInteger)rowIndexForLine:(NSUInteger)line; + +/** + The line index for a specified point. + + @discussion It returns NSNotFound if there's no text at the point. + + @param point A point in the container. + @return The line index, or NSNotFound if not found. + */ +- (NSUInteger)lineIndexForPoint:(CGPoint)point; + +/** + The line index closest to a specified point. + + @param point A point in the container. + @return The line index, or NSNotFound if no line exist in layout. + */ +- (NSUInteger)closestLineIndexForPoint:(CGPoint)point; + +/** + The offset in container for a text position in a specified line. + + @discussion The offset is the text position's baseline point.x. + If the container is vertical form, the offset is the baseline point.y; + + @param position The text position in string. + @param lineIndex The line index. + @return The offset in container, or CGFLOAT_MAX if not found. + */ +- (CGFloat)offsetForTextPosition:(NSUInteger)position lineIndex:(NSUInteger)lineIndex; + +/** + The text position for a point in a specified line. + + @discussion This method just call CTLineGetStringIndexForPosition() and does + NOT consider the emoji, line break character, binding text... + + @param point A point in the container. + @param lineIndex The line index. + @return The text position, or NSNotFound if not found. + */ +- (NSUInteger)textPositionForPoint:(CGPoint)point lineIndex:(NSUInteger)lineIndex; + +/** + The closest text position to a specified point. + + @discussion This method takes into account the restrict of emoji, line break + character, binding text and text affinity. + + @param point A point in the container. + @return A text position, or nil if not found. + */ +- (nullable ASTextPosition *)closestPositionToPoint:(CGPoint)point; + +/** + Returns the new position when moving selection grabber in text view. + + @discussion There are two grabber in the text selection period, user can only + move one grabber at the same time. + + @param point A point in the container. + @param oldPosition The old text position for the moving grabber. + @param otherPosition The other position in text selection view. + + @return A text position, or nil if not found. + */ +- (nullable ASTextPosition *)positionForPoint:(CGPoint)point + oldPosition:(ASTextPosition *)oldPosition + otherPosition:(ASTextPosition *)otherPosition; + +/** + Returns the character or range of characters that is at a given point in the container. + If there is no text at the point, returns nil. + + @discussion This method takes into account the restrict of emoji, line break + character, binding text and text affinity. + + @param point A point in the container. + @return An object representing a range that encloses a character (or characters) + at point. Or nil if not found. + */ +- (nullable ASTextRange *)textRangeAtPoint:(CGPoint)point; + +/** + Returns the closest character or range of characters that is at a given point in + the container. + + @discussion This method takes into account the restrict of emoji, line break + character, binding text and text affinity. + + @param point A point in the container. + @return An object representing a range that encloses a character (or characters) + at point. Or nil if not found. + */ +- (nullable ASTextRange *)closestTextRangeAtPoint:(CGPoint)point; + +/** + If the position is inside an emoji, composed character sequences, line break '\\r\\n' + or custom binding range, then returns the range by extend the position. Otherwise, + returns a zero length range from the position. + + @param position A text-position object that identifies a location in layout. + + @return A text-range object that extend the position. Or nil if an error occurs + */ +- (nullable ASTextRange *)textRangeByExtendingPosition:(ASTextPosition *)position; + +/** + Returns a text range at a given offset in a specified direction from another + text position to its farthest extent in a certain direction of layout. + + @param position A text-position object that identifies a location in layout. + @param direction A constant that indicates a direction of layout (right, left, up, down). + @param offset A character offset from position. + + @return A text-range object that represents the distance from position to the + farthest extent in direction. Or nil if an error occurs. + */ +- (nullable ASTextRange *)textRangeByExtendingPosition:(ASTextPosition *)position + inDirection:(UITextLayoutDirection)direction + offset:(NSInteger)offset; + +/** + Returns the line index for a given text position. + + @discussion This method takes into account the text affinity. + + @param position A text-position object that identifies a location in layout. + @return The line index, or NSNotFound if not found. + */ +- (NSUInteger)lineIndexForPosition:(ASTextPosition *)position; + +/** + Returns the baseline position for a given text position. + + @param position An object that identifies a location in the layout. + @return The baseline position for text, or CGPointZero if not found. + */ +- (CGPoint)linePositionForPosition:(ASTextPosition *)position; + +/** + Returns a rectangle used to draw the caret at a given insertion point. + + @param position An object that identifies a location in the layout. + @return A rectangle that defines the area for drawing the caret. The width is + always zero in normal container, the height is always zero in vertical form container. + If not found, it returns CGRectNull. + */ +- (CGRect)caretRectForPosition:(ASTextPosition *)position; + +/** + Returns the first rectangle that encloses a range of text in the layout. + + @param range An object that represents a range of text in layout. + + @return The first rectangle in a range of text. You might use this rectangle to + draw a correction rectangle. The "first" in the name refers the rectangle + enclosing the first line when the range encompasses multiple lines of text. + If not found, it returns CGRectNull. + */ +- (CGRect)firstRectForRange:(ASTextRange *)range; + +/** + Returns the rectangle union that encloses a range of text in the layout. + + @param range An object that represents a range of text in layout. + + @return A rectangle that defines the area than encloses the range. + If not found, it returns CGRectNull. + */ +- (CGRect)rectForRange:(ASTextRange *)range; + +/** + Returns an array of selection rects corresponding to the range of text. + The start and end rect can be used to show grabber. + + @param range An object representing a range in text. + @return An array of `ASTextSelectionRect` objects that encompass the selection. + If not found, the array is empty. + */ +- (NSArray *)selectionRectsForRange:(ASTextRange *)range; + +/** + Returns an array of selection rects corresponding to the range of text. + + @param range An object representing a range in text. + @return An array of `ASTextSelectionRect` objects that encompass the selection. + If not found, the array is empty. + */ +- (NSArray *)selectionRectsWithoutStartAndEndForRange:(ASTextRange *)range; + +/** + Returns the start and end selection rects corresponding to the range of text. + The start and end rect can be used to show grabber. + + @param range An object representing a range in text. + @return An array of `ASTextSelectionRect` objects contains the start and end to + the selection. If not found, the array is empty. + */ +- (NSArray *)selectionRectsWithOnlyStartAndEndForRange:(ASTextRange *)range; + + +#pragma mark - Draw text layout +///============================================================================= +/// @name Draw text layout +///============================================================================= + +/** + Draw the layout and show the attachments. + + @discussion If the `view` parameter is not nil, then the attachment views will + add to this `view`, and if the `layer` parameter is not nil, then the attachment + layers will add to this `layer`. + + @warning This method should be called on main thread if `view` or `layer` parameter + is not nil and there's UIView or CALayer attachments in layout. + Otherwise, it can be called on any thread. + + @param context The draw context. Pass nil to avoid text and image drawing. + @param size The context size. + @param point The point at which to draw the layout. + @param view The attachment views will add to this view. + @param layer The attachment layers will add to this layer. + @param debug The debug option. Pass nil to avoid debug drawing. + @param cancel The cancel checker block. It will be called in drawing progress. + If it returns YES, the further draw progress will be canceled. + Pass nil to ignore this feature. + */ +- (void)drawInContext:(nullable CGContextRef)context + size:(CGSize)size + point:(CGPoint)point + view:(nullable UIView *)view + layer:(nullable CALayer *)layer + debug:(nullable ASTextDebugOption *)debug + cancel:(nullable BOOL (^)(void))cancel; + +/** + Draw the layout text and image (without view or layer attachments). + + @discussion This method is thread safe and can be called on any thread. + + @param context The draw context. Pass nil to avoid text and image drawing. + @param size The context size. + @param debug The debug option. Pass nil to avoid debug drawing. + */ +- (void)drawInContext:(nullable CGContextRef)context + size:(CGSize)size + debug:(nullable ASTextDebugOption *)debug; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/TextExperiment/Component/ASTextLayout.m b/Source/Private/TextExperiment/Component/ASTextLayout.m new file mode 100755 index 0000000000..88dbfaea6b --- /dev/null +++ b/Source/Private/TextExperiment/Component/ASTextLayout.m @@ -0,0 +1,3370 @@ +// +// ASTextLayout.m +// Modified from YYText +// +// Created by ibireme on 15/3/3. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import "ASTextLayout.h" +#import "ASTextUtilities.h" +#import "ASTextAttribute.h" +#import "NSAttributedString+ASText.h" +#import "ASInternalHelpers.h" + +const CGSize ASTextContainerMaxSize = (CGSize){0x100000, 0x100000}; + +typedef struct { + CGFloat head; + CGFloat foot; +} ASRowEdge; + +static inline CGSize ASTextClipCGSize(CGSize size) { + if (size.width > ASTextContainerMaxSize.width) size.width = ASTextContainerMaxSize.width; + if (size.height > ASTextContainerMaxSize.height) size.height = ASTextContainerMaxSize.height; + return size; +} + +static inline UIEdgeInsets UIEdgeInsetRotateVertical(UIEdgeInsets insets) { + UIEdgeInsets one; + one.top = insets.left; + one.left = insets.bottom; + one.bottom = insets.right; + one.right = insets.top; + return one; +} + +/** + Sometimes CoreText may convert CGColor to UIColor for `kCTForegroundColorAttributeName` + attribute in iOS7. This should be a bug of CoreText, and may cause crash. Here's a workaround. + */ +static CGColorRef ASTextGetCGColor(CGColorRef color) { + static UIColor *defaultColor; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + defaultColor = [UIColor blackColor]; + }); + if (!color) return defaultColor.CGColor; + if ([((__bridge NSObject *)color) respondsToSelector:@selector(CGColor)]) { + return ((__bridge UIColor *)color).CGColor; + } + return color; +} + +@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; + } + } +} + +- (id)copyWithZone:(NSZone *)zone { + ASTextLinePositionSimpleModifier *one = [self.class new]; + one.fixedLineHeight = _fixedLineHeight; + return one; +} +@end + + +@implementation ASTextContainer { + @package + BOOL _readonly; ///< used only in ASTextLayout.implementation + dispatch_semaphore_t _lock; + + CGSize _size; + UIEdgeInsets _insets; + UIBezierPath *_path; + NSArray *_exclusionPaths; + BOOL _pathFillEvenOdd; + CGFloat _pathLineWidth; + BOOL _verticalForm; + NSUInteger _maximumNumberOfRows; + ASTextTruncationType _truncationType; + NSAttributedString *_truncationToken; + id _linePositionModifier; +} + ++ (instancetype)containerWithSize:(CGSize)size { + return [self containerWithSize:size insets:UIEdgeInsetsZero]; +} + ++ (instancetype)containerWithSize:(CGSize)size insets:(UIEdgeInsets)insets { + ASTextContainer *one = [self new]; + one.size = ASTextClipCGSize(size); + one.insets = insets; + return one; +} + ++ (instancetype)containerWithPath:(UIBezierPath *)path { + ASTextContainer *one = [self new]; + one.path = path; + return one; +} + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + _lock = dispatch_semaphore_create(1); + _pathFillEvenOdd = YES; + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + ASTextContainer *one = [self.class new]; + dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); + one->_size = _size; + one->_insets = _insets; + one->_path = _path; + one->_exclusionPaths = _exclusionPaths.copy; + one->_pathFillEvenOdd = _pathFillEvenOdd; + one->_pathLineWidth = _pathLineWidth; + one->_verticalForm = _verticalForm; + one->_maximumNumberOfRows = _maximumNumberOfRows; + one->_truncationType = _truncationType; + one->_truncationToken = _truncationToken.copy; + one->_linePositionModifier = [(NSObject *)_linePositionModifier copy]; + dispatch_semaphore_signal(_lock); + return one; +} + +- (id)mutableCopyWithZone:(nullable NSZone *)zone { + return [self copyWithZone:zone]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:[NSValue valueWithCGSize:_size] forKey:@"size"]; + [aCoder encodeObject:[NSValue valueWithUIEdgeInsets:_insets] forKey:@"insets"]; + [aCoder encodeObject:_path forKey:@"path"]; + [aCoder encodeObject:_exclusionPaths forKey:@"exclusionPaths"]; + [aCoder encodeBool:_pathFillEvenOdd forKey:@"pathFillEvenOdd"]; + [aCoder encodeDouble:_pathLineWidth forKey:@"pathLineWidth"]; + [aCoder encodeBool:_verticalForm forKey:@"verticalForm"]; + [aCoder encodeInteger:_maximumNumberOfRows forKey:@"maximumNumberOfRows"]; + [aCoder encodeInteger:_truncationType forKey:@"truncationType"]; + [aCoder encodeObject:_truncationToken forKey:@"truncationToken"]; + if ([_linePositionModifier respondsToSelector:@selector(encodeWithCoder:)] && + [_linePositionModifier respondsToSelector:@selector(initWithCoder:)]) { + [aCoder encodeObject:_linePositionModifier forKey:@"linePositionModifier"]; + } +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [self init]; + _size = ((NSValue *)[aDecoder decodeObjectForKey:@"size"]).CGSizeValue; + _insets = ((NSValue *)[aDecoder decodeObjectForKey:@"insets"]).UIEdgeInsetsValue; + _path = [aDecoder decodeObjectForKey:@"path"]; + _exclusionPaths = [aDecoder decodeObjectForKey:@"exclusionPaths"]; + _pathFillEvenOdd = [aDecoder decodeBoolForKey:@"pathFillEvenOdd"]; + _pathLineWidth = [aDecoder decodeDoubleForKey:@"pathLineWidth"]; + _verticalForm = [aDecoder decodeBoolForKey:@"verticalForm"]; + _maximumNumberOfRows = [aDecoder decodeIntegerForKey:@"maximumNumberOfRows"]; + _truncationType = [aDecoder decodeIntegerForKey:@"truncationType"]; + _truncationToken = [aDecoder decodeObjectForKey:@"truncationToken"]; + _linePositionModifier = [aDecoder decodeObjectForKey:@"linePositionModifier"]; + return self; +} + +#define Getter(...) \ +dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \ +__VA_ARGS__; \ +dispatch_semaphore_signal(_lock); + +#define Setter(...) \ +if (_readonly) { \ +@throw [NSException exceptionWithName:NSInternalInconsistencyException \ +reason:@"Cannot change the property of the 'container' in 'ASTextLayout'." userInfo:nil]; \ +return; \ +} \ +dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \ +__VA_ARGS__; \ +dispatch_semaphore_signal(_lock); + +- (CGSize)size { + Getter(CGSize size = _size) return size; +} + +- (void)setSize:(CGSize)size { + Setter(if(!_path) _size = ASTextClipCGSize(size)); +} + +- (UIEdgeInsets)insets { + Getter(UIEdgeInsets insets = _insets) return insets; +} + +- (void)setInsets:(UIEdgeInsets)insets { + Setter(if(!_path){ + if (insets.top < 0) insets.top = 0; + if (insets.left < 0) insets.left = 0; + if (insets.bottom < 0) insets.bottom = 0; + if (insets.right < 0) insets.right = 0; + _insets = insets; + }); +} + +- (UIBezierPath *)path { + Getter(UIBezierPath *path = _path) return path; +} + +- (void)setPath:(UIBezierPath *)path { + Setter( + _path = path.copy; + if (_path) { + CGRect bounds = _path.bounds; + CGSize size = bounds.size; + UIEdgeInsets insets = UIEdgeInsetsZero; + if (bounds.origin.x < 0) size.width += bounds.origin.x; + if (bounds.origin.x > 0) insets.left = bounds.origin.x; + if (bounds.origin.y < 0) size.height += bounds.origin.y; + if (bounds.origin.y > 0) insets.top = bounds.origin.y; + _size = size; + _insets = insets; + } + ); +} + +- (NSArray *)exclusionPaths { + Getter(NSArray *paths = _exclusionPaths) return paths; +} + +- (void)setExclusionPaths:(NSArray *)exclusionPaths { + Setter(_exclusionPaths = exclusionPaths.copy); +} + +- (BOOL)isPathFillEvenOdd { + Getter(BOOL is = _pathFillEvenOdd) return is; +} + +- (void)setPathFillEvenOdd:(BOOL)pathFillEvenOdd { + Setter(_pathFillEvenOdd = pathFillEvenOdd); +} + +- (CGFloat)pathLineWidth { + Getter(CGFloat width = _pathLineWidth) return width; +} + +- (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; +} + +- (void)setMaximumNumberOfRows:(NSUInteger)maximumNumberOfRows { + Setter(_maximumNumberOfRows = maximumNumberOfRows); +} + +- (ASTextTruncationType)truncationType { + Getter(ASTextTruncationType type = _truncationType) return type; +} + +- (void)setTruncationType:(ASTextTruncationType)truncationType { + Setter(_truncationType = truncationType); +} + +- (NSAttributedString *)truncationToken { + Getter(NSAttributedString *token = _truncationToken) return token; +} + +- (void)setTruncationToken:(NSAttributedString *)truncationToken { + Setter(_truncationToken = truncationToken.copy); +} + +- (void)setLinePositionModifier:(id)linePositionModifier { + Setter(_linePositionModifier = [(NSObject *)linePositionModifier copy]); +} + +- (id)linePositionModifier { + Getter(id m = _linePositionModifier) return m; +} + +#undef Getter +#undef Setter +@end + + + + +@interface ASTextLayout () + +@property (nonatomic, readwrite) ASTextContainer *container; +@property (nonatomic, readwrite) NSAttributedString *text; +@property (nonatomic, readwrite) NSRange range; + +@property (nonatomic, readwrite) CTFramesetterRef frameSetter; +@property (nonatomic, readwrite) CTFrameRef frame; +@property (nonatomic, readwrite) NSArray *lines; +@property (nonatomic, readwrite) ASTextLine *truncatedLine; +@property (nonatomic, readwrite) NSArray *attachments; +@property (nonatomic, readwrite) NSArray *attachmentRanges; +@property (nonatomic, readwrite) NSArray *attachmentRects; +@property (nonatomic, readwrite) NSSet *attachmentContentsSet; +@property (nonatomic, readwrite) NSUInteger rowCount; +@property (nonatomic, readwrite) NSRange visibleRange; +@property (nonatomic, readwrite) CGRect textBoundingRect; +@property (nonatomic, readwrite) CGSize textBoundingSize; + +@property (nonatomic, readwrite) BOOL containsHighlight; +@property (nonatomic, readwrite) BOOL needDrawBlockBorder; +@property (nonatomic, readwrite) BOOL needDrawBackgroundBorder; +@property (nonatomic, readwrite) BOOL needDrawShadow; +@property (nonatomic, readwrite) BOOL needDrawUnderline; +@property (nonatomic, readwrite) BOOL needDrawText; +@property (nonatomic, readwrite) BOOL needDrawAttachment; +@property (nonatomic, readwrite) BOOL needDrawInnerShadow; +@property (nonatomic, readwrite) BOOL needDrawStrikethrough; +@property (nonatomic, readwrite) BOOL needDrawBorder; + +@property (nonatomic, assign) NSUInteger *lineRowsIndex; +@property (nonatomic, assign) ASRowEdge *lineRowsEdge; ///< top-left origin + +@end + + + +@implementation ASTextLayout + +#pragma mark - Layout + +- (instancetype)_init { + self = [super init]; + return self; +} + ++ (ASTextLayout *)layoutWithContainerSize:(CGSize)size text:(NSAttributedString *)text { + ASTextContainer *container = [ASTextContainer containerWithSize:size]; + return [self layoutWithContainer:container text:text]; +} + ++ (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text { + return [self layoutWithContainer:container text:text range:NSMakeRange(0, text.length)]; +} + ++ (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; + CGPoint *lineOrigins = NULL; + NSUInteger lineCount = 0; + NSMutableArray *lines = nil; + NSMutableArray *attachments = nil; + NSMutableArray *attachmentRanges = nil; + NSMutableArray *attachmentRects = nil; + NSMutableSet *attachmentContentsSet = nil; + BOOL needTruncation = NO; + NSAttributedString *truncationToken = nil; + ASTextLine *truncatedLine = nil; + ASRowEdge *lineRowsEdge = NULL; + NSUInteger *lineRowsIndex = NULL; + NSRange visibleRange; + NSUInteger maximumNumberOfRows = 0; + BOOL constraintSizeIsExtended = NO; + CGRect constraintRectBeforeExtended = {0}; + + text = text.mutableCopy; + container = container.copy; + if (!text || !container) return nil; + if (range.location + range.length > text.length) return nil; + container->_readonly = YES; + maximumNumberOfRows = container.maximumNumberOfRows; + + // CoreText bug when draw joined emoji since iOS 8.3. + // See -[NSMutableAttributedString setClearColorToJoinedEmoji] for more information. + static BOOL needFixJoinedEmojiBug = NO; + // It may use larger constraint size when create CTFrame with + // CTFramesetterCreateFrame in iOS 10. + static BOOL needFixLayoutSizeBug = NO; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + double systemVersionDouble = [UIDevice currentDevice].systemVersion.doubleValue; + if (8.3 <= systemVersionDouble && systemVersionDouble < 9) { + needFixJoinedEmojiBug = YES; + } + if (systemVersionDouble >= 10) { + needFixLayoutSizeBug = YES; + } + }); + if (needFixJoinedEmojiBug) { + [((NSMutableAttributedString *)text) as_setClearColorToJoinedEmoji]; + } + + layout = [[ASTextLayout alloc] _init]; + layout.text = text; + layout.container = container; + layout.range = range; + isVerticalForm = container.verticalForm; + + // set cgPath and cgPathBox + if (container.path == nil && container.exclusionPaths.count == 0) { + if (container.size.width <= 0 || container.size.height <= 0) goto fail; + CGRect rect = (CGRect) {CGPointZero, container.size }; + if (needFixLayoutSizeBug) { + 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 = UIEdgeInsetsInsetRect(rect, container.insets); + rect = CGRectStandardize(rect); + cgPathBox = rect; + rect = CGRectApplyAffineTransform(rect, CGAffineTransformMakeScale(1, -1)); + cgPath = CGPathCreateWithRect(rect, NULL); // let CGPathIsRect() returns true + } else if (container.path && CGPathIsRect(container.path.CGPath, &cgPathBox) && container.exclusionPaths.count == 0) { + CGRect rect = CGRectApplyAffineTransform(cgPathBox, CGAffineTransformMakeScale(1, -1)); + cgPath = CGPathCreateWithRect(rect, NULL); // let CGPathIsRect() returns true + } else { + rowMaySeparated = YES; + CGMutablePathRef path = NULL; + if (container.path) { + path = CGPathCreateMutableCopy(container.path.CGPath); + } else { + CGRect rect = (CGRect) {CGPointZero, container.size }; + rect = UIEdgeInsetsInsetRect(rect, container.insets); + CGPathRef rectPath = CGPathCreateWithRect(rect, NULL); + if (rectPath) { + path = CGPathCreateMutableCopy(rectPath); + CGPathRelease(rectPath); + } + } + if (path) { + [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); + CGPathRelease(path); + path = transPath; + } + cgPath = path; + } + if (!cgPath) goto fail; + + // frame setter config + frameAttrs = [NSMutableDictionary dictionary]; + 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); + } + + // create CoreText objects + ctSetter = CTFramesetterCreateWithAttributedString((CFTypeRef)text); + if (!ctSetter) goto fail; + ctFrame = CTFramesetterCreateFrame(ctSetter, ASTextCFRangeFromNSRange(range), cgPath, (CFTypeRef)frameAttrs); + if (!ctFrame) goto fail; + lines = [NSMutableArray new]; + ctLines = CTFrameGetLines(ctFrame); + lineCount = CFArrayGetCount(ctLines); + if (lineCount > 0) { + lineOrigins = malloc(lineCount * sizeof(CGPoint)); + if (lineOrigins == NULL) goto fail; + 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; + for (NSUInteger i = 0; i < lineCount; i++) { + CTLineRef ctLine = CFArrayGetValueAtIndex(ctLines, i); + CFArrayRef ctRuns = CTLineGetGlyphRuns(ctLine); + if (!ctRuns || CFArrayGetCount(ctRuns) == 0) continue; + + // CoreText coordinate system + CGPoint ctLineOrigin = lineOrigins[i]; + + // UIKit coordinate system + 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]; + CGRect rect = line.bounds; + + if (constraintSizeIsExtended) { + if (isVerticalForm) { + if (rect.origin.x + rect.size.width > + constraintRectBeforeExtended.origin.x + + constraintRectBeforeExtended.size.width) break; + } else { + if (rect.origin.y + rect.size.height > + constraintRectBeforeExtended.origin.y + + constraintRectBeforeExtended.size.height) break; + } + } + + BOOL newRow = YES; + if (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; + } + } 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 (newRow) rowIdx++; + lastRect = rect; + lastPosition = position; + + line.index = lineCurrentIdx; + line.row = rowIdx; + [lines addObject:line]; + rowCount = rowIdx + 1; + lineCurrentIdx ++; + + if (i == 0) textBoundingRect = rect; + else { + if (maximumNumberOfRows == 0 || rowIdx < maximumNumberOfRows) { + textBoundingRect = CGRectUnion(textBoundingRect, rect); + } + } + } + + if (rowCount > 0) { + if (maximumNumberOfRows > 0) { + if (rowCount > maximumNumberOfRows) { + needTruncation = YES; + rowCount = maximumNumberOfRows; + do { + ASTextLine *line = lines.lastObject; + if (!line) break; + if (line.row < rowCount) break; + [lines removeLastObject]; + } while (1); + } + } + ASTextLine *lastLine = lines.lastObject; + if (!needTruncation && lastLine.range.location + lastLine.range.length < text.length) { + needTruncation = YES; + } + + // Give user a chance to modify the line's position. + if (container.linePositionModifier) { + [container.linePositionModifier modifyLines:lines fromText:text inContainer:container]; + textBoundingRect = CGRectZero; + for (NSUInteger i = 0, max = lines.count; i < max; i++) { + ASTextLine *line = lines[i]; + if (i == 0) textBoundingRect = line.bounds; + else textBoundingRect = CGRectUnion(textBoundingRect, line.bounds); + } + } + + lineRowsEdge = calloc(rowCount, sizeof(ASRowEdge)); + if (lineRowsEdge == NULL) goto fail; + lineRowsIndex = calloc(rowCount, sizeof(NSUInteger)); + if (lineRowsIndex == NULL) goto fail; + NSInteger lastRowIdx = -1; + CGFloat lastHead = 0; + CGFloat lastFoot = 0; + for (NSUInteger i = 0, max = lines.count; i < max; i++) { + ASTextLine *line = lines[i]; + CGRect rect = line.bounds; + if ((NSInteger)line.row != lastRowIdx) { + if (lastRowIdx >= 0) { + lineRowsEdge[lastRowIdx] = (ASRowEdge) {.head = lastHead, .foot = lastFoot }; + } + 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; + } + } 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); + } + } + } + lineRowsEdge[lastRowIdx] = (ASRowEdge) {.head = lastHead, .foot = lastFoot }; + + for (NSUInteger i = 1; i < rowCount; i++) { + ASRowEdge v0 = lineRowsEdge[i - 1]; + ASRowEdge v1 = lineRowsEdge[i]; + lineRowsEdge[i - 1].foot = lineRowsEdge[i].head = (v0.foot + v1.head) * 0.5; + } + } + + { // 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; + + // create truncated line + if (container.truncationType != ASTextTruncationTypeNone) { + CTLineRef truncationTokenLine = NULL; + if (container.truncationToken) { + truncationToken = container.truncationToken; + truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)truncationToken); + } else { + CFArrayRef runs = CTLineGetGlyphRuns(lastLine.CTLine); + NSUInteger runCount = CFArrayGetCount(runs); + NSMutableDictionary *attrs = nil; + if (runCount > 0) { + CTRunRef run = CFArrayGetValueAtIndex(runs, runCount - 1); + attrs = (id)CTRunGetAttributes(run); + attrs = attrs ? attrs.mutableCopy : [NSMutableArray new]; + [attrs removeObjectsForKeys:[NSMutableAttributedString as_allDiscontinuousAttributeKeys]]; + CTFontRef font = (__bridge CFTypeRef)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); + } + CGColorRef color = (__bridge CGColorRef)(attrs[(id)kCTForegroundColorAttributeName]); + if (color && CFGetTypeID(color) == CGColorGetTypeID() && CGColorGetAlpha(color) == 0) { + // ignore clear color + [attrs removeObjectForKey:(id)kCTForegroundColorAttributeName]; + } + if (!attrs) attrs = [NSMutableDictionary new]; + } + truncationToken = [[NSAttributedString alloc] initWithString:ASTextTruncationToken attributes: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; + [lastLineText appendAttributedString:truncationToken]; + CTLineRef ctLastLineExtend = CTLineCreateWithAttributedString((CFAttributedStringRef)lastLineText); + if (ctLastLineExtend) { + CGFloat truncatedWidth = lastLine.width; + CGRect cgPathRect = CGRectZero; + if (CGPathIsRect(cgPath, &cgPathRect)) { + if (isVerticalForm) { + truncatedWidth = cgPathRect.size.height; + } else { + truncatedWidth = cgPathRect.size.width; + } + } + CTLineRef ctTruncatedLine = CTLineCreateTruncatedLine(ctLastLineExtend, truncatedWidth, type, truncationTokenLine); + CFRelease(ctLastLineExtend); + if (ctTruncatedLine) { + truncatedLine = [ASTextLine lineWithCTLine:ctTruncatedLine position:lastLine.position vertical:isVerticalForm]; + truncatedLine.index = lastLine.index; + truncatedLine.row = lastLine.row; + CFRelease(ctTruncatedLine); + } + } + 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 = 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 = 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]; + } + + } + }; + for (ASTextLine *line in lines) { + lineBlock(line); + } + if (truncatedLine) lineBlock(truncatedLine); + } + + if (visibleRange.length > 0) { + layout.needDrawText = YES; + + 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; + if (attrs[ASTextBackgroundBorderAttributeName]) layout.needDrawBackgroundBorder = YES; + if (attrs[ASTextShadowAttributeName] || attrs[NSShadowAttributeName]) layout.needDrawShadow = YES; + if (attrs[ASTextUnderlineAttributeName]) layout.needDrawUnderline = YES; + if (attrs[ASTextAttachmentAttributeName]) layout.needDrawAttachment = YES; + if (attrs[ASTextInnerShadowAttributeName]) layout.needDrawInnerShadow = YES; + if (attrs[ASTextStrikethroughAttributeName]) layout.needDrawStrikethrough = YES; + if (attrs[ASTextBorderAttributeName]) layout.needDrawBorder = YES; + }; + + [layout.text enumerateAttributesInRange:visibleRange options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:block]; + if (truncatedLine) { + [truncationToken enumerateAttributesInRange:NSMakeRange(0, truncationToken.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:block]; + } + } + + attachments = [NSMutableArray new]; + attachmentRanges = [NSMutableArray new]; + attachmentRects = [NSMutableArray new]; + attachmentContentsSet = [NSMutableSet new]; + for (NSUInteger i = 0, max = lines.count; i < max; i++) { + ASTextLine *line = lines[i]; + if (truncatedLine && line.index == truncatedLine.index) line = truncatedLine; + if (line.attachments.count > 0) { + [attachments addObjectsFromArray:line.attachments]; + [attachmentRanges addObjectsFromArray:line.attachmentRanges]; + [attachmentRects addObjectsFromArray:line.attachmentRects]; + for (ASTextAttachment *attachment in line.attachments) { + if (attachment.content) { + [attachmentContentsSet addObject:attachment.content]; + } + } + } + } + if (attachments.count == 0) { + attachments = attachmentRanges = attachmentRects = nil; + } + + layout.frameSetter = ctSetter; + layout.frame = ctFrame; + layout.lines = lines; + layout.truncatedLine = truncatedLine; + layout.attachments = attachments; + layout.attachmentRanges = attachmentRanges; + layout.attachmentRects = attachmentRects; + layout.attachmentContentsSet = attachmentContentsSet; + layout.rowCount = rowCount; + layout.visibleRange = visibleRange; + layout.textBoundingRect = textBoundingRect; + layout.textBoundingSize = textBoundingSize; + layout.lineRowsEdge = lineRowsEdge; + layout.lineRowsIndex = lineRowsIndex; + CFRelease(cgPath); + CFRelease(ctSetter); + CFRelease(ctFrame); + if (lineOrigins) free(lineOrigins); + return layout; + +fail: + if (cgPath) CFRelease(cgPath); + if (ctSetter) CFRelease(ctSetter); + if (ctFrame) CFRelease(ctFrame); + if (lineOrigins) free(lineOrigins); + if (lineRowsEdge) free(lineRowsEdge); + if (lineRowsIndex) free(lineRowsIndex); + return nil; +} + ++ (NSArray *)layoutWithContainers:(NSArray *)containers text:(NSAttributedString *)text { + return [self layoutWithContainers:containers text:text range:NSMakeRange(0, text.length)]; +} + ++ (NSArray *)layoutWithContainers:(NSArray *)containers text:(NSAttributedString *)text range:(NSRange)range { + if (!containers || !text) return nil; + if (range.location + range.length > text.length) return nil; + NSMutableArray *layouts = [NSMutableArray array]; + for (NSUInteger i = 0, max = containers.count; i < max; i++) { + ASTextContainer *container = containers[i]; + ASTextLayout *layout = [self layoutWithContainer:container text:text range:range]; + if (!layout) return nil; + NSInteger length = (NSInteger)range.length - (NSInteger)layout.visibleRange.length; + if (length <= 0) { + range.length = 0; + range.location = text.length; + } else { + range.length = length; + range.location += layout.visibleRange.length; + } + } + return layouts; +} + +- (void)setFrameSetter:(CTFramesetterRef)frameSetter { + if (_frameSetter != frameSetter) { + if (frameSetter) CFRetain(frameSetter); + if (_frameSetter) CFRelease(_frameSetter); + _frameSetter = frameSetter; + } +} + +- (void)setFrame:(CTFrameRef)frame { + if (_frame != frame) { + if (frame) CFRetain(frame); + if (_frame) CFRelease(_frame); + _frame = frame; + } +} + +- (void)dealloc { + if (_frameSetter) CFRelease(_frameSetter); + if (_frame) CFRelease(_frame); + if (_lineRowsIndex) free(_lineRowsIndex); + if (_lineRowsEdge) free(_lineRowsEdge); +} + +#pragma mark - Copying + +- (id)copyWithZone:(NSZone *)zone { + return self; // readonly object +} + + +#pragma mark - Query + +/** + Get the row index with 'edge' distance. + + @param edge The distance from edge to the point. + If vertical form, the edge is left edge, otherwise the edge is top edge. + + @return Returns NSNotFound if there's no row at the point. + */ +- (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)) { + rowIdx = mid; + break; + } + if ((isVertical ? (edge > oneEdge.head) : (edge < oneEdge.head))) { + if (mid == 0) break; + hi = mid - 1; + } else { + lo = mid + 1; + } + } + return rowIdx; +} + +/** + Get the closest row index with 'edge' distance. + + @param edge The distance from edge to the point. + If vertical form, the edge is left edge, otherwise the edge is top edge. + + @return Returns NSNotFound if there's no line. + */ +- (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; + } + } + } + return rowIdx; +} + +/** + Get a CTRun from a line position. + + @param line The text line. + @param position The position in the whole text. + + @return Returns NULL if not found (no CTRun at the position). + */ +- (CTRunRef)_runForLine:(ASTextLine *)line position:(ASTextPosition *)position { + if (!line || !position) return NULL; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger i = 0, max = CFArrayGetCount(runs); i < max; i++) { + CTRunRef run = CFArrayGetValueAtIndex(runs, i); + CFRange range = CTRunGetStringRange(run); + if (position.affinity == ASTextAffinityBackward) { + if (range.location < position.offset && position.offset <= range.location + range.length) { + return run; + } + } else { + if (range.location <= position.offset && position.offset < range.location + range.length) { + return run; + } + } + } + return NULL; +} + +/** + Whether the position is inside a composed character sequence. + + @param line The text line. + @param position Text text position in whole text. + @param block The block to be executed before returns YES. + left: left X offset + right: right X offset + prev: left position + next: right position + */ +- (BOOL)_insideComposedCharacterSequences:(ASTextLine *)line position:(NSUInteger)position block:(void (^)(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next))block { + NSRange range = line.range; + if (range.length == 0) return NO; + __block BOOL inside = NO; + __block NSUInteger _prev, _next; + [_text.string enumerateSubstringsInRange:range options:NSStringEnumerationByComposedCharacterSequences usingBlock: ^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) { + NSUInteger prev = substringRange.location; + NSUInteger next = substringRange.location + substringRange.length; + if (prev == position || next == position) { + *stop = YES; + } + if (prev < position && position < next) { + inside = YES; + _prev = prev; + _next = next; + *stop = YES; + } + }]; + if (inside && block) { + CGFloat left = [self offsetForTextPosition:_prev lineIndex:line.index]; + CGFloat right = [self offsetForTextPosition:_next lineIndex:line.index]; + block(left, right, _prev, _next); + } + return inside; +} + +/** + Whether the position is inside an emoji (such as National Flag Emoji). + + @param line The text line. + @param position Text text position in whole text. + @param block Yhe block to be executed before returns YES. + left: emoji's left X offset + right: emoji's right X offset + prev: emoji's left position + next: emoji's right position + */ +- (BOOL)_insideEmoji:(ASTextLine *)line position:(NSUInteger)position block:(void (^)(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next))block { + if (!line) return NO; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) { + CTRunRef run = CFArrayGetValueAtIndex(runs, r); + NSUInteger glyphCount = CTRunGetGlyphCount(run); + if (glyphCount == 0) continue; + CFRange range = CTRunGetStringRange(run); + if (range.length <= 1) continue; + if (position <= range.location || position >= range.location + range.length) continue; + CFDictionaryRef attrs = CTRunGetAttributes(run); + CTFontRef font = CFDictionaryGetValue(attrs, kCTFontAttributeName); + if (!ASTextCTFontContainsColorBitmapGlyphs(font)) continue; + + // Here's Emoji runs (larger than 1 unichar), and position is inside the range. + CFIndex indices[glyphCount]; + CTRunGetStringIndices(run, CFRangeMake(0, glyphCount), indices); + for (NSUInteger g = 0; g < glyphCount; g++) { + CFIndex prev = indices[g]; + CFIndex next = g + 1 < glyphCount ? indices[g + 1] : range.location + range.length; + if (position == prev) break; // Emoji edge + if (prev < position && position < next) { // inside an emoji (such as National Flag Emoji) + CGPoint pos = CGPointZero; + CGSize adv = CGSizeZero; + CTRunGetPositions(run, CFRangeMake(g, 1), &pos); + CTRunGetAdvances(run, CFRangeMake(g, 1), &adv); + if (block) { + block(line.position.x + pos.x, + line.position.x + pos.x + adv.width, + prev, next); + } + return YES; + } + } + } + return NO; +} +/** + Whether the write direction is RTL at the specified point + + @param line The text line + @param point The point in layout. + + @return YES if RTL. + */ +- (BOOL)_isRightToLeftInLine:(ASTextLine *)line atPoint:(CGPoint)point { + if (!line) return NO; + // get write direction + BOOL RTL = NO; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger r = 0, max = CFArrayGetCount(runs); r < max; r++) { + CTRunRef run = 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; + } + } + } + return RTL; +} + +/** + Correct the range's edge. + */ +- (ASTextRange *)_correctedRangeWithEdge:(ASTextRange *)range { + NSRange visibleRange = self.visibleRange; + ASTextPosition *start = range.start; + ASTextPosition *end = range.end; + + if (start.offset == visibleRange.location && start.affinity == ASTextAffinityBackward) { + start = [ASTextPosition positionWithOffset:start.offset affinity:ASTextAffinityForward]; + } + + if (end.offset == visibleRange.location + visibleRange.length && start.affinity == ASTextAffinityForward) { + end = [ASTextPosition positionWithOffset:end.offset affinity:ASTextAffinityBackward]; + } + + if (start != range.start || end != range.end) { + range = [ASTextRange rangeWithStart:start end:end]; + } + return range; +} + +- (NSUInteger)lineIndexForRow:(NSUInteger)row { + if (row >= _rowCount) return NSNotFound; + return _lineRowsIndex[row]; +} + +- (NSUInteger)lineCountForRow:(NSUInteger)row { + if (row >= _rowCount) return NSNotFound; + if (row == _rowCount - 1) { + return _lines.count - _lineRowsIndex[row]; + } else { + return _lineRowsIndex[row + 1] - _lineRowsIndex[row]; + } +} + +- (NSUInteger)rowIndexForLine:(NSUInteger)line { + if (line >= _lines.count) return NSNotFound; + return ((ASTextLine *)_lines[line]).row; +} + +- (NSUInteger)lineIndexForPoint:(CGPoint)point { + if (_lines.count == 0 || _rowCount == 0) return NSNotFound; + NSUInteger rowIdx = [self _rowIndexForEdge:_container.verticalForm ? point.x : point.y]; + if (rowIdx == NSNotFound) return NSNotFound; + + NSUInteger lineIdx0 = _lineRowsIndex[rowIdx]; + NSUInteger lineIdx1 = rowIdx == _rowCount - 1 ? _lines.count - 1 : _lineRowsIndex[rowIdx + 1] - 1; + for (NSUInteger i = lineIdx0; i <= lineIdx1; i++) { + CGRect bounds = ((ASTextLine *)_lines[i]).bounds; + if (CGRectContainsPoint(bounds, point)) return i; + } + + return NSNotFound; +} + +- (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]; + if (rowIdx == NSNotFound) return NSNotFound; + + NSUInteger lineIdx0 = _lineRowsIndex[rowIdx]; + NSUInteger lineIdx1 = rowIdx == _rowCount - 1 ? _lines.count - 1 : _lineRowsIndex[rowIdx + 1] - 1; + if (lineIdx0 == lineIdx1) return lineIdx0; + + CGFloat minDistance = CGFLOAT_MAX; + 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; + } + } 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; + } + } + } + return minIndex; +} + +- (CGFloat)offsetForTextPosition:(NSUInteger)position lineIndex:(NSUInteger)lineIndex { + if (lineIndex >= _lines.count) return CGFLOAT_MAX; + ASTextLine *line = _lines[lineIndex]; + CFRange range = CTLineGetStringRange(line.CTLine); + 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); +} + +- (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; + } + CFIndex idx = CTLineGetStringIndexForPosition(line.CTLine, point); + if (idx == kCFNotFound) return NSNotFound; + + /* + If the emoji contains one or more variant form (such as โ˜”๏ธ "\u2614\uFE0F") + and the font size is smaller than 379/15, then each variant form ("\uFE0F") + will rendered as a single blank glyph behind the emoji glyph. Maybe it's a + bug in CoreText? Seems iOS8.3 fixes this problem. + + If the point hit the blank glyph, the CTLineGetStringIndexForPosition() + returns the position before the emoji glyph, but it should returns the + position after the emoji and variant form. + + Here's a workaround. + */ + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger r = 0, max = CFArrayGetCount(runs); r < max; r++) { + CTRunRef run = CFArrayGetValueAtIndex(runs, r); + CFRange range = CTRunGetStringRange(run); + if (range.location <= idx && idx < range.location + range.length) { + NSUInteger glyphCount = CTRunGetGlyphCount(run); + if (glyphCount == 0) break; + CFDictionaryRef attrs = CTRunGetAttributes(run); + CTFontRef font = CFDictionaryGetValue(attrs, kCTFontAttributeName); + if (!ASTextCTFontContainsColorBitmapGlyphs(font)) break; + + CFIndex indices[glyphCount]; + CGPoint positions[glyphCount]; + CTRunGetStringIndices(run, CFRangeMake(0, glyphCount), indices); + CTRunGetPositions(run, CFRangeMake(0, glyphCount), positions); + for (NSUInteger g = 0; g < glyphCount; g++) { + NSUInteger gIdx = indices[g]; + if (gIdx == idx && g + 1 < glyphCount) { + CGFloat right = positions[g + 1].x; + if (point.x < right) break; + NSUInteger next = indices[g + 1]; + do { + if (next == range.location + range.length) break; + unichar c = [_text.string characterAtIndex:next]; + if ((c == 0xFE0E || c == 0xFE0F)) { // unicode variant form for emoji style + next++; + } else break; + } + while (1); + if (next != indices[g + 1]) idx = next; + break; + } + } + break; + } + } + return idx; +} + +- (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; + + NSUInteger lineIndex = [self closestLineIndexForPoint:point]; + if (lineIndex == NSNotFound) return nil; + ASTextLine *line = _lines[lineIndex]; + __block NSUInteger position = [self textPositionForPoint:point lineIndex:lineIndex]; + if (position == NSNotFound) position = line.range.location; + if (position <= _visibleRange.location) { + return [ASTextPosition positionWithOffset:_visibleRange.location affinity:ASTextAffinityForward]; + } else if (position >= _visibleRange.location + _visibleRange.length) { + return [ASTextPosition positionWithOffset:_visibleRange.location + _visibleRange.length affinity:ASTextAffinityBackward]; + } + + ASTextAffinity finalAffinity = ASTextAffinityForward; + BOOL finalAffinityDetected = NO; + + // binding range + NSRange bindingRange; + ASTextBinding *binding = [_text attribute:ASTextBindingAttributeName atIndex:position longestEffectiveRange:&bindingRange inRange:NSMakeRange(0, _text.length)]; + if (binding && bindingRange.length > 0) { + NSUInteger headLineIdx = [self lineIndexForPosition:[ASTextPosition positionWithOffset:bindingRange.location]]; + NSUInteger tailLineIdx = [self lineIndexForPosition:[ASTextPosition positionWithOffset:bindingRange.location + bindingRange.length affinity:ASTextAffinityBackward]]; + if (headLineIdx == lineIndex && lineIndex == tailLineIdx) { // all in same line + 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; + } + } else { + if (fabs(point.x - left) < fabs(point.x - right)) { + position = bindingRange.location; + finalAffinity = ASTextAffinityForward; + } else { + position = bindingRange.location + bindingRange.length; + finalAffinity = ASTextAffinityBackward; + } + } + } else if (left != CGFLOAT_MAX) { + position = left; + finalAffinity = ASTextAffinityForward; + } else if (right != CGFLOAT_MAX) { + position = right; + finalAffinity = ASTextAffinityBackward; + } + finalAffinityDetected = YES; + } else if (headLineIdx == lineIndex) { + CGFloat left = [self offsetForTextPosition:bindingRange.location lineIndex:lineIndex]; + if (left != CGFLOAT_MAX) { + position = bindingRange.location; + finalAffinity = ASTextAffinityForward; + finalAffinityDetected = YES; + } + } else if (tailLineIdx == lineIndex) { + CGFloat right = [self offsetForTextPosition:bindingRange.location + bindingRange.length lineIndex:lineIndex]; + if (right != CGFLOAT_MAX) { + position = bindingRange.location + bindingRange.length; + finalAffinity = ASTextAffinityBackward; + finalAffinityDetected = YES; + } + } else { + BOOL onLeft = NO, onRight = NO; + if (headLineIdx != NSNotFound && tailLineIdx != NSNotFound) { + if (abs((int)headLineIdx - (int)lineIndex) < abs((int)tailLineIdx - (int)lineIndex)) onLeft = YES; + else onRight = YES; + } else if (headLineIdx != NSNotFound) { + onLeft = YES; + } else if (tailLineIdx != NSNotFound) { + onRight = YES; + } + + if (onLeft) { + CGFloat left = [self offsetForTextPosition:bindingRange.location lineIndex:headLineIdx]; + if (left != CGFLOAT_MAX) { + lineIndex = headLineIdx; + line = _lines[headLineIdx]; + position = bindingRange.location; + finalAffinity = ASTextAffinityForward; + finalAffinityDetected = YES; + } + } else if (onRight) { + CGFloat right = [self offsetForTextPosition:bindingRange.location + bindingRange.length lineIndex:tailLineIdx]; + if (right != CGFLOAT_MAX) { + lineIndex = tailLineIdx; + line = _lines[tailLineIdx]; + position = bindingRange.location + bindingRange.length; + finalAffinity = ASTextAffinityBackward; + finalAffinityDetected = YES; + } + } + } + } + + // empty line + if (line.range.length == 0) { + BOOL behind = (_lines.count > 1 && lineIndex == _lines.count - 1); //end line + return [ASTextPosition positionWithOffset:line.range.location affinity:behind ? ASTextAffinityBackward:ASTextAffinityForward]; + } + + // detect weather the line is a linebreak token + if (line.range.length <= 2) { + NSString *str = [_text.string substringWithRange:line.range]; + if (ASTextIsLinebreakString(str)) { // an empty line ("\r", "\n", "\r\n") + return [ASTextPosition positionWithOffset:line.range.location]; + } + } + + // above whole text frame + if (lineIndex == 0 && (isVertical ? (point.x > line.right) : (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))) { + position = line.range.location + line.range.length; + finalAffinity = ASTextAffinityBackward; + finalAffinityDetected = YES; + } + + // There must be at least one non-linebreak char, + // ignore the linebreak characters at line end if exists. + if (position >= line.range.location + line.range.length - 1) { + if (position > line.range.location) { + unichar c1 = [_text.string characterAtIndex:position - 1]; + if (ASTextIsLinebreakChar(c1)) { + position--; + if (position > line.range.location) { + unichar c0 = [_text.string characterAtIndex:position - 1]; + if (ASTextIsLinebreakChar(c0)) { + position--; + } + } + } + } + } + if (position == line.range.location) { + return [ASTextPosition positionWithOffset:position]; + } + if (position == line.range.location + line.range.length) { + return [ASTextPosition positionWithOffset:position affinity:ASTextAffinityBackward]; + } + + [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); + } + }]; + + [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); + } + }]; + + if (position < _visibleRange.location) position = _visibleRange.location; + else if (position > _visibleRange.location + _visibleRange.length) position = _visibleRange.location + _visibleRange.length; + + if (!finalAffinityDetected) { + CGFloat ofs = [self offsetForTextPosition:position lineIndex:lineIndex]; + if (ofs != CGFLOAT_MAX) { + BOOL RTL = [self _isRightToLeftInLine:line atPoint:point]; + if (position >= line.range.location + line.range.length) { + finalAffinity = RTL ? ASTextAffinityForward : ASTextAffinityBackward; + } else if (position <= line.range.location) { + finalAffinity = RTL ? ASTextAffinityBackward : ASTextAffinityForward; + } else { + finalAffinity = (ofs < (isVertical ? point.y : point.x) && !RTL) ? ASTextAffinityForward : ASTextAffinityBackward; + } + } + } + + return [ASTextPosition positionWithOffset:position affinity:finalAffinity]; +} + +- (ASTextPosition *)positionForPoint:(CGPoint)point + oldPosition:(ASTextPosition *)oldPosition + otherPosition:(ASTextPosition *)otherPosition { + if (!oldPosition || !otherPosition) { + return oldPosition; + } + ASTextPosition *newPos = [self closestPositionToPoint:point]; + if (!newPos) return oldPosition; + if ([newPos compare:otherPosition] == [oldPosition compare:otherPosition] && + newPos.offset != otherPosition.offset) { + return newPos; + } + NSUInteger lineIndex = [self lineIndexForPosition:otherPosition]; + 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; + } + 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; + } + } + + return oldPosition; +} + +- (ASTextRange *)textRangeAtPoint:(CGPoint)point { + NSUInteger lineIndex = [self lineIndexForPoint:point]; + if (lineIndex == NSNotFound) return nil; + NSUInteger textPosition = [self textPositionForPoint:point lineIndex:[self lineIndexForPoint:point]]; + if (textPosition == NSNotFound) return nil; + ASTextPosition *pos = [self closestPositionToPoint:point]; + if (!pos) return nil; + + // get write direction + 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 *)closestTextRangeAtPoint:(CGPoint)point { + ASTextPosition *pos = [self closestPositionToPoint:point]; + if (!pos) return nil; + NSUInteger lineIndex = [self lineIndexForPosition:pos]; + if (lineIndex == NSNotFound) return nil; + ASTextLine *line = _lines[lineIndex]; + BOOL RTL = [self _isRightToLeftInLine:line atPoint:point]; + CGRect rect = [self caretRectForPosition:pos]; + if (CGRectIsNull(rect)) return nil; + + UITextLayoutDirection direction = UITextLayoutDirectionRight; + if (pos.offset >= line.range.location + line.range.length) { + if (direction != RTL) { + direction = _container.verticalForm ? UITextLayoutDirectionUp : UITextLayoutDirectionLeft; + } else { + direction = _container.verticalForm ? UITextLayoutDirectionDown : UITextLayoutDirectionRight; + } + } else if (pos.offset <= line.range.location) { + if (direction != RTL) { + direction = _container.verticalForm ? UITextLayoutDirectionDown : UITextLayoutDirectionRight; + } else { + direction = _container.verticalForm ? UITextLayoutDirectionUp : UITextLayoutDirectionLeft; + } + } else { + if (_container.verticalForm) { + direction = (rect.origin.y >= point.y && !RTL) ? UITextLayoutDirectionUp:UITextLayoutDirectionDown; + } else { + direction = (rect.origin.x >= point.x && !RTL) ? UITextLayoutDirectionLeft:UITextLayoutDirectionRight; + } + } + + ASTextRange *range = [self textRangeByExtendingPosition:pos inDirection:direction offset:1]; + return range; +} + +- (ASTextRange *)textRangeByExtendingPosition:(ASTextPosition *)position { + NSUInteger visibleStart = _visibleRange.location; + NSUInteger visibleEnd = _visibleRange.location + _visibleRange.length; + + if (!position) return nil; + if (position.offset < visibleStart || position.offset > visibleEnd) return nil; + + // head or tail, returns immediately + if (position.offset == visibleStart) { + return [ASTextRange rangeWithRange:NSMakeRange(position.offset, 0)]; + } else if (position.offset == visibleEnd) { + return [ASTextRange rangeWithRange:NSMakeRange(position.offset, 0) affinity:ASTextAffinityBackward]; + } + + // binding range + NSRange tRange; + ASTextBinding *binding = [_text attribute:ASTextBindingAttributeName atIndex:position.offset longestEffectiveRange:&tRange inRange:_visibleRange]; + if (binding && tRange.length > 0 && tRange.location < position.offset) { + return [ASTextRange rangeWithRange:tRange]; + } + + // inside emoji or composed character sequences + NSUInteger lineIndex = [self lineIndexForPosition:position]; + if (lineIndex != NSNotFound) { + __block NSUInteger _prev, _next; + BOOL emoji = NO, seq = NO; + + ASTextLine *line = _lines[lineIndex]; + emoji = [self _insideEmoji:line position:position.offset block: ^(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next) { + _prev = prev; + _next = next; + }]; + if (!emoji) { + seq = [self _insideComposedCharacterSequences:line position:position.offset block: ^(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next) { + _prev = prev; + _next = next; + }]; + } + if (emoji || seq) { + return [ASTextRange rangeWithRange:NSMakeRange(_prev, _next - _prev)]; + } + } + + // inside linebreak '\r\n' + if (position.offset > visibleStart && position.offset < visibleEnd) { + unichar c0 = [_text.string characterAtIndex:position.offset - 1]; + if ((c0 == '\r') && position.offset < visibleEnd) { + unichar c1 = [_text.string characterAtIndex:position.offset]; + if (c1 == '\n') { + return [ASTextRange rangeWithStart:[ASTextPosition positionWithOffset:position.offset - 1] end:[ASTextPosition positionWithOffset:position.offset + 1]]; + } + } + if (ASTextIsLinebreakChar(c0) && position.affinity == ASTextAffinityBackward) { + NSString *str = [_text.string substringToIndex:position.offset]; + NSUInteger len = ASTextLinebreakTailLength(str); + return [ASTextRange rangeWithStart:[ASTextPosition positionWithOffset:position.offset - len] end:[ASTextPosition positionWithOffset:position.offset]]; + } + } + + return [ASTextRange rangeWithRange:NSMakeRange(position.offset, 0) affinity:position.affinity]; +} + +- (ASTextRange *)textRangeByExtendingPosition:(ASTextPosition *)position + inDirection:(UITextLayoutDirection)direction + offset:(NSInteger)offset { + NSInteger visibleStart = _visibleRange.location; + NSInteger visibleEnd = _visibleRange.location + _visibleRange.length; + + 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; + } + + if (offset < 0) { + forwardMove = !forwardMove; + offset = -offset; + } + + // head or tail, returns immediately + if (!forwardMove && position.offset == visibleStart) { + return [ASTextRange rangeWithRange:NSMakeRange(_visibleRange.location, 0)]; + } else if (forwardMove && position.offset == visibleEnd) { + return [ASTextRange rangeWithRange:NSMakeRange(position.offset, 0) affinity:ASTextAffinityBackward]; + } + + // extend from position + ASTextRange *fromRange = [self textRangeByExtendingPosition:position]; + if (!fromRange) return nil; + ASTextRange *allForward = [ASTextRange rangeWithStart:fromRange.start end:[ASTextPosition positionWithOffset:visibleEnd]]; + ASTextRange *allBackward = [ASTextRange rangeWithStart:[ASTextPosition positionWithOffset:visibleStart] end:fromRange.end]; + + if (verticalMove) { // up/down in text layout + NSInteger lineIndex = [self lineIndexForPosition:position]; + if (lineIndex == NSNotFound) return nil; + + ASTextLine *line = _lines[lineIndex]; + NSInteger moveToRowIndex = (NSInteger)line.row + (forwardMove ? offset : -offset); + if (moveToRowIndex < 0) return allBackward; + else if (moveToRowIndex >= (NSInteger)_rowCount) return allForward; + + CGFloat ofs = [self offsetForTextPosition:position.offset lineIndex:lineIndex]; + if (ofs == CGFLOAT_MAX) return nil; + + NSUInteger moveToLineFirstIndex = [self lineIndexForRow:moveToRowIndex]; + NSUInteger moveToLineCount = [self lineCountForRow:moveToRowIndex]; + if (moveToLineFirstIndex == NSNotFound || moveToLineCount == NSNotFound || moveToLineCount == 0) return nil; + CGFloat mostLeft = CGFLOAT_MAX, mostRight = -CGFLOAT_MAX; + ASTextLine *mostLeftLine = nil, *mostRightLine = nil; + NSUInteger insideIndex = NSNotFound; + 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; + } + } + } + BOOL afinityEdge = NO; + if (insideIndex == NSNotFound) { + if (ofs <= mostLeft) { + insideIndex = mostLeftLine.index; + } else { + insideIndex = mostRightLine.index; + } + 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]; + } + if (pos == NSNotFound) return nil; + ASTextPosition *extPos; + if (afinityEdge) { + if (pos == insideLine.range.location + insideLine.range.length) { + NSString *subStr = [_text.string substringWithRange:insideLine.range]; + NSUInteger lineBreakLen = ASTextLinebreakTailLength(subStr); + extPos = [ASTextPosition positionWithOffset:pos - lineBreakLen]; + } else { + extPos = [ASTextPosition positionWithOffset:pos]; + } + } else { + extPos = [ASTextPosition positionWithOffset:pos]; + } + ASTextRange *ext = [self textRangeByExtendingPosition:extPos]; + if (!ext) return nil; + if (forwardMove) { + return [ASTextRange rangeWithStart:fromRange.start end:ext.end]; + } else { + return [ASTextRange rangeWithStart:ext.start end:fromRange.end]; + } + + } else { // left/right in text layout + ASTextPosition *toPosition = [ASTextPosition positionWithOffset:position.offset + (forwardMove ? offset : -offset)]; + if (toPosition.offset <= visibleStart) return allBackward; + else if (toPosition.offset >= visibleEnd) return allForward; + + ASTextRange *toRange = [self textRangeByExtendingPosition:toPosition]; + if (!toRange) return nil; + + NSInteger start = MIN(fromRange.start.offset, toRange.start.offset); + NSInteger end = MAX(fromRange.end.offset, toRange.end.offset); + return [ASTextRange rangeWithRange:NSMakeRange(start, end - start)]; + } +} + +- (NSUInteger)lineIndexForPosition:(ASTextPosition *)position { + if (!position) return NSNotFound; + if (_lines.count == 0) return NSNotFound; + NSUInteger location = position.offset; + NSInteger lo = 0, hi = _lines.count - 1, mid = 0; + if (position.affinity == ASTextAffinityBackward) { + while (lo <= hi) { + mid = (lo + hi) / 2; + ASTextLine *line = _lines[mid]; + NSRange range = line.range; + if (range.location < location && location <= range.location + range.length) { + return mid; + } + if (location <= range.location) { + hi = mid - 1; + } else { + lo = mid + 1; + } + } + } else { + while (lo <= hi) { + mid = (lo + hi) / 2; + ASTextLine *line = _lines[mid]; + NSRange range = line.range; + if (range.location <= location && location < range.location + range.length) { + return mid; + } + if (location < range.location) { + hi = mid - 1; + } else { + lo = mid + 1; + } + } + } + return NSNotFound; +} + +- (CGPoint)linePositionForPosition:(ASTextPosition *)position { + NSUInteger lineIndex = [self lineIndexForPosition:position]; + if (lineIndex == NSNotFound) return CGPointZero; + 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); + } +} + +- (CGRect)caretRectForPosition:(ASTextPosition *)position { + NSUInteger lineIndex = [self lineIndexForPosition:position]; + if (lineIndex == NSNotFound) return CGRectNull; + 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); + } +} + +- (CGRect)firstRectForRange:(ASTextRange *)range { + range = [self _correctedRangeWithEdge:range]; + + NSUInteger startLineIndex = [self lineIndexForPosition:range.start]; + NSUInteger endLineIndex = [self lineIndexForPosition:range.end]; + if (startLineIndex == NSNotFound || endLineIndex == NSNotFound) return CGRectNull; + if (startLineIndex > endLineIndex) return CGRectNull; + ASTextLine *startLine = _lines[startLineIndex]; + ASTextLine *endLine = _lines[endLineIndex]; + NSMutableArray *lines = [NSMutableArray new]; + for (NSUInteger i = startLineIndex; i <= startLineIndex; i++) { + ASTextLine *line = _lines[i]; + 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); + } 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; + } + } 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; + } + } +} + +- (CGRect)rectForRange:(ASTextRange *)range { + NSArray *rects = [self selectionRectsForRange:range]; + if (rects.count == 0) return CGRectNull; + CGRect rectUnion = ((ASTextSelectionRect *)rects.firstObject).rect; + for (NSUInteger i = 1; i < rects.count; i++) { + ASTextSelectionRect *rect = rects[i]; + rectUnion = CGRectUnion(rectUnion, rect.rect); + } + return rectUnion; +} + +- (NSArray *)selectionRectsForRange:(ASTextRange *)range { + range = [self _correctedRangeWithEdge:range]; + + BOOL isVertical = _container.verticalForm; + NSMutableArray *rects = [NSMutableArray array]; + if (!range) return rects; + + NSUInteger startLineIndex = [self lineIndexForPosition:range.start]; + NSUInteger endLineIndex = [self lineIndexForPosition:range.end]; + if (startLineIndex == NSNotFound || endLineIndex == NSNotFound) return rects; + if (startLineIndex > endLineIndex) ASTEXT_SWAP(startLineIndex, endLineIndex); + ASTextLine *startLine = _lines[startLineIndex]; + ASTextLine *endLine = _lines[endLineIndex]; + CGFloat offsetStart = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; + 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.containsStart = YES; + start.isVertical = isVertical; + [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.containsEnd = YES; + end.isVertical = isVertical; + [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; + [rects addObject:rect]; + + } else { // more than one row + + // start line select rect + ASTextSelectionRect *topRect = [ASTextSelectionRect new]; + topRect.isVertical = isVertical; + 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.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 { + topRect.rect = CGRectMake(topOffset, startLine.top, (_container.path ? startLine.right : _container.size.width - _container.insets.right) - topOffset, startLine.height); + } + } + [rects addObject:topRect]; + + // end line select rect + ASTextSelectionRect *bottomRect = [ASTextSelectionRect new]; + bottomRect.isVertical = isVertical; + 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.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); + } + } + [rects addObject:bottomRect]; + + if (endLineIndex - startLineIndex >= 2) { + CGRect r = CGRectZero; + BOOL startLineDetected = NO; + for (NSUInteger l = startLineIndex + 1; l < endLineIndex; l++) { + ASTextLine *line = _lines[l]; + if (line.row == startLine.row || line.row == endLine.row) continue; + if (!startLineDetected) { + r = line.bounds; + startLineDetected = YES; + } else { + r = CGRectUnion(r, line.bounds); + } + } + 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; + } + + ASTextSelectionRect *rect = [ASTextSelectionRect new]; + rect.rect = r; + rect.isVertical = isVertical; + [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; + } + } + } + return rects; +} + +- (NSArray *)selectionRectsWithoutStartAndEndForRange:(ASTextRange *)range { + NSMutableArray *rects = [self selectionRectsForRange:range].mutableCopy; + for (NSInteger i = 0, max = rects.count; i < max; i++) { + ASTextSelectionRect *rect = rects[i]; + if (rect.containsStart || rect.containsEnd) { + [rects removeObjectAtIndex:i]; + i--; + max--; + } + } + return rects; +} + +- (NSArray *)selectionRectsWithOnlyStartAndEndForRange:(ASTextRange *)range { + NSMutableArray *rects = [self selectionRectsForRange:range].mutableCopy; + for (NSInteger i = 0, max = rects.count; i < max; i++) { + ASTextSelectionRect *rect = rects[i]; + if (!rect.containsStart && !rect.containsEnd) { + [rects removeObjectAtIndex:i]; + i--; + max--; + } + } + return rects; +} + + +#pragma mark - Draw + + +typedef NS_OPTIONS(NSUInteger, ASTextDecorationType) { + ASTextDecorationTypeUnderline = 1 << 0, + ASTextDecorationTypeStrikethrough = 1 << 1, +}; + +typedef NS_OPTIONS(NSUInteger, ASTextBorderType) { + ASTextBorderTypeBackgound = 1 << 0, + 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 void ASTextGetRunsMaxMetric(CFArrayRef runs, CGFloat *xHeight, CGFloat *underlinePosition, CGFloat *lineThickness) { + CGFloat maxXHeight = 0; + CGFloat maxUnderlinePos = 0; + CGFloat maxLineThickness = 0; + for (NSUInteger i = 0, max = CFArrayGetCount(runs); i < max; i++) { + CTRunRef run = CFArrayGetValueAtIndex(runs, i); + CFDictionaryRef attrs = CTRunGetAttributes(run); + if (attrs) { + CTFontRef font = CFDictionaryGetValue(attrs, kCTFontAttributeName); + if (font) { + CGFloat xHeight = CTFontGetXHeight(font); + if (xHeight > maxXHeight) maxXHeight = xHeight; + CGFloat underlinePos = CTFontGetUnderlinePosition(font); + if (underlinePos < maxUnderlinePos) maxUnderlinePos = underlinePos; + CGFloat lineThickness = CTFontGetUnderlineThickness(font); + if (lineThickness > maxLineThickness) maxLineThickness = lineThickness; + } + } + } + if (xHeight) *xHeight = maxXHeight; + if (underlinePosition) *underlinePosition = maxUnderlinePos; + if (lineThickness) *lineThickness = maxLineThickness; +} + +static void ASTextDrawRun(ASTextLine *line, CTRunRef run, CGContextRef context, CGSize size, BOOL isVertical, NSArray *runRanges, CGFloat verticalOffset) { + CGAffineTransform runTextMatrix = CTRunGetTextMatrix(run); + BOOL runTextMatrixIsID = CGAffineTransformIsIdentity(runTextMatrix); + + CFDictionaryRef runAttrs = CTRunGetAttributes(run); + NSValue *glyphTransformValue = CFDictionaryGetValue(runAttrs, (__bridge const void *)(ASTextGlyphTransformAttributeName)); + if (!isVertical && !glyphTransformValue) { // draw run + if (!runTextMatrixIsID) { + CGContextSaveGState(context); + CGAffineTransform trans = CGContextGetTextMatrix(context); + CGContextSetTextMatrix(context, CGAffineTransformConcat(trans, runTextMatrix)); + } + CTRunDraw(run, context, CFRangeMake(0, 0)); + if (!runTextMatrixIsID) { + CGContextRestoreGState(context); + } + } else { // draw glyph + CTFontRef runFont = CFDictionaryGetValue(runAttrs, kCTFontAttributeName); + if (!runFont) return; + NSUInteger glyphCount = CTRunGetGlyphCount(run); + if (glyphCount <= 0) return; + + CGGlyph glyphs[glyphCount]; + CGPoint glyphPositions[glyphCount]; + CTRunGetGlyphs(run, CFRangeMake(0, 0), glyphs); + CTRunGetPositions(run, CFRangeMake(0, 0), glyphPositions); + + CGColorRef fillColor = (CGColorRef)CFDictionaryGetValue(runAttrs, kCTForegroundColorAttributeName); + fillColor = ASTextGetCGColor(fillColor); + NSNumber *strokeWidth = CFDictionaryGetValue(runAttrs, kCTStrokeWidthAttributeName); + + CGContextSaveGState(context); { + CGContextSetFillColorWithColor(context, fillColor); + if (!strokeWidth || strokeWidth.floatValue == 0) { + CGContextSetTextDrawingMode(context, kCGTextFill); + } else { + CGColorRef strokeColor = (CGColorRef)CFDictionaryGetValue(runAttrs, kCTStrokeColorAttributeName); + if (!strokeColor) strokeColor = fillColor; + CGContextSetStrokeColorWithColor(context, strokeColor); + CGContextSetLineWidth(context, CTFontGetSize(runFont) * fabs(strokeWidth.floatValue * 0.01)); + if (strokeWidth.floatValue > 0) { + CGContextSetTextDrawingMode(context, kCGTextStroke); + } else { + CGContextSetTextDrawingMode(context, kCGTextFillStroke); + } + } + + if (isVertical) { + 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 = 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); + } + } + } 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 { + CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL); + CGContextSetFont(context, cgFont); + CGContextSetFontSize(context, CTFontGetSize(runFont)); + CGContextShowGlyphsAtPositions(context, glyphs, glyphPositions, glyphCount); + CGFontRelease(cgFont); + } + } + } + + } CGContextRestoreGState(context); + } +} + +static void ASTextSetLinePatternInContext(ASTextLineStyle style, CGFloat width, CGFloat phase, CGContextRef context){ + CGContextSetLineWidth(context, width); + CGContextSetLineCap(context, kCGLineCapButt); + CGContextSetLineJoin(context, kCGLineJoinMiter); + + CGFloat dash = 12, dot = 5, space = 3; + NSUInteger pattern = style & 0xF00; + if (pattern == ASTextLineStylePatternSolid) { + CGContextSetLineDash(context, phase, NULL, 0); + } else if (pattern == ASTextLineStylePatternDot) { + CGFloat lengths[2] = {width * dot, width * space}; + CGContextSetLineDash(context, phase, lengths, 2); + } else if (pattern == ASTextLineStylePatternDash) { + CGFloat lengths[2] = {width * dash, width * space}; + CGContextSetLineDash(context, phase, lengths, 2); + } else if (pattern == ASTextLineStylePatternDashDot) { + CGFloat lengths[4] = {width * dash, width * space, width * dot, width * space}; + CGContextSetLineDash(context, phase, lengths, 4); + } else if (pattern == ASTextLineStylePatternDashDotDot) { + CGFloat lengths[6] = {width * dash, width * space,width * dot, width * space, width * dot, width * space}; + CGContextSetLineDash(context, phase, lengths, 6); + } else if (pattern == ASTextLineStylePatternCircleDot) { + CGFloat lengths[2] = {width * 0, width * 3}; + CGContextSetLineDash(context, phase, lengths, 2); + CGContextSetLineCap(context, kCGLineCapRound); + CGContextSetLineJoin(context, kCGLineJoinRound); + } +} + + +static void ASTextDrawBorderRects(CGContextRef context, CGSize size, ASTextBorder *border, NSArray *rects, BOOL isVertical) { + if (rects.count == 0) return; + + ASTextShadow *shadow = border.shadow; + if (shadow.color) { + CGContextSaveGState(context); + CGContextSetShadowWithColor(context, shadow.offset, shadow.radius, shadow.color.CGColor); + CGContextBeginTransparencyLayer(context, NULL); + } + + 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 = ASTextCGRectPixelRound(rect); + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:border.cornerRadius]; + [path closePath]; + [paths addObject:path]; + } + + if (border.fillColor) { + CGContextSaveGState(context); + CGContextSetFillColorWithColor(context, border.fillColor.CGColor); + for (UIBezierPath *path in paths) { + CGContextAddPath(context, path.CGPath); + } + CGContextFillPath(context); + CGContextRestoreGState(context); + } + + if (border.strokeColor && border.lineStyle > 0 && border.strokeWidth > 0) { + + //-------------------------- single line ------------------------------// + CGContextSaveGState(context); + for (UIBezierPath *path in paths) { + CGRect bounds = CGRectUnion(path.bounds, (CGRect){CGPointZero, size}); + bounds = CGRectInset(bounds, -2 * border.strokeWidth, -2 * border.strokeWidth); + CGContextAddRect(context, bounds); + CGContextAddPath(context, path.CGPath); + CGContextEOClip(context); + } + [border.strokeColor setStroke]; + ASTextSetLinePatternInContext(border.lineStyle, border.strokeWidth, 0, context); + CGFloat inset = -border.strokeWidth * 0.5; + if ((border.lineStyle & 0xFF) == ASTextLineStyleThick) { + inset *= 2; + CGContextSetLineWidth(context, border.strokeWidth * 2); + } + CGFloat radiusDelta = -inset; + if (border.cornerRadius <= 0) { + radiusDelta = 0; + } + 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 = CGRectInset(rect, inset, inset); + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:border.cornerRadius + radiusDelta]; + [path closePath]; + CGContextAddPath(context, path.CGPath); + } + CGContextStrokePath(context); + CGContextRestoreGState(context); + + //------------------------- second line ------------------------------// + if ((border.lineStyle & 0xFF) == ASTextLineStyleDouble) { + CGContextSaveGState(context); + CGFloat inset = -border.strokeWidth * 2; + for (NSValue *value in rects) { + CGRect rect = value.CGRectValue; + rect = UIEdgeInsetsInsetRect(rect, border.insets); + rect = CGRectInset(rect, inset, inset); + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:border.cornerRadius + 2 * border.strokeWidth]; + [path closePath]; + + CGRect bounds = CGRectUnion(path.bounds, (CGRect){CGPointZero, size}); + bounds = CGRectInset(bounds, -2 * border.strokeWidth, -2 * border.strokeWidth); + CGContextAddRect(context, bounds); + CGContextAddPath(context, path.CGPath); + CGContextEOClip(context); + } + CGContextSetStrokeColorWithColor(context, border.strokeColor.CGColor); + ASTextSetLinePatternInContext(border.lineStyle, border.strokeWidth, 0, context); + CGContextSetLineJoin(context, border.lineJoin); + inset = -border.strokeWidth * 2.5; + radiusDelta = border.strokeWidth * 2; + if (border.cornerRadius <= 0) { + radiusDelta = 0; + } + for (NSValue *value in rects) { + CGRect rect = value.CGRectValue; + rect = UIEdgeInsetsInsetRect(rect, border.insets); + rect = CGRectInset(rect, inset, inset); + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:border.cornerRadius + radiusDelta]; + [path closePath]; + CGContextAddPath(context, path.CGPath); + } + CGContextStrokePath(context); + CGContextRestoreGState(context); + } + } + + if (shadow.color) { + CGContextEndTransparencyLayer(context); + CGContextRestoreGState(context); + } +} + +static void ASTextDrawLineStyle(CGContextRef context, CGFloat length, CGFloat lineWidth, ASTextLineStyle style, CGPoint position, CGColorRef color, BOOL isVertical) { + 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); + } + } 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); + } + } 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); + } + } + } CGContextRestoreGState(context); +} + +static void ASTextDrawText(ASTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, BOOL (^cancel)(void)) { + CGContextSaveGState(context); { + + CGContextTranslateCTM(context, point.x, point.y); + CGContextTranslateCTM(context, 0, size.height); + CGContextScaleCTM(context, 1, -1); + + BOOL isVertical = layout.container.verticalForm; + CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + + NSArray *lines = layout.lines; + for (NSUInteger l = 0, lMax = lines.count; l < lMax; l++) { + ASTextLine *line = lines[l]; + if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine; + NSArray *lineRunRanges = line.verticalRotateRange; + CGFloat posX = line.position.x + verticalOffset; + CGFloat posY = size.height - line.position.y; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) { + CTRunRef run = CFArrayGetValueAtIndex(runs, r); + CGContextSetTextMatrix(context, CGAffineTransformIdentity); + CGContextSetTextPosition(context, posX, posY); + ASTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset); + } + if (cancel && cancel()) break; + } + + // Use this to draw frame for test/debug. + // CGContextTranslateCTM(context, verticalOffset, size.height); + // CTFrameDraw(layout.frame, context); + + } CGContextRestoreGState(context); +} + +static void ASTextDrawBlockBorder(ASTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, BOOL (^cancel)(void)) { + CGContextSaveGState(context); + CGContextTranslateCTM(context, point.x, point.y); + + BOOL isVertical = layout.container.verticalForm; + CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + + NSArray *lines = layout.lines; + for (NSInteger l = 0, lMax = lines.count; l < lMax; l++) { + if (cancel && cancel()) break; + + ASTextLine *line = lines[l]; + if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) { + CTRunRef run = CFArrayGetValueAtIndex(runs, r); + CFIndex glyphCount = CTRunGetGlyphCount(run); + if (glyphCount == 0) continue; + NSDictionary *attrs = (id)CTRunGetAttributes(run); + ASTextBorder *border = attrs[ASTextBlockBorderAttributeName]; + if (!border) continue; + + NSUInteger lineStartIndex = line.index; + while (lineStartIndex > 0) { + if (((ASTextLine *)lines[lineStartIndex - 1]).row == line.row) lineStartIndex--; + else break; + } + + CGRect unionRect = CGRectZero; + NSUInteger lineStartRow = ((ASTextLine *)lines[lineStartIndex]).row; + NSUInteger lineContinueIndex = lineStartIndex; + NSUInteger lineContinueRow = lineStartRow; + do { + ASTextLine *one = lines[lineContinueIndex]; + if (lineContinueIndex == lineStartIndex) { + unionRect = one.bounds; + } else { + unionRect = CGRectUnion(unionRect, one.bounds); + } + if (lineContinueIndex + 1 == lMax) break; + ASTextLine *next = lines[lineContinueIndex + 1]; + if (next.row != lineContinueRow) { + ASTextBorder *nextBorder = [layout.text as_attribute:ASTextBlockBorderAttributeName atIndex:next.range.location]; + if ([nextBorder isEqual:border]) { + lineContinueRow++; + } else { + break; + } + } + 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; + } + unionRect.origin.x += verticalOffset; + ASTextDrawBorderRects(context, size, border, @[[NSValue valueWithCGRect:unionRect]], isVertical); + + l = lineContinueIndex; + break; + } + } + + + CGContextRestoreGState(context); +} + +static void ASTextDrawBorder(ASTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, ASTextBorderType type, BOOL (^cancel)(void)) { + CGContextSaveGState(context); + CGContextTranslateCTM(context, point.x, point.y); + + BOOL isVertical = layout.container.verticalForm; + CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + + NSArray *lines = layout.lines; + NSString *borderKey = (type == ASTextBorderTypeNormal ? ASTextBorderAttributeName : ASTextBackgroundBorderAttributeName); + + BOOL needJumpRun = NO; + NSUInteger jumpRunIndex = 0; + + for (NSInteger l = 0, lMax = lines.count; l < lMax; l++) { + if (cancel && cancel()) break; + + ASTextLine *line = lines[l]; + if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) { + if (needJumpRun) { + needJumpRun = NO; + r = jumpRunIndex + 1; + if (r >= rMax) break; + } + + CTRunRef run = CFArrayGetValueAtIndex(runs, r); + CFIndex glyphCount = CTRunGetGlyphCount(run); + if (glyphCount == 0) continue; + + NSDictionary *attrs = (id)CTRunGetAttributes(run); + ASTextBorder *border = attrs[borderKey]; + if (!border) continue; + + CFRange runRange = CTRunGetStringRange(run); + if (runRange.location == kCFNotFound || runRange.length == 0) continue; + if (runRange.location + runRange.length > layout.text.length) continue; + + NSMutableArray *runRects = [NSMutableArray new]; + NSInteger endLineIndex = l; + NSInteger endRunIndex = r; + BOOL endFound = NO; + for (NSInteger ll = l; ll < lMax; ll++) { + if (endFound) break; + ASTextLine *iLine = lines[ll]; + CFArrayRef iRuns = CTLineGetGlyphRuns(iLine.CTLine); + + CGRect extLineRect = CGRectNull; + for (NSInteger rr = (ll == l) ? r : 0, rrMax = CFArrayGetCount(iRuns); rr < rrMax; rr++) { + CTRunRef iRun = CFArrayGetValueAtIndex(iRuns, rr); + NSDictionary *iAttrs = (id)CTRunGetAttributes(iRun); + ASTextBorder *iBorder = iAttrs[borderKey]; + if (![border isEqual:iBorder]) { + endFound = YES; + break; + } + endLineIndex = ll; + endRunIndex = rr; + + CGPoint iRunPosition = CGPointZero; + 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); + } + } 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); + } + } + } + + if (!CGRectIsNull(extLineRect)) { + [runRects addObject:[NSValue valueWithCGRect:extLineRect]]; + } + } + + NSMutableArray *drawRects = [NSMutableArray new]; + 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; + } + } else { + if (fabs(rect.origin.y - curRect.origin.y) < 1) { + curRect = ASTextMergeRectInSameLine(rect, curRect, isVertical); + } else { + [drawRects addObject:[NSValue valueWithCGRect:curRect]]; + curRect = rect; + } + } + } + if (!CGRectEqualToRect(curRect, CGRectZero)) { + [drawRects addObject:[NSValue valueWithCGRect:curRect]]; + } + + ASTextDrawBorderRects(context, size, border, drawRects, isVertical); + + if (l == endLineIndex) { + r = endRunIndex; + } else { + l = endLineIndex - 1; + needJumpRun = YES; + jumpRunIndex = endRunIndex; + break; + } + + } + } + + CGContextRestoreGState(context); +} + +static void ASTextDrawDecoration(ASTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, ASTextDecorationType type, BOOL (^cancel)(void)) { + NSArray *lines = layout.lines; + + CGContextSaveGState(context); + CGContextTranslateCTM(context, point.x, point.y); + + BOOL isVertical = layout.container.verticalForm; + CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGContextTranslateCTM(context, verticalOffset, 0); + + for (NSUInteger l = 0, lMax = layout.lines.count; l < lMax; l++) { + if (cancel && cancel()) break; + + ASTextLine *line = lines[l]; + if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) { + CTRunRef run = CFArrayGetValueAtIndex(runs, r); + CFIndex glyphCount = CTRunGetGlyphCount(run); + if (glyphCount == 0) continue; + + NSDictionary *attrs = (id)CTRunGetAttributes(run); + ASTextDecoration *underline = attrs[ASTextUnderlineAttributeName]; + ASTextDecoration *strikethrough = attrs[ASTextStrikethroughAttributeName]; + + BOOL needDrawUnderline = NO, needDrawStrikethrough = NO; + if ((type & ASTextDecorationTypeUnderline) && underline.style > 0) { + needDrawUnderline = YES; + } + if ((type & ASTextDecorationTypeStrikethrough) && strikethrough.style > 0) { + needDrawStrikethrough = YES; + } + if (!needDrawUnderline && !needDrawStrikethrough) continue; + + CFRange runRange = CTRunGetStringRange(run); + if (runRange.location == kCFNotFound || runRange.length == 0) continue; + if (runRange.location + runRange.length > layout.text.length) continue; + NSString *runStr = [layout.text attributedSubstringFromRange:NSMakeRange(runRange.location, runRange.length)].string; + if (ASTextIsLinebreakString(runStr)) continue; // may need more checks... + + CGFloat xHeight, underlinePosition, lineThickness; + ASTextGetRunsMaxMetric(runs, &xHeight, &underlinePosition, &lineThickness); + + 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); + } + + if (needDrawUnderline) { + CGColorRef color = underline.color.CGColor; + if (!color) { + color = (__bridge CGColorRef)(attrs[(id)kCTForegroundColorAttributeName]); + color = ASTextGetCGColor(color); + } + CGFloat thickness = underline.width ? underline.width.floatValue : lineThickness; + ASTextShadow *shadow = underline.shadow; + while (shadow) { + if (!shadow.color) { + shadow = shadow.subShadow; + continue; + } + CGFloat offsetAlterX = size.width + 0xFFFF; + CGContextSaveGState(context); { + CGSize offset = shadow.offset; + offset.width -= offsetAlterX; + CGContextSaveGState(context); { + 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); + } CGContextRestoreGState(context); + } CGContextRestoreGState(context); + shadow = shadow.subShadow; + } + ASTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color, isVertical); + } + + if (needDrawStrikethrough) { + CGColorRef color = strikethrough.color.CGColor; + if (!color) { + color = (__bridge CGColorRef)(attrs[(id)kCTForegroundColorAttributeName]); + color = ASTextGetCGColor(color); + } + CGFloat thickness = strikethrough.width ? strikethrough.width.floatValue : lineThickness; + ASTextShadow *shadow = underline.shadow; + while (shadow) { + if (!shadow.color) { + shadow = shadow.subShadow; + continue; + } + CGFloat offsetAlterX = size.width + 0xFFFF; + CGContextSaveGState(context); { + CGSize offset = shadow.offset; + offset.width -= offsetAlterX; + CGContextSaveGState(context); { + 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); + } CGContextRestoreGState(context); + } CGContextRestoreGState(context); + shadow = shadow.subShadow; + } + ASTextDrawLineStyle(context, length, thickness, strikethrough.style, strikethroughStart, color, isVertical); + } + } + } + CGContextRestoreGState(context); +} + +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; + + for (NSUInteger i = 0, max = layout.attachments.count; i < max; i++) { + ASTextAttachment *a = layout.attachments[i]; + if (!a.content) continue; + + UIImage *image = nil; + UIView *view = nil; + CALayer *layer = 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; + } + if (!image && !view && !layer) continue; + if (image && !context) continue; + if (view && !targetView) continue; + if (layer && !targetLayer) continue; + if (cancel && cancel()) break; + + 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 = ASTextCGRectFitWithContentMode(rect, asize, a.contentMode); + rect = ASTextCGRectPixelRound(rect); + rect = CGRectStandardize(rect); + rect.origin.x += point.x + verticalOffset; + rect.origin.y += point.y; + if (image) { + CGImageRef ref = image.CGImage; + if (ref) { + CGContextSaveGState(context); + CGContextTranslateCTM(context, 0, CGRectGetMaxY(rect) + CGRectGetMinY(rect)); + CGContextScaleCTM(context, 1, -1); + CGContextDrawImage(context, rect, ref); + CGContextRestoreGState(context); + } + } else if (view) { + view.frame = rect; + [targetView addSubview:view]; + } else if (layer) { + layer.frame = rect; + [targetLayer addSublayer:layer]; + } + } +} + +static void ASTextDrawShadow(ASTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, BOOL (^cancel)(void)) { + //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; + + CGContextSaveGState(context); { + CGContextTranslateCTM(context, point.x, point.y); + CGContextTranslateCTM(context, 0, size.height); + CGContextScaleCTM(context, 1, -1); + NSArray *lines = layout.lines; + for (NSUInteger l = 0, lMax = layout.lines.count; l < lMax; l++) { + if (cancel && cancel()) break; + ASTextLine *line = lines[l]; + if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine; + NSArray *lineRunRanges = line.verticalRotateRange; + CGFloat linePosX = line.position.x; + CGFloat linePosY = size.height - line.position.y; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) { + CTRunRef run = CFArrayGetValueAtIndex(runs, r); + CGContextSetTextMatrix(context, CGAffineTransformIdentity); + CGContextSetTextPosition(context, linePosX, linePosY); + NSDictionary *attrs = (id)CTRunGetAttributes(run); + ASTextShadow *shadow = attrs[ASTextShadowAttributeName]; + ASTextShadow *nsShadow = [ASTextShadow shadowWithNSShadow:attrs[NSShadowAttributeName]]; // NSShadow compatible + if (nsShadow) { + nsShadow.subShadow = shadow; + shadow = nsShadow; + } + while (shadow) { + if (!shadow.color) { + shadow = shadow.subShadow; + continue; + } + CGSize offset = shadow.offset; + offset.width -= offsetAlterX; + CGContextSaveGState(context); { + 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); + } CGContextRestoreGState(context); + shadow = shadow.subShadow; + } + } + } + } CGContextRestoreGState(context); +} + +static void ASTextDrawInnerShadow(ASTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, BOOL (^cancel)(void)) { + CGContextSaveGState(context); + CGContextTranslateCTM(context, point.x, point.y); + CGContextTranslateCTM(context, 0, size.height); + CGContextScaleCTM(context, 1, -1); + CGContextSetTextMatrix(context, CGAffineTransformIdentity); + + BOOL isVertical = layout.container.verticalForm; + CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + + NSArray *lines = layout.lines; + for (NSUInteger l = 0, lMax = lines.count; l < lMax; l++) { + if (cancel && cancel()) break; + + ASTextLine *line = lines[l]; + if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine; + NSArray *lineRunRanges = line.verticalRotateRange; + CGFloat linePosX = line.position.x; + CGFloat linePosY = size.height - line.position.y; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) { + CTRunRef run = CFArrayGetValueAtIndex(runs, r); + if (CTRunGetGlyphCount(run) == 0) continue; + CGContextSetTextMatrix(context, CGAffineTransformIdentity); + CGContextSetTextPosition(context, linePosX, linePosY); + NSDictionary *attrs = (id)CTRunGetAttributes(run); + ASTextShadow *shadow = attrs[ASTextInnerShadowAttributeName]; + while (shadow) { + if (!shadow.color) { + shadow = shadow.subShadow; + continue; + } + CGPoint runPosition = CGPointZero; + CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition); + CGRect runImageBounds = CTRunGetImageBounds(run, context, CFRangeMake(0, 0)); + runImageBounds.origin.x += runPosition.x; + if (runImageBounds.size.width < 0.1 || runImageBounds.size.height < 0.1) continue; + + CFDictionaryRef runAttrs = CTRunGetAttributes(run); + NSValue *glyphTransformValue = CFDictionaryGetValue(runAttrs, (__bridge const void *)(ASTextGlyphTransformAttributeName)); + if (glyphTransformValue) { + runImageBounds = CGRectMake(0, 0, size.width, size.height); + } + + // text inner shadow + CGContextSaveGState(context); { + CGContextSetBlendMode(context, shadow.blendMode); + CGContextSetShadowWithColor(context, CGSizeZero, 0, NULL); + CGContextSetAlpha(context, CGColorGetAlpha(shadow.color.CGColor)); + CGContextClipToRect(context, runImageBounds); + CGContextBeginTransparencyLayer(context, NULL); { + UIColor *opaqueShadowColor = [shadow.color colorWithAlphaComponent:1]; + CGContextSetShadowWithColor(context, shadow.offset, shadow.radius, opaqueShadowColor.CGColor); + CGContextSetFillColorWithColor(context, opaqueShadowColor.CGColor); + CGContextSetBlendMode(context, kCGBlendModeSourceOut); + CGContextBeginTransparencyLayer(context, NULL); { + CGContextFillRect(context, runImageBounds); + CGContextSetBlendMode(context, kCGBlendModeDestinationIn); + CGContextBeginTransparencyLayer(context, NULL); { + ASTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset); + } CGContextEndTransparencyLayer(context); + } CGContextEndTransparencyLayer(context); + } CGContextEndTransparencyLayer(context); + } CGContextRestoreGState(context); + shadow = shadow.subShadow; + } + } + } + + CGContextRestoreGState(context); +} + +static void ASTextDrawDebug(ASTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, ASTextDebugOption *op) { + UIGraphicsPushContext(context); + CGContextSaveGState(context); + CGContextTranslateCTM(context, point.x, point.y); + CGContextSetLineWidth(context, 1.0 / ASScreenScale()); + CGContextSetLineDash(context, 0, NULL, 0); + CGContextSetLineJoin(context, kCGLineJoinMiter); + CGContextSetLineCap(context, kCGLineCapButt); + + BOOL isVertical = layout.container.verticalForm; + CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGContextTranslateCTM(context, verticalOffset, 0); + + if (op.CTFrameBorderColor || op.CTFrameFillColor) { + UIBezierPath *path = layout.container.path; + if (!path) { + CGRect rect = (CGRect){CGPointZero, layout.container.size}; + rect = UIEdgeInsetsInsetRect(rect, layout.container.insets); + if (op.CTFrameBorderColor) rect = ASTextCGRectPixelHalf(rect); + else rect = ASTextCGRectPixelRound(rect); + path = [UIBezierPath bezierPathWithRect:rect]; + } + [path closePath]; + + for (UIBezierPath *ex in layout.container.exclusionPaths) { + [path appendPath:ex]; + } + if (op.CTFrameFillColor) { + [op.CTFrameFillColor setFill]; + if (layout.container.pathLineWidth > 0) { + CGContextSaveGState(context); { + CGContextBeginTransparencyLayer(context, NULL); { + CGContextAddPath(context, path.CGPath); + if (layout.container.pathFillEvenOdd) { + CGContextEOFillPath(context); + } else { + CGContextFillPath(context); + } + CGContextSetBlendMode(context, kCGBlendModeDestinationOut); + [[UIColor blackColor] setFill]; + CGPathRef cgPath = CGPathCreateCopyByStrokingPath(path.CGPath, NULL, layout.container.pathLineWidth, kCGLineCapButt, kCGLineJoinMiter, 0); + if (cgPath) { + CGContextAddPath(context, cgPath); + CGContextFillPath(context); + } + CGPathRelease(cgPath); + } CGContextEndTransparencyLayer(context); + } CGContextRestoreGState(context); + } else { + CGContextAddPath(context, path.CGPath); + if (layout.container.pathFillEvenOdd) { + CGContextEOFillPath(context); + } else { + CGContextFillPath(context); + } + } + } + if (op.CTFrameBorderColor) { + CGContextSaveGState(context); { + if (layout.container.pathLineWidth > 0) { + CGContextSetLineWidth(context, layout.container.pathLineWidth); + } + [op.CTFrameBorderColor setStroke]; + CGContextAddPath(context, path.CGPath); + CGContextStrokePath(context); + } CGContextRestoreGState(context); + } + } + + NSArray *lines = layout.lines; + for (NSUInteger l = 0, lMax = lines.count; l < lMax; l++) { + ASTextLine *line = lines[l]; + if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine; + CGRect lineBounds = line.bounds; + if (op.CTLineFillColor) { + [op.CTLineFillColor setFill]; + CGContextAddRect(context, ASTextCGRectPixelRound(lineBounds)); + CGContextFillPath(context); + } + if (op.CTLineBorderColor) { + [op.CTLineBorderColor setStroke]; + CGContextAddRect(context, ASTextCGRectPixelHalf(lineBounds)); + CGContextStrokePath(context); + } + 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); + } + } + 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))]; + } + if (op.CTRunFillColor || op.CTRunBorderColor || op.CTRunNumberColor || op.CGGlyphFillColor || op.CGGlyphBorderColor) { + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) { + CTRunRef run = CFArrayGetValueAtIndex(runs, r); + CFIndex glyphCount = CTRunGetGlyphCount(run); + if (glyphCount == 0) continue; + + CGPoint glyphPositions[glyphCount]; + CTRunGetPositions(run, CFRangeMake(0, glyphCount), glyphPositions); + + CGSize glyphAdvances[glyphCount]; + 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; + } + + 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); + } + + if (op.CTRunFillColor) { + [op.CTRunFillColor setFill]; + CGContextAddRect(context, ASTextCGRectPixelRound(runTypoBounds)); + CGContextFillPath(context); + } + if (op.CTRunBorderColor) { + [op.CTRunBorderColor setStroke]; + CGContextAddRect(context, ASTextCGRectPixelHalf(runTypoBounds)); + CGContextStrokePath(context); + } + if (op.CTRunNumberColor) { + [op.CTRunNumberColor set]; + NSMutableAttributedString *num = [[NSMutableAttributedString alloc] initWithString:@(r).description]; + num.as_color = op.CTRunNumberColor; + num.as_font = [UIFont systemFontOfSize:6]; + [num drawAtPoint:CGPointMake(runTypoBounds.origin.x, runTypoBounds.origin.y - 1)]; + } + if (op.CGGlyphBorderColor || op.CGGlyphFillColor) { + for (NSUInteger g = 0; g < glyphCount; g++) { + 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); + } + if (op.CGGlyphFillColor) { + [op.CGGlyphFillColor setFill]; + CGContextAddRect(context, ASTextCGRectPixelRound(rect)); + CGContextFillPath(context); + } + if (op.CGGlyphBorderColor) { + [op.CGGlyphBorderColor setStroke]; + CGContextAddRect(context, ASTextCGRectPixelHalf(rect)); + CGContextStrokePath(context); + } + } + } + } + } + } + CGContextRestoreGState(context); + UIGraphicsPopContext(); +} + + +- (void)drawInContext:(CGContextRef)context + size:(CGSize)size + point:(CGPoint)point + view:(UIView *)view + layer:(CALayer *)layer + debug:(ASTextDebugOption *)debug + cancel:(BOOL (^)(void))cancel{ + @autoreleasepool { + if (self.needDrawBlockBorder && context) { + if (cancel && cancel()) return; + ASTextDrawBlockBorder(self, context, size, point, cancel); + } + if (self.needDrawBackgroundBorder && context) { + if (cancel && cancel()) return; + ASTextDrawBorder(self, context, size, point, ASTextBorderTypeBackgound, cancel); + } + if (self.needDrawShadow && context) { + if (cancel && cancel()) return; + ASTextDrawShadow(self, context, size, point, cancel); + } + if (self.needDrawUnderline && context) { + if (cancel && cancel()) return; + ASTextDrawDecoration(self, context, size, point, ASTextDecorationTypeUnderline, cancel); + } + if (self.needDrawText && context) { + if (cancel && cancel()) return; + ASTextDrawText(self, context, size, point, cancel); + } + if (self.needDrawAttachment && (context || view || layer)) { + if (cancel && cancel()) return; + ASTextDrawAttachment(self, context, size, point, view, layer, cancel); + } + if (self.needDrawInnerShadow && context) { + if (cancel && cancel()) return; + ASTextDrawInnerShadow(self, context, size, point, cancel); + } + if (self.needDrawStrikethrough && context) { + if (cancel && cancel()) return; + ASTextDrawDecoration(self, context, size, point, ASTextDecorationTypeStrikethrough, cancel); + } + if (self.needDrawBorder && context) { + if (cancel && cancel()) return; + ASTextDrawBorder(self, context, size, point, ASTextBorderTypeNormal, cancel); + } + if (debug.needDrawDebug && context) { + if (cancel && cancel()) return; + ASTextDrawDebug(self, context, size, point, debug); + } + } +} + +- (void)drawInContext:(CGContextRef)context + size:(CGSize)size + debug:(ASTextDebugOption *)debug { + [self drawInContext:context size:size point:CGPointZero view:nil layer:nil debug:debug cancel:nil]; +} + +@end diff --git a/Source/Private/TextExperiment/Component/ASTextLine.h b/Source/Private/TextExperiment/Component/ASTextLine.h new file mode 100755 index 0000000000..0db8227f20 --- /dev/null +++ b/Source/Private/TextExperiment/Component/ASTextLine.h @@ -0,0 +1,79 @@ +// +// ASTextLine.h +// Modified from YYText +// +// Created by ibireme on 15/3/10. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import +#import +#import "ASTextAttribute.h" + +@class ASTextRunGlyphRange; + +NS_ASSUME_NONNULL_BEGIN + +/** + A text line object wrapped `CTLineRef`, see `ASTextLayout` for more. + */ +@interface ASTextLine : NSObject + ++ (instancetype)lineWithCTLine:(CTLineRef)CTLine position:(CGPoint)position vertical:(BOOL)isVertical; + +@property (nonatomic) NSUInteger index; ///< line index +@property (nonatomic) NSUInteger row; ///< line row +@property (nullable, nonatomic, strong) NSArray *> *verticalRotateRange; ///< Run rotate range + +@property (nonatomic, readonly) CTLineRef CTLine; ///< CoreText line +@property (nonatomic, readonly) NSRange range; ///< string range +@property (nonatomic, readonly) BOOL vertical; ///< vertical form + +@property (nonatomic, readonly) CGRect bounds; ///< bounds (ascent + descent) +@property (nonatomic, readonly) CGSize size; ///< bounds.size +@property (nonatomic, readonly) CGFloat width; ///< bounds.size.width +@property (nonatomic, readonly) CGFloat height; ///< bounds.size.height +@property (nonatomic, readonly) CGFloat top; ///< bounds.origin.y +@property (nonatomic, readonly) CGFloat bottom; ///< bounds.origin.y + bounds.size.height +@property (nonatomic, readonly) CGFloat left; ///< bounds.origin.x +@property (nonatomic, readonly) CGFloat right; ///< bounds.origin.x + bounds.size.width + +@property (nonatomic) CGPoint position; ///< baseline position +@property (nonatomic, readonly) CGFloat ascent; ///< line ascent +@property (nonatomic, readonly) CGFloat descent; ///< line descent +@property (nonatomic, readonly) CGFloat leading; ///< line leading +@property (nonatomic, readonly) CGFloat lineWidth; ///< line width +@property (nonatomic, readonly) CGFloat trailingWhitespaceWidth; + +@property (nullable, nonatomic, readonly) NSArray *attachments; ///< ASTextAttachment +@property (nullable, nonatomic, readonly) NSArray *attachmentRanges; ///< NSRange(NSValue) +@property (nullable, nonatomic, readonly) NSArray *attachmentRects; ///< CGRect(NSValue) + +@end + + +typedef NS_ENUM(NSUInteger, ASTextRunGlyphDrawMode) { + /// No rotate. + ASTextRunGlyphDrawModeHorizontal = 0, + + /// Rotate vertical for single glyph. + ASTextRunGlyphDrawModeVerticalRotate = 1, + + /// Rotate vertical for single glyph, and move the glyph to a better position, + /// such as fullwidth punctuation. + ASTextRunGlyphDrawModeVerticalRotateMove = 2, +}; + +/** + A range in CTRun, used for vertical form. + */ +@interface ASTextRunGlyphRange : NSObject +@property (nonatomic) NSRange glyphRangeInRun; +@property (nonatomic) ASTextRunGlyphDrawMode drawMode; ++ (instancetype)rangeWithRange:(NSRange)range drawMode:(ASTextRunGlyphDrawMode)mode; +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/TextExperiment/Component/ASTextLine.m b/Source/Private/TextExperiment/Component/ASTextLine.m new file mode 100755 index 0000000000..189d509360 --- /dev/null +++ b/Source/Private/TextExperiment/Component/ASTextLine.m @@ -0,0 +1,167 @@ +// +// ASYTextLine.m +// Modified from YYText +// +// Created by ibireme on 15/3/3. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import "ASTextLine.h" +#import "ASTextUtilities.h" + + +@implementation ASTextLine { + CGFloat _firstGlyphPos; // first glyph position for baseline, typically 0. +} + ++ (instancetype)lineWithCTLine:(CTLineRef)CTLine position:(CGPoint)position vertical:(BOOL)isVertical { + if (!CTLine) return nil; + ASTextLine *line = [self new]; + line->_position = position; + line->_vertical = isVertical; + [line setCTLine:CTLine]; + return line; +} + +- (void)dealloc { + if (_CTLine) CFRelease(_CTLine); +} + +- (void)setCTLine:(_Nonnull CTLineRef)CTLine { + if (_CTLine != CTLine) { + if (CTLine) CFRetain(CTLine); + if (_CTLine) CFRelease(_CTLine); + _CTLine = CTLine; + if (_CTLine) { + _lineWidth = CTLineGetTypographicBounds(_CTLine, &_ascent, &_descent, &_leading); + CFRange range = CTLineGetStringRange(_CTLine); + _range = NSMakeRange(range.location, range.length); + if (CTLineGetGlyphCount(_CTLine) > 0) { + CFArrayRef runs = CTLineGetGlyphRuns(_CTLine); + CTRunRef run = CFArrayGetValueAtIndex(runs, 0); + CGPoint pos; + CTRunGetPositions(run, CFRangeMake(0, 1), &pos); + _firstGlyphPos = pos.x; + } else { + _firstGlyphPos = 0; + } + _trailingWhitespaceWidth = CTLineGetTrailingWhitespaceWidth(_CTLine); + } else { + _lineWidth = _ascent = _descent = _leading = _firstGlyphPos = _trailingWhitespaceWidth = 0; + _range = NSMakeRange(0, 0); + } + [self reloadBounds]; + } +} + +- (void)setPosition:(CGPoint)position { + _position = position; + [self reloadBounds]; +} + +- (void)reloadBounds { + if (_vertical) { + _bounds = CGRectMake(_position.x - _descent, _position.y, _ascent + _descent, _lineWidth); + _bounds.origin.y += _firstGlyphPos; + } else { + _bounds = CGRectMake(_position.x, _position.y - _ascent, _lineWidth, _ascent + _descent); + _bounds.origin.x += _firstGlyphPos; + } + + _attachments = nil; + _attachmentRanges = nil; + _attachmentRects = nil; + if (!_CTLine) return; + CFArrayRef runs = CTLineGetGlyphRuns(_CTLine); + NSUInteger runCount = CFArrayGetCount(runs); + if (runCount == 0) return; + + NSMutableArray *attachments = [NSMutableArray new]; + NSMutableArray *attachmentRanges = [NSMutableArray new]; + NSMutableArray *attachmentRects = [NSMutableArray new]; + for (NSUInteger r = 0; r < runCount; r++) { + CTRunRef run = CFArrayGetValueAtIndex(runs, r); + CFIndex glyphCount = CTRunGetGlyphCount(run); + if (glyphCount == 0) continue; + NSDictionary *attrs = (id)CTRunGetAttributes(run); + ASTextAttachment *attachment = attrs[ASTextAttachmentAttributeName]; + if (attachment) { + CGPoint runPosition = CGPointZero; + CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition); + + CGFloat ascent, descent, leading, runWidth; + CGRect runTypoBounds; + runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading); + + if (_vertical) { + ASTEXT_SWAP(runPosition.x, runPosition.y); + runPosition.y = _position.y + runPosition.y; + runTypoBounds = CGRectMake(_position.x + runPosition.x - descent, runPosition.y , ascent + descent, runWidth); + } else { + runPosition.x += _position.x; + runPosition.y = _position.y - runPosition.y; + runTypoBounds = CGRectMake(runPosition.x, runPosition.y - ascent, runWidth, ascent + descent); + } + + NSRange runRange = ASTextNSRangeFromCFRange(CTRunGetStringRange(run)); + [attachments addObject:attachment]; + [attachmentRanges addObject:[NSValue valueWithRange:runRange]]; + [attachmentRects addObject:[NSValue valueWithCGRect:runTypoBounds]]; + } + } + _attachments = attachments.count ? attachments : nil; + _attachmentRanges = attachmentRanges.count ? attachmentRanges : nil; + _attachmentRects = attachmentRects.count ? attachmentRects : nil; +} + +- (CGSize)size { + return _bounds.size; +} + +- (CGFloat)width { + return CGRectGetWidth(_bounds); +} + +- (CGFloat)height { + return CGRectGetHeight(_bounds); +} + +- (CGFloat)top { + return CGRectGetMinY(_bounds); +} + +- (CGFloat)bottom { + return CGRectGetMaxY(_bounds); +} + +- (CGFloat)left { + return CGRectGetMinX(_bounds); +} + +- (CGFloat)right { + return CGRectGetMaxX(_bounds); +} + +- (NSString *)description { + NSMutableString *desc = @"".mutableCopy; + NSRange range = self.range; + [desc appendFormat:@" row:%zd range:%tu,%tu",self, self.row, range.location, range.length]; + [desc appendFormat:@" position:%@",NSStringFromCGPoint(self.position)]; + [desc appendFormat:@" bounds:%@",NSStringFromCGRect(self.bounds)]; + return desc; +} + +@end + + +@implementation ASTextRunGlyphRange ++ (instancetype)rangeWithRange:(NSRange)range drawMode:(ASTextRunGlyphDrawMode)mode { + ASTextRunGlyphRange *one = [self new]; + one.glyphRangeInRun = range; + one.drawMode = mode; + return one; +} +@end diff --git a/Source/Private/TextExperiment/String/ASTextAttribute.h b/Source/Private/TextExperiment/String/ASTextAttribute.h new file mode 100755 index 0000000000..80cdf64ec8 --- /dev/null +++ b/Source/Private/TextExperiment/String/ASTextAttribute.h @@ -0,0 +1,347 @@ +// +// ASTextAttribute.h +// Modified from YYText +// +// Created by ibireme on 14/10/26. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - Enum Define + +/// The attribute type +typedef NS_OPTIONS(NSInteger, ASTextAttributeType) { + ASTextAttributeTypeNone = 0, + ASTextAttributeTypeUIKit = 1 << 0, ///< UIKit attributes, such as UILabel/UITextField/drawInRect. + ASTextAttributeTypeCoreText = 1 << 1, ///< CoreText attributes, used by CoreText. + ASTextAttributeTypeASText = 1 << 2, ///< ASText attributes, used by ASText. +}; + +/// Get the attribute type from an attribute name. +extern ASTextAttributeType ASTextAttributeGetType(NSString *attributeName); + +/** + Line style in ASText (similar to NSUnderlineStyle). + */ +typedef NS_OPTIONS (NSInteger, ASTextLineStyle) { + // basic style (bitmask:0xFF) + ASTextLineStyleNone = 0x00, ///< ( ) Do not draw a line (Default). + ASTextLineStyleSingle = 0x01, ///< (โ”€โ”€โ”€โ”€โ”€โ”€) Draw a single line. + ASTextLineStyleThick = 0x02, ///< (โ”โ”โ”โ”โ”โ”โ”) Draw a thick line. + ASTextLineStyleDouble = 0x09, ///< (โ•โ•โ•โ•โ•โ•) Draw a double line. + + // style pattern (bitmask:0xF00) + ASTextLineStylePatternSolid = 0x000, ///< (โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€) Draw a solid line (Default). + ASTextLineStylePatternDot = 0x100, ///< (โ€‘ โ€‘ โ€‘ โ€‘ โ€‘ โ€‘) Draw a line of dots. + ASTextLineStylePatternDash = 0x200, ///< (โ€” โ€” โ€” โ€”) Draw a line of dashes. + ASTextLineStylePatternDashDot = 0x300, ///< (โ€” โ€‘ โ€” โ€‘ โ€” โ€‘) Draw a line of alternating dashes and dots. + ASTextLineStylePatternDashDotDot = 0x400, ///< (โ€” โ€‘ โ€‘ โ€” โ€‘ โ€‘) Draw a line of alternating dashes and two dots. + ASTextLineStylePatternCircleDot = 0x900, ///< (โ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข) Draw a line of small circle dots. +}; + +/** + Text vertical alignment. + */ +typedef NS_ENUM(NSInteger, ASTextVerticalAlignment) { + ASTextVerticalAlignmentTop = 0, ///< Top alignment. + ASTextVerticalAlignmentCenter = 1, ///< Center alignment. + ASTextVerticalAlignmentBottom = 2, ///< Bottom alignment. +}; + +/** + The direction define in ASText. + */ +typedef NS_OPTIONS(NSUInteger, ASTextDirection) { + ASTextDirectionNone = 0, + ASTextDirectionTop = 1 << 0, + ASTextDirectionRight = 1 << 1, + ASTextDirectionBottom = 1 << 2, + ASTextDirectionLeft = 1 << 3, +}; + +/** + The trunction type, tells the truncation engine which type of truncation is being requested. + */ +typedef NS_ENUM (NSUInteger, ASTextTruncationType) { + /// No truncate. + ASTextTruncationTypeNone = 0, + + /// Truncate at the beginning of the line, leaving the end portion visible. + ASTextTruncationTypeStart = 1, + + /// Truncate at the end of the line, leaving the start portion visible. + ASTextTruncationTypeEnd = 2, + + /// Truncate in the middle of the line, leaving both the start and the end portions visible. + ASTextTruncationTypeMiddle = 3, +}; + + + +#pragma mark - Attribute Name Defined in ASText + +/// The value of this attribute is a `ASTextBackedString` object. +/// Use this attribute to store the original plain text if it is replaced by something else (such as attachment). +UIKIT_EXTERN NSString *const ASTextBackedStringAttributeName; + +/// The value of this attribute is a `ASTextBinding` object. +/// Use this attribute to bind a range of text together, as if it was a single charactor. +UIKIT_EXTERN NSString *const ASTextBindingAttributeName; + +/// The value of this attribute is a `ASTextShadow` object. +/// Use this attribute to add shadow to a range of text. +/// Shadow will be drawn below text glyphs. Use ASTextShadow.subShadow to add multi-shadow. +UIKIT_EXTERN NSString *const ASTextShadowAttributeName; + +/// The value of this attribute is a `ASTextShadow` object. +/// Use this attribute to add inner shadow to a range of text. +/// Inner shadow will be drawn above text glyphs. Use ASTextShadow.subShadow to add multi-shadow. +UIKIT_EXTERN NSString *const ASTextInnerShadowAttributeName; + +/// The value of this attribute is a `ASTextDecoration` object. +/// Use this attribute to add underline to a range of text. +/// The underline will be drawn below text glyphs. +UIKIT_EXTERN NSString *const ASTextUnderlineAttributeName; + +/// The value of this attribute is a `ASTextDecoration` object. +/// Use this attribute to add strikethrough (delete line) to a range of text. +/// The strikethrough will be drawn above text glyphs. +UIKIT_EXTERN NSString *const ASTextStrikethroughAttributeName; + +/// The value of this attribute is a `ASTextBorder` object. +/// Use this attribute to add cover border or cover color to a range of text. +/// The border will be drawn above the text glyphs. +UIKIT_EXTERN NSString *const ASTextBorderAttributeName; + +/// The value of this attribute is a `ASTextBorder` object. +/// Use this attribute to add background border or background color to a range of text. +/// The border will be drawn below the text glyphs. +UIKIT_EXTERN NSString *const ASTextBackgroundBorderAttributeName; + +/// The value of this attribute is a `ASTextBorder` object. +/// Use this attribute to add a code block border to one or more line of text. +/// The border will be drawn below the text glyphs. +UIKIT_EXTERN NSString *const ASTextBlockBorderAttributeName; + +/// The value of this attribute is a `ASTextAttachment` object. +/// Use this attribute to add attachment to text. +/// It should be used in conjunction with a CTRunDelegate. +UIKIT_EXTERN NSString *const ASTextAttachmentAttributeName; + +/// The value of this attribute is a `ASTextHighlight` object. +/// Use this attribute to add a touchable highlight state to a range of text. +UIKIT_EXTERN NSString *const ASTextHighlightAttributeName; + +/// The value of this attribute is a `NSValue` object stores CGAffineTransform. +/// Use this attribute to add transform to each glyph in a range of text. +UIKIT_EXTERN NSString *const ASTextGlyphTransformAttributeName; + + + +#pragma mark - String Token Define + +UIKIT_EXTERN NSString *const ASTextAttachmentToken; ///< Object replacement character (U+FFFC), used for text attachment. +UIKIT_EXTERN NSString *const ASTextTruncationToken; ///< Horizontal ellipsis (U+2026), used for text truncation "โ€ฆ". + + + +#pragma mark - Attribute Value Define + +/** + The tap/long press action callback defined in ASText. + + @param containerView The text container view (such as ASLabel/ASTextView). + @param text The whole text. + @param range The text range in `text` (if no range, the range.location is NSNotFound). + @param rect The text frame in `containerView` (if no data, the rect is CGRectNull). + */ +typedef void(^ASTextAction)(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect); + + +/** + ASTextBackedString objects are used by the NSAttributedString class cluster + as the values for text backed string attributes (stored in the attributed + string under the key named ASTextBackedStringAttributeName). + + It may used for copy/paste plain text from attributed string. + Example: If :) is replace by a custom emoji (such as๐Ÿ˜Š), the backed string can be set to @":)". + */ +@interface ASTextBackedString : NSObject ++ (instancetype)stringWithString:(nullable NSString *)string; +@property (nullable, nonatomic, copy) NSString *string; ///< backed string +@end + + +/** + ASTextBinding objects are used by the NSAttributedString class cluster + as the values for shadow attributes (stored in the attributed string under + the key named ASTextBindingAttributeName). + + Add this to a range of text will make the specified characters 'binding together'. + ASTextView will treat the range of text as a single character during text + selection and edit. + */ +@interface ASTextBinding : NSObject ++ (instancetype)bindingWithDeleteConfirm:(BOOL)deleteConfirm; +@property (nonatomic) BOOL deleteConfirm; ///< confirm the range when delete in ASTextView +@end + + +/** + ASTextShadow objects are used by the NSAttributedString class cluster + as the values for shadow attributes (stored in the attributed string under + the key named ASTextShadowAttributeName or ASTextInnerShadowAttributeName). + + It's similar to `NSShadow`, but offers more options. + */ +@interface ASTextShadow : NSObject ++ (instancetype)shadowWithColor:(nullable UIColor *)color offset:(CGSize)offset radius:(CGFloat)radius; + +@property (nullable, nonatomic, strong) UIColor *color; ///< shadow color +@property (nonatomic) CGSize offset; ///< shadow offset +@property (nonatomic) CGFloat radius; ///< shadow blur radius +@property (nonatomic) CGBlendMode blendMode; ///< shadow blend mode +@property (nullable, nonatomic, strong) ASTextShadow *subShadow; ///< a sub shadow which will be added above the parent shadow + ++ (instancetype)shadowWithNSShadow:(NSShadow *)nsShadow; ///< convert NSShadow to ASTextShadow +- (NSShadow *)nsShadow; ///< convert ASTextShadow to NSShadow +@end + + +/** + ASTextDecorationLine objects are used by the NSAttributedString class cluster + as the values for decoration line attributes (stored in the attributed string under + the key named ASTextUnderlineAttributeName or ASTextStrikethroughAttributeName). + + When it's used as underline, the line is drawn below text glyphs; + when it's used as strikethrough, the line is drawn above text glyphs. + */ +@interface ASTextDecoration : NSObject ++ (instancetype)decorationWithStyle:(ASTextLineStyle)style; ++ (instancetype)decorationWithStyle:(ASTextLineStyle)style width:(nullable NSNumber *)width color:(nullable UIColor *)color; +@property (nonatomic) ASTextLineStyle style; ///< line style +@property (nullable, nonatomic, strong) NSNumber *width; ///< line width (nil means automatic width) +@property (nullable, nonatomic, strong) UIColor *color; ///< line color (nil means automatic color) +@property (nullable, nonatomic, strong) ASTextShadow *shadow; ///< line shadow +@end + + +/** + ASTextBorder objects are used by the NSAttributedString class cluster + as the values for border attributes (stored in the attributed string under + the key named ASTextBorderAttributeName or ASTextBackgroundBorderAttributeName). + + It can be used to draw a border around a range of text, or draw a background + to a range of text. + + Example: + โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ + โ”‚ Text โ”‚ + โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ + */ +@interface ASTextBorder : NSObject ++ (instancetype)borderWithLineStyle:(ASTextLineStyle)lineStyle lineWidth:(CGFloat)width strokeColor:(nullable UIColor *)color; ++ (instancetype)borderWithFillColor:(nullable UIColor *)color cornerRadius:(CGFloat)cornerRadius; +@property (nonatomic) ASTextLineStyle lineStyle; ///< border line style +@property (nonatomic) CGFloat strokeWidth; ///< border line width +@property (nullable, nonatomic, strong) UIColor *strokeColor; ///< border line color +@property (nonatomic) CGLineJoin lineJoin; ///< border line join +@property (nonatomic) UIEdgeInsets insets; ///< border insets for text bounds +@property (nonatomic) CGFloat cornerRadius; ///< border corder radius +@property (nullable, nonatomic, strong) ASTextShadow *shadow; ///< border shadow +@property (nullable, nonatomic, strong) UIColor *fillColor; ///< inner fill color +@end + + +/** + ASTextAttachment objects are used by the NSAttributedString class cluster + as the values for attachment attributes (stored in the attributed string under + the key named ASTextAttachmentAttributeName). + + When display an attributed string which contains `ASTextAttachment` object, + the content will be placed in text metric. If the content is `UIImage`, + then it will be drawn to CGContext; if the content is `UIView` or `CALayer`, + then it will be added to the text container's view or layer. + */ +@interface ASTextAttachment : NSObject ++ (instancetype)attachmentWithContent:(nullable id)content; +@property (nullable, nonatomic, strong) id content; ///< Supported type: UIImage, UIView, CALayer +@property (nonatomic) UIViewContentMode contentMode; ///< Content display mode. +@property (nonatomic) UIEdgeInsets contentInsets; ///< The insets when drawing content. +@property (nullable, nonatomic, strong) NSDictionary *userInfo; ///< The user information dictionary. +@end + + +/** + ASTextHighlight objects are used by the NSAttributedString class cluster + as the values for touchable highlight attributes (stored in the attributed string + under the key named ASTextHighlightAttributeName). + + When display an attributed string in `ASLabel` or `ASTextView`, the range of + highlight text can be toucheds down by users. If a range of text is turned into + highlighted state, the `attributes` in `ASTextHighlight` will be used to modify + (set or remove) the original attributes in the range for display. + */ +@interface ASTextHighlight : NSObject + +/** + Attributes that you can apply to text in an attributed string when highlight. + Key: Same as CoreText/ASText Attribute Name. + Value: Modify attribute value when highlight (NSNull for remove attribute). + */ +@property (nullable, nonatomic, copy) NSDictionary *attributes; + +/** + Creates a highlight object with specified attributes. + + @param attributes The attributes which will replace original attributes when highlight, + If the value is NSNull, it will removed when highlight. + */ ++ (instancetype)highlightWithAttributes:(nullable NSDictionary *)attributes; + +/** + Convenience methods to create a default highlight with the specifeid background color. + + @param color The background border color. + */ ++ (instancetype)highlightWithBackgroundColor:(nullable UIColor *)color; + +// Convenience methods below to set the `attributes`. +- (void)setFont:(nullable UIFont *)font; +- (void)setColor:(nullable UIColor *)color; +- (void)setStrokeWidth:(nullable NSNumber *)width; +- (void)setStrokeColor:(nullable UIColor *)color; +- (void)setShadow:(nullable ASTextShadow *)shadow; +- (void)setInnerShadow:(nullable ASTextShadow *)shadow; +- (void)setUnderline:(nullable ASTextDecoration *)underline; +- (void)setStrikethrough:(nullable ASTextDecoration *)strikethrough; +- (void)setBackgroundBorder:(nullable ASTextBorder *)border; +- (void)setBorder:(nullable ASTextBorder *)border; +- (void)setAttachment:(nullable ASTextAttachment *)attachment; + +/** + The user information dictionary, default is nil. + */ +@property (nullable, nonatomic, copy) NSDictionary *userInfo; + +/** + Tap action when user tap the highlight, default is nil. + If the value is nil, ASTextView or ASLabel will ask it's delegate to handle the tap action. + */ +@property (nullable, nonatomic, copy) ASTextAction tapAction; + +/** + Long press action when user long press the highlight, default is nil. + If the value is nil, ASTextView or ASLabel will ask it's delegate to handle the long press action. + */ +@property (nullable, nonatomic, copy) ASTextAction longPressAction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/TextExperiment/String/ASTextAttribute.m b/Source/Private/TextExperiment/String/ASTextAttribute.m new file mode 100755 index 0000000000..ffdcf34928 --- /dev/null +++ b/Source/Private/TextExperiment/String/ASTextAttribute.m @@ -0,0 +1,485 @@ +// +// ASTextAttribute.m +// Modified from YYText +// +// Created by ibireme on 14/10/26. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import "ASTextAttribute.h" +#import +#import +#import + +NSString *const ASTextBackedStringAttributeName = @"ASTextBackedString"; +NSString *const ASTextBindingAttributeName = @"ASTextBinding"; +NSString *const ASTextShadowAttributeName = @"ASTextShadow"; +NSString *const ASTextInnerShadowAttributeName = @"ASTextInnerShadow"; +NSString *const ASTextUnderlineAttributeName = @"ASTextUnderline"; +NSString *const ASTextStrikethroughAttributeName = @"ASTextStrikethrough"; +NSString *const ASTextBorderAttributeName = @"ASTextBorder"; +NSString *const ASTextBackgroundBorderAttributeName = @"ASTextBackgroundBorder"; +NSString *const ASTextBlockBorderAttributeName = @"ASTextBlockBorder"; +NSString *const ASTextAttachmentAttributeName = @"ASTextAttachment"; +NSString *const ASTextHighlightAttributeName = @"ASTextHighlight"; +NSString *const ASTextGlyphTransformAttributeName = @"ASTextGlyphTransform"; + +NSString *const ASTextAttachmentToken = @"\uFFFC"; +NSString *const ASTextTruncationToken = @"\u2026"; + + +ASTextAttributeType ASTextAttributeGetType(NSString *name){ + if (name.length == 0) return ASTextAttributeTypeNone; + + static NSMutableDictionary *dic; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dic = [NSMutableDictionary new]; + NSNumber *All = @(ASTextAttributeTypeUIKit | ASTextAttributeTypeCoreText | ASTextAttributeTypeASText); + NSNumber *CoreText_ASText = @(ASTextAttributeTypeCoreText | ASTextAttributeTypeASText); + NSNumber *UIKit_ASText = @(ASTextAttributeTypeUIKit | ASTextAttributeTypeASText); + NSNumber *UIKit_CoreText = @(ASTextAttributeTypeUIKit | ASTextAttributeTypeCoreText); + NSNumber *UIKit = @(ASTextAttributeTypeUIKit); + NSNumber *CoreText = @(ASTextAttributeTypeCoreText); + NSNumber *ASText = @(ASTextAttributeTypeASText); + + dic[NSFontAttributeName] = All; + dic[NSKernAttributeName] = All; + dic[NSForegroundColorAttributeName] = UIKit; + dic[(id)kCTForegroundColorAttributeName] = CoreText; + dic[(id)kCTForegroundColorFromContextAttributeName] = CoreText; + dic[NSBackgroundColorAttributeName] = UIKit; + dic[NSStrokeWidthAttributeName] = All; + dic[NSStrokeColorAttributeName] = UIKit; + dic[(id)kCTStrokeColorAttributeName] = CoreText_ASText; + dic[NSShadowAttributeName] = UIKit_ASText; + dic[NSStrikethroughStyleAttributeName] = UIKit; + dic[NSUnderlineStyleAttributeName] = UIKit_CoreText; + dic[(id)kCTUnderlineColorAttributeName] = CoreText; + dic[NSLigatureAttributeName] = All; + dic[(id)kCTSuperscriptAttributeName] = UIKit; //it's a CoreText attrubite, but only supported by UIKit... + dic[NSVerticalGlyphFormAttributeName] = All; + dic[(id)kCTGlyphInfoAttributeName] = CoreText_ASText; + dic[(id)kCTCharacterShapeAttributeName] = CoreText_ASText; + dic[(id)kCTRunDelegateAttributeName] = CoreText_ASText; + dic[(id)kCTBaselineClassAttributeName] = CoreText_ASText; + dic[(id)kCTBaselineInfoAttributeName] = CoreText_ASText; + dic[(id)kCTBaselineReferenceInfoAttributeName] = CoreText_ASText; + dic[(id)kCTWritingDirectionAttributeName] = CoreText_ASText; + dic[NSParagraphStyleAttributeName] = All; + + dic[NSStrikethroughColorAttributeName] = UIKit; + dic[NSUnderlineColorAttributeName] = UIKit; + dic[NSTextEffectAttributeName] = UIKit; + dic[NSObliquenessAttributeName] = UIKit; + dic[NSExpansionAttributeName] = UIKit; + dic[(id)kCTLanguageAttributeName] = CoreText_ASText; + dic[NSBaselineOffsetAttributeName] = UIKit; + dic[NSWritingDirectionAttributeName] = All; + dic[NSAttachmentAttributeName] = UIKit; + dic[NSLinkAttributeName] = UIKit; + dic[(id)kCTRubyAnnotationAttributeName] = CoreText; + + dic[ASTextBackedStringAttributeName] = ASText; + dic[ASTextBindingAttributeName] = ASText; + dic[ASTextShadowAttributeName] = ASText; + dic[ASTextInnerShadowAttributeName] = ASText; + dic[ASTextUnderlineAttributeName] = ASText; + dic[ASTextStrikethroughAttributeName] = ASText; + dic[ASTextBorderAttributeName] = ASText; + dic[ASTextBackgroundBorderAttributeName] = ASText; + dic[ASTextBlockBorderAttributeName] = ASText; + dic[ASTextAttachmentAttributeName] = ASText; + dic[ASTextHighlightAttributeName] = ASText; + dic[ASTextGlyphTransformAttributeName] = ASText; + }); + NSNumber *num = dic[name]; + if (num) return num.integerValue; + return ASTextAttributeTypeNone; +} + + +@implementation ASTextBackedString + ++ (instancetype)stringWithString:(NSString *)string { + ASTextBackedString *one = [self new]; + one.string = string; + return one; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:self.string forKey:@"string"]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + _string = [aDecoder decodeObjectForKey:@"string"]; + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + typeof(self) one = [self.class new]; + one.string = self.string; + return one; +} + +@end + + +@implementation ASTextBinding + ++ (instancetype)bindingWithDeleteConfirm:(BOOL)deleteConfirm { + ASTextBinding *one = [self new]; + one.deleteConfirm = deleteConfirm; + return one; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:@(self.deleteConfirm) forKey:@"deleteConfirm"]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + _deleteConfirm = ((NSNumber *)[aDecoder decodeObjectForKey:@"deleteConfirm"]).boolValue; + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + typeof(self) one = [self.class new]; + one.deleteConfirm = self.deleteConfirm; + return one; +} + +@end + + +@implementation ASTextShadow + ++ (instancetype)shadowWithColor:(UIColor *)color offset:(CGSize)offset radius:(CGFloat)radius { + ASTextShadow *one = [self new]; + one.color = color; + one.offset = offset; + one.radius = radius; + return one; +} + ++ (instancetype)shadowWithNSShadow:(NSShadow *)nsShadow { + if (!nsShadow) return nil; + ASTextShadow *shadow = [self new]; + shadow.offset = nsShadow.shadowOffset; + shadow.radius = nsShadow.shadowBlurRadius; + id color = nsShadow.shadowColor; + if (color) { + if (CGColorGetTypeID() == CFGetTypeID((__bridge CFTypeRef)(color))) { + color = [UIColor colorWithCGColor:(__bridge CGColorRef)(color)]; + } + if ([color isKindOfClass:[UIColor class]]) { + shadow.color = color; + } + } + return shadow; +} + +- (NSShadow *)nsShadow { + NSShadow *shadow = [NSShadow new]; + shadow.shadowOffset = self.offset; + shadow.shadowBlurRadius = self.radius; + shadow.shadowColor = self.color; + return shadow; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:self.color forKey:@"color"]; + [aCoder encodeObject:@(self.radius) forKey:@"radius"]; + [aCoder encodeObject:[NSValue valueWithCGSize:self.offset] forKey:@"offset"]; + [aCoder encodeObject:self.subShadow forKey:@"subShadow"]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + _color = [aDecoder decodeObjectForKey:@"color"]; + _radius = ((NSNumber *)[aDecoder decodeObjectForKey:@"radius"]).floatValue; + _offset = ((NSValue *)[aDecoder decodeObjectForKey:@"offset"]).CGSizeValue; + _subShadow = [aDecoder decodeObjectForKey:@"subShadow"]; + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + typeof(self) one = [self.class new]; + one.color = self.color; + one.radius = self.radius; + one.offset = self.offset; + one.subShadow = self.subShadow.copy; + return one; +} + +@end + + +@implementation ASTextDecoration + +- (instancetype)init { + self = [super init]; + _style = ASTextLineStyleSingle; + return self; +} + ++ (instancetype)decorationWithStyle:(ASTextLineStyle)style { + ASTextDecoration *one = [self new]; + one.style = style; + return one; +} ++ (instancetype)decorationWithStyle:(ASTextLineStyle)style width:(NSNumber *)width color:(UIColor *)color { + ASTextDecoration *one = [self new]; + one.style = style; + one.width = width; + one.color = color; + return one; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:@(self.style) forKey:@"style"]; + [aCoder encodeObject:self.width forKey:@"width"]; + [aCoder encodeObject:self.color forKey:@"color"]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + self.style = ((NSNumber *)[aDecoder decodeObjectForKey:@"style"]).unsignedIntegerValue; + self.width = [aDecoder decodeObjectForKey:@"width"]; + self.color = [aDecoder decodeObjectForKey:@"color"]; + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + typeof(self) one = [self.class new]; + one.style = self.style; + one.width = self.width; + one.color = self.color; + return one; +} + +@end + + +@implementation ASTextBorder + ++ (instancetype)borderWithLineStyle:(ASTextLineStyle)lineStyle lineWidth:(CGFloat)width strokeColor:(UIColor *)color { + ASTextBorder *one = [self new]; + one.lineStyle = lineStyle; + one.strokeWidth = width; + one.strokeColor = color; + return one; +} + ++ (instancetype)borderWithFillColor:(UIColor *)color cornerRadius:(CGFloat)cornerRadius { + ASTextBorder *one = [self new]; + one.fillColor = color; + one.cornerRadius = cornerRadius; + one.insets = UIEdgeInsetsMake(-2, 0, 0, -2); + return one; +} + +- (instancetype)init { + self = [super init]; + self.lineStyle = ASTextLineStyleSingle; + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:@(self.lineStyle) forKey:@"lineStyle"]; + [aCoder encodeObject:@(self.strokeWidth) forKey:@"strokeWidth"]; + [aCoder encodeObject:self.strokeColor forKey:@"strokeColor"]; + [aCoder encodeObject:@(self.lineJoin) forKey:@"lineJoin"]; + [aCoder encodeObject:[NSValue valueWithUIEdgeInsets:self.insets] forKey:@"insets"]; + [aCoder encodeObject:@(self.cornerRadius) forKey:@"cornerRadius"]; + [aCoder encodeObject:self.shadow forKey:@"shadow"]; + [aCoder encodeObject:self.fillColor forKey:@"fillColor"]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + _lineStyle = ((NSNumber *)[aDecoder decodeObjectForKey:@"lineStyle"]).unsignedIntegerValue; + _strokeWidth = ((NSNumber *)[aDecoder decodeObjectForKey:@"strokeWidth"]).doubleValue; + _strokeColor = [aDecoder decodeObjectForKey:@"strokeColor"]; + _lineJoin = (CGLineJoin)((NSNumber *)[aDecoder decodeObjectForKey:@"join"]).unsignedIntegerValue; + _insets = ((NSValue *)[aDecoder decodeObjectForKey:@"insets"]).UIEdgeInsetsValue; + _cornerRadius = ((NSNumber *)[aDecoder decodeObjectForKey:@"cornerRadius"]).doubleValue; + _shadow = [aDecoder decodeObjectForKey:@"shadow"]; + _fillColor = [aDecoder decodeObjectForKey:@"fillColor"]; + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + typeof(self) one = [self.class new]; + one.lineStyle = self.lineStyle; + one.strokeWidth = self.strokeWidth; + one.strokeColor = self.strokeColor; + one.lineJoin = self.lineJoin; + one.insets = self.insets; + one.cornerRadius = self.cornerRadius; + one.shadow = self.shadow.copy; + one.fillColor = self.fillColor; + return one; +} + +@end + + +@implementation ASTextAttachment + ++ (instancetype)attachmentWithContent:(id)content { + ASTextAttachment *one = [self new]; + one.content = content; + return one; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:self.content forKey:@"content"]; + [aCoder encodeObject:[NSValue valueWithUIEdgeInsets:self.contentInsets] forKey:@"contentInsets"]; + [aCoder encodeObject:self.userInfo forKey:@"userInfo"]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + _content = [aDecoder decodeObjectForKey:@"content"]; + _contentInsets = ((NSValue *)[aDecoder decodeObjectForKey:@"contentInsets"]).UIEdgeInsetsValue; + _userInfo = [aDecoder decodeObjectForKey:@"userInfo"]; + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + typeof(self) one = [self.class new]; + if ([self.content respondsToSelector:@selector(copy)]) { + one.content = [self.content copy]; + } else { + one.content = self.content; + } + one.contentInsets = self.contentInsets; + one.userInfo = self.userInfo.copy; + return one; +} + +@end + + +@implementation ASTextHighlight + ++ (instancetype)highlightWithAttributes:(NSDictionary *)attributes { + ASTextHighlight *one = [self new]; + one.attributes = attributes; + return one; +} + ++ (instancetype)highlightWithBackgroundColor:(UIColor *)color { + ASTextBorder *highlightBorder = [ASTextBorder new]; + highlightBorder.insets = UIEdgeInsetsMake(-2, -1, -2, -1); + highlightBorder.cornerRadius = 3; + highlightBorder.fillColor = color; + + ASTextHighlight *one = [self new]; + [one setBackgroundBorder:highlightBorder]; + return one; +} + +- (void)setAttributes:(NSDictionary *)attributes { + _attributes = attributes.mutableCopy; +} + +- (id)copyWithZone:(NSZone *)zone { + typeof(self) one = [self.class new]; + one.attributes = self.attributes.mutableCopy; + return one; +} + +- (void)_makeMutableAttributes { + if (!_attributes) { + _attributes = [NSMutableDictionary new]; + } else if (![_attributes isKindOfClass:[NSMutableDictionary class]]) { + _attributes = _attributes.mutableCopy; + } +} + +- (void)setFont:(UIFont *)font { + [self _makeMutableAttributes]; + if (font == (id)[NSNull null] || font == nil) { + ((NSMutableDictionary *)_attributes)[(id)kCTFontAttributeName] = [NSNull null]; + } else { + CTFontRef ctFont = CTFontCreateWithName((__bridge CFStringRef)font.fontName, font.pointSize, NULL); + if (ctFont) { + ((NSMutableDictionary *)_attributes)[(id)kCTFontAttributeName] = (__bridge id)(ctFont); + CFRelease(ctFont); + } + } +} + +- (void)setColor:(UIColor *)color { + [self _makeMutableAttributes]; + if (color == (id)[NSNull null] || color == nil) { + ((NSMutableDictionary *)_attributes)[(id)kCTForegroundColorAttributeName] = [NSNull null]; + ((NSMutableDictionary *)_attributes)[NSForegroundColorAttributeName] = [NSNull null]; + } else { + ((NSMutableDictionary *)_attributes)[(id)kCTForegroundColorAttributeName] = (__bridge id)(color.CGColor); + ((NSMutableDictionary *)_attributes)[NSForegroundColorAttributeName] = color; + } +} + +- (void)setStrokeWidth:(NSNumber *)width { + [self _makeMutableAttributes]; + if (width == (id)[NSNull null] || width == nil) { + ((NSMutableDictionary *)_attributes)[(id)kCTStrokeWidthAttributeName] = [NSNull null]; + } else { + ((NSMutableDictionary *)_attributes)[(id)kCTStrokeWidthAttributeName] = width; + } +} + +- (void)setStrokeColor:(UIColor *)color { + [self _makeMutableAttributes]; + if (color == (id)[NSNull null] || color == nil) { + ((NSMutableDictionary *)_attributes)[(id)kCTStrokeColorAttributeName] = [NSNull null]; + ((NSMutableDictionary *)_attributes)[NSStrokeColorAttributeName] = [NSNull null]; + } else { + ((NSMutableDictionary *)_attributes)[(id)kCTStrokeColorAttributeName] = (__bridge id)(color.CGColor); + ((NSMutableDictionary *)_attributes)[NSStrokeColorAttributeName] = color; + } +} + +- (void)setTextAttribute:(NSString *)attribute value:(id)value { + [self _makeMutableAttributes]; + if (value == nil) value = [NSNull null]; + ((NSMutableDictionary *)_attributes)[attribute] = value; +} + +- (void)setShadow:(ASTextShadow *)shadow { + [self setTextAttribute:ASTextShadowAttributeName value:shadow]; +} + +- (void)setInnerShadow:(ASTextShadow *)shadow { + [self setTextAttribute:ASTextInnerShadowAttributeName value:shadow]; +} + +- (void)setUnderline:(ASTextDecoration *)underline { + [self setTextAttribute:ASTextUnderlineAttributeName value:underline]; +} + +- (void)setStrikethrough:(ASTextDecoration *)strikethrough { + [self setTextAttribute:ASTextStrikethroughAttributeName value:strikethrough]; +} + +- (void)setBackgroundBorder:(ASTextBorder *)border { + [self setTextAttribute:ASTextBackgroundBorderAttributeName value:border]; +} + +- (void)setBorder:(ASTextBorder *)border { + [self setTextAttribute:ASTextBorderAttributeName value:border]; +} + +- (void)setAttachment:(ASTextAttachment *)attachment { + [self setTextAttribute:ASTextAttachmentAttributeName value:attachment]; +} + +@end + diff --git a/Source/Private/TextExperiment/String/ASTextRunDelegate.h b/Source/Private/TextExperiment/String/ASTextRunDelegate.h new file mode 100755 index 0000000000..80bbc83aec --- /dev/null +++ b/Source/Private/TextExperiment/String/ASTextRunDelegate.h @@ -0,0 +1,68 @@ +// +// ASTextRunDelegate.h +// ASText +// +// Created by ibireme on 14/10/14. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Wrapper for CTRunDelegateRef. + + Example: + + ASTextRunDelegate *delegate = [ASTextRunDelegate new]; + delegate.ascent = 20; + delegate.descent = 4; + delegate.width = 20; + CTRunDelegateRef ctRunDelegate = delegate.CTRunDelegate; + if (ctRunDelegate) { + /// add to attributed string + CFRelease(ctRunDelegate); + } + + */ +@interface ASTextRunDelegate : NSObject + +/** + Creates and returns the CTRunDelegate. + + @discussion You need call CFRelease() after used. + The CTRunDelegateRef has a strong reference to this ASTextRunDelegate object. + In CoreText, use CTRunDelegateGetRefCon() to get this ASTextRunDelegate object. + + @return The CTRunDelegate object. + */ +- (nullable CTRunDelegateRef)CTRunDelegate CF_RETURNS_RETAINED; + +/** + Additional information about the the run delegate. + */ +@property (nullable, nonatomic, strong) NSDictionary *userInfo; + +/** + The typographic ascent of glyphs in the run. + */ +@property (nonatomic) CGFloat ascent; + +/** + The typographic descent of glyphs in the run. + */ +@property (nonatomic) CGFloat descent; + +/** + The typographic width of glyphs in the run. + */ +@property (nonatomic) CGFloat width; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/TextExperiment/String/ASTextRunDelegate.m b/Source/Private/TextExperiment/String/ASTextRunDelegate.m new file mode 100755 index 0000000000..94c740cae1 --- /dev/null +++ b/Source/Private/TextExperiment/String/ASTextRunDelegate.m @@ -0,0 +1,71 @@ +// +// ASTextRunDelegate.m +// ASText +// +// Created by ibireme on 14/10/14. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import "ASTextRunDelegate.h" + +static void DeallocCallback(void *ref) { + ASTextRunDelegate *self = (__bridge_transfer ASTextRunDelegate *)(ref); + self = nil; // release +} + +static CGFloat GetAscentCallback(void *ref) { + ASTextRunDelegate *self = (__bridge ASTextRunDelegate *)(ref); + return self.ascent; +} + +static CGFloat GetDecentCallback(void *ref) { + ASTextRunDelegate *self = (__bridge ASTextRunDelegate *)(ref); + return self.descent; +} + +static CGFloat GetWidthCallback(void *ref) { + ASTextRunDelegate *self = (__bridge ASTextRunDelegate *)(ref); + return self.width; +} + +@implementation ASTextRunDelegate + +- (CTRunDelegateRef)CTRunDelegate CF_RETURNS_RETAINED { + CTRunDelegateCallbacks callbacks; + callbacks.version = kCTRunDelegateCurrentVersion; + callbacks.dealloc = DeallocCallback; + callbacks.getAscent = GetAscentCallback; + callbacks.getDescent = GetDecentCallback; + callbacks.getWidth = GetWidthCallback; + return CTRunDelegateCreate(&callbacks, (__bridge_retained void *)(self.copy)); +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:@(_ascent) forKey:@"ascent"]; + [aCoder encodeObject:@(_descent) forKey:@"descent"]; + [aCoder encodeObject:@(_width) forKey:@"width"]; + [aCoder encodeObject:_userInfo forKey:@"userInfo"]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + _ascent = ((NSNumber *)[aDecoder decodeObjectForKey:@"ascent"]).floatValue; + _descent = ((NSNumber *)[aDecoder decodeObjectForKey:@"descent"]).floatValue; + _width = ((NSNumber *)[aDecoder decodeObjectForKey:@"width"]).floatValue; + _userInfo = [aDecoder decodeObjectForKey:@"userInfo"]; + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + typeof(self) one = [self.class new]; + one.ascent = self.ascent; + one.descent = self.descent; + one.width = self.width; + one.userInfo = self.userInfo; + return one; +} + +@end diff --git a/Source/Private/TextExperiment/Utility/ASTextUtilities.h b/Source/Private/TextExperiment/Utility/ASTextUtilities.h new file mode 100755 index 0000000000..55e8aa5b5b --- /dev/null +++ b/Source/Private/TextExperiment/Utility/ASTextUtilities.h @@ -0,0 +1,319 @@ +// +// ASTextUtilities.h +// Modified from YYText +// +// Created by ibireme on 15/4/6. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import +#import +#import +#import +#import + + +#ifndef ASTEXT_CLAMP // return the clamped value +#define ASTEXT_CLAMP(_x_, _low_, _high_) (((_x_) > (_high_)) ? (_high_) : (((_x_) < (_low_)) ? (_low_) : (_x_))) +#endif + +#ifndef ASTEXT_SWAP // swap two value +#define ASTEXT_SWAP(_a_, _b_) do { __typeof__(_a_) _tmp_ = (_a_); (_a_) = (_b_); (_b_) = _tmp_; } while (0) +#endif + +NS_ASSUME_NONNULL_BEGIN + +/** + Whether the character is 'line break char': + U+000D (\\r or CR) + U+2028 (Unicode line separator) + U+000A (\\n or LF) + U+2029 (Unicode paragraph separator) + + @param c A character + @return YES or NO. + */ +static inline BOOL ASTextIsLinebreakChar(unichar c) { + switch (c) { + case 0x000D: + case 0x2028: + case 0x000A: + case 0x2029: + return YES; + default: + return NO; + } +} + +/** + Whether the string is a 'line break': + U+000D (\\r or CR) + U+2028 (Unicode line separator) + U+000A (\\n or LF) + U+2029 (Unicode paragraph separator) + \\r\\n, in that order (also known as CRLF) + + @param str A string + @return YES or NO. + */ +static inline BOOL ASTextIsLinebreakString(NSString * _Nullable str) { + if (str.length > 2 || str.length == 0) return NO; + if (str.length == 1) { + unichar c = [str characterAtIndex:0]; + return ASTextIsLinebreakChar(c); + } else { + return ([str characterAtIndex:0] == '\r') && ([str characterAtIndex:1] == '\n'); + } +} + +/** + If the string has a 'line break' suffix, return the 'line break' length. + + @param str A string. + @return The length of the tail line break: 0, 1 or 2. + */ +static inline NSUInteger ASTextLinebreakTailLength(NSString * _Nullable str) { + if (str.length >= 2) { + unichar c2 = [str characterAtIndex:str.length - 1]; + if (ASTextIsLinebreakChar(c2)) { + unichar c1 = [str characterAtIndex:str.length - 2]; + if (c1 == '\r' && c2 == '\n') return 2; + else return 1; + } else { + return 0; + } + } else if (str.length == 1) { + return ASTextIsLinebreakChar([str characterAtIndex:0]) ? 1 : 0; + } else { + return 0; + } +} + +/** + Whether the font contains color bitmap glyphs. + + @discussion Only `AppleColorEmoji` contains color bitmap glyphs in iOS system fonts. + @param font A font. + @return YES: the font contains color bitmap glyphs, NO: the font has no color bitmap glyph. + */ +static inline BOOL ASTextCTFontContainsColorBitmapGlyphs(CTFontRef font) { + return (CTFontGetSymbolicTraits(font) & kCTFontTraitColorGlyphs) != 0; +} + +/** + Get the `AppleColorEmoji` font's ascent with a specified font size. + It may used to create custom emoji. + + @param fontSize The specified font size. + @return The font ascent. + */ +static inline CGFloat ASTextEmojiGetAscentWithFontSize(CGFloat fontSize) { + if (fontSize < 16) { + return 1.25 * fontSize; + } else if (16 <= fontSize && fontSize <= 24) { + return 0.5 * fontSize + 12; + } else { + return fontSize; + } +} + +/** + Get the `AppleColorEmoji` font's descent with a specified font size. + It may used to create custom emoji. + + @param fontSize The specified font size. + @return The font descent. + */ +static inline CGFloat ASTextEmojiGetDescentWithFontSize(CGFloat fontSize) { + if (fontSize < 16) { + return 0.390625 * fontSize; + } else if (16 <= fontSize && fontSize <= 24) { + return 0.15625 * fontSize + 3.75; + } else { + return 0.3125 * fontSize; + } + return 0; +} + +/** + Get the `AppleColorEmoji` font's glyph bounding rect with a specified font size. + It may used to create custom emoji. + + @param fontSize The specified font size. + @return The font glyph bounding rect. + */ +static inline CGRect ASTextEmojiGetGlyphBoundingRectWithFontSize(CGFloat fontSize) { + CGRect rect; + rect.origin.x = 0.75; + rect.size.width = rect.size.height = ASTextEmojiGetAscentWithFontSize(fontSize); + if (fontSize < 16) { + rect.origin.y = -0.2525 * fontSize; + } else if (16 <= fontSize && fontSize <= 24) { + rect.origin.y = 0.1225 * fontSize -6; + } else { + rect.origin.y = -0.1275 * fontSize; + } + return rect; +} + + +/** + Get the character set which should rotate in vertical form. + @return The shared character set. + */ +NSCharacterSet *ASTextVerticalFormRotateCharacterSet(); + +/** + Get the character set which should rotate and move in vertical form. + @return The shared character set. + */ +NSCharacterSet *ASTextVerticalFormRotateAndMoveCharacterSet(); + + +/// Get the transform rotation. +/// @return the rotation in radians [-PI,PI] ([-180ยฐ,180ยฐ]) +static inline CGFloat ASTextCGAffineTransformGetRotation(CGAffineTransform transform) { + return atan2(transform.b, transform.a); +} + +/// Negates/inverts a UIEdgeInsets. +static inline UIEdgeInsets ASTextUIEdgeInsetsInvert(UIEdgeInsets insets) { + return UIEdgeInsetsMake(-insets.top, -insets.left, -insets.bottom, -insets.right); +} + +/** + Returns a rectangle to fit the @param rect with specified content mode. + + @param rect The constrant rect + @param size The content size + @param mode The content mode + @return A rectangle for the given content mode. + @discussion UIViewContentModeRedraw is same as UIViewContentModeScaleToFill. + */ +CGRect ASTextCGRectFitWithContentMode(CGRect rect, CGSize size, UIViewContentMode mode); + +/// Returns the center for the rectangle. +static inline CGPoint ASTextCGRectGetCenter(CGRect rect) { + return CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); +} + +/// Returns the area of the rectangle. +static inline CGFloat ASTextCGRectGetArea(CGRect rect) { + if (CGRectIsNull(rect)) return 0; + rect = CGRectStandardize(rect); + return rect.size.width * rect.size.height; +} + +/// Returns the minmium distance between a point to a rectangle. +static inline CGFloat ASTextCGPointGetDistanceToRect(CGPoint p, CGRect r) { + r = CGRectStandardize(r); + if (CGRectContainsPoint(r, p)) return 0; + CGFloat distV, distH; + if (CGRectGetMinY(r) <= p.y && p.y <= CGRectGetMaxY(r)) { + distV = 0; + } else { + distV = p.y < CGRectGetMinY(r) ? CGRectGetMinY(r) - p.y : p.y - CGRectGetMaxY(r); + } + if (CGRectGetMinX(r) <= p.x && p.x <= CGRectGetMaxX(r)) { + distH = 0; + } else { + distH = p.x < CGRectGetMinX(r) ? CGRectGetMinX(r) - p.x : p.x - CGRectGetMaxX(r); + } + return MAX(distV, distH); +} + +/// Convert point to pixel. +static inline CGFloat ASTextCGFloatToPixel(CGFloat value) { + return value * ASScreenScale(); +} + +/// Convert pixel to point. +static inline CGFloat ASTextCGFloatFromPixel(CGFloat value) { + return value / ASScreenScale(); +} + +/// round point value to .5 pixel for path stroke (odd pixel line width pixel-aligned) +static inline CGFloat ASTextCGFloatPixelHalf(CGFloat value) { + CGFloat scale = ASScreenScale(); + return (floor(value * scale) + 0.5) / scale; +} + +/// floor point value for pixel-aligned +static inline CGPoint ASTextCGPointPixelFloor(CGPoint point) { + CGFloat scale = ASScreenScale(); + return CGPointMake(floor(point.x * scale) / scale, + floor(point.y * scale) / scale); +} + +/// round point value for pixel-aligned +static inline CGPoint ASTextCGPointPixelRound(CGPoint point) { + CGFloat scale = ASScreenScale(); + return CGPointMake(round(point.x * scale) / scale, + round(point.y * scale) / scale); +} + +/// ceil point value for pixel-aligned +static inline CGPoint ASTextCGPointPixelCeil(CGPoint point) { + CGFloat scale = ASScreenScale(); + return CGPointMake(ceil(point.x * scale) / scale, + ceil(point.y * scale) / scale); +} + +/// round point value to .5 pixel for path stroke (odd pixel line width pixel-aligned) +static inline CGPoint ASTextCGPointPixelHalf(CGPoint point) { + CGFloat scale = ASScreenScale(); + return CGPointMake((floor(point.x * scale) + 0.5) / scale, + (floor(point.y * scale) + 0.5) / scale); +} + +/// round point value for pixel-aligned +static inline CGRect ASTextCGRectPixelRound(CGRect rect) { + CGPoint origin = ASTextCGPointPixelRound(rect.origin); + CGPoint corner = ASTextCGPointPixelRound(CGPointMake(rect.origin.x + rect.size.width, + rect.origin.y + rect.size.height)); + return CGRectMake(origin.x, origin.y, corner.x - origin.x, corner.y - origin.y); +} + +/// round point value to .5 pixel for path stroke (odd pixel line width pixel-aligned) +static inline CGRect ASTextCGRectPixelHalf(CGRect rect) { + CGPoint origin = ASTextCGPointPixelHalf(rect.origin); + CGPoint corner = ASTextCGPointPixelHalf(CGPointMake(rect.origin.x + rect.size.width, + rect.origin.y + rect.size.height)); + return CGRectMake(origin.x, origin.y, corner.x - origin.x, corner.y - origin.y); +} + + +static inline UIFont * _Nullable ASTextFontWithBold(UIFont *font) { + return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold] size:font.pointSize]; +} + +static inline UIFont * _Nullable ASTextFontWithItalic(UIFont *font) { + return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitItalic] size:font.pointSize]; +} + +static inline UIFont * _Nullable ASTextFontWithBoldItalic(UIFont *font) { + return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold | UIFontDescriptorTraitItalic] size:font.pointSize]; +} + + + +/** + Convert CFRange to NSRange + @param range CFRange @return NSRange + */ +static inline NSRange ASTextNSRangeFromCFRange(CFRange range) { + return NSMakeRange(range.location, range.length); +} + +/** + Convert NSRange to CFRange + @param range NSRange @return CFRange + */ +static inline CFRange ASTextCFRangeFromNSRange(NSRange range) { + return CFRangeMake(range.location, range.length); +} + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/TextExperiment/Utility/ASTextUtilities.m b/Source/Private/TextExperiment/Utility/ASTextUtilities.m new file mode 100755 index 0000000000..17e6971355 --- /dev/null +++ b/Source/Private/TextExperiment/Utility/ASTextUtilities.m @@ -0,0 +1,146 @@ +// +// ASTextUtilities.m +// Modified from YYText +// +// Created by ibireme on 15/4/6. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import "ASTextUtilities.h" +#import + +NSCharacterSet *ASTextVerticalFormRotateCharacterSet() { + static NSMutableCharacterSet *set; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + set = [NSMutableCharacterSet new]; + [set addCharactersInRange:NSMakeRange(0x1100, 256)]; // Hangul Jamo + [set addCharactersInRange:NSMakeRange(0x2460, 160)]; // Enclosed Alphanumerics + [set addCharactersInRange:NSMakeRange(0x2600, 256)]; // Miscellaneous Symbols + [set addCharactersInRange:NSMakeRange(0x2700, 192)]; // Dingbats + [set addCharactersInRange:NSMakeRange(0x2E80, 128)]; // CJK Radicals Supplement + [set addCharactersInRange:NSMakeRange(0x2F00, 224)]; // Kangxi Radicals + [set addCharactersInRange:NSMakeRange(0x2FF0, 16)]; // Ideographic Description Characters + [set addCharactersInRange:NSMakeRange(0x3000, 64)]; // CJK Symbols and Punctuation + [set removeCharactersInRange:NSMakeRange(0x3008, 10)]; + [set removeCharactersInRange:NSMakeRange(0x3014, 12)]; + [set addCharactersInRange:NSMakeRange(0x3040, 96)]; // Hiragana + [set addCharactersInRange:NSMakeRange(0x30A0, 96)]; // Katakana + [set addCharactersInRange:NSMakeRange(0x3100, 48)]; // Bopomofo + [set addCharactersInRange:NSMakeRange(0x3130, 96)]; // Hangul Compatibility Jamo + [set addCharactersInRange:NSMakeRange(0x3190, 16)]; // Kanbun + [set addCharactersInRange:NSMakeRange(0x31A0, 32)]; // Bopomofo Extended + [set addCharactersInRange:NSMakeRange(0x31C0, 48)]; // CJK Strokes + [set addCharactersInRange:NSMakeRange(0x31F0, 16)]; // Katakana Phonetic Extensions + [set addCharactersInRange:NSMakeRange(0x3200, 256)]; // Enclosed CJK Letters and Months + [set addCharactersInRange:NSMakeRange(0x3300, 256)]; // CJK Compatibility + [set addCharactersInRange:NSMakeRange(0x3400, 2582)]; // CJK Unified Ideographs Extension A + [set addCharactersInRange:NSMakeRange(0x4E00, 20941)]; // CJK Unified Ideographs + [set addCharactersInRange:NSMakeRange(0xAC00, 11172)]; // Hangul Syllables + [set addCharactersInRange:NSMakeRange(0xD7B0, 80)]; // Hangul Jamo Extended-B + [set addCharactersInString:@"๏ฃฟ"]; // U+F8FF (Private Use Area) + [set addCharactersInRange:NSMakeRange(0xF900, 512)]; // CJK Compatibility Ideographs + [set addCharactersInRange:NSMakeRange(0xFE10, 16)]; // Vertical Forms + [set addCharactersInRange:NSMakeRange(0xFF00, 240)]; // Halfwidth and Fullwidth Forms + [set addCharactersInRange:NSMakeRange(0x1F200, 256)]; // Enclosed Ideographic Supplement + [set addCharactersInRange:NSMakeRange(0x1F300, 768)]; // Enclosed Ideographic Supplement + [set addCharactersInRange:NSMakeRange(0x1F600, 80)]; // Emoticons (Emoji) + [set addCharactersInRange:NSMakeRange(0x1F680, 128)]; // Transport and Map Symbols + + // See http://unicode-table.com/ for more information. + }); + return set; +} + +NSCharacterSet *ASTextVerticalFormRotateAndMoveCharacterSet() { + static NSMutableCharacterSet *set; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + set = [NSMutableCharacterSet new]; + [set addCharactersInString:@"๏ผŒใ€‚ใ€๏ผŽ"]; + }); + return set; +} + +CGRect ASTextCGRectFitWithContentMode(CGRect rect, CGSize size, UIViewContentMode mode) { + rect = CGRectStandardize(rect); + size.width = size.width < 0 ? -size.width : size.width; + size.height = size.height < 0 ? -size.height : size.height; + CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); + switch (mode) { + case UIViewContentModeScaleAspectFit: + case UIViewContentModeScaleAspectFill: { + if (rect.size.width < 0.01 || rect.size.height < 0.01 || + size.width < 0.01 || size.height < 0.01) { + rect.origin = center; + rect.size = CGSizeZero; + } else { + CGFloat scale; + if (mode == UIViewContentModeScaleAspectFit) { + if (size.width / size.height < rect.size.width / rect.size.height) { + scale = rect.size.height / size.height; + } else { + scale = rect.size.width / size.width; + } + } else { + if (size.width / size.height < rect.size.width / rect.size.height) { + scale = rect.size.width / size.width; + } else { + scale = rect.size.height / size.height; + } + } + size.width *= scale; + size.height *= scale; + rect.size = size; + rect.origin = CGPointMake(center.x - size.width * 0.5, center.y - size.height * 0.5); + } + } break; + case UIViewContentModeCenter: { + rect.size = size; + rect.origin = CGPointMake(center.x - size.width * 0.5, center.y - size.height * 0.5); + } break; + case UIViewContentModeTop: { + rect.origin.x = center.x - size.width * 0.5; + rect.size = size; + } break; + case UIViewContentModeBottom: { + rect.origin.x = center.x - size.width * 0.5; + rect.origin.y += rect.size.height - size.height; + rect.size = size; + } break; + case UIViewContentModeLeft: { + rect.origin.y = center.y - size.height * 0.5; + rect.size = size; + } break; + case UIViewContentModeRight: { + rect.origin.y = center.y - size.height * 0.5; + rect.origin.x += rect.size.width - size.width; + rect.size = size; + } break; + case UIViewContentModeTopLeft: { + rect.size = size; + } break; + case UIViewContentModeTopRight: { + rect.origin.x += rect.size.width - size.width; + rect.size = size; + } break; + case UIViewContentModeBottomLeft: { + rect.origin.y += rect.size.height - size.height; + rect.size = size; + } break; + case UIViewContentModeBottomRight: { + rect.origin.x += rect.size.width - size.width; + rect.origin.y += rect.size.height - size.height; + rect.size = size; + } break; + case UIViewContentModeScaleToFill: + case UIViewContentModeRedraw: + default: { + rect = rect; + } + } + return rect; +} diff --git a/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.h b/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.h new file mode 100755 index 0000000000..3e103f2c82 --- /dev/null +++ b/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.h @@ -0,0 +1,1393 @@ +// +// NSAttributedString+ASText.h +// Modified from YYText +// +// Created by ibireme on 14/10/7. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import +#import + +#import "ASTextAttribute.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + Get pre-defined attributes from attributed string. + All properties defined in UIKit, CoreText and ASText are included. + */ +@interface NSAttributedString (ASText) + +#pragma mark - Retrieving character attribute information +///============================================================================= +/// @name Retrieving character attribute information +///============================================================================= + +/** + Returns the attributes at first charactor. + */ +@property (nullable, nonatomic, copy, readonly) NSDictionary *as_attributes; + +/** + Returns the attributes for the character at a given index. + + @discussion Raises an `NSRangeException` if index lies beyond the end of the + receiver's characters. + + @param index The index for which to return attributes. + This value must lie within the bounds of the receiver. + + @return The attributes for the character at index. + */ +- (nullable NSDictionary *)as_attributesAtIndex:(NSUInteger)index; + +/** + Returns the value for an attribute with a given name of the character at a given index. + + @discussion Raises an `NSRangeException` if index lies beyond the end of the + receiver's characters. + + @param attributeName The name of an attribute. + @param index The index for which to return attributes. + This value must not exceed the bounds of the receiver. + + @return The value for the attribute named `attributeName` of the character at + index `index`, or nil if there is no such attribute. + */ +- (nullable id)as_attribute:(NSString *)attributeName atIndex:(NSUInteger)index; + + +#pragma mark - Get character attribute as property +///============================================================================= +/// @name Get character attribute as property +///============================================================================= + +/** + The font of the text. (read-only) + + @discussion Default is Helvetica (Neue) 12. + @discussion Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, strong, readonly) UIFont *as_font; +- (nullable UIFont *)as_fontAtIndex:(NSUInteger)index; + +/** + A kerning adjustment. (read-only) + + @discussion Default is standard kerning. The kerning attribute indicate how many + points the following character should be shifted from its default offset as + defined by the current character's font in points; a positive kern indicates a + shift farther along and a negative kern indicates a shift closer to the current + character. If this attribute is not present, standard kerning will be used. + If this attribute is set to 0.0, no kerning will be done at all. + @discussion Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, strong, readonly) NSNumber *as_kern; +- (nullable NSNumber *)as_kernAtIndex:(NSUInteger)index; + +/** + The foreground color. (read-only) + + @discussion Default is Black. + @discussion Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, strong, readonly) UIColor *as_color; +- (nullable UIColor *)as_colorAtIndex:(NSUInteger)index; + +/** + The background color. (read-only) + + @discussion Default is nil (or no background). + @discussion Get this property returns the first character's attribute. + @since UIKit:6.0 + */ +@property (nullable, nonatomic, strong, readonly) UIColor *as_backgroundColor; +- (nullable UIColor *)as_backgroundColorAtIndex:(NSUInteger)index; + +/** + The stroke width. (read-only) + + @discussion Default value is 0.0 (no stroke). This attribute, interpreted as + a percentage of font point size, controls the text drawing mode: positive + values effect drawing with stroke only; negative values are for stroke and fill. + A typical value for outlined text is 3.0. + @discussion Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 + */ +@property (nullable, nonatomic, strong, readonly) NSNumber *as_strokeWidth; +- (nullable NSNumber *)as_strokeWidthAtIndex:(NSUInteger)index; + +/** + The stroke color. (read-only) + + @discussion Default value is nil (same as foreground color). + @discussion Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 + */ +@property (nullable, nonatomic, strong, readonly) UIColor *as_strokeColor; +- (nullable UIColor *)as_strokeColorAtIndex:(NSUInteger)index; + +/** + The text shadow. (read-only) + + @discussion Default value is nil (no shadow). + @discussion Get this property returns the first character's attribute. + @since UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, strong, readonly) NSShadow *as_shadow; +- (nullable NSShadow *)as_shadowAtIndex:(NSUInteger)index; + +/** + The strikethrough style. (read-only) + + @discussion Default value is NSUnderlineStyleNone (no strikethrough). + @discussion Get this property returns the first character's attribute. + @since UIKit:6.0 + */ +@property (nonatomic, readonly) NSUnderlineStyle as_strikethroughStyle; +- (NSUnderlineStyle)as_strikethroughStyleAtIndex:(NSUInteger)index; + +/** + The strikethrough color. (read-only) + + @discussion Default value is nil (same as foreground color). + @discussion Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic, strong, readonly) UIColor *as_strikethroughColor; +- (nullable UIColor *)as_strikethroughColorAtIndex:(NSUInteger)index; + +/** + The underline style. (read-only) + + @discussion Default value is NSUnderlineStyleNone (no underline). + @discussion Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 + */ +@property (nonatomic, readonly) NSUnderlineStyle as_underlineStyle; +- (NSUnderlineStyle)as_underlineStyleAtIndex:(NSUInteger)index; + +/** + The underline color. (read-only) + + @discussion Default value is nil (same as foreground color). + @discussion Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:7.0 + */ +@property (nullable, nonatomic, strong, readonly) UIColor *as_underlineColor; +- (nullable UIColor *)as_underlineColorAtIndex:(NSUInteger)index; + +/** + Ligature formation control. (read-only) + + @discussion Default is int value 1. The ligature attribute determines what kinds + of ligatures should be used when displaying the string. A value of 0 indicates + that only ligatures essential for proper rendering of text should be used, + 1 indicates that standard ligatures should be used, and 2 indicates that all + available ligatures should be used. Which ligatures are standard depends on the + script and possibly the font. + @discussion Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, strong, readonly) NSNumber *as_ligature; +- (nullable NSNumber *)as_ligatureAtIndex:(NSUInteger)index; + +/** + The text effect. (read-only) + + @discussion Default is nil (no effect). The only currently supported value + is NSTextEffectLetterpressStyle. + @discussion Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic, strong, readonly) NSString *as_textEffect; +- (nullable NSString *)as_textEffectAtIndex:(NSUInteger)index; + +/** + The skew to be applied to glyphs. (read-only) + + @discussion Default is 0 (no skew). + @discussion Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic, strong, readonly) NSNumber *as_obliqueness; +- (nullable NSNumber *)as_obliquenessAtIndex:(NSUInteger)index; + +/** + The log of the expansion factor to be applied to glyphs. (read-only) + + @discussion Default is 0 (no expansion). + @discussion Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic, strong, readonly) NSNumber *as_expansion; +- (nullable NSNumber *)as_expansionAtIndex:(NSUInteger)index; + +/** + The character's offset from the baseline, in points. (read-only) + + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic, strong, readonly) NSNumber *as_baselineOffset; +- (nullable NSNumber *)as_baselineOffsetAtIndex:(NSUInteger)index; + +/** + Glyph orientation control. (read-only) + + @discussion Default is NO. A value of NO indicates that horizontal glyph forms + are to be used, YES indicates that vertical glyph forms are to be used. + @discussion Get this property returns the first character's attribute. + @since CoreText:4.3 ASText:6.0 + */ +@property (nonatomic, readonly) BOOL as_verticalGlyphForm; +- (BOOL)as_verticalGlyphFormAtIndex:(NSUInteger)index; + +/** + Specifies text language. (read-only) + + @discussion Value must be a NSString containing a locale identifier. Default is + unset. When this attribute is set to a valid identifier, it will be used to select + localized glyphs (if supported by the font) and locale-specific line breaking rules. + @discussion Get this property returns the first character's attribute. + @since CoreText:7.0 ASText:7.0 + */ +@property (nullable, nonatomic, strong, readonly) NSString *as_language; +- (nullable NSString *)as_languageAtIndex:(NSUInteger)index; + +/** + Specifies a bidirectional override or embedding. (read-only) + + @discussion See alse NSWritingDirection and NSWritingDirectionAttributeName. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:7.0 ASText:6.0 + */ +@property (nullable, nonatomic, strong, readonly) NSArray *as_writingDirection; +- (nullable NSArray *)as_writingDirectionAtIndex:(NSUInteger)index; + +/** + An NSParagraphStyle object which is used to specify things like + line alignment, tab rulers, writing direction, etc. (read-only) + + @discussion Default is nil ([NSParagraphStyle defaultParagraphStyle]). + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, strong, readonly) NSParagraphStyle *as_paragraphStyle; +- (nullable NSParagraphStyle *)as_paragraphStyleAtIndex:(NSUInteger)index; + +#pragma mark - Get paragraph attribute as property +///============================================================================= +/// @name Get paragraph attribute as property +///============================================================================= + +/** + The text alignment (A wrapper for NSParagraphStyle). (read-only) + + @discussion Natural text alignment is realized as left or right alignment + depending on the line sweep direction of the first script contained in the paragraph. + @discussion Default is NSTextAlignmentNatural. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) NSTextAlignment as_alignment; +- (NSTextAlignment)as_alignmentAtIndex:(NSUInteger)index; + +/** + The mode that should be used to break lines (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property contains the line break mode to be used laying out the paragraph's text. + @discussion Default is NSLineBreakByWordWrapping. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) NSLineBreakMode as_lineBreakMode; +- (NSLineBreakMode)as_lineBreakModeAtIndex:(NSUInteger)index; + +/** + The distance in points between the bottom of one line fragment and the top of the next. + (A wrapper for NSParagraphStyle) (read-only) + + @discussion This value is always nonnegative. This value is included in the line + fragment heights in the layout manager. + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_lineSpacing; +- (CGFloat)as_lineSpacingAtIndex:(NSUInteger)index; + +/** + The space after the end of the paragraph (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property contains the space (measured in points) added at the + end of the paragraph to separate it from the following paragraph. This value must + be nonnegative. The space between paragraphs is determined by adding the previous + paragraph's paragraphSpacing and the current paragraph's paragraphSpacingBefore. + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_paragraphSpacing; +- (CGFloat)as_paragraphSpacingAtIndex:(NSUInteger)index; + +/** + The distance between the paragraph's top and the beginning of its text content. + (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property contains the space (measured in points) between the + paragraph's top and the beginning of its text content. + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_paragraphSpacingBefore; +- (CGFloat)as_paragraphSpacingBeforeAtIndex:(NSUInteger)index; + +/** + The indentation of the first line (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property contains the distance (in points) from the leading margin + of a text container to the beginning of the paragraph's first line. This value + is always nonnegative. + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_firstLineHeadIndent; +- (CGFloat)as_firstLineHeadIndentAtIndex:(NSUInteger)index; + +/** + The indentation of the receiver's lines other than the first. (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property contains the distance (in points) from the leading margin + of a text container to the beginning of lines other than the first. This value is + always nonnegative. + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_headIndent; +- (CGFloat)as_headIndentAtIndex:(NSUInteger)index; + +/** + The trailing indentation (A wrapper for NSParagraphStyle). (read-only) + + @discussion If positive, this value is the distance from the leading margin + (for example, the left margin in left-to-right text). If 0 or negative, it's the + distance from the trailing margin. + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_tailIndent; +- (CGFloat)as_tailIndentAtIndex:(NSUInteger)index; + +/** + The receiver's minimum height (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property contains the minimum height in points that any line in + the receiver will occupy, regardless of the font size or size of any attached graphic. + This value must be nonnegative. + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_minimumLineHeight; +- (CGFloat)as_minimumLineHeightAtIndex:(NSUInteger)index; + +/** + The receiver's maximum line height (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property contains the maximum height in points that any line in + the receiver will occupy, regardless of the font size or size of any attached graphic. + This value is always nonnegative. Glyphs and graphics exceeding this height will + overlap neighboring lines; however, a maximum height of 0 implies no line height limit. + Although this limit applies to the line itself, line spacing adds extra space between adjacent lines. + @discussion Default is 0 (no limit). + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_maximumLineHeight; +- (CGFloat)as_maximumLineHeightAtIndex:(NSUInteger)index; + +/** + The line height multiple (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property contains the line break mode to be used laying out the paragraph's text. + @discussion Default is 0 (no multiple). + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_lineHeightMultiple; +- (CGFloat)as_lineHeightMultipleAtIndex:(NSUInteger)index; + +/** + The base writing direction (A wrapper for NSParagraphStyle). (read-only) + + @discussion If you specify NSWritingDirectionNaturalDirection, the receiver resolves + the writing direction to either NSWritingDirectionLeftToRight or NSWritingDirectionRightToLeft, + depending on the direction for the user's `language` preference setting. + @discussion Default is NSWritingDirectionNatural. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) NSWritingDirection as_baseWritingDirection; +- (NSWritingDirection)as_baseWritingDirectionAtIndex:(NSUInteger)index; + +/** + The paragraph's threshold for hyphenation. (A wrapper for NSParagraphStyle). (read-only) + + @discussion Valid values lie between 0.0 and 1.0 inclusive. Hyphenation is attempted + when the ratio of the text width (as broken without hyphenation) to the width of the + line fragment is less than the hyphenation factor. When the paragraph's hyphenation + factor is 0.0, the layout manager's hyphenation factor is used instead. When both + are 0.0, hyphenation is disabled. + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since UIKit:6.0 + */ +@property (nonatomic, readonly) float as_hyphenationFactor; +- (float)as_hyphenationFactorAtIndex:(NSUInteger)index; + +/** + The document-wide default tab interval (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property represents the default tab interval in points. Tabs after the + last specified in tabStops are placed at integer multiples of this distance (if positive). + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since CoreText:7.0 UIKit:7.0 ASText:7.0 + */ +@property (nonatomic, readonly) CGFloat as_defaultTabInterval; +- (CGFloat)as_defaultTabIntervalAtIndex:(NSUInteger)index; + +/** + An array of NSTextTab objects representing the receiver's tab stops. + (A wrapper for NSParagraphStyle). (read-only) + + @discussion The NSTextTab objects, sorted by location, define the tab stops for + the paragraph style. + @discussion Default is 12 TabStops with 28.0 tab interval. + @discussion Get this property returns the first character's attribute. + @since CoreText:7.0 UIKit:7.0 ASText:7.0 + */ +@property (nullable, nonatomic, copy, readonly) NSArray *as_tabStops; +- (nullable NSArray *)as_tabStopsAtIndex:(NSUInteger)index; + +#pragma mark - Get ASText attribute as property +///============================================================================= +/// @name Get ASText attribute as property +///============================================================================= + +/** + The text shadow. (read-only) + + @discussion Default value is nil (no shadow). + @discussion Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, strong, readonly) ASTextShadow *as_textShadow; +- (nullable ASTextShadow *)as_textShadowAtIndex:(NSUInteger)index; + +/** + The text inner shadow. (read-only) + + @discussion Default value is nil (no shadow). + @discussion Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, strong, readonly) ASTextShadow *as_textInnerShadow; +- (nullable ASTextShadow *)as_textInnerShadowAtIndex:(NSUInteger)index; + +/** + The text underline. (read-only) + + @discussion Default value is nil (no underline). + @discussion Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, strong, readonly) ASTextDecoration *as_textUnderline; +- (nullable ASTextDecoration *)as_textUnderlineAtIndex:(NSUInteger)index; + +/** + The text strikethrough. (read-only) + + @discussion Default value is nil (no strikethrough). + @discussion Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, strong, readonly) ASTextDecoration *as_textStrikethrough; +- (nullable ASTextDecoration *)as_textStrikethroughAtIndex:(NSUInteger)index; + +/** + The text border. (read-only) + + @discussion Default value is nil (no border). + @discussion Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, strong, readonly) ASTextBorder *as_textBorder; +- (nullable ASTextBorder *)as_textBorderAtIndex:(NSUInteger)index; + +/** + The text background border. (read-only) + + @discussion Default value is nil (no background border). + @discussion Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, strong, readonly) ASTextBorder *as_textBackgroundBorder; +- (nullable ASTextBorder *)as_textBackgroundBorderAtIndex:(NSUInteger)index; + +/** + The glyph transform. (read-only) + + @discussion Default value is CGAffineTransformIdentity (no transform). + @discussion Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nonatomic, readonly) CGAffineTransform as_textGlyphTransform; +- (CGAffineTransform)as_textGlyphTransformAtIndex:(NSUInteger)index; + + +#pragma mark - Query for ASText +///============================================================================= +/// @name Query for ASText +///============================================================================= + +/** + Returns the plain text from a range. + If there's `ASTextBackedStringAttributeName` attribute, the backed string will + replace the attributed string range. + + @param range A range in receiver. + @return The plain text. + */ +- (nullable NSString *)as_plainTextForRange:(NSRange)range; + + +#pragma mark - Create attachment string for ASText +///============================================================================= +/// @name Create attachment string for ASText +///============================================================================= + +/** + Creates and returns an attachment. + + @param content The attachment (UIImage/UIView/CALayer). + @param contentMode The attachment's content mode. + @param width The attachment's container width in layout. + @param ascent The attachment's container ascent in layout. + @param descent The attachment's container descent in layout. + + @return An attributed string, or nil if an error occurs. + @since ASText:6.0 + */ ++ (NSMutableAttributedString *)as_attachmentStringWithContent:(nullable id)content + contentMode:(UIViewContentMode)contentMode + width:(CGFloat)width + ascent:(CGFloat)ascent + descent:(CGFloat)descent; + +/** + Creates and returns an attachment. + + + Example: ContentMode:bottom Alignment:Top. + + The text The attachment holder + โ†“ โ†“ + โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + / \ โ”‚ โ”‚ / ___| + / _ \ โ”‚ โ”‚| | + / ___ \ โ”‚ โ”‚| |___ โ†โ”€โ”€ The text line + /_/ \_\โ”‚ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ โ”‚ \____| + โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ”‚ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ โ”‚ + โ”‚ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ โ†โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 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 fontSize The attachment will align to this font. + @param alignment The attachment holder's alignment to text line. + + @return An attributed string, or nil if an error occurs. + @since ASText:6.0 + */ ++ (NSMutableAttributedString *)as_attachmentStringWithContent:(nullable id)content + contentMode:(UIViewContentMode)contentMode + attachmentSize:(CGSize)attachmentSize + alignToFont:(UIFont *)font + alignment:(ASTextVerticalAlignment)alignment; + +/** + Creates and returns an attahment from a fourquare image as if it was an emoji. + + @param image A fourquare image. + @param fontSize The font size. + + @return An attributed string, or nil if an error occurs. + @since ASText:6.0 + */ ++ (nullable NSMutableAttributedString *)as_attachmentStringWithEmojiImage:(UIImage *)image + fontSize:(CGFloat)fontSize; + +#pragma mark - Utility +///============================================================================= +/// @name Utility +///============================================================================= + +/** + Returns NSMakeRange(0, self.length). + */ +- (NSRange)as_rangeOfAll; + +/** + If YES, it share the same attribute in entire text range. + */ +- (BOOL)as_isSharedAttributesInAllRange; + +/** + If YES, it can be drawn with the [drawWithRect:options:context:] method or displayed with UIKit. + If NO, it should be drawn with CoreText or ASText. + + @discussion If the method returns NO, it means that there's at least one attribute + which is not supported by UIKit (such as CTParagraphStyleRef). If display this string + in UIKit, it may lose some attribute, or even crash the app. + */ +- (BOOL)as_canDrawWithUIKit; + +@end + + + + +/** + Set pre-defined attributes to attributed string. + All properties defined in UIKit, CoreText and ASText are included. + */ +@interface NSMutableAttributedString (ASText) + +#pragma mark - Set character attribute +///============================================================================= +/// @name Set character attribute +///============================================================================= + +/** + Sets the attributes to the entire text string. + + @discussion The old attributes will be removed. + + @param attributes A dictionary containing the attributes to set, or nil to remove all attributes. + */ +- (void)as_setAttributes:(nullable NSDictionary *)attributes; +- (void)setAs_attributes:(nullable NSDictionary *)attributes; + +/** + Sets an attribute with the given name and value to the entire text string. + + @param name A string specifying the attribute name. + @param value The attribute value associated with name. Pass `nil` or `NSNull` to + remove the attribute. + */ +- (void)as_setAttribute:(NSString *)name value:(nullable id)value; + +/** + Sets an attribute with the given name and value to the characters in the specified range. + + @param name A string specifying the attribute name. + @param value The attribute value associated with name. Pass `nil` or `NSNull` to + remove the attribute. + @param range The range of characters to which the specified attribute/value pair applies. + */ +- (void)as_setAttribute:(NSString *)name value:(nullable id)value range:(NSRange)range; + +/** + Removes all attributes in the specified range. + + @param range The range of characters. + */ +- (void)as_removeAttributesInRange:(NSRange)range; + + +#pragma mark - Set character attribute as property +///============================================================================= +/// @name Set character attribute as property +///============================================================================= + +/** + The font of the text. + + @discussion Default is Helvetica (Neue) 12. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, strong, readwrite) UIFont *as_font; +- (void)as_setFont:(nullable UIFont *)font range:(NSRange)range; + +/** + A kerning adjustment. + + @discussion Default is standard kerning. The kerning attribute indicate how many + points the following character should be shifted from its default offset as + defined by the current character's font in points; a positive kern indicates a + shift farther along and a negative kern indicates a shift closer to the current + character. If this attribute is not present, standard kerning will be used. + If this attribute is set to 0.0, no kerning will be done at all. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, strong, readwrite) NSNumber *as_kern; +- (void)as_setKern:(nullable NSNumber *)kern range:(NSRange)range; + +/** + The foreground color. + + @discussion Default is Black. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, strong, readwrite) UIColor *as_color; +- (void)as_setColor:(nullable UIColor *)color range:(NSRange)range; + +/** + The background color. + + @discussion Default is nil (or no background). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:6.0 + */ +@property (nullable, nonatomic, strong, readwrite) UIColor *as_backgroundColor; +- (void)as_setBackgroundColor:(nullable UIColor *)backgroundColor range:(NSRange)range; + +/** + The stroke width. + + @discussion Default value is 0.0 (no stroke). This attribute, interpreted as + a percentage of font point size, controls the text drawing mode: positive + values effect drawing with stroke only; negative values are for stroke and fill. + A typical value for outlined text is 3.0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, strong, readwrite) NSNumber *as_strokeWidth; +- (void)as_setStrokeWidth:(nullable NSNumber *)strokeWidth range:(NSRange)range; + +/** + The stroke color. + + @discussion Default value is nil (same as foreground color). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, strong, readwrite) UIColor *as_strokeColor; +- (void)as_setStrokeColor:(nullable UIColor *)strokeColor range:(NSRange)range; + +/** + The text shadow. + + @discussion Default value is nil (no shadow). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, strong, readwrite) NSShadow *as_shadow; +- (void)as_setShadow:(nullable NSShadow *)shadow range:(NSRange)range; + +/** + The strikethrough style. + + @discussion Default value is NSUnderlineStyleNone (no strikethrough). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:6.0 + */ +@property (nonatomic, readwrite) NSUnderlineStyle as_strikethroughStyle; +- (void)as_setStrikethroughStyle:(NSUnderlineStyle)strikethroughStyle range:(NSRange)range; + +/** + The strikethrough color. + + @discussion Default value is nil (same as foreground color). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic, strong, readwrite) UIColor *as_strikethroughColor; +- (void)as_setStrikethroughColor:(nullable UIColor *)strikethroughColor range:(NSRange)range NS_AVAILABLE_IOS(7_0); + +/** + The underline style. + + @discussion Default value is NSUnderlineStyleNone (no underline). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 + */ +@property (nonatomic, readwrite) NSUnderlineStyle as_underlineStyle; +- (void)as_setUnderlineStyle:(NSUnderlineStyle)underlineStyle range:(NSRange)range; + +/** + The underline color. + + @discussion Default value is nil (same as foreground color). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:7.0 + */ +@property (nullable, nonatomic, strong, readwrite) UIColor *as_underlineColor; +- (void)as_setUnderlineColor:(nullable UIColor *)underlineColor range:(NSRange)range; + +/** + Ligature formation control. + + @discussion Default is int value 1. The ligature attribute determines what kinds + of ligatures should be used when displaying the string. A value of 0 indicates + that only ligatures essential for proper rendering of text should be used, + 1 indicates that standard ligatures should be used, and 2 indicates that all + available ligatures should be used. Which ligatures are standard depends on the + script and possibly the font. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, strong, readwrite) NSNumber *as_ligature; +- (void)as_setLigature:(nullable NSNumber *)ligature range:(NSRange)range; + +/** + The text effect. + + @discussion Default is nil (no effect). The only currently supported value + is NSTextEffectLetterpressStyle. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic, strong, readwrite) NSString *as_textEffect; +- (void)as_setTextEffect:(nullable NSString *)textEffect range:(NSRange)range NS_AVAILABLE_IOS(7_0); + +/** + The skew to be applied to glyphs. + + @discussion Default is 0 (no skew). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic, strong, readwrite) NSNumber *as_obliqueness; +- (void)as_setObliqueness:(nullable NSNumber *)obliqueness range:(NSRange)range NS_AVAILABLE_IOS(7_0); + +/** + The log of the expansion factor to be applied to glyphs. + + @discussion Default is 0 (no expansion). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic, strong, readwrite) NSNumber *as_expansion; +- (void)as_setExpansion:(nullable NSNumber *)expansion range:(NSRange)range NS_AVAILABLE_IOS(7_0); + +/** + The character's offset from the baseline, in points. + + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic, strong, readwrite) NSNumber *as_baselineOffset; +- (void)as_setBaselineOffset:(nullable NSNumber *)baselineOffset range:(NSRange)range NS_AVAILABLE_IOS(7_0); + +/** + Glyph orientation control. + + @discussion Default is NO. A value of NO indicates that horizontal glyph forms + are to be used, YES indicates that vertical glyph forms are to be used. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:4.3 ASText:6.0 + */ +@property (nonatomic, readwrite) BOOL as_verticalGlyphForm; +- (void)as_setVerticalGlyphForm:(BOOL)verticalGlyphForm range:(NSRange)range; + +/** + Specifies text language. + + @discussion Value must be a NSString containing a locale identifier. Default is + unset. When this attribute is set to a valid identifier, it will be used to select + localized glyphs (if supported by the font) and locale-specific line breaking rules. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:7.0 ASText:7.0 + */ +@property (nullable, nonatomic, strong, readwrite) NSString *as_language; +- (void)as_setLanguage:(nullable NSString *)language range:(NSRange)range NS_AVAILABLE_IOS(7_0); + +/** + Specifies a bidirectional override or embedding. + + @discussion See alse NSWritingDirection and NSWritingDirectionAttributeName. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:7.0 ASText:6.0 + */ +@property (nullable, nonatomic, strong, readwrite) NSArray *as_writingDirection; +- (void)as_setWritingDirection:(nullable NSArray *)writingDirection range:(NSRange)range; + +/** + An NSParagraphStyle object which is used to specify things like + line alignment, tab rulers, writing direction, etc. + + @discussion Default is nil ([NSParagraphStyle defaultParagraphStyle]). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, strong, readwrite) NSParagraphStyle *as_paragraphStyle; +- (void)as_setParagraphStyle:(nullable NSParagraphStyle *)paragraphStyle range:(NSRange)range; + + +#pragma mark - Set paragraph attribute as property +///============================================================================= +/// @name Set paragraph attribute as property +///============================================================================= + +/** + The text alignment (A wrapper for NSParagraphStyle). + + @discussion Natural text alignment is realized as left or right alignment + depending on the line sweep direction of the first script contained in the paragraph. + @discussion Default is NSTextAlignmentNatural. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readwrite) NSTextAlignment as_alignment; +- (void)as_setAlignment:(NSTextAlignment)alignment range:(NSRange)range; + +/** + The mode that should be used to break lines (A wrapper for NSParagraphStyle). + + @discussion This property contains the line break mode to be used laying out the paragraph's text. + @discussion Default is NSLineBreakByWordWrapping. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readwrite) NSLineBreakMode as_lineBreakMode; +- (void)as_setLineBreakMode:(NSLineBreakMode)lineBreakMode range:(NSRange)range; + +/** + The distance in points between the bottom of one line fragment and the top of the next. + (A wrapper for NSParagraphStyle) + + @discussion This value is always nonnegative. This value is included in the line + fragment heights in the layout manager. + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readwrite) CGFloat as_lineSpacing; +- (void)as_setLineSpacing:(CGFloat)lineSpacing range:(NSRange)range; + +/** + The space after the end of the paragraph (A wrapper for NSParagraphStyle). + + @discussion This property contains the space (measured in points) added at the + end of the paragraph to separate it from the following paragraph. This value must + be nonnegative. The space between paragraphs is determined by adding the previous + paragraph's paragraphSpacing and the current paragraph's paragraphSpacingBefore. + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readwrite) CGFloat as_paragraphSpacing; +- (void)as_setParagraphSpacing:(CGFloat)paragraphSpacing range:(NSRange)range; + +/** + The distance between the paragraph's top and the beginning of its text content. + (A wrapper for NSParagraphStyle). + + @discussion This property contains the space (measured in points) between the + paragraph's top and the beginning of its text content. + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readwrite) CGFloat as_paragraphSpacingBefore; +- (void)as_setParagraphSpacingBefore:(CGFloat)paragraphSpacingBefore range:(NSRange)range; + +/** + The indentation of the first line (A wrapper for NSParagraphStyle). + + @discussion This property contains the distance (in points) from the leading margin + of a text container to the beginning of the paragraph's first line. This value + is always nonnegative. + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readwrite) CGFloat as_firstLineHeadIndent; +- (void)as_setFirstLineHeadIndent:(CGFloat)firstLineHeadIndent range:(NSRange)range; + +/** + The indentation of the receiver's lines other than the first. (A wrapper for NSParagraphStyle). + + @discussion This property contains the distance (in points) from the leading margin + of a text container to the beginning of lines other than the first. This value is + always nonnegative. + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readwrite) CGFloat as_headIndent; +- (void)as_setHeadIndent:(CGFloat)headIndent range:(NSRange)range; + +/** + The trailing indentation (A wrapper for NSParagraphStyle). + + @discussion If positive, this value is the distance from the leading margin + (for example, the left margin in left-to-right text). If 0 or negative, it's the + distance from the trailing margin. + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readwrite) CGFloat as_tailIndent; +- (void)as_setTailIndent:(CGFloat)tailIndent range:(NSRange)range; + +/** + The receiver's minimum height (A wrapper for NSParagraphStyle). + + @discussion This property contains the minimum height in points that any line in + the receiver will occupy, regardless of the font size or size of any attached graphic. + This value must be nonnegative. + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readwrite) CGFloat as_minimumLineHeight; +- (void)as_setMinimumLineHeight:(CGFloat)minimumLineHeight range:(NSRange)range; + +/** + The receiver's maximum line height (A wrapper for NSParagraphStyle). + + @discussion This property contains the maximum height in points that any line in + the receiver will occupy, regardless of the font size or size of any attached graphic. + This value is always nonnegative. Glyphs and graphics exceeding this height will + overlap neighboring lines; however, a maximum height of 0 implies no line height limit. + Although this limit applies to the line itself, line spacing adds extra space between adjacent lines. + @discussion Default is 0 (no limit). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readwrite) CGFloat as_maximumLineHeight; +- (void)as_setMaximumLineHeight:(CGFloat)maximumLineHeight range:(NSRange)range; + +/** + The line height multiple (A wrapper for NSParagraphStyle). + + @discussion This property contains the line break mode to be used laying out the paragraph's text. + @discussion Default is 0 (no multiple). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readwrite) CGFloat as_lineHeightMultiple; +- (void)as_setLineHeightMultiple:(CGFloat)lineHeightMultiple range:(NSRange)range; + +/** + The base writing direction (A wrapper for NSParagraphStyle). + + @discussion If you specify NSWritingDirectionNaturalDirection, the receiver resolves + the writing direction to either NSWritingDirectionLeftToRight or NSWritingDirectionRightToLeft, + depending on the direction for the user's `language` preference setting. + @discussion Default is NSWritingDirectionNatural. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readwrite) NSWritingDirection as_baseWritingDirection; +- (void)as_setBaseWritingDirection:(NSWritingDirection)baseWritingDirection range:(NSRange)range; + +/** + The paragraph's threshold for hyphenation. (A wrapper for NSParagraphStyle). + + @discussion Valid values lie between 0.0 and 1.0 inclusive. Hyphenation is attempted + when the ratio of the text width (as broken without hyphenation) to the width of the + line fragment is less than the hyphenation factor. When the paragraph's hyphenation + factor is 0.0, the layout manager's hyphenation factor is used instead. When both + are 0.0, hyphenation is disabled. + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:6.0 + */ +@property (nonatomic, readwrite) float as_hyphenationFactor; +- (void)as_setHyphenationFactor:(float)hyphenationFactor range:(NSRange)range; + +/** + The document-wide default tab interval (A wrapper for NSParagraphStyle). + + @discussion This property represents the default tab interval in points. Tabs after the + last specified in tabStops are placed at integer multiples of this distance (if positive). + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:7.0 UIKit:7.0 ASText:7.0 + */ +@property (nonatomic, readwrite) CGFloat as_defaultTabInterval; +- (void)as_setDefaultTabInterval:(CGFloat)defaultTabInterval range:(NSRange)range NS_AVAILABLE_IOS(7_0); + +/** + An array of NSTextTab objects representing the receiver's tab stops. + (A wrapper for NSParagraphStyle). + + @discussion The NSTextTab objects, sorted by location, define the tab stops for + the paragraph style. + @discussion Default is 12 TabStops with 28.0 tab interval. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:7.0 UIKit:7.0 ASText:7.0 + */ +@property (nullable, nonatomic, copy, readwrite) NSArray *as_tabStops; +- (void)as_setTabStops:(nullable NSArray *)tabStops range:(NSRange)range NS_AVAILABLE_IOS(7_0); + +#pragma mark - Set ASText attribute as property +///============================================================================= +/// @name Set ASText attribute as property +///============================================================================= + +/** + The text shadow. + + @discussion Default value is nil (no shadow). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, strong, readwrite) ASTextShadow *as_textShadow; +- (void)as_setTextShadow:(nullable ASTextShadow *)textShadow range:(NSRange)range; + +/** + The text inner shadow. + + @discussion Default value is nil (no shadow). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, strong, readwrite) ASTextShadow *as_textInnerShadow; +- (void)as_setTextInnerShadow:(nullable ASTextShadow *)textInnerShadow range:(NSRange)range; + +/** + The text underline. + + @discussion Default value is nil (no underline). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, strong, readwrite) ASTextDecoration *as_textUnderline; +- (void)as_setTextUnderline:(nullable ASTextDecoration *)textUnderline range:(NSRange)range; + +/** + The text strikethrough. + + @discussion Default value is nil (no strikethrough). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, strong, readwrite) ASTextDecoration *as_textStrikethrough; +- (void)as_setTextStrikethrough:(nullable ASTextDecoration *)textStrikethrough range:(NSRange)range; + +/** + The text border. + + @discussion Default value is nil (no border). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, strong, readwrite) ASTextBorder *as_textBorder; +- (void)as_setTextBorder:(nullable ASTextBorder *)textBorder range:(NSRange)range; + +/** + The text background border. + + @discussion Default value is nil (no background border). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, strong, readwrite) ASTextBorder *as_textBackgroundBorder; +- (void)as_setTextBackgroundBorder:(nullable ASTextBorder *)textBackgroundBorder range:(NSRange)range; + +/** + The glyph transform. + + @discussion Default value is CGAffineTransformIdentity (no transform). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nonatomic, readwrite) CGAffineTransform as_textGlyphTransform; +- (void)as_setTextGlyphTransform:(CGAffineTransform)textGlyphTransform range:(NSRange)range; + + +#pragma mark - Set discontinuous attribute for range +///============================================================================= +/// @name Set discontinuous attribute for range +///============================================================================= + +- (void)as_setSuperscript:(nullable NSNumber *)superscript range:(NSRange)range; +- (void)as_setGlyphInfo:(nullable CTGlyphInfoRef)glyphInfo range:(NSRange)range; +- (void)as_setCharacterShape:(nullable NSNumber *)characterShape range:(NSRange)range; +- (void)as_setRunDelegate:(nullable CTRunDelegateRef)runDelegate range:(NSRange)range; +- (void)as_setBaselineClass:(nullable CFStringRef)baselineClass range:(NSRange)range; +- (void)as_setBaselineInfo:(nullable CFDictionaryRef)baselineInfo range:(NSRange)range; +- (void)as_setBaselineReferenceInfo:(nullable CFDictionaryRef)referenceInfo range:(NSRange)range; +- (void)as_setRubyAnnotation:(nullable CTRubyAnnotationRef)ruby range:(NSRange)range NS_AVAILABLE_IOS(8_0); +- (void)as_setAttachment:(nullable NSTextAttachment *)attachment range:(NSRange)range NS_AVAILABLE_IOS(7_0); +- (void)as_setLink:(nullable id)link range:(NSRange)range NS_AVAILABLE_IOS(7_0); +- (void)as_setTextBackedString:(nullable ASTextBackedString *)textBackedString range:(NSRange)range; +- (void)as_setTextBinding:(nullable ASTextBinding *)textBinding range:(NSRange)range; +- (void)as_setTextAttachment:(nullable ASTextAttachment *)textAttachment range:(NSRange)range; +- (void)as_setTextHighlight:(nullable ASTextHighlight *)textHighlight range:(NSRange)range; +- (void)as_setTextBlockBorder:(nullable ASTextBorder *)textBlockBorder range:(NSRange)range; + + +#pragma mark - Convenience methods for text highlight +///============================================================================= +/// @name Convenience methods for text highlight +///============================================================================= + +/** + Convenience method to set text highlight + + @param range text range + @param color text color (pass nil to ignore) + @param backgroundColor text background color when highlight + @param userInfo user information dictionary (pass nil to ignore) + @param tapAction tap action when user tap the highlight (pass nil to ignore) + @param longPressAction long press action when user long press the highlight (pass nil to ignore) + */ +- (void)as_setTextHighlightRange:(NSRange)range + color:(nullable UIColor *)color + backgroundColor:(nullable UIColor *)backgroundColor + userInfo:(nullable NSDictionary *)userInfo + tapAction:(nullable ASTextAction)tapAction + longPressAction:(nullable ASTextAction)longPressAction; + +/** + Convenience method to set text highlight + + @param range text range + @param color text color (pass nil to ignore) + @param backgroundColor text background color when highlight + @param tapAction tap action when user tap the highlight (pass nil to ignore) + */ +- (void)as_setTextHighlightRange:(NSRange)range + color:(nullable UIColor *)color + backgroundColor:(nullable UIColor *)backgroundColor + tapAction:(nullable ASTextAction)tapAction; + +/** + Convenience method to set text highlight + + @param range text range + @param color text color (pass nil to ignore) + @param backgroundColor text background color when highlight + @param userInfo tap action when user tap the highlight (pass nil to ignore) + */ +- (void)as_setTextHighlightRange:(NSRange)range + color:(nullable UIColor *)color + backgroundColor:(nullable UIColor *)backgroundColor + userInfo:(nullable NSDictionary *)userInfo; + +#pragma mark - Utilities +///============================================================================= +/// @name Utilities +///============================================================================= + +/** + Inserts into the receiver the characters of a given string at a given location. + The new string inherit the attributes of the first replaced character from location. + + @param string The string to insert into the receiver, must not be nil. + @param location The location at which string is inserted. The location must not + exceed the bounds of the receiver. + @throw Raises an NSRangeException if the location out of bounds. + */ +- (void)as_insertString:(NSString *)string atIndex:(NSUInteger)location; + +/** + Adds to the end of the receiver the characters of a given string. + The new string inherit the attributes of the receiver's tail. + + @param string The string to append to the receiver, must not be nil. + */ +- (void)as_appendString:(NSString *)string; + +/** + Set foreground color with [UIColor clearColor] in joined-emoji range. + Emoji drawing will not be affected by the foreground color. + + @discussion In iOS 8.3, Apple releases some new diversified emojis. + There's some single emoji which can be assembled to a new 'joined-emoji'. + The joiner is unicode character 'ZERO WIDTH JOINER' (U+200D). + For example: ๐Ÿ‘จ๐Ÿ‘ฉ๐Ÿ‘ง๐Ÿ‘ง -> ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง. + + When there are more than 5 'joined-emoji' in a same CTLine, CoreText may render some + extra glyphs above the emoji. It's a bug in CoreText, try this method to avoid. + This bug is fixed in iOS 9. + */ +- (void)as_setClearColorToJoinedEmoji; + +/** + Removes all discontinuous attributes in a specified range. + See `allDiscontinuousAttributeKeys`. + + @param range A text range. + */ +- (void)as_removeDiscontinuousAttributesInRange:(NSRange)range; + +/** + Returns all discontinuous attribute keys, such as RunDelegate/Attachment/Ruby. + + @discussion These attributes can only set to a specified range of text, and + should not extend to other range when editing text. + */ ++ (NSArray *)as_allDiscontinuousAttributeKeys; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.m b/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.m new file mode 100755 index 0000000000..c3a413a460 --- /dev/null +++ b/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.m @@ -0,0 +1,1248 @@ +// +// NSAttributedString+ASText.m +// Modified from YYText +// +// Created by ibireme on 14/10/7. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import "NSAttributedString+ASText.h" +#import "NSParagraphStyle+ASText.h" +#import "ASTextRunDelegate.h" +#import "ASTextUtilities.h" +#import + + +// Dummy class for category +@interface NSAttributedString_ASText : NSObject @end +@implementation NSAttributedString_ASText @end + + +@implementation NSAttributedString (ASText) + +- (NSDictionary *)as_attributesAtIndex:(NSUInteger)index { + if (index > self.length || self.length == 0) return nil; + if (self.length > 0 && index == self.length) index--; + return [self attributesAtIndex:index effectiveRange:NULL]; +} + +- (id)as_attribute:(NSString *)attributeName atIndex:(NSUInteger)index { + if (!attributeName) return nil; + if (index > self.length || self.length == 0) return nil; + if (self.length > 0 && index == self.length) index--; + return [self attribute:attributeName atIndex:index effectiveRange:NULL]; +} + +- (NSDictionary *)as_attributes { + return [self as_attributesAtIndex:0]; +} + +- (UIFont *)as_font { + return [self as_fontAtIndex:0]; +} + +- (UIFont *)as_fontAtIndex:(NSUInteger)index { + return [self as_attribute:NSFontAttributeName atIndex:index]; +} + +- (NSNumber *)as_kern { + return [self as_kernAtIndex:0]; +} + +- (NSNumber *)as_kernAtIndex:(NSUInteger)index { + return [self as_attribute:NSKernAttributeName atIndex:index]; +} + +- (UIColor *)as_color { + return [self as_colorAtIndex:0]; +} + +- (UIColor *)as_colorAtIndex:(NSUInteger)index { + UIColor *color = [self as_attribute:NSForegroundColorAttributeName atIndex:index]; + if (!color) { + CGColorRef ref = (__bridge CGColorRef)([self as_attribute:(NSString *)kCTForegroundColorAttributeName atIndex:index]); + color = [UIColor colorWithCGColor:ref]; + } + if (color && ![color isKindOfClass:[UIColor class]]) { + if (CFGetTypeID((__bridge CFTypeRef)(color)) == CGColorGetTypeID()) { + color = [UIColor colorWithCGColor:(__bridge CGColorRef)(color)]; + } else { + color = nil; + } + } + return color; +} + +- (UIColor *)as_backgroundColor { + return [self as_backgroundColorAtIndex:0]; +} + +- (UIColor *)as_backgroundColorAtIndex:(NSUInteger)index { + return [self as_attribute:NSBackgroundColorAttributeName atIndex:index]; +} + +- (NSNumber *)as_strokeWidth { + return [self as_strokeWidthAtIndex:0]; +} + +- (NSNumber *)as_strokeWidthAtIndex:(NSUInteger)index { + return [self as_attribute:NSStrokeWidthAttributeName atIndex:index]; +} + +- (UIColor *)as_strokeColor { + return [self as_strokeColorAtIndex:0]; +} + +- (UIColor *)as_strokeColorAtIndex:(NSUInteger)index { + UIColor *color = [self as_attribute:NSStrokeColorAttributeName atIndex:index]; + if (!color) { + CGColorRef ref = (__bridge CGColorRef)([self as_attribute:(NSString *)kCTStrokeColorAttributeName atIndex:index]); + color = [UIColor colorWithCGColor:ref]; + } + return color; +} + +- (NSShadow *)as_shadow { + return [self as_shadowAtIndex:0]; +} + +- (NSShadow *)as_shadowAtIndex:(NSUInteger)index { + return [self as_attribute:NSShadowAttributeName atIndex:index]; +} + +- (NSUnderlineStyle)as_strikethroughStyle { + return [self as_strikethroughStyleAtIndex:0]; +} + +- (NSUnderlineStyle)as_strikethroughStyleAtIndex:(NSUInteger)index { + NSNumber *style = [self as_attribute:NSStrikethroughStyleAttributeName atIndex:index]; + return style.integerValue; +} + +- (UIColor *)as_strikethroughColor { + return [self as_strikethroughColorAtIndex:0]; +} + +- (UIColor *)as_strikethroughColorAtIndex:(NSUInteger)index { + return [self as_attribute:NSStrikethroughColorAttributeName atIndex:index]; +} + +- (NSUnderlineStyle)as_underlineStyle { + return [self as_underlineStyleAtIndex:0]; +} + +- (NSUnderlineStyle)as_underlineStyleAtIndex:(NSUInteger)index { + NSNumber *style = [self as_attribute:NSUnderlineStyleAttributeName atIndex:index]; + return style.integerValue; +} + +- (UIColor *)as_underlineColor { + return [self as_underlineColorAtIndex:0]; +} + +- (UIColor *)as_underlineColorAtIndex:(NSUInteger)index { + UIColor *color = [self as_attribute:NSUnderlineColorAttributeName atIndex:index]; + if (!color) { + CGColorRef ref = (__bridge CGColorRef)([self as_attribute:(NSString *)kCTUnderlineColorAttributeName atIndex:index]); + color = [UIColor colorWithCGColor:ref]; + } + return color; +} + +- (NSNumber *)as_ligature { + return [self as_ligatureAtIndex:0]; +} + +- (NSNumber *)as_ligatureAtIndex:(NSUInteger)index { + return [self as_attribute:NSLigatureAttributeName atIndex:index]; +} + +- (NSString *)as_textEffect { + return [self as_textEffectAtIndex:0]; +} + +- (NSString *)as_textEffectAtIndex:(NSUInteger)index { + return [self as_attribute:NSTextEffectAttributeName atIndex:index]; +} + +- (NSNumber *)as_obliqueness { + return [self as_obliquenessAtIndex:0]; +} + +- (NSNumber *)as_obliquenessAtIndex:(NSUInteger)index { + return [self as_attribute:NSObliquenessAttributeName atIndex:index]; +} + +- (NSNumber *)as_expansion { + return [self as_expansionAtIndex:0]; +} + +- (NSNumber *)as_expansionAtIndex:(NSUInteger)index { + return [self as_attribute:NSExpansionAttributeName atIndex:index]; +} + +- (NSNumber *)as_baselineOffset { + return [self as_baselineOffsetAtIndex:0]; +} + +- (NSNumber *)as_baselineOffsetAtIndex:(NSUInteger)index { + return [self as_attribute:NSBaselineOffsetAttributeName atIndex:index]; +} + +- (BOOL)as_verticalGlyphForm { + return [self as_verticalGlyphFormAtIndex:0]; +} + +- (BOOL)as_verticalGlyphFormAtIndex:(NSUInteger)index { + NSNumber *num = [self as_attribute:NSVerticalGlyphFormAttributeName atIndex:index]; + return num.boolValue; +} + +- (NSString *)as_language { + return [self as_languageAtIndex:0]; +} + +- (NSString *)as_languageAtIndex:(NSUInteger)index { + return [self as_attribute:(id)kCTLanguageAttributeName atIndex:index]; +} + +- (NSArray *)as_writingDirection { + return [self as_writingDirectionAtIndex:0]; +} + +- (NSArray *)as_writingDirectionAtIndex:(NSUInteger)index { + return [self as_attribute:(id)kCTWritingDirectionAttributeName atIndex:index]; +} + +- (NSParagraphStyle *)as_paragraphStyle { + return [self as_paragraphStyleAtIndex:0]; +} + +- (NSParagraphStyle *)as_paragraphStyleAtIndex:(NSUInteger)index { + /* + NSParagraphStyle is NOT toll-free bridged to CTParagraphStyleRef. + + CoreText can use both NSParagraphStyle and CTParagraphStyleRef, + but UILabel/UITextView can only use NSParagraphStyle. + + We use NSParagraphStyle in both CoreText and UIKit. + */ + NSParagraphStyle *style = [self as_attribute:NSParagraphStyleAttributeName atIndex:index]; + if (style) { + if (CFGetTypeID((__bridge CFTypeRef)(style)) == CTParagraphStyleGetTypeID()) { \ + style = [NSParagraphStyle as_styleWithCTStyle:(__bridge CTParagraphStyleRef)(style)]; + } + } + return style; +} + +#define ParagraphAttribute(_attr_) \ +NSParagraphStyle *style = self.as_paragraphStyle; \ +if (!style) style = [NSParagraphStyle defaultParagraphStyle]; \ +return style. _attr_; + +#define ParagraphAttributeAtIndex(_attr_) \ +NSParagraphStyle *style = [self as_paragraphStyleAtIndex:index]; \ +if (!style) style = [NSParagraphStyle defaultParagraphStyle]; \ +return style. _attr_; + +- (NSTextAlignment)as_alignment { + ParagraphAttribute(alignment); +} + +- (NSLineBreakMode)as_lineBreakMode { + ParagraphAttribute(lineBreakMode); +} + +- (CGFloat)as_lineSpacing { + ParagraphAttribute(lineSpacing); +} + +- (CGFloat)as_paragraphSpacing { + ParagraphAttribute(paragraphSpacing); +} + +- (CGFloat)as_paragraphSpacingBefore { + ParagraphAttribute(paragraphSpacingBefore); +} + +- (CGFloat)as_firstLineHeadIndent { + ParagraphAttribute(firstLineHeadIndent); +} + +- (CGFloat)as_headIndent { + ParagraphAttribute(headIndent); +} + +- (CGFloat)as_tailIndent { + ParagraphAttribute(tailIndent); +} + +- (CGFloat)as_minimumLineHeight { + ParagraphAttribute(minimumLineHeight); +} + +- (CGFloat)as_maximumLineHeight { + ParagraphAttribute(maximumLineHeight); +} + +- (CGFloat)as_lineHeightMultiple { + ParagraphAttribute(lineHeightMultiple); +} + +- (NSWritingDirection)as_baseWritingDirection { + ParagraphAttribute(baseWritingDirection); +} + +- (float)as_hyphenationFactor { + ParagraphAttribute(hyphenationFactor); +} + +- (CGFloat)as_defaultTabInterval { + ParagraphAttribute(defaultTabInterval); +} + +- (NSArray *)as_tabStops { + ParagraphAttribute(tabStops); +} + +- (NSTextAlignment)as_alignmentAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(alignment); +} + +- (NSLineBreakMode)as_lineBreakModeAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(lineBreakMode); +} + +- (CGFloat)as_lineSpacingAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(lineSpacing); +} + +- (CGFloat)as_paragraphSpacingAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(paragraphSpacing); +} + +- (CGFloat)as_paragraphSpacingBeforeAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(paragraphSpacingBefore); +} + +- (CGFloat)as_firstLineHeadIndentAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(firstLineHeadIndent); +} + +- (CGFloat)as_headIndentAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(headIndent); +} + +- (CGFloat)as_tailIndentAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(tailIndent); +} + +- (CGFloat)as_minimumLineHeightAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(minimumLineHeight); +} + +- (CGFloat)as_maximumLineHeightAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(maximumLineHeight); +} + +- (CGFloat)as_lineHeightMultipleAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(lineHeightMultiple); +} + +- (NSWritingDirection)as_baseWritingDirectionAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(baseWritingDirection); +} + +- (float)as_hyphenationFactorAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(hyphenationFactor); +} + +- (CGFloat)as_defaultTabIntervalAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(defaultTabInterval); +} + +- (NSArray *)as_tabStopsAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(tabStops); +} + +#undef ParagraphAttribute +#undef ParagraphAttributeAtIndex + +- (ASTextShadow *)as_textShadow { + return [self as_textShadowAtIndex:0]; +} + +- (ASTextShadow *)as_textShadowAtIndex:(NSUInteger)index { + return [self as_attribute:ASTextShadowAttributeName atIndex:index]; +} + +- (ASTextShadow *)as_textInnerShadow { + return [self as_textInnerShadowAtIndex:0]; +} + +- (ASTextShadow *)as_textInnerShadowAtIndex:(NSUInteger)index { + return [self as_attribute:ASTextInnerShadowAttributeName atIndex:index]; +} + +- (ASTextDecoration *)as_textUnderline { + return [self as_textUnderlineAtIndex:0]; +} + +- (ASTextDecoration *)as_textUnderlineAtIndex:(NSUInteger)index { + return [self as_attribute:ASTextUnderlineAttributeName atIndex:index]; +} + +- (ASTextDecoration *)as_textStrikethrough { + return [self as_textStrikethroughAtIndex:0]; +} + +- (ASTextDecoration *)as_textStrikethroughAtIndex:(NSUInteger)index { + return [self as_attribute:ASTextStrikethroughAttributeName atIndex:index]; +} + +- (ASTextBorder *)as_textBorder { + return [self as_textBorderAtIndex:0]; +} + +- (ASTextBorder *)as_textBorderAtIndex:(NSUInteger)index { + return [self as_attribute:ASTextBorderAttributeName atIndex:index]; +} + +- (ASTextBorder *)as_textBackgroundBorder { + return [self as_textBackgroundBorderAtIndex:0]; +} + +- (ASTextBorder *)as_textBackgroundBorderAtIndex:(NSUInteger)index { + return [self as_attribute:ASTextBackedStringAttributeName atIndex:index]; +} + +- (CGAffineTransform)as_textGlyphTransform { + return [self as_textGlyphTransformAtIndex:0]; +} + +- (CGAffineTransform)as_textGlyphTransformAtIndex:(NSUInteger)index { + NSValue *value = [self as_attribute:ASTextGlyphTransformAttributeName atIndex:index]; + if (!value) return CGAffineTransformIdentity; + return [value CGAffineTransformValue]; +} + +- (NSString *)as_plainTextForRange:(NSRange)range { + if (range.location == NSNotFound ||range.length == NSNotFound) return nil; + NSMutableString *result = [NSMutableString string]; + if (range.length == 0) return result; + NSString *string = self.string; + [self enumerateAttribute:ASTextBackedStringAttributeName inRange:range options:kNilOptions usingBlock:^(id value, NSRange range, BOOL *stop) { + ASTextBackedString *backed = value; + if (backed && backed.string) { + [result appendString:backed.string]; + } else { + [result appendString:[string substringWithRange:range]]; + } + }]; + return result; +} + ++ (NSMutableAttributedString *)as_attachmentStringWithContent:(id)content + contentMode:(UIViewContentMode)contentMode + width:(CGFloat)width + ascent:(CGFloat)ascent + descent:(CGFloat)descent { + NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:ASTextAttachmentToken]; + + ASTextAttachment *attach = [ASTextAttachment new]; + attach.content = content; + attach.contentMode = contentMode; + [atr as_setTextAttachment:attach range:NSMakeRange(0, atr.length)]; + + ASTextRunDelegate *delegate = [ASTextRunDelegate new]; + delegate.width = width; + delegate.ascent = ascent; + delegate.descent = descent; + CTRunDelegateRef delegateRef = delegate.CTRunDelegate; + [atr as_setRunDelegate:delegateRef range:NSMakeRange(0, atr.length)]; + if (delegate) CFRelease(delegateRef); + + return atr; +} + ++ (NSMutableAttributedString *)as_attachmentStringWithContent:(id)content + contentMode:(UIViewContentMode)contentMode + attachmentSize:(CGSize)attachmentSize + alignToFont:(UIFont *)font + alignment:(ASTextVerticalAlignment)alignment { + NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:ASTextAttachmentToken]; + + ASTextAttachment *attach = [ASTextAttachment new]; + attach.content = content; + attach.contentMode = contentMode; + [atr as_setTextAttachment:attach range:NSMakeRange(0, atr.length)]; + + ASTextRunDelegate *delegate = [ASTextRunDelegate new]; + delegate.width = attachmentSize.width; + switch (alignment) { + case ASTextVerticalAlignmentTop: { + delegate.ascent = font.ascender; + delegate.descent = attachmentSize.height - font.ascender; + if (delegate.descent < 0) { + delegate.descent = 0; + delegate.ascent = attachmentSize.height; + } + } break; + case ASTextVerticalAlignmentCenter: { + CGFloat fontHeight = font.ascender - font.descender; + CGFloat yOffset = font.ascender - fontHeight * 0.5; + delegate.ascent = attachmentSize.height * 0.5 + yOffset; + delegate.descent = attachmentSize.height - delegate.ascent; + if (delegate.descent < 0) { + delegate.descent = 0; + delegate.ascent = attachmentSize.height; + } + } break; + case ASTextVerticalAlignmentBottom: { + delegate.ascent = attachmentSize.height + font.descender; + delegate.descent = -font.descender; + if (delegate.ascent < 0) { + delegate.ascent = 0; + delegate.descent = attachmentSize.height; + } + } break; + 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; +} + ++ (NSMutableAttributedString *)as_attachmentStringWithEmojiImage:(UIImage *)image + fontSize:(CGFloat)fontSize { + if (!image || fontSize <= 0) return nil; + + BOOL hasAnim = NO; + if (image.images.count > 1) { + hasAnim = YES; + } else if (NSProtocolFromString(@"ASAnimatedImage") && + [image conformsToProtocol:NSProtocolFromString(@"ASAnimatedImage")]) { + NSNumber *frameCount = [image valueForKey:@"animatedImageFrameCount"]; + if (frameCount.intValue > 1) hasAnim = YES; + } + + CGFloat ascent = ASTextEmojiGetAscentWithFontSize(fontSize); + CGFloat descent = ASTextEmojiGetDescentWithFontSize(fontSize); + CGRect bounding = ASTextEmojiGetGlyphBoundingRectWithFontSize(fontSize); + + ASTextRunDelegate *delegate = [ASTextRunDelegate new]; + delegate.ascent = ascent; + delegate.descent = descent; + delegate.width = bounding.size.width + 2 * bounding.origin.x; + + ASTextAttachment *attachment = [ASTextAttachment new]; + attachment.contentMode = UIViewContentModeScaleAspectFit; + attachment.contentInsets = UIEdgeInsetsMake(ascent - (bounding.size.height + bounding.origin.y), bounding.origin.x, descent + bounding.origin.y, bounding.origin.x); + if (hasAnim) { + Class imageClass = NSClassFromString(@"ASAnimatedImageView"); + if (!imageClass) imageClass = [UIImageView class]; + UIImageView *view = (id)[imageClass new]; + view.frame = bounding; + view.image = image; + view.contentMode = UIViewContentModeScaleAspectFit; + attachment.content = view; + } else { + attachment.content = image; + } + + NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:ASTextAttachmentToken]; + [atr as_setTextAttachment:attachment range:NSMakeRange(0, atr.length)]; + CTRunDelegateRef ctDelegate = delegate.CTRunDelegate; + [atr as_setRunDelegate:ctDelegate range:NSMakeRange(0, atr.length)]; + if (ctDelegate) CFRelease(ctDelegate); + + return atr; +} + +- (NSRange)as_rangeOfAll { + return NSMakeRange(0, self.length); +} + +- (BOOL)as_isSharedAttributesInAllRange { + __block BOOL shared = YES; + __block NSDictionary *firstAttrs = nil; + [self enumerateAttributesInRange:self.as_rangeOfAll options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) { + if (range.location == 0) { + firstAttrs = attrs; + } else { + if (firstAttrs.count != attrs.count) { + shared = NO; + *stop = YES; + } else if (firstAttrs) { + if (![firstAttrs isEqualToDictionary:attrs]) { + shared = NO; + *stop = YES; + } + } + } + }]; + return shared; +} + +- (BOOL)as_canDrawWithUIKit { + static NSMutableSet *failSet; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + failSet = [NSMutableSet new]; + [failSet addObject:(id)kCTGlyphInfoAttributeName]; + [failSet addObject:(id)kCTCharacterShapeAttributeName]; + [failSet addObject:(id)kCTLanguageAttributeName]; + [failSet addObject:(id)kCTRunDelegateAttributeName]; + [failSet addObject:(id)kCTBaselineClassAttributeName]; + [failSet addObject:(id)kCTBaselineInfoAttributeName]; + [failSet addObject:(id)kCTBaselineReferenceInfoAttributeName]; + [failSet addObject:(id)kCTRubyAnnotationAttributeName]; + [failSet addObject:ASTextShadowAttributeName]; + [failSet addObject:ASTextInnerShadowAttributeName]; + [failSet addObject:ASTextUnderlineAttributeName]; + [failSet addObject:ASTextStrikethroughAttributeName]; + [failSet addObject:ASTextBorderAttributeName]; + [failSet addObject:ASTextBackgroundBorderAttributeName]; + [failSet addObject:ASTextBlockBorderAttributeName]; + [failSet addObject:ASTextAttachmentAttributeName]; + [failSet addObject:ASTextHighlightAttributeName]; + [failSet addObject:ASTextGlyphTransformAttributeName]; + }); + +#define Fail { result = NO; *stop = YES; return; } + __block BOOL result = YES; + [self enumerateAttributesInRange:self.as_rangeOfAll options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) { + if (attrs.count == 0) return; + for (NSString *str in attrs.allKeys) { + if ([failSet containsObject:str]) Fail; + } + if (attrs[(id)kCTForegroundColorAttributeName] && !attrs[NSForegroundColorAttributeName]) Fail; + if (attrs[(id)kCTStrokeColorAttributeName] && !attrs[NSStrokeColorAttributeName]) Fail; + if (attrs[(id)kCTUnderlineColorAttributeName]) { + if (!attrs[NSUnderlineColorAttributeName]) Fail; + } + NSParagraphStyle *style = attrs[NSParagraphStyleAttributeName]; + if (style && CFGetTypeID((__bridge CFTypeRef)(style)) == CTParagraphStyleGetTypeID()) Fail; + }]; + return result; +#undef Fail +} + +@end + +@implementation NSMutableAttributedString (ASText) + +- (void)as_setAttributes:(NSDictionary *)attributes { + [self setAs_attributes:attributes]; +} + +- (void)setAs_attributes:(NSDictionary *)attributes { + if (attributes == (id)[NSNull null]) attributes = nil; + [self setAttributes:@{} range:NSMakeRange(0, self.length)]; + [attributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [self as_setAttribute:key value:obj]; + }]; +} + +- (void)as_setAttribute:(NSString *)name value:(id)value { + [self as_setAttribute:name value:value range:NSMakeRange(0, self.length)]; +} + +- (void)as_setAttribute:(NSString *)name value:(id)value range:(NSRange)range { + if (!name || [NSNull isEqual:name]) return; + if (value && ![NSNull isEqual:value]) [self addAttribute:name value:value range:range]; + else [self removeAttribute:name range:range]; +} + +- (void)as_removeAttributesInRange:(NSRange)range { + [self setAttributes:nil range:range]; +} + +#pragma mark - Property Setter + +- (void)setAs_font:(UIFont *)font { + /* + In iOS7 and later, UIFont is toll-free bridged to CTFontRef, + although Apple does not mention it in documentation. + + In iOS6, UIFont is a wrapper for CTFontRef, so CoreText can alse use UIfont, + but UILabel/UITextView cannot use CTFontRef. + + We use UIFont for both CoreText and UIKit. + */ + [self as_setFont:font range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_kern:(NSNumber *)kern { + [self as_setKern:kern range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_color:(UIColor *)color { + [self as_setColor:color range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_backgroundColor:(UIColor *)backgroundColor { + [self as_setBackgroundColor:backgroundColor range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_strokeWidth:(NSNumber *)strokeWidth { + [self as_setStrokeWidth:strokeWidth range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_strokeColor:(UIColor *)strokeColor { + [self as_setStrokeColor:strokeColor range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_shadow:(NSShadow *)shadow { + [self as_setShadow:shadow range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_strikethroughStyle:(NSUnderlineStyle)strikethroughStyle { + [self as_setStrikethroughStyle:strikethroughStyle range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_strikethroughColor:(UIColor *)strikethroughColor { + [self as_setStrikethroughColor:strikethroughColor range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_underlineStyle:(NSUnderlineStyle)underlineStyle { + [self as_setUnderlineStyle:underlineStyle range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_underlineColor:(UIColor *)underlineColor { + [self as_setUnderlineColor:underlineColor range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_ligature:(NSNumber *)ligature { + [self as_setLigature:ligature range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_textEffect:(NSString *)textEffect { + [self as_setTextEffect:textEffect range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_obliqueness:(NSNumber *)obliqueness { + [self as_setObliqueness:obliqueness range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_expansion:(NSNumber *)expansion { + [self as_setExpansion:expansion range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_baselineOffset:(NSNumber *)baselineOffset { + [self as_setBaselineOffset:baselineOffset range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_verticalGlyphForm:(BOOL)verticalGlyphForm { + [self as_setVerticalGlyphForm:verticalGlyphForm range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_language:(NSString *)language { + [self as_setLanguage:language range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_writingDirection:(NSArray *)writingDirection { + [self as_setWritingDirection:writingDirection range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_paragraphStyle:(NSParagraphStyle *)paragraphStyle { + /* + NSParagraphStyle is NOT toll-free bridged to CTParagraphStyleRef. + + CoreText can use both NSParagraphStyle and CTParagraphStyleRef, + but UILabel/UITextView can only use NSParagraphStyle. + + We use NSParagraphStyle in both CoreText and UIKit. + */ + [self as_setParagraphStyle:paragraphStyle range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_alignment:(NSTextAlignment)alignment { + [self as_setAlignment:alignment range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_baseWritingDirection:(NSWritingDirection)baseWritingDirection { + [self as_setBaseWritingDirection:baseWritingDirection range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_lineSpacing:(CGFloat)lineSpacing { + [self as_setLineSpacing:lineSpacing range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_paragraphSpacing:(CGFloat)paragraphSpacing { + [self as_setParagraphSpacing:paragraphSpacing range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_paragraphSpacingBefore:(CGFloat)paragraphSpacingBefore { + [self as_setParagraphSpacing:paragraphSpacingBefore range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_firstLineHeadIndent:(CGFloat)firstLineHeadIndent { + [self as_setFirstLineHeadIndent:firstLineHeadIndent range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_headIndent:(CGFloat)headIndent { + [self as_setHeadIndent:headIndent range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_tailIndent:(CGFloat)tailIndent { + [self as_setTailIndent:tailIndent range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_lineBreakMode:(NSLineBreakMode)lineBreakMode { + [self as_setLineBreakMode:lineBreakMode range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_minimumLineHeight:(CGFloat)minimumLineHeight { + [self as_setMinimumLineHeight:minimumLineHeight range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_maximumLineHeight:(CGFloat)maximumLineHeight { + [self as_setMaximumLineHeight:maximumLineHeight range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_lineHeightMultiple:(CGFloat)lineHeightMultiple { + [self as_setLineHeightMultiple:lineHeightMultiple range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_hyphenationFactor:(float)hyphenationFactor { + [self as_setHyphenationFactor:hyphenationFactor range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_defaultTabInterval:(CGFloat)defaultTabInterval { + [self as_setDefaultTabInterval:defaultTabInterval range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_tabStops:(NSArray *)tabStops { + [self as_setTabStops:tabStops range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_textShadow:(ASTextShadow *)textShadow { + [self as_setTextShadow:textShadow range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_textInnerShadow:(ASTextShadow *)textInnerShadow { + [self as_setTextInnerShadow:textInnerShadow range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_textUnderline:(ASTextDecoration *)textUnderline { + [self as_setTextUnderline:textUnderline range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_textStrikethrough:(ASTextDecoration *)textStrikethrough { + [self as_setTextStrikethrough:textStrikethrough range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_textBorder:(ASTextBorder *)textBorder { + [self as_setTextBorder:textBorder range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_textBackgroundBorder:(ASTextBorder *)textBackgroundBorder { + [self as_setTextBackgroundBorder:textBackgroundBorder range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_textGlyphTransform:(CGAffineTransform)textGlyphTransform { + [self as_setTextGlyphTransform:textGlyphTransform range:NSMakeRange(0, self.length)]; +} + +#pragma mark - Range Setter + +- (void)as_setFont:(UIFont *)font range:(NSRange)range { + [self as_setAttribute:NSFontAttributeName value:font range:range]; +} + +- (void)as_setKern:(NSNumber *)kern range:(NSRange)range { + [self as_setAttribute:NSKernAttributeName value:kern range:range]; +} + +- (void)as_setColor:(UIColor *)color range:(NSRange)range { + [self as_setAttribute:(id)kCTForegroundColorAttributeName value:(id)color.CGColor range:range]; + [self as_setAttribute:NSForegroundColorAttributeName value:color range:range]; +} + +- (void)as_setBackgroundColor:(UIColor *)backgroundColor range:(NSRange)range { + [self as_setAttribute:NSBackgroundColorAttributeName value:backgroundColor range:range]; +} + +- (void)as_setStrokeWidth:(NSNumber *)strokeWidth range:(NSRange)range { + [self as_setAttribute:NSStrokeWidthAttributeName value:strokeWidth range:range]; +} + +- (void)as_setStrokeColor:(UIColor *)strokeColor range:(NSRange)range { + [self as_setAttribute:(id)kCTStrokeColorAttributeName value:(id)strokeColor.CGColor range:range]; + [self as_setAttribute:NSStrokeColorAttributeName value:strokeColor range:range]; +} + +- (void)as_setShadow:(NSShadow *)shadow range:(NSRange)range { + [self as_setAttribute:NSShadowAttributeName value:shadow range:range]; +} + +- (void)as_setStrikethroughStyle:(NSUnderlineStyle)strikethroughStyle range:(NSRange)range { + NSNumber *style = strikethroughStyle == 0 ? nil : @(strikethroughStyle); + [self as_setAttribute:NSStrikethroughStyleAttributeName value:style range:range]; +} + +- (void)as_setStrikethroughColor:(UIColor *)strikethroughColor range:(NSRange)range { + [self as_setAttribute:NSStrikethroughColorAttributeName value:strikethroughColor range:range]; +} + +- (void)as_setUnderlineStyle:(NSUnderlineStyle)underlineStyle range:(NSRange)range { + NSNumber *style = underlineStyle == 0 ? nil : @(underlineStyle); + [self as_setAttribute:NSUnderlineStyleAttributeName value:style range:range]; +} + +- (void)as_setUnderlineColor:(UIColor *)underlineColor range:(NSRange)range { + [self as_setAttribute:(id)kCTUnderlineColorAttributeName value:(id)underlineColor.CGColor range:range]; + [self as_setAttribute:NSUnderlineColorAttributeName value:underlineColor range:range]; +} + +- (void)as_setLigature:(NSNumber *)ligature range:(NSRange)range { + [self as_setAttribute:NSLigatureAttributeName value:ligature range:range]; +} + +- (void)as_setTextEffect:(NSString *)textEffect range:(NSRange)range { + [self as_setAttribute:NSTextEffectAttributeName value:textEffect range:range]; +} + +- (void)as_setObliqueness:(NSNumber *)obliqueness range:(NSRange)range { + [self as_setAttribute:NSObliquenessAttributeName value:obliqueness range:range]; +} + +- (void)as_setExpansion:(NSNumber *)expansion range:(NSRange)range { + [self as_setAttribute:NSExpansionAttributeName value:expansion range:range]; +} + +- (void)as_setBaselineOffset:(NSNumber *)baselineOffset range:(NSRange)range { + [self as_setAttribute:NSBaselineOffsetAttributeName value:baselineOffset range:range]; +} + +- (void)as_setVerticalGlyphForm:(BOOL)verticalGlyphForm range:(NSRange)range { + NSNumber *v = verticalGlyphForm ? @(YES) : nil; + [self as_setAttribute:NSVerticalGlyphFormAttributeName value:v range:range]; +} + +- (void)as_setLanguage:(NSString *)language range:(NSRange)range { + [self as_setAttribute:(id)kCTLanguageAttributeName value:language range:range]; +} + +- (void)as_setWritingDirection:(NSArray *)writingDirection range:(NSRange)range { + [self as_setAttribute:(id)kCTWritingDirectionAttributeName value:writingDirection range:range]; +} + +- (void)as_setParagraphStyle:(NSParagraphStyle *)paragraphStyle range:(NSRange)range { + /* + NSParagraphStyle is NOT toll-free bridged to CTParagraphStyleRef. + + CoreText can use both NSParagraphStyle and CTParagraphStyleRef, + but UILabel/UITextView can only use NSParagraphStyle. + + We use NSParagraphStyle in both CoreText and UIKit. + */ + [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]; \ +}]; + +- (void)as_setAlignment:(NSTextAlignment)alignment range:(NSRange)range { + ParagraphStyleSet(alignment); +} + +- (void)as_setBaseWritingDirection:(NSWritingDirection)baseWritingDirection range:(NSRange)range { + ParagraphStyleSet(baseWritingDirection); +} + +- (void)as_setLineSpacing:(CGFloat)lineSpacing range:(NSRange)range { + ParagraphStyleSet(lineSpacing); +} + +- (void)as_setParagraphSpacing:(CGFloat)paragraphSpacing range:(NSRange)range { + ParagraphStyleSet(paragraphSpacing); +} + +- (void)as_setParagraphSpacingBefore:(CGFloat)paragraphSpacingBefore range:(NSRange)range { + ParagraphStyleSet(paragraphSpacingBefore); +} + +- (void)as_setFirstLineHeadIndent:(CGFloat)firstLineHeadIndent range:(NSRange)range { + ParagraphStyleSet(firstLineHeadIndent); +} + +- (void)as_setHeadIndent:(CGFloat)headIndent range:(NSRange)range { + ParagraphStyleSet(headIndent); +} + +- (void)as_setTailIndent:(CGFloat)tailIndent range:(NSRange)range { + ParagraphStyleSet(tailIndent); +} + +- (void)as_setLineBreakMode:(NSLineBreakMode)lineBreakMode range:(NSRange)range { + ParagraphStyleSet(lineBreakMode); +} + +- (void)as_setMinimumLineHeight:(CGFloat)minimumLineHeight range:(NSRange)range { + ParagraphStyleSet(minimumLineHeight); +} + +- (void)as_setMaximumLineHeight:(CGFloat)maximumLineHeight range:(NSRange)range { + ParagraphStyleSet(maximumLineHeight); +} + +- (void)as_setLineHeightMultiple:(CGFloat)lineHeightMultiple range:(NSRange)range { + ParagraphStyleSet(lineHeightMultiple); +} + +- (void)as_setHyphenationFactor:(float)hyphenationFactor range:(NSRange)range { + ParagraphStyleSet(hyphenationFactor); +} + +- (void)as_setDefaultTabInterval:(CGFloat)defaultTabInterval range:(NSRange)range { + ParagraphStyleSet(defaultTabInterval); +} + +- (void)as_setTabStops:(NSArray *)tabStops range:(NSRange)range { + ParagraphStyleSet(tabStops); +} + +#undef ParagraphStyleSet + +- (void)as_setSuperscript:(NSNumber *)superscript range:(NSRange)range { + if ([superscript isEqualToNumber:@(0)]) { + superscript = nil; + } + [self as_setAttribute:(id)kCTSuperscriptAttributeName value:superscript range:range]; +} + +- (void)as_setGlyphInfo:(CTGlyphInfoRef)glyphInfo range:(NSRange)range { + [self as_setAttribute:(id)kCTGlyphInfoAttributeName value:(__bridge id)glyphInfo range:range]; +} + +- (void)as_setCharacterShape:(NSNumber *)characterShape range:(NSRange)range { + [self as_setAttribute:(id)kCTCharacterShapeAttributeName value:characterShape range:range]; +} + +- (void)as_setRunDelegate:(CTRunDelegateRef)runDelegate range:(NSRange)range { + [self as_setAttribute:(id)kCTRunDelegateAttributeName value:(__bridge id)runDelegate range:range]; +} + +- (void)as_setBaselineClass:(CFStringRef)baselineClass range:(NSRange)range { + [self as_setAttribute:(id)kCTBaselineClassAttributeName value:(__bridge id)baselineClass range:range]; +} + +- (void)as_setBaselineInfo:(CFDictionaryRef)baselineInfo range:(NSRange)range { + [self as_setAttribute:(id)kCTBaselineInfoAttributeName value:(__bridge id)baselineInfo range:range]; +} + +- (void)as_setBaselineReferenceInfo:(CFDictionaryRef)referenceInfo range:(NSRange)range { + [self as_setAttribute:(id)kCTBaselineReferenceInfoAttributeName value:(__bridge id)referenceInfo range:range]; +} + +- (void)as_setRubyAnnotation:(CTRubyAnnotationRef)ruby range:(NSRange)range { + [self as_setAttribute:(id)kCTRubyAnnotationAttributeName value:(__bridge id)ruby range:range]; +} + +- (void)as_setAttachment:(NSTextAttachment *)attachment range:(NSRange)range { + [self as_setAttribute:NSAttachmentAttributeName value:attachment range:range]; +} + +- (void)as_setLink:(id)link range:(NSRange)range { + [self as_setAttribute:NSLinkAttributeName value:link range:range]; +} + +- (void)as_setTextBackedString:(ASTextBackedString *)textBackedString range:(NSRange)range { + [self as_setAttribute:ASTextBackedStringAttributeName value:textBackedString range:range]; +} + +- (void)as_setTextBinding:(ASTextBinding *)textBinding range:(NSRange)range { + [self as_setAttribute:ASTextBindingAttributeName value:textBinding range:range]; +} + +- (void)as_setTextShadow:(ASTextShadow *)textShadow range:(NSRange)range { + [self as_setAttribute:ASTextShadowAttributeName value:textShadow range:range]; +} + +- (void)as_setTextInnerShadow:(ASTextShadow *)textInnerShadow range:(NSRange)range { + [self as_setAttribute:ASTextInnerShadowAttributeName value:textInnerShadow range:range]; +} + +- (void)as_setTextUnderline:(ASTextDecoration *)textUnderline range:(NSRange)range { + [self as_setAttribute:ASTextUnderlineAttributeName value:textUnderline range:range]; +} + +- (void)as_setTextStrikethrough:(ASTextDecoration *)textStrikethrough range:(NSRange)range { + [self as_setAttribute:ASTextStrikethroughAttributeName value:textStrikethrough range:range]; +} + +- (void)as_setTextBorder:(ASTextBorder *)textBorder range:(NSRange)range { + [self as_setAttribute:ASTextBorderAttributeName value:textBorder range:range]; +} + +- (void)as_setTextBackgroundBorder:(ASTextBorder *)textBackgroundBorder range:(NSRange)range { + [self as_setAttribute:ASTextBackgroundBorderAttributeName value:textBackgroundBorder range:range]; +} + +- (void)as_setTextAttachment:(ASTextAttachment *)textAttachment range:(NSRange)range { + [self as_setAttribute:ASTextAttachmentAttributeName value:textAttachment range:range]; +} + +- (void)as_setTextHighlight:(ASTextHighlight *)textHighlight range:(NSRange)range { + [self as_setAttribute:ASTextHighlightAttributeName value:textHighlight range:range]; +} + +- (void)as_setTextBlockBorder:(ASTextBorder *)textBlockBorder range:(NSRange)range { + [self as_setAttribute:ASTextBlockBorderAttributeName value:textBlockBorder range:range]; +} + +- (void)as_setTextGlyphTransform:(CGAffineTransform)textGlyphTransform range:(NSRange)range { + NSValue *value = CGAffineTransformIsIdentity(textGlyphTransform) ? nil : [NSValue valueWithCGAffineTransform:textGlyphTransform]; + [self as_setAttribute:ASTextGlyphTransformAttributeName value:value range:range]; +} + +- (void)as_setTextHighlightRange:(NSRange)range + color:(UIColor *)color + backgroundColor:(UIColor *)backgroundColor + userInfo:(NSDictionary *)userInfo + tapAction:(ASTextAction)tapAction + longPressAction:(ASTextAction)longPressAction { + ASTextHighlight *highlight = [ASTextHighlight highlightWithBackgroundColor:backgroundColor]; + highlight.userInfo = userInfo; + highlight.tapAction = tapAction; + highlight.longPressAction = longPressAction; + if (color) [self as_setColor:color range:range]; + [self as_setTextHighlight:highlight range:range]; +} + +- (void)as_setTextHighlightRange:(NSRange)range + color:(UIColor *)color + backgroundColor:(UIColor *)backgroundColor + tapAction:(ASTextAction)tapAction { + [self as_setTextHighlightRange:range + color:color + backgroundColor:backgroundColor + userInfo:nil + tapAction:tapAction + longPressAction:nil]; +} + +- (void)as_setTextHighlightRange:(NSRange)range + color:(UIColor *)color + backgroundColor:(UIColor *)backgroundColor + userInfo:(NSDictionary *)userInfo { + [self as_setTextHighlightRange:range + color:color + backgroundColor:backgroundColor + userInfo:userInfo + tapAction:nil + longPressAction:nil]; +} + +- (void)as_insertString:(NSString *)string atIndex:(NSUInteger)location { + [self replaceCharactersInRange:NSMakeRange(location, 0) withString:string]; + [self as_removeDiscontinuousAttributesInRange:NSMakeRange(location, string.length)]; +} + +- (void)as_appendString:(NSString *)string { + NSUInteger length = self.length; + [self replaceCharactersInRange:NSMakeRange(length, 0) withString:string]; + [self as_removeDiscontinuousAttributesInRange:NSMakeRange(length, string.length)]; +} + +- (void)as_setClearColorToJoinedEmoji { + NSString *str = self.string; + if (str.length < 8) return; + + // Most string do not contains the joined-emoji, test the joiner first. + BOOL containsJoiner = NO; + { + CFStringRef cfStr = (__bridge CFStringRef)str; + BOOL needFree = NO; + UniChar *chars = NULL; + chars = (void *)CFStringGetCharactersPtr(cfStr); + if (!chars) { + chars = malloc(str.length * sizeof(UniChar)); + if (chars) { + needFree = YES; + CFStringGetCharacters(cfStr, CFRangeMake(0, str.length), chars); + } + } + if (!chars) { // fail to get unichar.. + containsJoiner = YES; + } else { + for (int i = 0, max = (int)str.length; i < max; i++) { + if (chars[i] == 0x200D) { // 'ZERO WIDTH JOINER' (U+200D) + containsJoiner = YES; + break; + } + } + if (needFree) free(chars); + } + } + if (!containsJoiner) return; + + // NSRegularExpression is designed to be immutable and thread safe. + static NSRegularExpression *regex; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + regex = [NSRegularExpression regularExpressionWithPattern:@"((๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ|๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ|๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง|๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ|๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ|๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง|๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ|๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ|๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง)+|(๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง|๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ|๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ง|๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆ|๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ง))" options:kNilOptions error:nil]; + }); + + UIColor *clear = [UIColor clearColor]; + [regex enumerateMatchesInString:str options:kNilOptions range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { + [self as_setColor:clear range:result.range]; + }]; +} + +- (void)as_removeDiscontinuousAttributesInRange:(NSRange)range { + NSArray *keys = [NSMutableAttributedString as_allDiscontinuousAttributeKeys]; + for (NSString *key in keys) { + [self removeAttribute:key range:range]; + } +} + ++ (NSArray *)as_allDiscontinuousAttributeKeys { + static NSArray *keys; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + keys = @[(id)kCTSuperscriptAttributeName, + (id)kCTRunDelegateAttributeName, + ASTextBackedStringAttributeName, + ASTextBindingAttributeName, + ASTextAttachmentAttributeName, + (id)kCTRubyAnnotationAttributeName, + NSAttachmentAttributeName]; + }); + return keys; +} + +@end diff --git a/Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.h b/Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.h new file mode 100755 index 0000000000..b7c21f8121 --- /dev/null +++ b/Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.h @@ -0,0 +1,37 @@ +// +// NSParagraphStyle+ASText.h +// Modified from YYText +// +// Created by ibireme on 14/10/7. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Provides extensions for `NSParagraphStyle` to work with CoreText. + */ +@interface NSParagraphStyle (ASText) + +/** + Creates a new NSParagraphStyle object from the CoreText Style. + + @param CTStyle CoreText Paragraph Style. + + @return a new NSParagraphStyle + */ ++ (nullable NSParagraphStyle *)as_styleWithCTStyle:(CTParagraphStyleRef)CTStyle; + +/** + Creates and returns a CoreText Paragraph Style. (need call CFRelease() after used) + */ +- (nullable CTParagraphStyleRef)as_CTStyle CF_RETURNS_RETAINED; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.m b/Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.m new file mode 100755 index 0000000000..92847d9b3b --- /dev/null +++ b/Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.m @@ -0,0 +1,218 @@ +// +// NSParagraphStyle+ASText.m +// Modified from YYText +// +// Created by ibireme on 14/10/7. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import "NSParagraphStyle+ASText.h" +#import "ASTextAttribute.h" +#import + +// Dummy class for category +@interface NSParagraphStyle_ASText : NSObject @end +@implementation NSParagraphStyle_ASText @end + + +@implementation NSParagraphStyle (ASText) + ++ (NSParagraphStyle *)as_styleWithCTStyle:(CTParagraphStyleRef)CTStyle { + if (CTStyle == NULL) return nil; + + NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + CGFloat lineSpacing; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierLineSpacing, sizeof(CGFloat), &lineSpacing)) { + style.lineSpacing = lineSpacing; + } +#pragma clang diagnostic pop + + CGFloat paragraphSpacing; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierParagraphSpacing, sizeof(CGFloat), ¶graphSpacing)) { + style.paragraphSpacing = paragraphSpacing; + } + + CTTextAlignment alignment; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierAlignment, sizeof(CTTextAlignment), &alignment)) { + style.alignment = NSTextAlignmentFromCTTextAlignment(alignment); + } + + CGFloat firstLineHeadIndent; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(CGFloat), &firstLineHeadIndent)) { + style.firstLineHeadIndent = firstLineHeadIndent; + } + + CGFloat headIndent; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierHeadIndent, sizeof(CGFloat), &headIndent)) { + style.headIndent = headIndent; + } + + CGFloat tailIndent; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierTailIndent, sizeof(CGFloat), &tailIndent)) { + style.tailIndent = tailIndent; + } + + CTLineBreakMode lineBreakMode; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierLineBreakMode, sizeof(CTLineBreakMode), &lineBreakMode)) { + style.lineBreakMode = (NSLineBreakMode)lineBreakMode; + } + + CGFloat minimumLineHeight; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(CGFloat), &minimumLineHeight)) { + style.minimumLineHeight = minimumLineHeight; + } + + CGFloat maximumLineHeight; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(CGFloat), &maximumLineHeight)) { + style.maximumLineHeight = maximumLineHeight; + } + + CTWritingDirection baseWritingDirection; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierBaseWritingDirection, sizeof(CTWritingDirection), &baseWritingDirection)) { + style.baseWritingDirection = (NSWritingDirection)baseWritingDirection; + } + + CGFloat lineHeightMultiple; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(CGFloat), &lineHeightMultiple)) { + style.lineHeightMultiple = lineHeightMultiple; + } + + CGFloat paragraphSpacingBefore; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(CGFloat), ¶graphSpacingBefore)) { + style.paragraphSpacingBefore = paragraphSpacingBefore; + } + + CFArrayRef tabStops; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierTabStops, sizeof(CFArrayRef), &tabStops)) { + NSMutableArray *tabs = [NSMutableArray new]; + [((__bridge NSArray *)(tabStops))enumerateObjectsUsingBlock : ^(id obj, NSUInteger idx, BOOL *stop) { + CTTextTabRef ctTab = (__bridge CFTypeRef)obj; + + NSTextTab *tab = [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentFromCTTextAlignment(CTTextTabGetAlignment(ctTab)) location:CTTextTabGetLocation(ctTab) options:(__bridge id)CTTextTabGetOptions(ctTab)]; + [tabs addObject:tab]; + }]; + if (tabs.count) { + style.tabStops = tabs; + } + } + + CGFloat defaultTabInterval; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierDefaultTabInterval, sizeof(CGFloat), &defaultTabInterval)) { + style.defaultTabInterval = defaultTabInterval; + } + + return style; +} + +- (CTParagraphStyleRef)as_CTStyle CF_RETURNS_RETAINED { + CTParagraphStyleSetting set[kCTParagraphStyleSpecifierCount] = { 0 }; + int count = 0; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + CGFloat lineSpacing = self.lineSpacing; + set[count].spec = kCTParagraphStyleSpecifierLineSpacing; + set[count].valueSize = sizeof(CGFloat); + set[count].value = &lineSpacing; + count++; +#pragma clang diagnostic pop + + CGFloat paragraphSpacing = self.paragraphSpacing; + set[count].spec = kCTParagraphStyleSpecifierParagraphSpacing; + set[count].valueSize = sizeof(CGFloat); + set[count].value = ¶graphSpacing; + count++; + + CTTextAlignment alignment = NSTextAlignmentToCTTextAlignment(self.alignment); + set[count].spec = kCTParagraphStyleSpecifierAlignment; + set[count].valueSize = sizeof(CTTextAlignment); + set[count].value = &alignment; + count++; + + CGFloat firstLineHeadIndent = self.firstLineHeadIndent; + set[count].spec = kCTParagraphStyleSpecifierFirstLineHeadIndent; + set[count].valueSize = sizeof(CGFloat); + set[count].value = &firstLineHeadIndent; + count++; + + CGFloat headIndent = self.headIndent; + set[count].spec = kCTParagraphStyleSpecifierHeadIndent; + set[count].valueSize = sizeof(CGFloat); + set[count].value = &headIndent; + count++; + + CGFloat tailIndent = self.tailIndent; + set[count].spec = kCTParagraphStyleSpecifierTailIndent; + set[count].valueSize = sizeof(CGFloat); + set[count].value = &tailIndent; + count++; + + CTLineBreakMode paraLineBreak = (CTLineBreakMode)self.lineBreakMode; + set[count].spec = kCTParagraphStyleSpecifierLineBreakMode; + set[count].valueSize = sizeof(CTLineBreakMode); + set[count].value = ¶LineBreak; + count++; + + CGFloat minimumLineHeight = self.minimumLineHeight; + set[count].spec = kCTParagraphStyleSpecifierMinimumLineHeight; + set[count].valueSize = sizeof(CGFloat); + set[count].value = &minimumLineHeight; + count++; + + CGFloat maximumLineHeight = self.maximumLineHeight; + set[count].spec = kCTParagraphStyleSpecifierMaximumLineHeight; + set[count].valueSize = sizeof(CGFloat); + set[count].value = &maximumLineHeight; + count++; + + CTWritingDirection paraWritingDirection = (CTWritingDirection)self.baseWritingDirection; + set[count].spec = kCTParagraphStyleSpecifierBaseWritingDirection; + set[count].valueSize = sizeof(CTWritingDirection); + set[count].value = ¶WritingDirection; + count++; + + CGFloat lineHeightMultiple = self.lineHeightMultiple; + set[count].spec = kCTParagraphStyleSpecifierLineHeightMultiple; + set[count].valueSize = sizeof(CGFloat); + set[count].value = &lineHeightMultiple; + count++; + + CGFloat paragraphSpacingBefore = self.paragraphSpacingBefore; + set[count].spec = kCTParagraphStyleSpecifierParagraphSpacingBefore; + set[count].valueSize = sizeof(CGFloat); + set[count].value = ¶graphSpacingBefore; + count++; + + NSMutableArray *tabs = [NSMutableArray array]; + NSInteger numTabs = self.tabStops.count; + if (numTabs) { + [self.tabStops enumerateObjectsUsingBlock: ^(NSTextTab *tab, NSUInteger idx, BOOL *stop) { + CTTextTabRef ctTab = CTTextTabCreate(NSTextAlignmentToCTTextAlignment(tab.alignment), tab.location, (__bridge CFTypeRef)tab.options); + [tabs addObject:(__bridge id)ctTab]; + CFRelease(ctTab); + }]; + + CFArrayRef tabStops = (__bridge CFArrayRef)(tabs); + set[count].spec = kCTParagraphStyleSpecifierTabStops; + set[count].valueSize = sizeof(CFArrayRef); + set[count].value = &tabStops; + count++; + } + + CGFloat defaultTabInterval = self.defaultTabInterval; + set[count].spec = kCTParagraphStyleSpecifierDefaultTabInterval; + set[count].valueSize = sizeof(CGFloat); + set[count].value = &defaultTabInterval; + count++; + + CTParagraphStyleRef style = CTParagraphStyleCreate(set, count); + return style; +} + +@end diff --git a/examples/Kittens/Sample/AppDelegate.m b/examples/Kittens/Sample/AppDelegate.m index 58fe564898..230173d515 100644 --- a/examples/Kittens/Sample/AppDelegate.m +++ b/examples/Kittens/Sample/AppDelegate.m @@ -18,11 +18,13 @@ #import "AppDelegate.h" #import "ViewController.h" +#import @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [ASTextNode setExperimentOptions:ASTextNodeExperimentRandomInstances]; self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; self.window.backgroundColor = [UIColor whiteColor]; self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[[ViewController alloc] init]]; diff --git a/examples/Kittens/Sample/KittenNode.mm b/examples/Kittens/Sample/KittenNode.mm index db215cb40b..c27d54bd2c 100644 --- a/examples/Kittens/Sample/KittenNode.mm +++ b/examples/Kittens/Sample/KittenNode.mm @@ -98,7 +98,20 @@ static const CGFloat kInnerPadding = 10.0f; [self addSubnode:_imageNode]; // lorem ipsum text, plus some nice styling + _textNode = [[ASTextNode alloc] init]; + _textNode.shadowColor = [UIColor blackColor].CGColor; + _textNode.shadowRadius = 3; + _textNode.shadowOffset = CGSizeMake(-2, -2); + _textNode.shadowOpacity = 0.3; + if (_textNode.usingExperiment) { + _textNode.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:1 alpha:1]; + } else { + _textNode.backgroundColor = [UIColor colorWithRed:1 green:0.9 blue:0.9 alpha:1]; + } + _textNode.maximumNumberOfLines = 2; + _textNode.truncationAttributedText = [[NSAttributedString alloc] initWithString:@"โ€ฆ"]; + _textNode.additionalTruncationMessage = [[NSAttributedString alloc] initWithString:@"More"]; _textNode.attributedText = [[NSAttributedString alloc] initWithString:[self kittyIpsum] attributes:[self textStyle]]; [self addSubnode:_textNode];