TUIAttributedLabel.m 78 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797
  1. // Created by Tencent on 2023/06/09.
  2. // Copyright © 2023 Tencent. All rights reserved.
  3. // TUIAttributedLabel.m
  4. #import "TUIAttributedLabel.h"
  5. #import <Availability.h>
  6. #import <QuartzCore/QuartzCore.h>
  7. #import <objc/runtime.h>
  8. #define kTUILineBreakWordWrapTextWidthScalingFactor (M_PI / M_E)
  9. static CGFloat const TUIFLOAT_MAX = 100000;
  10. NSString *const kTUIStrikeOutAttributeName = @"TUIStrikeOutAttribute";
  11. NSString *const kTUIBackgroundFillColorAttributeName = @"TUIBackgroundFillColor";
  12. NSString *const kTUIBackgroundFillPaddingAttributeName = @"TUIBackgroundFillPadding";
  13. NSString *const kTUIBackgroundStrokeColorAttributeName = @"TUIBackgroundStrokeColor";
  14. NSString *const kTUIBackgroundLineWidthAttributeName = @"TUIBackgroundLineWidth";
  15. NSString *const kTUIBackgroundCornerRadiusAttributeName = @"TUIBackgroundCornerRadius";
  16. const NSTextAlignment TUITextAlignmentLeft = NSTextAlignmentLeft;
  17. const NSTextAlignment TUITextAlignmentCenter = NSTextAlignmentCenter;
  18. const NSTextAlignment TUITextAlignmentRight = NSTextAlignmentRight;
  19. const NSTextAlignment TUITextAlignmentJustified = NSTextAlignmentJustified;
  20. const NSTextAlignment TUITextAlignmentNatural = NSTextAlignmentNatural;
  21. const NSLineBreakMode TUILineBreakByWordWrapping = NSLineBreakByWordWrapping;
  22. const NSLineBreakMode TUILineBreakByCharWrapping = NSLineBreakByCharWrapping;
  23. const NSLineBreakMode TUILineBreakByClipping = NSLineBreakByClipping;
  24. const NSLineBreakMode TUILineBreakByTruncatingHead = NSLineBreakByTruncatingHead;
  25. const NSLineBreakMode TUILineBreakByTruncatingMiddle = NSLineBreakByTruncatingMiddle;
  26. const NSLineBreakMode TUILineBreakByTruncatingTail = NSLineBreakByTruncatingTail;
  27. typedef NSTextAlignment TUITextAlignment;
  28. typedef NSLineBreakMode TUILineBreakMode;
  29. static inline CGFLOAT_TYPE formatCGFloatCeil(CGFLOAT_TYPE cgfloat) {
  30. #if CGFLOAT_IS_DOUBLE
  31. return ceil(cgfloat);
  32. #else
  33. return ceilf(cgfloat);
  34. #endif
  35. }
  36. static inline CGFLOAT_TYPE formatCGFloatFloor(CGFLOAT_TYPE cgfloat) {
  37. #if CGFLOAT_IS_DOUBLE
  38. return floor(cgfloat);
  39. #else
  40. return floorf(cgfloat);
  41. #endif
  42. }
  43. static inline CGFLOAT_TYPE formatCGFloatRound(CGFLOAT_TYPE cgfloat) {
  44. #if CGFLOAT_IS_DOUBLE
  45. return round(cgfloat);
  46. #else
  47. return roundf(cgfloat);
  48. #endif
  49. }
  50. static inline CGFLOAT_TYPE formatCGFloatSqrt(CGFLOAT_TYPE cgfloat) {
  51. #if CGFLOAT_IS_DOUBLE
  52. return sqrt(cgfloat);
  53. #else
  54. return sqrtf(cgfloat);
  55. #endif
  56. }
  57. static inline CGFloat flushFactorForTextAlignment(NSTextAlignment textAlignment) {
  58. switch (textAlignment) {
  59. case TUITextAlignmentCenter:
  60. return 0.5f;
  61. case TUITextAlignmentRight:
  62. return 1.0f;
  63. case TUITextAlignmentLeft:
  64. default:
  65. return 0.0f;
  66. }
  67. }
  68. static inline NSDictionary *formatNSAttributedStringAttributesFromLabel(TUIAttributedLabel *label) {
  69. NSMutableDictionary *mutableAttributes = [NSMutableDictionary dictionary];
  70. [mutableAttributes setObject:label.font forKey:(NSString *)kCTFontAttributeName];
  71. [mutableAttributes setObject:label.textColor forKey:(NSString *)kCTForegroundColorAttributeName];
  72. [mutableAttributes setObject:@(label.kern) forKey:(NSString *)kCTKernAttributeName];
  73. NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
  74. paragraphStyle.alignment = label.textAlignment;
  75. paragraphStyle.lineSpacing = label.lineSpacing;
  76. paragraphStyle.minimumLineHeight = label.minimumLineHeight > 0 ? label.minimumLineHeight : label.font.lineHeight * label.lineHeightMultiple;
  77. paragraphStyle.maximumLineHeight = label.maximumLineHeight > 0 ? label.maximumLineHeight : label.font.lineHeight * label.lineHeightMultiple;
  78. paragraphStyle.lineHeightMultiple = label.lineHeightMultiple;
  79. paragraphStyle.firstLineHeadIndent = label.firstLineIndent;
  80. if (label.numberOfLines == 1) {
  81. paragraphStyle.lineBreakMode = label.lineBreakMode;
  82. } else {
  83. paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
  84. }
  85. [mutableAttributes setObject:paragraphStyle forKey:(NSString *)kCTParagraphStyleAttributeName];
  86. return [NSDictionary dictionaryWithDictionary:mutableAttributes];
  87. }
  88. static inline CGColorRef formatCGColorRefFromColor(id color);
  89. static inline NSDictionary *convertNSAttributedStringAttributesToCTAttributes(NSDictionary *attributes);
  90. static inline NSAttributedString *formatNSAttributedStringByScalingFontSize(NSAttributedString *attributedString, CGFloat scale) {
  91. NSMutableAttributedString *mutableAttributedString = [attributedString mutableCopy];
  92. [mutableAttributedString enumerateAttribute:(NSString *)kCTFontAttributeName
  93. inRange:NSMakeRange(0, [mutableAttributedString length])
  94. options:0
  95. usingBlock:^(id value, NSRange range, BOOL *__unused stop) {
  96. UIFont *font = (UIFont *)value;
  97. if (font) {
  98. NSString *fontName;
  99. CGFloat pointSize;
  100. if ([font isKindOfClass:[UIFont class]]) {
  101. fontName = font.fontName;
  102. pointSize = font.pointSize;
  103. } else {
  104. fontName = (NSString *)CFBridgingRelease(CTFontCopyName((__bridge CTFontRef)font, kCTFontPostScriptNameKey));
  105. pointSize = CTFontGetSize((__bridge CTFontRef)font);
  106. }
  107. [mutableAttributedString removeAttribute:(NSString *)kCTFontAttributeName range:range];
  108. CTFontRef fontRef =
  109. CTFontCreateWithName((__bridge CFStringRef)fontName, formatCGFloatFloor(pointSize * scale), NULL);
  110. [mutableAttributedString addAttribute:(NSString *)kCTFontAttributeName value:(__bridge id)fontRef range:range];
  111. CFRelease(fontRef);
  112. }
  113. }];
  114. return mutableAttributedString;
  115. }
  116. static inline NSAttributedString *formatNSAttributedStringBySettingColorFromContext(NSAttributedString *attributedString, UIColor *color) {
  117. if (!color) {
  118. return attributedString;
  119. }
  120. NSMutableAttributedString *mutableAttributedString = [attributedString mutableCopy];
  121. [mutableAttributedString enumerateAttribute:(NSString *)kCTForegroundColorFromContextAttributeName
  122. inRange:NSMakeRange(0, [mutableAttributedString length])
  123. options:0
  124. usingBlock:^(id value, NSRange range, __unused BOOL *stop) {
  125. BOOL usesColorFromContext = (BOOL)value;
  126. if (usesColorFromContext) {
  127. [mutableAttributedString
  128. setAttributes:[NSDictionary dictionaryWithObject:color forKey:(NSString *)kCTForegroundColorAttributeName]
  129. range:range];
  130. [mutableAttributedString removeAttribute:(NSString *)kCTForegroundColorFromContextAttributeName range:range];
  131. }
  132. }];
  133. return mutableAttributedString;
  134. }
  135. static inline CGSize formatCTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(CTFramesetterRef framesetter, NSAttributedString *attributedString,
  136. CGSize size, NSUInteger numberOfLines) {
  137. CFRange rangeToSize = CFRangeMake(0, (CFIndex)[attributedString length]);
  138. CGSize constraints = CGSizeMake(size.width, TUIFLOAT_MAX);
  139. if (numberOfLines == 1) {
  140. // If there is one line, the size that fits is the full width of the line
  141. constraints = CGSizeMake(TUIFLOAT_MAX, TUIFLOAT_MAX);
  142. } else if (numberOfLines > 0) {
  143. // If the line count of the label more than 1, limit the range to size to the number of lines that have been set
  144. CGMutablePathRef path = CGPathCreateMutable();
  145. CGPathAddRect(path, NULL, CGRectMake(0.0f, 0.0f, constraints.width, TUIFLOAT_MAX));
  146. CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
  147. CFArrayRef lines = CTFrameGetLines(frame);
  148. if (CFArrayGetCount(lines) > 0) {
  149. NSInteger lastVisibleLineIndex = MIN((CFIndex)numberOfLines, CFArrayGetCount(lines)) - 1;
  150. CTLineRef lastVisibleLine = CFArrayGetValueAtIndex(lines, lastVisibleLineIndex);
  151. CFRange rangeToLayout = CTLineGetStringRange(lastVisibleLine);
  152. rangeToSize = CFRangeMake(0, rangeToLayout.location + rangeToLayout.length);
  153. }
  154. CFRelease(frame);
  155. CGPathRelease(path);
  156. }
  157. CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, rangeToSize, NULL, constraints, NULL);
  158. return CGSizeMake(formatCGFloatCeil(suggestedSize.width), formatCGFloatCeil(suggestedSize.height));
  159. }
  160. @interface TUIAccessibilityElement : UIAccessibilityElement
  161. @property(nonatomic, weak) UIView *superview;
  162. @property(nonatomic, assign) CGRect boundingRect;
  163. @end
  164. @implementation TUIAccessibilityElement
  165. - (CGRect)accessibilityFrame {
  166. return UIAccessibilityConvertFrameToScreenCoordinates(self.boundingRect, self.superview);
  167. }
  168. @end
  169. @interface TUIAttributedLabel ()
  170. @property(readwrite, nonatomic, copy) NSAttributedString *inactiveAttributedText;
  171. @property(readwrite, nonatomic, copy) NSAttributedString *renderedAttributedText;
  172. @property(readwrite, atomic, strong) NSDataDetector *dataDetector;
  173. @property(readwrite, nonatomic, strong) NSArray *linkModels;
  174. @property(readwrite, nonatomic, strong) TUIAttributedLabelLink *activeLink;
  175. @property(readwrite, nonatomic, strong) NSArray *accessibilityElements;
  176. - (void)longPressGestureDidFire:(UILongPressGestureRecognizer *)sender;
  177. @end
  178. @implementation TUIAttributedLabel {
  179. @private
  180. BOOL _needsFramesetter;
  181. CTFramesetterRef _framesetter;
  182. CTFramesetterRef _highlightFramesetter;
  183. }
  184. @dynamic text;
  185. @synthesize attributedText = _attributedText;
  186. #ifndef kCFCoreFoundationVersionNumber_iOS_7_0
  187. #define kCFCoreFoundationVersionNumber_iOS_7_0 847.2
  188. #endif
  189. + (void)load {
  190. static dispatch_once_t onceToken;
  191. dispatch_once(&onceToken, ^{
  192. if (kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_7_0) {
  193. Class class = [self class];
  194. Class superclass = class_getSuperclass(class);
  195. NSArray *strings = @[
  196. NSStringFromSelector(@selector(isAccessibilityElement)),
  197. NSStringFromSelector(@selector(accessibilityElementCount)),
  198. NSStringFromSelector(@selector(accessibilityElementAtIndex:)),
  199. NSStringFromSelector(@selector(indexOfAccessibilityElement:)),
  200. ];
  201. for (NSString *string in strings) {
  202. SEL selector = NSSelectorFromString(string);
  203. IMP superImplementation = class_getMethodImplementation(superclass, selector);
  204. Method method = class_getInstanceMethod(class, selector);
  205. const char *types = method_getTypeEncoding(method);
  206. class_replaceMethod(class, selector, superImplementation, types);
  207. }
  208. }
  209. });
  210. }
  211. - (instancetype)initWithFrame:(CGRect)frame {
  212. self = [super initWithFrame:frame];
  213. if (!self) {
  214. return nil;
  215. }
  216. [self commonInit];
  217. return self;
  218. }
  219. - (void)commonInit {
  220. self.userInteractionEnabled = YES;
  221. #if !TARGET_OS_TV
  222. self.multipleTouchEnabled = NO;
  223. #endif
  224. self.textInsets = UIEdgeInsetsZero;
  225. self.lineHeightMultiple = 1.0f;
  226. self.linkModels = [NSArray array];
  227. self.linkBackgroundEdgeInset = UIEdgeInsetsMake(0.0f, -1.0f, 0.0f, -1.0f);
  228. NSMutableDictionary *mutableLinkAttributes = [NSMutableDictionary dictionary];
  229. [mutableLinkAttributes setObject:[NSNumber numberWithBool:YES] forKey:(NSString *)kCTUnderlineStyleAttributeName];
  230. NSMutableDictionary *mutableActiveLinkAttributes = [NSMutableDictionary dictionary];
  231. [mutableActiveLinkAttributes setObject:[NSNumber numberWithBool:NO] forKey:(NSString *)kCTUnderlineStyleAttributeName];
  232. NSMutableDictionary *mutableInactiveLinkAttributes = [NSMutableDictionary dictionary];
  233. [mutableInactiveLinkAttributes setObject:[NSNumber numberWithBool:NO] forKey:(NSString *)kCTUnderlineStyleAttributeName];
  234. if ([NSMutableParagraphStyle class]) {
  235. [mutableLinkAttributes setObject:[UIColor blueColor] forKey:(NSString *)kCTForegroundColorAttributeName];
  236. [mutableActiveLinkAttributes setObject:[UIColor redColor] forKey:(NSString *)kCTForegroundColorAttributeName];
  237. [mutableInactiveLinkAttributes setObject:[UIColor grayColor] forKey:(NSString *)kCTForegroundColorAttributeName];
  238. } else {
  239. [mutableLinkAttributes setObject:(__bridge id)[[UIColor blueColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName];
  240. [mutableActiveLinkAttributes setObject:(__bridge id)[[UIColor redColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName];
  241. [mutableInactiveLinkAttributes setObject:(__bridge id)[[UIColor grayColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName];
  242. }
  243. self.linkAttributes = [NSDictionary dictionaryWithDictionary:mutableLinkAttributes];
  244. self.activeLinkAttributes = [NSDictionary dictionaryWithDictionary:mutableActiveLinkAttributes];
  245. self.inactiveLinkAttributes = [NSDictionary dictionaryWithDictionary:mutableInactiveLinkAttributes];
  246. _extendsLinkTouchArea = NO;
  247. _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGestureDidFire:)];
  248. self.longPressGestureRecognizer.delegate = self;
  249. [self addGestureRecognizer:self.longPressGestureRecognizer];
  250. }
  251. - (void)dealloc {
  252. if (_framesetter) {
  253. CFRelease(_framesetter);
  254. }
  255. if (_highlightFramesetter) {
  256. CFRelease(_highlightFramesetter);
  257. }
  258. if (_longPressGestureRecognizer) {
  259. [self removeGestureRecognizer:_longPressGestureRecognizer];
  260. }
  261. }
  262. #pragma mark -
  263. + (CGSize)sizeThatFitsAttributedString:(NSAttributedString *)attributedString withConstraints:(CGSize)size limitedToNumberOfLines:(NSUInteger)numberOfLines {
  264. if (!attributedString || attributedString.length == 0) {
  265. return CGSizeZero;
  266. }
  267. CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attributedString);
  268. CGSize calculatedSize = formatCTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(framesetter, attributedString, size, numberOfLines);
  269. CFRelease(framesetter);
  270. return calculatedSize;
  271. }
  272. #pragma mark -
  273. - (void)setAttributedText:(NSAttributedString *)text {
  274. if ([text isEqualToAttributedString:_attributedText]) {
  275. return;
  276. }
  277. _attributedText = [text copy];
  278. [self setNeedsFramesetter];
  279. [self setNeedsDisplay];
  280. if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) {
  281. [self invalidateIntrinsicContentSize];
  282. }
  283. [super setText:[self.attributedText string]];
  284. }
  285. - (NSAttributedString *)renderedAttributedText {
  286. if (!_renderedAttributedText) {
  287. NSMutableAttributedString *fullString = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText];
  288. if (self.attributedTruncationToken) {
  289. [fullString appendAttributedString:self.attributedTruncationToken];
  290. }
  291. NSAttributedString *string = [[NSAttributedString alloc] initWithAttributedString:fullString];
  292. self.renderedAttributedText = formatNSAttributedStringBySettingColorFromContext(string, self.textColor);
  293. }
  294. return _renderedAttributedText;
  295. }
  296. - (NSArray *)links {
  297. return [_linkModels valueForKey:@"result"];
  298. }
  299. - (void)setLinkModels:(NSArray *)linkModels {
  300. _linkModels = linkModels;
  301. self.accessibilityElements = nil;
  302. }
  303. - (void)setNeedsFramesetter {
  304. // Reset the rendered attributed text so it has a chance to regenerate
  305. self.renderedAttributedText = nil;
  306. _needsFramesetter = YES;
  307. }
  308. - (CTFramesetterRef)framesetter {
  309. if (_needsFramesetter) {
  310. @synchronized(self) {
  311. CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)self.renderedAttributedText);
  312. [self setFramesetter:framesetter];
  313. [self setHighlightFramesetter:nil];
  314. _needsFramesetter = NO;
  315. if (framesetter) {
  316. CFRelease(framesetter);
  317. }
  318. }
  319. }
  320. return _framesetter;
  321. }
  322. - (void)setFramesetter:(CTFramesetterRef)framesetter {
  323. if (framesetter) {
  324. CFRetain(framesetter);
  325. }
  326. if (_framesetter) {
  327. CFRelease(_framesetter);
  328. }
  329. _framesetter = framesetter;
  330. }
  331. - (CTFramesetterRef)highlightFramesetter {
  332. return _highlightFramesetter;
  333. }
  334. - (void)setHighlightFramesetter:(CTFramesetterRef)highlightFramesetter {
  335. if (highlightFramesetter) {
  336. CFRetain(highlightFramesetter);
  337. }
  338. if (_highlightFramesetter) {
  339. CFRelease(_highlightFramesetter);
  340. }
  341. _highlightFramesetter = highlightFramesetter;
  342. }
  343. #pragma mark -
  344. - (void)setEnabledTextCheckingTypes:(NSTextCheckingTypes)enabledTextCheckingTypes {
  345. if (self.enabledTextCheckingTypes == enabledTextCheckingTypes) {
  346. return;
  347. }
  348. _enabledTextCheckingTypes = enabledTextCheckingTypes;
  349. // one detector instance per type (combination), fast reuse e.g. in cells
  350. static NSMutableDictionary *dataDetectorsByType = nil;
  351. if (!dataDetectorsByType) {
  352. dataDetectorsByType = [NSMutableDictionary dictionary];
  353. }
  354. if (enabledTextCheckingTypes) {
  355. if (![dataDetectorsByType objectForKey:@(enabledTextCheckingTypes)]) {
  356. NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:enabledTextCheckingTypes error:nil];
  357. if (detector) {
  358. [dataDetectorsByType setObject:detector forKey:@(enabledTextCheckingTypes)];
  359. }
  360. }
  361. self.dataDetector = [dataDetectorsByType objectForKey:@(enabledTextCheckingTypes)];
  362. } else {
  363. self.dataDetector = nil;
  364. }
  365. }
  366. - (void)addLink:(TUIAttributedLabelLink *)link {
  367. [self addLinks:@[ link ]];
  368. }
  369. - (void)addLinks:(NSArray *)links {
  370. NSMutableArray *mutableLinkModels = [NSMutableArray arrayWithArray:self.linkModels];
  371. NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy];
  372. for (TUIAttributedLabelLink *link in links) {
  373. if (link.attributes) {
  374. [mutableAttributedString addAttributes:link.attributes range:link.result.range];
  375. }
  376. }
  377. self.attributedText = mutableAttributedString;
  378. [self setNeedsDisplay];
  379. [mutableLinkModels addObjectsFromArray:links];
  380. self.linkModels = [NSArray arrayWithArray:mutableLinkModels];
  381. }
  382. - (TUIAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result attributes:(NSDictionary *)attributes {
  383. return [self addLinksWithTextCheckingResults:@[ result ] attributes:attributes].firstObject;
  384. }
  385. - (NSArray *)addLinksWithTextCheckingResults:(NSArray *)results attributes:(NSDictionary *)attributes {
  386. NSMutableArray *links = [NSMutableArray array];
  387. for (NSTextCheckingResult *result in results) {
  388. NSDictionary *activeAttributes = attributes ? self.activeLinkAttributes : nil;
  389. NSDictionary *inactiveAttributes = attributes ? self.inactiveLinkAttributes : nil;
  390. TUIAttributedLabelLink *link = [[TUIAttributedLabelLink alloc] initWithAttributes:attributes
  391. activeAttributes:activeAttributes
  392. inactiveAttributes:inactiveAttributes
  393. textCheckingResult:result];
  394. [links addObject:link];
  395. }
  396. [self addLinks:links];
  397. return links;
  398. }
  399. - (TUIAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result {
  400. return [self addLinkWithTextCheckingResult:result attributes:self.linkAttributes];
  401. }
  402. - (TUIAttributedLabelLink *)addLinkToURL:(NSURL *)url withRange:(NSRange)range {
  403. return [self addLinkWithTextCheckingResult:[NSTextCheckingResult linkCheckingResultWithRange:range URL:url]];
  404. }
  405. - (TUIAttributedLabelLink *)addLinkToAddress:(NSDictionary *)addressComponents withRange:(NSRange)range {
  406. return [self addLinkWithTextCheckingResult:[NSTextCheckingResult addressCheckingResultWithRange:range components:addressComponents]];
  407. }
  408. - (TUIAttributedLabelLink *)addLinkToPhoneNumber:(NSString *)phoneNumber withRange:(NSRange)range {
  409. return [self addLinkWithTextCheckingResult:[NSTextCheckingResult phoneNumberCheckingResultWithRange:range phoneNumber:phoneNumber]];
  410. }
  411. - (TUIAttributedLabelLink *)addLinkToDate:(NSDate *)date withRange:(NSRange)range {
  412. return [self addLinkWithTextCheckingResult:[NSTextCheckingResult dateCheckingResultWithRange:range date:date]];
  413. }
  414. - (TUIAttributedLabelLink *)addLinkToDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone duration:(NSTimeInterval)duration withRange:(NSRange)range {
  415. return [self addLinkWithTextCheckingResult:[NSTextCheckingResult dateCheckingResultWithRange:range date:date timeZone:timeZone duration:duration]];
  416. }
  417. - (TUIAttributedLabelLink *)addLinkToTransitInformation:(NSDictionary *)components withRange:(NSRange)range {
  418. return [self addLinkWithTextCheckingResult:[NSTextCheckingResult transitInformationCheckingResultWithRange:range components:components]];
  419. }
  420. #pragma mark -
  421. - (BOOL)containslinkAtPoint:(CGPoint)point {
  422. return [self linkAtPoint:point] != nil;
  423. }
  424. - (TUIAttributedLabelLink *)linkAtPoint:(CGPoint)point {
  425. // Stop quickly if none of the points to be tested are in the bounds.
  426. if (!CGRectContainsPoint(CGRectInset(self.bounds, -15.f, -15.f), point) || self.links.count == 0) {
  427. return nil;
  428. }
  429. TUIAttributedLabelLink *result = [self linkAtCharacterIndex:[self characterIndexAtPoint:point]];
  430. if (!result && self.extendsLinkTouchArea) {
  431. result = [self linkAtRadius:2.5f aroundPoint:point]
  432. ?: [self linkAtRadius:5.f aroundPoint:point]
  433. ?: [self linkAtRadius:7.5f aroundPoint:point]
  434. ?: [self linkAtRadius:12.5f aroundPoint:point]
  435. ?: [self linkAtRadius:15.f aroundPoint:point];
  436. }
  437. return result;
  438. }
  439. - (TUIAttributedLabelLink *)linkAtRadius:(const CGFloat)radius aroundPoint:(CGPoint)point {
  440. const CGFloat diagonal = formatCGFloatSqrt(2 * radius * radius);
  441. const CGPoint deltas[] = {
  442. CGPointMake(0, -radius), CGPointMake(0, radius), // Above and below
  443. CGPointMake(-radius, 0), CGPointMake(radius, 0), // Beside
  444. CGPointMake(-diagonal, -diagonal), CGPointMake(-diagonal, diagonal), CGPointMake(diagonal, diagonal), CGPointMake(diagonal, -diagonal) // Diagonal
  445. };
  446. const size_t count = sizeof(deltas) / sizeof(CGPoint);
  447. TUIAttributedLabelLink *link = nil;
  448. for (NSUInteger i = 0; i < count && link.result == nil; i++) {
  449. CGPoint currentPoint = CGPointMake(point.x + deltas[i].x, point.y + deltas[i].y);
  450. link = [self linkAtCharacterIndex:[self characterIndexAtPoint:currentPoint]];
  451. }
  452. return link;
  453. }
  454. - (TUIAttributedLabelLink *)linkAtCharacterIndex:(CFIndex)idx {
  455. // Do not enumerate if the index is outside of the bounds of the text.
  456. if (!NSLocationInRange((NSUInteger)idx, NSMakeRange(0, self.attributedText.length))) {
  457. return nil;
  458. }
  459. NSEnumerator *enumerator = [self.linkModels reverseObjectEnumerator];
  460. TUIAttributedLabelLink *link = nil;
  461. while ((link = [enumerator nextObject])) {
  462. if (NSLocationInRange((NSUInteger)idx, link.result.range)) {
  463. return link;
  464. }
  465. }
  466. return nil;
  467. }
  468. - (CFIndex)characterIndexAtPoint:(CGPoint)p {
  469. if (!CGRectContainsPoint(self.bounds, p)) {
  470. return NSNotFound;
  471. }
  472. CGRect textRect = [self textRectForBounds:self.bounds limitedToNumberOfLines:self.numberOfLines];
  473. if (!CGRectContainsPoint(textRect, p)) {
  474. return NSNotFound;
  475. }
  476. // Offset tap coordinates by textRect origin to make them relative to the origin of frame
  477. p = CGPointMake(p.x - textRect.origin.x, p.y - textRect.origin.y);
  478. // Convert tap coordinates (start at top left) to CT coordinates (start at bottom left)
  479. p = CGPointMake(p.x, textRect.size.height - p.y);
  480. CGMutablePathRef path = CGPathCreateMutable();
  481. CGPathAddRect(path, NULL, textRect);
  482. CTFrameRef frame = CTFramesetterCreateFrame([self framesetter], CFRangeMake(0, (CFIndex)[self.attributedText length]), path, NULL);
  483. if (frame == NULL) {
  484. CGPathRelease(path);
  485. return NSNotFound;
  486. }
  487. CFArrayRef lines = CTFrameGetLines(frame);
  488. NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);
  489. if (numberOfLines == 0) {
  490. CFRelease(frame);
  491. CGPathRelease(path);
  492. return NSNotFound;
  493. }
  494. CFIndex idx = NSNotFound;
  495. CGPoint lineOrigins[numberOfLines];
  496. CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);
  497. for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
  498. CGPoint lineOrigin = lineOrigins[lineIndex];
  499. CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
  500. // Get bounding information of line
  501. CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
  502. CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
  503. CGFloat yMin = (CGFloat)floor(lineOrigin.y - descent);
  504. CGFloat yMax = (CGFloat)ceil(lineOrigin.y + ascent);
  505. // Apply penOffset using flushFactor for horizontal alignment to set lineOrigin since this is the horizontal offset from drawFramesetter
  506. CGFloat flushFactor = flushFactorForTextAlignment(self.textAlignment);
  507. CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, textRect.size.width);
  508. lineOrigin.x = penOffset;
  509. // Check if we've already passed the line
  510. if (p.y > yMax) {
  511. break;
  512. }
  513. // Check if the point is within this line vertically
  514. if (p.y >= yMin) {
  515. // Check if the point is within this line horizontally
  516. if (p.x >= lineOrigin.x && p.x <= lineOrigin.x + width) {
  517. // Convert CT coordinates to line-relative coordinates
  518. CGPoint relativePoint = CGPointMake(p.x - lineOrigin.x, p.y - lineOrigin.y);
  519. idx = CTLineGetStringIndexForPosition(line, relativePoint);
  520. break;
  521. }
  522. }
  523. }
  524. CFRelease(frame);
  525. CGPathRelease(path);
  526. return idx;
  527. }
  528. - (CGRect)boundingRectForCharacterRange:(NSRange)range {
  529. NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy];
  530. NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:mutableAttributedString];
  531. NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
  532. [textStorage addLayoutManager:layoutManager];
  533. NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:self.bounds.size];
  534. [layoutManager addTextContainer:textContainer];
  535. NSRange glyphRange;
  536. [layoutManager characterRangeForGlyphRange:range actualGlyphRange:&glyphRange];
  537. return [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
  538. }
  539. - (void)drawFramesetter:(CTFramesetterRef)framesetter
  540. attributedString:(NSAttributedString *)attributedString
  541. textRange:(CFRange)textRange
  542. inRect:(CGRect)rect
  543. context:(CGContextRef)c {
  544. CGMutablePathRef path = CGPathCreateMutable();
  545. CGPathAddRect(path, NULL, rect);
  546. CTFrameRef frame = CTFramesetterCreateFrame(framesetter, textRange, path, NULL);
  547. [self drawBackground:frame inRect:rect context:c];
  548. CFArrayRef lines = CTFrameGetLines(frame);
  549. NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);
  550. BOOL truncateLastLine = (self.lineBreakMode == TUILineBreakByTruncatingHead || self.lineBreakMode == TUILineBreakByTruncatingMiddle ||
  551. self.lineBreakMode == TUILineBreakByTruncatingTail);
  552. CGPoint lineOrigins[numberOfLines];
  553. CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);
  554. for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
  555. CGPoint lineOrigin = lineOrigins[lineIndex];
  556. CGContextSetTextPosition(c, lineOrigin.x, lineOrigin.y);
  557. CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
  558. CGFloat descent = 0.0f;
  559. CTLineGetTypographicBounds((CTLineRef)line, NULL, &descent, NULL);
  560. // Adjust pen offset for flush depending on text alignment
  561. CGFloat flushFactor = flushFactorForTextAlignment(self.textAlignment);
  562. if (lineIndex == numberOfLines - 1 && truncateLastLine) {
  563. // Check if the range of text in the last line reaches the end of the full attributed string
  564. CFRange lastLineRange = CTLineGetStringRange(line);
  565. if (!(lastLineRange.length == 0 && lastLineRange.location == 0) &&
  566. lastLineRange.location + lastLineRange.length < textRange.location + textRange.length) {
  567. // Get correct truncationType and attribute position
  568. CTLineTruncationType truncationType;
  569. CFIndex truncationAttributePosition = lastLineRange.location;
  570. TUILineBreakMode lineBreakMode = self.lineBreakMode;
  571. // Multiple lines, only use UILineBreakModeTailTruncation
  572. if (numberOfLines != 1) {
  573. lineBreakMode = TUILineBreakByTruncatingTail;
  574. }
  575. switch (lineBreakMode) {
  576. case TUILineBreakByTruncatingHead:
  577. truncationType = kCTLineTruncationStart;
  578. break;
  579. case TUILineBreakByTruncatingMiddle:
  580. truncationType = kCTLineTruncationMiddle;
  581. truncationAttributePosition += (lastLineRange.length / 2);
  582. break;
  583. case TUILineBreakByTruncatingTail:
  584. default:
  585. truncationType = kCTLineTruncationEnd;
  586. truncationAttributePosition += (lastLineRange.length - 1);
  587. break;
  588. }
  589. NSAttributedString *attributedTruncationString = self.attributedTruncationToken;
  590. if (!attributedTruncationString) {
  591. NSString *truncationTokenString = @"\u2026"; // Unicode Character 'HORIZONTAL ELLIPSIS' (U+2026)
  592. NSDictionary *truncationTokenStringAttributes = truncationTokenStringAttributes =
  593. [attributedString attributesAtIndex:(NSUInteger)truncationAttributePosition effectiveRange:NULL];
  594. attributedTruncationString = [[NSAttributedString alloc] initWithString:truncationTokenString attributes:truncationTokenStringAttributes];
  595. }
  596. CTLineRef truncationToken = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attributedTruncationString);
  597. // Append truncationToken to the string
  598. // because if string isn't too long, CT won't add the truncationToken on its own.
  599. // There is no chance of a double truncationToken because CT only adds the
  600. // token if it removes characters (and the one we add will go first)
  601. NSMutableAttributedString *truncationString = [[NSMutableAttributedString alloc]
  602. initWithAttributedString:[attributedString attributedSubstringFromRange:NSMakeRange((NSUInteger)lastLineRange.location,
  603. (NSUInteger)lastLineRange.length)]];
  604. if (lastLineRange.length > 0) {
  605. // Remove any newline at the end (we don't want newline space between the text and the truncation token). There can only be one, because the
  606. // second would be on the next line.
  607. unichar lastCharacter = [[truncationString string] characterAtIndex:(NSUInteger)(lastLineRange.length - 1)];
  608. if ([[NSCharacterSet newlineCharacterSet] characterIsMember:lastCharacter]) {
  609. [truncationString deleteCharactersInRange:NSMakeRange((NSUInteger)(lastLineRange.length - 1), 1)];
  610. }
  611. }
  612. [truncationString appendAttributedString:attributedTruncationString];
  613. CTLineRef truncationLine = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)truncationString);
  614. // Truncate the line in case it is too long.
  615. CTLineRef truncatedLine = CTLineCreateTruncatedLine(truncationLine, rect.size.width, truncationType, truncationToken);
  616. if (!truncatedLine) {
  617. // If the line is not as wide as the truncationToken, truncatedLine is NULL
  618. truncatedLine = CFRetain(truncationToken);
  619. }
  620. CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(truncatedLine, flushFactor, rect.size.width);
  621. CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender);
  622. CTLineDraw(truncatedLine, c);
  623. NSRange linkRange;
  624. if ([attributedTruncationString attribute:NSLinkAttributeName atIndex:0 effectiveRange:&linkRange]) {
  625. NSRange tokenRange = [truncationString.string rangeOfString:attributedTruncationString.string];
  626. NSRange tokenLinkRange =
  627. NSMakeRange((NSUInteger)(lastLineRange.location + lastLineRange.length) - tokenRange.length, (NSUInteger)tokenRange.length);
  628. [self addLinkToURL:[attributedTruncationString attribute:NSLinkAttributeName atIndex:0 effectiveRange:&linkRange] withRange:tokenLinkRange];
  629. }
  630. CFRelease(truncatedLine);
  631. CFRelease(truncationLine);
  632. CFRelease(truncationToken);
  633. } else {
  634. CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, rect.size.width);
  635. CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender);
  636. CTLineDraw(line, c);
  637. }
  638. } else {
  639. CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, rect.size.width);
  640. CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender);
  641. CTLineDraw(line, c);
  642. }
  643. }
  644. [self drawStrike:frame inRect:rect context:c];
  645. CFRelease(frame);
  646. CGPathRelease(path);
  647. }
  648. - (void)drawBackground:(CTFrameRef)frame inRect:(CGRect)rect context:(CGContextRef)c {
  649. NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
  650. CGPoint origins[[lines count]];
  651. CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
  652. CFIndex lineIndex = 0;
  653. for (id line in lines) {
  654. CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
  655. CGFloat width = (CGFloat)CTLineGetTypographicBounds((__bridge CTLineRef)line, &ascent, &descent, &leading);
  656. for (id glyphRun in (__bridge NSArray *)CTLineGetGlyphRuns((__bridge CTLineRef)line)) {
  657. NSDictionary *attributes = (__bridge NSDictionary *)CTRunGetAttributes((__bridge CTRunRef)glyphRun);
  658. CGColorRef strokeColor = formatCGColorRefFromColor([attributes objectForKey:kTUIBackgroundStrokeColorAttributeName]);
  659. CGColorRef fillColor = formatCGColorRefFromColor([attributes objectForKey:kTUIBackgroundFillColorAttributeName]);
  660. UIEdgeInsets fillPadding = [[attributes objectForKey:kTUIBackgroundFillPaddingAttributeName] UIEdgeInsetsValue];
  661. CGFloat cornerRadius = [[attributes objectForKey:kTUIBackgroundCornerRadiusAttributeName] floatValue];
  662. CGFloat lineWidth = [[attributes objectForKey:kTUIBackgroundLineWidthAttributeName] floatValue];
  663. if (strokeColor || fillColor) {
  664. CGRect runBounds = CGRectZero;
  665. CGFloat runAscent = 0.0f;
  666. CGFloat runDescent = 0.0f;
  667. runBounds.size.width = (CGFloat)CTRunGetTypographicBounds((__bridge CTRunRef)glyphRun, CFRangeMake(0, 0), &runAscent, &runDescent, NULL) +
  668. fillPadding.left + fillPadding.right;
  669. runBounds.size.height = runAscent + runDescent + fillPadding.top + fillPadding.bottom;
  670. CGFloat xOffset = 0.0f;
  671. CFRange glyphRange = CTRunGetStringRange((__bridge CTRunRef)glyphRun);
  672. switch (CTRunGetStatus((__bridge CTRunRef)glyphRun)) {
  673. case kCTRunStatusRightToLeft:
  674. xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location + glyphRange.length, NULL);
  675. break;
  676. default:
  677. xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location, NULL);
  678. break;
  679. }
  680. runBounds.origin.x = origins[lineIndex].x + rect.origin.x + xOffset - fillPadding.left - rect.origin.x;
  681. runBounds.origin.y = origins[lineIndex].y + rect.origin.y - fillPadding.bottom - rect.origin.y;
  682. runBounds.origin.y -= runDescent;
  683. // Don't draw higlightedLinkBackground too far to the right
  684. if (CGRectGetWidth(runBounds) > width) {
  685. runBounds.size.width = width;
  686. }
  687. CGPathRef path =
  688. [[UIBezierPath bezierPathWithRoundedRect:CGRectInset(UIEdgeInsetsInsetRect(runBounds, self.linkBackgroundEdgeInset), lineWidth, lineWidth)
  689. cornerRadius:cornerRadius] CGPath];
  690. CGContextSetLineJoin(c, kCGLineJoinRound);
  691. if (fillColor) {
  692. CGContextSetFillColorWithColor(c, fillColor);
  693. CGContextAddPath(c, path);
  694. CGContextFillPath(c);
  695. }
  696. if (strokeColor) {
  697. CGContextSetStrokeColorWithColor(c, strokeColor);
  698. CGContextAddPath(c, path);
  699. CGContextStrokePath(c);
  700. }
  701. }
  702. }
  703. lineIndex++;
  704. }
  705. }
  706. - (void)drawStrike:(CTFrameRef)frame inRect:(__unused CGRect)rect context:(CGContextRef)c {
  707. NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
  708. CGPoint origins[[lines count]];
  709. CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
  710. CFIndex lineIndex = 0;
  711. for (id line in lines) {
  712. CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
  713. CGFloat width = (CGFloat)CTLineGetTypographicBounds((__bridge CTLineRef)line, &ascent, &descent, &leading);
  714. for (id glyphRun in (__bridge NSArray *)CTLineGetGlyphRuns((__bridge CTLineRef)line)) {
  715. NSDictionary *attributes = (__bridge NSDictionary *)CTRunGetAttributes((__bridge CTRunRef)glyphRun);
  716. BOOL strikeOut = [[attributes objectForKey:kTUIStrikeOutAttributeName] boolValue];
  717. NSInteger superscriptStyle = [[attributes objectForKey:(id)kCTSuperscriptAttributeName] integerValue];
  718. if (strikeOut) {
  719. CGRect runBounds = CGRectZero;
  720. CGFloat runAscent = 0.0f;
  721. CGFloat runDescent = 0.0f;
  722. runBounds.size.width = (CGFloat)CTRunGetTypographicBounds((__bridge CTRunRef)glyphRun, CFRangeMake(0, 0), &runAscent, &runDescent, NULL);
  723. runBounds.size.height = runAscent + runDescent;
  724. CGFloat xOffset = 0.0f;
  725. CFRange glyphRange = CTRunGetStringRange((__bridge CTRunRef)glyphRun);
  726. switch (CTRunGetStatus((__bridge CTRunRef)glyphRun)) {
  727. case kCTRunStatusRightToLeft:
  728. xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location + glyphRange.length, NULL);
  729. break;
  730. default:
  731. xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location, NULL);
  732. break;
  733. }
  734. runBounds.origin.x = origins[lineIndex].x + xOffset;
  735. runBounds.origin.y = origins[lineIndex].y;
  736. runBounds.origin.y -= runDescent;
  737. // Don't draw strikeout too far to the right
  738. if (CGRectGetWidth(runBounds) > width) {
  739. runBounds.size.width = width;
  740. }
  741. switch (superscriptStyle) {
  742. case 1:
  743. runBounds.origin.y -= runAscent * 0.47f;
  744. break;
  745. case -1:
  746. runBounds.origin.y += runAscent * 0.25f;
  747. break;
  748. default:
  749. break;
  750. }
  751. // Use text color, or default to black
  752. id color = [attributes objectForKey:(id)kCTForegroundColorAttributeName];
  753. if (color) {
  754. CGContextSetStrokeColorWithColor(c, formatCGColorRefFromColor(color));
  755. } else {
  756. CGContextSetGrayStrokeColor(c, 0.0f, 1.0);
  757. }
  758. CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)self.font.fontName, self.font.pointSize, NULL);
  759. CGContextSetLineWidth(c, CTFontGetUnderlineThickness(font));
  760. CFRelease(font);
  761. CGFloat y = formatCGFloatRound(runBounds.origin.y + runBounds.size.height / 2.0f);
  762. CGContextMoveToPoint(c, runBounds.origin.x, y);
  763. CGContextAddLineToPoint(c, runBounds.origin.x + runBounds.size.width, y);
  764. CGContextStrokePath(c);
  765. }
  766. }
  767. lineIndex++;
  768. }
  769. }
  770. #pragma mark - TUIAttributedLabel
  771. - (void)setText:(id)text {
  772. NSParameterAssert(!text || [text isKindOfClass:[NSAttributedString class]] || [text isKindOfClass:[NSString class]]);
  773. if ([text isKindOfClass:[NSString class]]) {
  774. [self setText:text afterInheritingLabelAttributesAndConfiguringWithBlock:nil];
  775. return;
  776. }
  777. self.attributedText = text;
  778. self.activeLink = nil;
  779. self.linkModels = [NSArray array];
  780. if (text && self.attributedText && self.enabledTextCheckingTypes) {
  781. __weak __typeof(self) weakSelf = self;
  782. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  783. __strong __typeof(weakSelf) strongSelf = weakSelf;
  784. NSDataDetector *dataDetector = strongSelf.dataDetector;
  785. if (dataDetector && [dataDetector respondsToSelector:@selector(matchesInString:options:range:)]) {
  786. NSArray *results = [dataDetector matchesInString:[(NSAttributedString *)text string]
  787. options:0
  788. range:NSMakeRange(0, [(NSAttributedString *)text length])];
  789. if ([results count] > 0) {
  790. dispatch_async(dispatch_get_main_queue(), ^{
  791. if ([[strongSelf.attributedText string] isEqualToString:[(NSAttributedString *)text string]]) {
  792. [strongSelf addLinksWithTextCheckingResults:results attributes:strongSelf.linkAttributes];
  793. }
  794. });
  795. }
  796. }
  797. });
  798. }
  799. [self.attributedText enumerateAttribute:NSLinkAttributeName
  800. inRange:NSMakeRange(0, self.attributedText.length)
  801. options:0
  802. usingBlock:^(id value, __unused NSRange range, __unused BOOL *stop) {
  803. if (value) {
  804. NSURL *URL = [value isKindOfClass:[NSString class]] ? [NSURL URLWithString:value] : value;
  805. [self addLinkToURL:URL withRange:range];
  806. }
  807. }];
  808. }
  809. - (void)setText:(id)text
  810. afterInheritingLabelAttributesAndConfiguringWithBlock:(NSMutableAttributedString * (^)(NSMutableAttributedString *mutableAttributedString))block {
  811. NSMutableAttributedString *mutableAttributedString = nil;
  812. if ([text isKindOfClass:[NSString class]]) {
  813. mutableAttributedString = [[NSMutableAttributedString alloc] initWithString:text attributes:formatNSAttributedStringAttributesFromLabel(self)];
  814. } else {
  815. mutableAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:text];
  816. [mutableAttributedString addAttributes:formatNSAttributedStringAttributesFromLabel(self) range:NSMakeRange(0, [mutableAttributedString length])];
  817. }
  818. if (block) {
  819. mutableAttributedString = block(mutableAttributedString);
  820. }
  821. [self setText:mutableAttributedString];
  822. }
  823. - (void)setActiveLink:(TUIAttributedLabelLink *)activeLink {
  824. _activeLink = activeLink;
  825. NSDictionary *activeAttributes = activeLink.activeAttributes ?: self.activeLinkAttributes;
  826. if (_activeLink && activeAttributes.count > 0) {
  827. if (!self.inactiveAttributedText) {
  828. self.inactiveAttributedText = [self.attributedText copy];
  829. }
  830. NSMutableAttributedString *mutableAttributedString = [self.inactiveAttributedText mutableCopy];
  831. if (self.activeLink.result.range.length > 0 &&
  832. NSLocationInRange(NSMaxRange(self.activeLink.result.range) - 1, NSMakeRange(0, [self.inactiveAttributedText length]))) {
  833. [mutableAttributedString addAttributes:activeAttributes range:self.activeLink.result.range];
  834. }
  835. self.attributedText = mutableAttributedString;
  836. [self setNeedsDisplay];
  837. [CATransaction flush];
  838. } else if (self.inactiveAttributedText) {
  839. self.attributedText = self.inactiveAttributedText;
  840. self.inactiveAttributedText = nil;
  841. [self setNeedsDisplay];
  842. }
  843. }
  844. - (void)setLinkAttributes:(NSDictionary *)linkAttributes {
  845. _linkAttributes = convertNSAttributedStringAttributesToCTAttributes(linkAttributes);
  846. }
  847. - (void)setActiveLinkAttributes:(NSDictionary *)activeLinkAttributes {
  848. _activeLinkAttributes = convertNSAttributedStringAttributesToCTAttributes(activeLinkAttributes);
  849. }
  850. - (void)setInactiveLinkAttributes:(NSDictionary *)inactiveLinkAttributes {
  851. _inactiveLinkAttributes = convertNSAttributedStringAttributesToCTAttributes(inactiveLinkAttributes);
  852. }
  853. #pragma mark - UILabel
  854. - (void)setHighlighted:(BOOL)highlighted {
  855. [super setHighlighted:highlighted];
  856. [self setNeedsDisplay];
  857. }
  858. // Fixes crash when loading from a UIStoryboard
  859. - (UIColor *)textColor {
  860. UIColor *color = [super textColor];
  861. if (!color) {
  862. color = [UIColor blackColor];
  863. }
  864. return color;
  865. }
  866. - (void)setTextColor:(UIColor *)textColor {
  867. UIColor *oldTextColor = self.textColor;
  868. [super setTextColor:textColor];
  869. // Redraw to allow any ColorFromContext attributes a chance to update
  870. if (textColor != oldTextColor) {
  871. [self setNeedsFramesetter];
  872. [self setNeedsDisplay];
  873. }
  874. }
  875. - (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines {
  876. bounds = UIEdgeInsetsInsetRect(bounds, self.textInsets);
  877. if (!self.attributedText) {
  878. return [super textRectForBounds:bounds limitedToNumberOfLines:numberOfLines];
  879. }
  880. CGRect textRect = bounds;
  881. // Calculate height with a minimum of double the font pointSize, to ensure that CTFramesetterSuggestFrameSizeWithConstraints doesn't return CGSizeZero, as
  882. // it would if textRect height is insufficient.
  883. textRect.size.height = MAX(self.font.lineHeight * MAX(2, numberOfLines), bounds.size.height);
  884. // Adjust the text to be in the center vertically, if the text size is smaller than bounds
  885. CGSize textSize =
  886. CTFramesetterSuggestFrameSizeWithConstraints([self framesetter], CFRangeMake(0, (CFIndex)[self.attributedText length]), NULL, textRect.size, NULL);
  887. textSize =
  888. CGSizeMake(formatCGFloatCeil(textSize.width),
  889. formatCGFloatCeil(textSize.height));
  890. // Fix for iOS 4, CTFramesetterSuggestFrameSizeWithConstraints sometimes returns fractional sizes
  891. if (textSize.height < bounds.size.height) {
  892. CGFloat yOffset = 0.0f;
  893. switch (self.verticalAlignment) {
  894. case TUIAttributedLabelVerticalAlignmentCenter:
  895. yOffset = formatCGFloatFloor((bounds.size.height - textSize.height) / 2.0f);
  896. break;
  897. case TUIAttributedLabelVerticalAlignmentBottom:
  898. yOffset = bounds.size.height - textSize.height;
  899. break;
  900. case TUIAttributedLabelVerticalAlignmentTop:
  901. default:
  902. break;
  903. }
  904. textRect.origin.y += yOffset;
  905. }
  906. return textRect;
  907. }
  908. - (void)drawTextInRect:(CGRect)rect {
  909. CGRect insetRect = UIEdgeInsetsInsetRect(rect, self.textInsets);
  910. if (!self.attributedText) {
  911. [super drawTextInRect:insetRect];
  912. return;
  913. }
  914. NSAttributedString *originalAttributedText = nil;
  915. // Adjust the font size to fit width, if necessarry
  916. if (self.adjustsFontSizeToFitWidth && self.numberOfLines > 0) {
  917. // Framesetter could still be working with a resized version of the text;
  918. // need to reset so we start from the original font size.
  919. // See #393.
  920. [self setNeedsFramesetter];
  921. [self setNeedsDisplay];
  922. if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) {
  923. [self invalidateIntrinsicContentSize];
  924. }
  925. // Use infinite width to find the max width, which will be compared to availableWidth if needed.
  926. CGSize maxSize = (self.numberOfLines > 1) ? CGSizeMake(TUIFLOAT_MAX, TUIFLOAT_MAX) : CGSizeZero;
  927. CGFloat textWidth = [self sizeThatFits:maxSize].width;
  928. CGFloat availableWidth = self.frame.size.width * self.numberOfLines;
  929. if (self.numberOfLines > 1 && self.lineBreakMode == TUILineBreakByWordWrapping) {
  930. textWidth *= kTUILineBreakWordWrapTextWidthScalingFactor;
  931. }
  932. if (textWidth > availableWidth && textWidth > 0.0f) {
  933. originalAttributedText = [self.attributedText copy];
  934. CGFloat scaleFactor = availableWidth / textWidth;
  935. if ([self respondsToSelector:@selector(minimumScaleFactor)] && self.minimumScaleFactor > scaleFactor) {
  936. scaleFactor = self.minimumScaleFactor;
  937. }
  938. self.attributedText = formatNSAttributedStringByScalingFontSize(self.attributedText, scaleFactor);
  939. }
  940. }
  941. CGContextRef c = UIGraphicsGetCurrentContext();
  942. CGContextSaveGState(c);
  943. {
  944. CGContextSetTextMatrix(c, CGAffineTransformIdentity);
  945. // Inverts the CTM to match iOS coordinates (otherwise text draws upside-down; Mac OS's system is different)
  946. CGContextTranslateCTM(c, 0.0f, insetRect.size.height);
  947. CGContextScaleCTM(c, 1.0f, -1.0f);
  948. CFRange textRange = CFRangeMake(0, (CFIndex)[self.attributedText length]);
  949. // First, get the text rect (which takes vertical centering into account)
  950. CGRect textRect = [self textRectForBounds:rect limitedToNumberOfLines:self.numberOfLines];
  951. // CoreText draws its text aligned to the bottom, so we move the CTM here to take our vertical offsets into account
  952. CGContextTranslateCTM(c, insetRect.origin.x, insetRect.size.height - textRect.origin.y - textRect.size.height);
  953. // Second, trace the shadow before the actual text, if we have one
  954. if (self.shadowColor && !self.highlighted) {
  955. CGContextSetShadowWithColor(c, self.shadowOffset, self.shadowRadius, [self.shadowColor CGColor]);
  956. } else if (self.highlightedShadowColor) {
  957. CGContextSetShadowWithColor(c, self.highlightedShadowOffset, self.highlightedShadowRadius, [self.highlightedShadowColor CGColor]);
  958. }
  959. // Finally, draw the text or highlighted text itself (on top of the shadow, if there is one)
  960. if (self.highlightedTextColor && self.highlighted) {
  961. NSMutableAttributedString *highlightAttributedString = [self.renderedAttributedText mutableCopy];
  962. [highlightAttributedString addAttribute:(__bridge NSString *)kCTForegroundColorAttributeName
  963. value:(id)[self.highlightedTextColor CGColor]
  964. range:NSMakeRange(0, highlightAttributedString.length)];
  965. if (![self highlightFramesetter]) {
  966. CTFramesetterRef highlightFramesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)highlightAttributedString);
  967. [self setHighlightFramesetter:highlightFramesetter];
  968. CFRelease(highlightFramesetter);
  969. }
  970. [self drawFramesetter:[self highlightFramesetter] attributedString:highlightAttributedString textRange:textRange inRect:textRect context:c];
  971. } else {
  972. [self drawFramesetter:[self framesetter] attributedString:self.renderedAttributedText textRange:textRange inRect:textRect context:c];
  973. }
  974. // If we adjusted the font size, set it back to its original size
  975. if (originalAttributedText) {
  976. // Use ivar directly to avoid clearing out framesetter and renderedAttributedText
  977. _attributedText = originalAttributedText;
  978. }
  979. }
  980. CGContextRestoreGState(c);
  981. }
  982. #pragma mark - UIAccessibilityElement
  983. - (BOOL)isAccessibilityElement {
  984. return NO;
  985. }
  986. - (NSInteger)accessibilityElementCount {
  987. return (NSInteger)[[self accessibilityElements] count];
  988. }
  989. - (id)accessibilityElementAtIndex:(NSInteger)index {
  990. return [[self accessibilityElements] objectAtIndex:(NSUInteger)index];
  991. }
  992. - (NSInteger)indexOfAccessibilityElement:(id)element {
  993. return (NSInteger)[[self accessibilityElements] indexOfObject:element];
  994. }
  995. - (NSArray *)accessibilityElements {
  996. if (!_accessibilityElements) {
  997. @synchronized(self) {
  998. NSMutableArray *mutableAccessibilityItems = [NSMutableArray array];
  999. for (TUIAttributedLabelLink *link in self.linkModels) {
  1000. if (link.result.range.location == NSNotFound) {
  1001. continue;
  1002. }
  1003. NSString *sourceText = [self.text isKindOfClass:[NSString class]] ? self.text : [(NSAttributedString *)self.text string];
  1004. NSString *accessibilityLabel = [sourceText substringWithRange:link.result.range];
  1005. NSString *accessibilityValue = link.accessibilityValue;
  1006. if (accessibilityLabel) {
  1007. TUIAccessibilityElement *linkElement = [[TUIAccessibilityElement alloc] initWithAccessibilityContainer:self];
  1008. linkElement.accessibilityTraits = UIAccessibilityTraitLink;
  1009. linkElement.boundingRect = [self boundingRectForCharacterRange:link.result.range];
  1010. linkElement.superview = self;
  1011. linkElement.accessibilityLabel = accessibilityLabel;
  1012. if (![accessibilityLabel isEqualToString:accessibilityValue]) {
  1013. linkElement.accessibilityValue = accessibilityValue;
  1014. }
  1015. [mutableAccessibilityItems addObject:linkElement];
  1016. }
  1017. }
  1018. TUIAccessibilityElement *baseElement = [[TUIAccessibilityElement alloc] initWithAccessibilityContainer:self];
  1019. baseElement.accessibilityLabel = [super accessibilityLabel];
  1020. baseElement.accessibilityHint = [super accessibilityHint];
  1021. baseElement.accessibilityValue = [super accessibilityValue];
  1022. baseElement.boundingRect = self.bounds;
  1023. baseElement.superview = self;
  1024. baseElement.accessibilityTraits = [super accessibilityTraits];
  1025. [mutableAccessibilityItems addObject:baseElement];
  1026. self.accessibilityElements = [NSArray arrayWithArray:mutableAccessibilityItems];
  1027. }
  1028. }
  1029. return _accessibilityElements;
  1030. }
  1031. #pragma mark - UIView
  1032. - (CGSize)sizeThatFits:(CGSize)size {
  1033. if (!self.attributedText) {
  1034. return [super sizeThatFits:size];
  1035. } else {
  1036. NSAttributedString *string = [self renderedAttributedText];
  1037. CGSize labelSize =
  1038. formatCTFramesetterSuggestFrameSizeForAttributedStringWithConstraints([self framesetter], string, size, (NSUInteger)self.numberOfLines);
  1039. labelSize.width += self.textInsets.left + self.textInsets.right;
  1040. labelSize.height += self.textInsets.top + self.textInsets.bottom;
  1041. return labelSize;
  1042. }
  1043. }
  1044. - (CGSize)intrinsicContentSize {
  1045. // There's an implicit width from the original UILabel implementation
  1046. return [self sizeThatFits:[super intrinsicContentSize]];
  1047. }
  1048. - (void)tintColorDidChange {
  1049. if (!self.inactiveLinkAttributes || [self.inactiveLinkAttributes count] == 0) {
  1050. return;
  1051. }
  1052. BOOL isInactive = (self.tintAdjustmentMode == UIViewTintAdjustmentModeDimmed);
  1053. NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy];
  1054. for (TUIAttributedLabelLink *link in self.linkModels) {
  1055. NSDictionary *attributesToRemove = isInactive ? link.attributes : link.inactiveAttributes;
  1056. NSDictionary *attributesToAdd = isInactive ? link.inactiveAttributes : link.attributes;
  1057. [attributesToRemove enumerateKeysAndObjectsUsingBlock:^(NSString *name, __unused id value, __unused BOOL *stop) {
  1058. if (NSMaxRange(link.result.range) <= mutableAttributedString.length) {
  1059. [mutableAttributedString removeAttribute:name range:link.result.range];
  1060. }
  1061. }];
  1062. if (attributesToAdd) {
  1063. if (NSMaxRange(link.result.range) <= mutableAttributedString.length) {
  1064. [mutableAttributedString addAttributes:attributesToAdd range:link.result.range];
  1065. }
  1066. }
  1067. }
  1068. self.attributedText = mutableAttributedString;
  1069. [self setNeedsDisplay];
  1070. }
  1071. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  1072. if (![self linkAtPoint:point] || !self.userInteractionEnabled || self.hidden || self.alpha < 0.01) {
  1073. return [super hitTest:point withEvent:event];
  1074. }
  1075. return self;
  1076. }
  1077. #pragma mark - UIResponder
  1078. - (BOOL)canBecomeFirstResponder {
  1079. return YES;
  1080. }
  1081. - (BOOL)canPerformAction:(SEL)action withSender:(__unused id)sender {
  1082. #if !TARGET_OS_TV
  1083. return (action == @selector(copy:));
  1084. #else
  1085. return NO;
  1086. #endif
  1087. }
  1088. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  1089. UITouch *touch = [touches anyObject];
  1090. self.activeLink = [self linkAtPoint:[touch locationInView:self]];
  1091. if (!self.activeLink) {
  1092. [super touchesBegan:touches withEvent:event];
  1093. }
  1094. }
  1095. - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
  1096. if (self.activeLink) {
  1097. UITouch *touch = [touches anyObject];
  1098. if (self.activeLink != [self linkAtPoint:[touch locationInView:self]]) {
  1099. self.activeLink = nil;
  1100. }
  1101. } else {
  1102. [super touchesMoved:touches withEvent:event];
  1103. }
  1104. }
  1105. - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  1106. if (self.activeLink) {
  1107. if (self.activeLink.linkTapBlock) {
  1108. self.activeLink.linkTapBlock(self, self.activeLink);
  1109. self.activeLink = nil;
  1110. return;
  1111. }
  1112. NSTextCheckingResult *result = self.activeLink.result;
  1113. self.activeLink = nil;
  1114. switch (result.resultType) {
  1115. case NSTextCheckingTypeLink:
  1116. if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithURL:)]) {
  1117. [self.delegate attributedLabel:self didSelectLinkWithURL:result.URL];
  1118. return;
  1119. }
  1120. break;
  1121. case NSTextCheckingTypeAddress:
  1122. if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithAddress:)]) {
  1123. [self.delegate attributedLabel:self didSelectLinkWithAddress:result.addressComponents];
  1124. return;
  1125. }
  1126. break;
  1127. case NSTextCheckingTypePhoneNumber:
  1128. if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithPhoneNumber:)]) {
  1129. [self.delegate attributedLabel:self didSelectLinkWithPhoneNumber:result.phoneNumber];
  1130. return;
  1131. }
  1132. break;
  1133. case NSTextCheckingTypeDate:
  1134. if (result.timeZone && [self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithDate:timeZone:duration:)]) {
  1135. [self.delegate attributedLabel:self didSelectLinkWithDate:result.date timeZone:result.timeZone duration:result.duration];
  1136. return;
  1137. } else if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithDate:)]) {
  1138. [self.delegate attributedLabel:self didSelectLinkWithDate:result.date];
  1139. return;
  1140. }
  1141. break;
  1142. case NSTextCheckingTypeTransitInformation:
  1143. if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithTransitInformation:)]) {
  1144. [self.delegate attributedLabel:self didSelectLinkWithTransitInformation:result.components];
  1145. return;
  1146. }
  1147. default:
  1148. break;
  1149. }
  1150. // Fallback to `attributedLabel:didSelectLinkWithTextCheckingResult:` if no other delegate method matched.
  1151. if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithTextCheckingResult:)]) {
  1152. [self.delegate attributedLabel:self didSelectLinkWithTextCheckingResult:result];
  1153. }
  1154. } else {
  1155. [super touchesEnded:touches withEvent:event];
  1156. }
  1157. }
  1158. - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
  1159. if (self.activeLink) {
  1160. self.activeLink = nil;
  1161. } else {
  1162. [super touchesCancelled:touches withEvent:event];
  1163. }
  1164. }
  1165. #pragma mark - UIGestureRecognizerDelegate
  1166. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
  1167. return [self containslinkAtPoint:[touch locationInView:self]];
  1168. }
  1169. #pragma mark - UILongPressGestureRecognizer
  1170. - (void)longPressGestureDidFire:(UILongPressGestureRecognizer *)sender {
  1171. switch (sender.state) {
  1172. case UIGestureRecognizerStateBegan: {
  1173. CGPoint touchPoint = [sender locationInView:self];
  1174. TUIAttributedLabelLink *link = [self linkAtPoint:touchPoint];
  1175. if (link) {
  1176. if (link.linkLongPressBlock) {
  1177. link.linkLongPressBlock(self, link);
  1178. return;
  1179. }
  1180. NSTextCheckingResult *result = link.result;
  1181. if (!result) {
  1182. return;
  1183. }
  1184. switch (result.resultType) {
  1185. case NSTextCheckingTypeLink:
  1186. if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithURL:atPoint:)]) {
  1187. [self.delegate attributedLabel:self didLongPressLinkWithURL:result.URL atPoint:touchPoint];
  1188. return;
  1189. }
  1190. break;
  1191. case NSTextCheckingTypeAddress:
  1192. if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithAddress:atPoint:)]) {
  1193. [self.delegate attributedLabel:self didLongPressLinkWithAddress:result.addressComponents atPoint:touchPoint];
  1194. return;
  1195. }
  1196. break;
  1197. case NSTextCheckingTypePhoneNumber:
  1198. if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithPhoneNumber:atPoint:)]) {
  1199. [self.delegate attributedLabel:self didLongPressLinkWithPhoneNumber:result.phoneNumber atPoint:touchPoint];
  1200. return;
  1201. }
  1202. break;
  1203. case NSTextCheckingTypeDate:
  1204. if (result.timeZone && [self.delegate respondsToSelector:@selector(attributedLabel:
  1205. didLongPressLinkWithDate:timeZone:duration:atPoint:)]) {
  1206. [self.delegate attributedLabel:self
  1207. didLongPressLinkWithDate:result.date
  1208. timeZone:result.timeZone
  1209. duration:result.duration
  1210. atPoint:touchPoint];
  1211. return;
  1212. } else if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithDate:atPoint:)]) {
  1213. [self.delegate attributedLabel:self didLongPressLinkWithDate:result.date atPoint:touchPoint];
  1214. return;
  1215. }
  1216. break;
  1217. case NSTextCheckingTypeTransitInformation:
  1218. if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithTransitInformation:atPoint:)]) {
  1219. [self.delegate attributedLabel:self didLongPressLinkWithTransitInformation:result.components atPoint:touchPoint];
  1220. return;
  1221. }
  1222. default:
  1223. break;
  1224. }
  1225. // Fallback to `attributedLabel:didLongPressLinkWithTextCheckingResult:atPoint:` if no other delegate method matched.
  1226. if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithTextCheckingResult:atPoint:)]) {
  1227. [self.delegate attributedLabel:self didLongPressLinkWithTextCheckingResult:result atPoint:touchPoint];
  1228. }
  1229. }
  1230. break;
  1231. }
  1232. default:
  1233. break;
  1234. }
  1235. }
  1236. #if !TARGET_OS_TV
  1237. #pragma mark - UIResponderStandardEditActions
  1238. - (void)copy:(__unused id)sender {
  1239. [[UIPasteboard generalPasteboard] setString:self.text];
  1240. }
  1241. #endif
  1242. #pragma mark - NSCoding
  1243. - (void)encodeWithCoder:(NSCoder *)coder {
  1244. [super encodeWithCoder:coder];
  1245. [coder encodeObject:@(self.enabledTextCheckingTypes) forKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))];
  1246. [coder encodeObject:self.linkModels forKey:NSStringFromSelector(@selector(linkModels))];
  1247. if ([NSMutableParagraphStyle class]) {
  1248. [coder encodeObject:self.linkAttributes forKey:NSStringFromSelector(@selector(linkAttributes))];
  1249. [coder encodeObject:self.activeLinkAttributes forKey:NSStringFromSelector(@selector(activeLinkAttributes))];
  1250. [coder encodeObject:self.inactiveLinkAttributes forKey:NSStringFromSelector(@selector(inactiveLinkAttributes))];
  1251. }
  1252. [coder encodeObject:@(self.shadowRadius) forKey:NSStringFromSelector(@selector(shadowRadius))];
  1253. [coder encodeObject:@(self.highlightedShadowRadius) forKey:NSStringFromSelector(@selector(highlightedShadowRadius))];
  1254. [coder encodeCGSize:self.highlightedShadowOffset forKey:NSStringFromSelector(@selector(highlightedShadowOffset))];
  1255. [coder encodeObject:self.highlightedShadowColor forKey:NSStringFromSelector(@selector(highlightedShadowColor))];
  1256. [coder encodeObject:@(self.kern) forKey:NSStringFromSelector(@selector(kern))];
  1257. [coder encodeObject:@(self.firstLineIndent) forKey:NSStringFromSelector(@selector(firstLineIndent))];
  1258. [coder encodeObject:@(self.lineSpacing) forKey:NSStringFromSelector(@selector(lineSpacing))];
  1259. [coder encodeObject:@(self.lineHeightMultiple) forKey:NSStringFromSelector(@selector(lineHeightMultiple))];
  1260. [coder encodeUIEdgeInsets:self.textInsets forKey:NSStringFromSelector(@selector(textInsets))];
  1261. [coder encodeInteger:self.verticalAlignment forKey:NSStringFromSelector(@selector(verticalAlignment))];
  1262. [coder encodeObject:self.attributedTruncationToken forKey:NSStringFromSelector(@selector(attributedTruncationToken))];
  1263. [coder encodeObject:NSStringFromUIEdgeInsets(self.linkBackgroundEdgeInset) forKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))];
  1264. [coder encodeObject:self.attributedText forKey:NSStringFromSelector(@selector(attributedText))];
  1265. [coder encodeObject:self.text forKey:NSStringFromSelector(@selector(text))];
  1266. }
  1267. - (id)initWithCoder:(NSCoder *)coder {
  1268. self = [super initWithCoder:coder];
  1269. if (!self) {
  1270. return nil;
  1271. }
  1272. [self commonInit];
  1273. if ([coder containsValueForKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))]) {
  1274. self.enabledTextCheckingTypes = [[coder decodeObjectForKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))] unsignedLongLongValue];
  1275. }
  1276. if ([NSMutableParagraphStyle class]) {
  1277. if ([coder containsValueForKey:NSStringFromSelector(@selector(linkAttributes))]) {
  1278. self.linkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(linkAttributes))];
  1279. }
  1280. if ([coder containsValueForKey:NSStringFromSelector(@selector(activeLinkAttributes))]) {
  1281. self.activeLinkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(activeLinkAttributes))];
  1282. }
  1283. if ([coder containsValueForKey:NSStringFromSelector(@selector(inactiveLinkAttributes))]) {
  1284. self.inactiveLinkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(inactiveLinkAttributes))];
  1285. }
  1286. }
  1287. if ([coder containsValueForKey:NSStringFromSelector(@selector(links))]) {
  1288. NSArray *oldLinks = [coder decodeObjectForKey:NSStringFromSelector(@selector(links))];
  1289. [self addLinksWithTextCheckingResults:oldLinks attributes:nil];
  1290. }
  1291. if ([coder containsValueForKey:NSStringFromSelector(@selector(linkModels))]) {
  1292. self.linkModels = [coder decodeObjectForKey:NSStringFromSelector(@selector(linkModels))];
  1293. }
  1294. if ([coder containsValueForKey:NSStringFromSelector(@selector(shadowRadius))]) {
  1295. self.shadowRadius = [[coder decodeObjectForKey:NSStringFromSelector(@selector(shadowRadius))] floatValue];
  1296. }
  1297. if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowRadius))]) {
  1298. self.highlightedShadowRadius = [[coder decodeObjectForKey:NSStringFromSelector(@selector(highlightedShadowRadius))] floatValue];
  1299. }
  1300. if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowOffset))]) {
  1301. self.highlightedShadowOffset = [coder decodeCGSizeForKey:NSStringFromSelector(@selector(highlightedShadowOffset))];
  1302. }
  1303. if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowColor))]) {
  1304. self.highlightedShadowColor = [coder decodeObjectForKey:NSStringFromSelector(@selector(highlightedShadowColor))];
  1305. }
  1306. if ([coder containsValueForKey:NSStringFromSelector(@selector(kern))]) {
  1307. self.kern = [[coder decodeObjectForKey:NSStringFromSelector(@selector(kern))] floatValue];
  1308. }
  1309. if ([coder containsValueForKey:NSStringFromSelector(@selector(firstLineIndent))]) {
  1310. self.firstLineIndent = [[coder decodeObjectForKey:NSStringFromSelector(@selector(firstLineIndent))] floatValue];
  1311. }
  1312. if ([coder containsValueForKey:NSStringFromSelector(@selector(lineSpacing))]) {
  1313. self.lineSpacing = [[coder decodeObjectForKey:NSStringFromSelector(@selector(lineSpacing))] floatValue];
  1314. }
  1315. if ([coder containsValueForKey:NSStringFromSelector(@selector(minimumLineHeight))]) {
  1316. self.minimumLineHeight = [[coder decodeObjectForKey:NSStringFromSelector(@selector(minimumLineHeight))] floatValue];
  1317. }
  1318. if ([coder containsValueForKey:NSStringFromSelector(@selector(maximumLineHeight))]) {
  1319. self.maximumLineHeight = [[coder decodeObjectForKey:NSStringFromSelector(@selector(maximumLineHeight))] floatValue];
  1320. }
  1321. if ([coder containsValueForKey:NSStringFromSelector(@selector(lineHeightMultiple))]) {
  1322. self.lineHeightMultiple = [[coder decodeObjectForKey:NSStringFromSelector(@selector(lineHeightMultiple))] floatValue];
  1323. }
  1324. if ([coder containsValueForKey:NSStringFromSelector(@selector(textInsets))]) {
  1325. self.textInsets = [coder decodeUIEdgeInsetsForKey:NSStringFromSelector(@selector(textInsets))];
  1326. }
  1327. if ([coder containsValueForKey:NSStringFromSelector(@selector(verticalAlignment))]) {
  1328. self.verticalAlignment = [coder decodeIntegerForKey:NSStringFromSelector(@selector(verticalAlignment))];
  1329. }
  1330. if ([coder containsValueForKey:NSStringFromSelector(@selector(attributedTruncationToken))]) {
  1331. self.attributedTruncationToken = [coder decodeObjectForKey:NSStringFromSelector(@selector(attributedTruncationToken))];
  1332. }
  1333. if ([coder containsValueForKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))]) {
  1334. self.linkBackgroundEdgeInset = UIEdgeInsetsFromString([coder decodeObjectForKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))]);
  1335. }
  1336. if ([coder containsValueForKey:NSStringFromSelector(@selector(attributedText))]) {
  1337. self.attributedText = [coder decodeObjectForKey:NSStringFromSelector(@selector(attributedText))];
  1338. } else {
  1339. self.text = super.text;
  1340. }
  1341. return self;
  1342. }
  1343. @end
  1344. #pragma mark - TUIAttributedLabelLink
  1345. @implementation TUIAttributedLabelLink
  1346. - (instancetype)initWithAttributes:(NSDictionary *)attributes
  1347. activeAttributes:(NSDictionary *)activeAttributes
  1348. inactiveAttributes:(NSDictionary *)inactiveAttributes
  1349. textCheckingResult:(NSTextCheckingResult *)result {
  1350. if ((self = [super init])) {
  1351. _result = result;
  1352. _attributes = [attributes copy];
  1353. _activeAttributes = [activeAttributes copy];
  1354. _inactiveAttributes = [inactiveAttributes copy];
  1355. }
  1356. return self;
  1357. }
  1358. - (instancetype)initWithAttributesFromLabel:(TUIAttributedLabel *)label textCheckingResult:(NSTextCheckingResult *)result {
  1359. return [self initWithAttributes:label.linkAttributes
  1360. activeAttributes:label.activeLinkAttributes
  1361. inactiveAttributes:label.inactiveLinkAttributes
  1362. textCheckingResult:result];
  1363. }
  1364. #pragma mark - Accessibility
  1365. - (NSString *)accessibilityValue {
  1366. if ([_accessibilityValue length] == 0) {
  1367. switch (self.result.resultType) {
  1368. case NSTextCheckingTypeLink:
  1369. _accessibilityValue = self.result.URL.absoluteString;
  1370. break;
  1371. case NSTextCheckingTypePhoneNumber:
  1372. _accessibilityValue = self.result.phoneNumber;
  1373. break;
  1374. case NSTextCheckingTypeDate:
  1375. _accessibilityValue = [NSDateFormatter localizedStringFromDate:self.result.date
  1376. dateStyle:NSDateFormatterLongStyle
  1377. timeStyle:NSDateFormatterLongStyle];
  1378. break;
  1379. default:
  1380. break;
  1381. }
  1382. }
  1383. return _accessibilityValue;
  1384. }
  1385. #pragma mark - NSCoding
  1386. - (void)encodeWithCoder:(NSCoder *)aCoder {
  1387. [aCoder encodeObject:self.result forKey:NSStringFromSelector(@selector(result))];
  1388. [aCoder encodeObject:self.attributes forKey:NSStringFromSelector(@selector(attributes))];
  1389. [aCoder encodeObject:self.activeAttributes forKey:NSStringFromSelector(@selector(activeAttributes))];
  1390. [aCoder encodeObject:self.inactiveAttributes forKey:NSStringFromSelector(@selector(inactiveAttributes))];
  1391. [aCoder encodeObject:self.accessibilityValue forKey:NSStringFromSelector(@selector(accessibilityValue))];
  1392. }
  1393. - (id)initWithCoder:(NSCoder *)aDecoder {
  1394. if ((self = [super init])) {
  1395. _result = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(result))];
  1396. _attributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(attributes))];
  1397. _activeAttributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(activeAttributes))];
  1398. _inactiveAttributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(inactiveAttributes))];
  1399. self.accessibilityValue = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(accessibilityValue))];
  1400. }
  1401. return self;
  1402. }
  1403. @end
  1404. #pragma mark -
  1405. static inline CGColorRef formatCGColorRefFromColor(id color) { return [color isKindOfClass:[UIColor class]] ? [color CGColor] : (__bridge CGColorRef)color; }
  1406. static inline CTFontRef formatCTFontRefFromUIFont(UIFont *font) {
  1407. CTFontRef ctfont = CTFontCreateWithName((__bridge CFStringRef)font.fontName, font.pointSize, NULL);
  1408. return CFAutorelease(ctfont);
  1409. }
  1410. static inline NSDictionary *convertNSAttributedStringAttributesToCTAttributes(NSDictionary *attributes) {
  1411. if (!attributes) return nil;
  1412. NSMutableDictionary *mutableAttributes = [NSMutableDictionary dictionary];
  1413. NSDictionary *convertMap = @{
  1414. NSFontAttributeName : (NSString *)kCTFontAttributeName,
  1415. NSBackgroundColorAttributeName : (NSString *)kTUIBackgroundFillColorAttributeName,
  1416. NSForegroundColorAttributeName : (NSString *)kCTForegroundColorAttributeName,
  1417. NSUnderlineColorAttributeName : (NSString *)kCTUnderlineColorAttributeName,
  1418. NSUnderlineStyleAttributeName : (NSString *)kCTUnderlineStyleAttributeName,
  1419. NSStrokeWidthAttributeName : (NSString *)kCTStrokeWidthAttributeName,
  1420. NSStrokeColorAttributeName : (NSString *)kCTStrokeWidthAttributeName,
  1421. NSKernAttributeName : (NSString *)kCTKernAttributeName,
  1422. NSLigatureAttributeName : (NSString *)kCTLigatureAttributeName
  1423. };
  1424. [attributes enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
  1425. key = [convertMap objectForKey:key] ?: key;
  1426. if (![NSMutableParagraphStyle class]) {
  1427. if ([value isKindOfClass:[UIFont class]]) {
  1428. value = (__bridge id)formatCTFontRefFromUIFont(value);
  1429. } else if ([value isKindOfClass:[UIColor class]]) {
  1430. value = (__bridge id)((UIColor *)value).CGColor;
  1431. }
  1432. }
  1433. [mutableAttributes setObject:value forKey:key];
  1434. }];
  1435. return [NSDictionary dictionaryWithDictionary:mutableAttributes];
  1436. }