From 7b02207537d35bfa1714bf8beafc921f717d100a Mon Sep 17 00:00:00 2001 From: 单军华 Date: Wed, 11 Jul 2018 10:47:42 +0800 Subject: [PATCH] 首次上传 --- screendisplay/Pods/YYText/YYText/Component/YYTextLayout.m | 3407 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 3,407 insertions(+), 0 deletions(-) diff --git a/screendisplay/Pods/YYText/YYText/Component/YYTextLayout.m b/screendisplay/Pods/YYText/YYText/Component/YYTextLayout.m new file mode 100755 index 0000000..808b55f --- /dev/null +++ b/screendisplay/Pods/YYText/YYText/Component/YYTextLayout.m @@ -0,0 +1,3407 @@ +// +// YYTextLayout.m +// YYText <https://github.com/ibireme/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 "YYTextLayout.h" +#import "YYTextUtilities.h" +#import "YYTextAttribute.h" +#import "YYTextArchiver.h" +#import "NSAttributedString+YYText.h" + +const CGSize YYTextContainerMaxSize = (CGSize){0x100000, 0x100000}; + +typedef struct { + CGFloat head; + CGFloat foot; +} YYRowEdge; + +static inline CGSize YYTextClipCGSize(CGSize size) { + if (size.width > YYTextContainerMaxSize.width) size.width = YYTextContainerMaxSize.width; + if (size.height > YYTextContainerMaxSize.height) size.height = YYTextContainerMaxSize.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 YYTextGetCGColor(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 YYTextLinePositionSimpleModifier +- (void)modifyLines:(NSArray *)lines fromText:(NSAttributedString *)text inContainer:(YYTextContainer *)container { + if (container.verticalForm) { + for (NSUInteger i = 0, max = lines.count; i < max; i++) { + YYTextLine *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++) { + YYTextLine *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 { + YYTextLinePositionSimpleModifier *one = [self.class new]; + one.fixedLineHeight = _fixedLineHeight; + return one; +} +@end + + +@implementation YYTextContainer { + @package + BOOL _readonly; ///< used only in YYTextLayout.implementation + dispatch_semaphore_t _lock; + + CGSize _size; + UIEdgeInsets _insets; + UIBezierPath *_path; + NSArray *_exclusionPaths; + BOOL _pathFillEvenOdd; + CGFloat _pathLineWidth; + BOOL _verticalForm; + NSUInteger _maximumNumberOfRows; + YYTextTruncationType _truncationType; + NSAttributedString *_truncationToken; + id<YYTextLinePositionModifier> _linePositionModifier; +} + ++ (instancetype)containerWithSize:(CGSize)size { + return [self containerWithSize:size insets:UIEdgeInsetsZero]; +} + ++ (instancetype)containerWithSize:(CGSize)size insets:(UIEdgeInsets)insets { + YYTextContainer *one = [self new]; + one.size = YYTextClipCGSize(size); + one.insets = insets; + return one; +} + ++ (instancetype)containerWithPath:(UIBezierPath *)path { + YYTextContainer *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 { + YYTextContainer *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 'YYTextLayout'." 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 = YYTextClipCGSize(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); +} + +- (YYTextTruncationType)truncationType { + Getter(YYTextTruncationType type = _truncationType) return type; +} + +- (void)setTruncationType:(YYTextTruncationType)truncationType { + Setter(_truncationType = truncationType); +} + +- (NSAttributedString *)truncationToken { + Getter(NSAttributedString *token = _truncationToken) return token; +} + +- (void)setTruncationToken:(NSAttributedString *)truncationToken { + Setter(_truncationToken = truncationToken.copy); +} + +- (void)setLinePositionModifier:(id<YYTextLinePositionModifier>)linePositionModifier { + Setter(_linePositionModifier = [(NSObject *)linePositionModifier copy]); +} + +- (id<YYTextLinePositionModifier>)linePositionModifier { + Getter(id<YYTextLinePositionModifier> m = _linePositionModifier) return m; +} + +#undef Getter +#undef Setter +@end + + + + +@interface YYTextLayout () + +@property (nonatomic, readwrite) YYTextContainer *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) YYTextLine *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) YYRowEdge *lineRowsEdge; ///< top-left origin + +@end + + + +@implementation YYTextLayout + +#pragma mark - Layout + +- (instancetype)_init { + self = [super init]; + return self; +} + ++ (YYTextLayout *)layoutWithContainerSize:(CGSize)size text:(NSAttributedString *)text { + YYTextContainer *container = [YYTextContainer containerWithSize:size]; + return [self layoutWithContainer:container text:text]; +} + ++ (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text { + return [self layoutWithContainer:container text:text range:NSMakeRange(0, text.length)]; +} + ++ (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range { + YYTextLayout *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; + YYTextLine *truncatedLine = nil; + YYRowEdge *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) yy_setClearColorToJoinedEmoji]; + } + + layout = [[YYTextLayout 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 = YYTextContainerMaxSize.width; + } else { + rect.size.height = YYTextContainerMaxSize.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, YYTextCFRangeFromNSRange(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; + + YYTextLine *line = [YYTextLine 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 { + YYTextLine *line = lines.lastObject; + if (!line) break; + if (line.row < rowCount) break; + [lines removeLastObject]; + } while (1); + } + } + YYTextLine *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++) { + YYTextLine *line = lines[i]; + if (i == 0) textBoundingRect = line.bounds; + else textBoundingRect = CGRectUnion(textBoundingRect, line.bounds); + } + } + + lineRowsEdge = calloc(rowCount, sizeof(YYRowEdge)); + 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++) { + YYTextLine *line = lines[i]; + CGRect rect = line.bounds; + if ((NSInteger)line.row != lastRowIdx) { + if (lastRowIdx >= 0) { + lineRowsEdge[lastRowIdx] = (YYRowEdge) {.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] = (YYRowEdge) {.head = lastHead, .foot = lastFoot }; + + for (NSUInteger i = 1; i < rowCount; i++) { + YYRowEdge v0 = lineRowsEdge[i - 1]; + YYRowEdge 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,YYTextUIEdgeInsetsInvert(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 = YYTextNSRangeFromCFRange(CTFrameGetVisibleStringRange(ctFrame)); + if (needTruncation) { + YYTextLine *lastLine = lines.lastObject; + NSRange lastRange = lastLine.range; + visibleRange.length = lastRange.location + lastRange.length - visibleRange.location; + + // create truncated line + if (container.truncationType != YYTextTruncationTypeNone) { + 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 yy_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:YYTextTruncationToken attributes:attrs]; + truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)truncationToken); + } + if (truncationTokenLine) { + CTLineTruncationType type = kCTLineTruncationEnd; + if (container.truncationType == YYTextTruncationTypeStart) { + type = kCTLineTruncationStart; + } else if (container.truncationType == YYTextTruncationTypeMiddle) { + 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 = [YYTextLine lineWithCTLine:ctTruncatedLine position:lastLine.position vertical:isVerticalForm]; + truncatedLine.index = lastLine.index; + truncatedLine.row = lastLine.row; + CFRelease(ctTruncatedLine); + } + } + CFRelease(truncationTokenLine); + } + } + } + + if (isVerticalForm) { + NSCharacterSet *rotateCharset = YYTextVerticalFormRotateCharacterSet(); + NSCharacterSet *rotateMoveCharset = YYTextVerticalFormRotateAndMoveCharacterSet(); + + void (^lineBlock)(YYTextLine *) = ^(YYTextLine *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 = YYTextCTFontContainsColorBitmapGlyphs(font); + + NSUInteger prevIdx = 0; + YYTextRunGlyphDrawMode prevMode = YYTextRunGlyphDrawModeHorizontal; + 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; + } + + YYTextRunGlyphDrawMode mode = glyphRotateMove ? YYTextRunGlyphDrawModeVerticalRotateMove : (glyphRotate ? YYTextRunGlyphDrawModeVerticalRotate : YYTextRunGlyphDrawModeHorizontal); + if (g == 0) { + prevMode = mode; + } else if (mode != prevMode) { + YYTextRunGlyphRange *aRange = [YYTextRunGlyphRange rangeWithRange:NSMakeRange(prevIdx, g - prevIdx) drawMode:prevMode]; + [runRanges addObject:aRange]; + prevIdx = g; + prevMode = mode; + } + } + if (prevIdx < glyphCount) { + YYTextRunGlyphRange *aRange = [YYTextRunGlyphRange rangeWithRange:NSMakeRange(prevIdx, glyphCount - prevIdx) drawMode:prevMode]; + [runRanges addObject:aRange]; + } + + } + }; + for (YYTextLine *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[YYTextHighlightAttributeName]) layout.containsHighlight = YES; + if (attrs[YYTextBlockBorderAttributeName]) layout.needDrawBlockBorder = YES; + if (attrs[YYTextBackgroundBorderAttributeName]) layout.needDrawBackgroundBorder = YES; + if (attrs[YYTextShadowAttributeName] || attrs[NSShadowAttributeName]) layout.needDrawShadow = YES; + if (attrs[YYTextUnderlineAttributeName]) layout.needDrawUnderline = YES; + if (attrs[YYTextAttachmentAttributeName]) layout.needDrawAttachment = YES; + if (attrs[YYTextInnerShadowAttributeName]) layout.needDrawInnerShadow = YES; + if (attrs[YYTextStrikethroughAttributeName]) layout.needDrawStrikethrough = YES; + if (attrs[YYTextBorderAttributeName]) 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++) { + YYTextLine *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 (YYTextAttachment *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++) { + YYTextContainer *container = containers[i]; + YYTextLayout *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 - Coding + + +- (void)encodeWithCoder:(NSCoder *)aCoder { + NSData *textData = [YYTextArchiver archivedDataWithRootObject:_text]; + [aCoder encodeObject:textData forKey:@"text"]; + [aCoder encodeObject:_container forKey:@"container"]; + [aCoder encodeObject:[NSValue valueWithRange:_range] forKey:@"range"]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + NSData *textData = [aDecoder decodeObjectForKey:@"text"]; + NSAttributedString *text = [YYTextUnarchiver unarchiveObjectWithData:textData]; + YYTextContainer *container = [aDecoder decodeObjectForKey:@"container"]; + NSRange range = ((NSValue *)[aDecoder decodeObjectForKey:@"range"]).rangeValue; + self = [self.class layoutWithContainer:container text:text range:range]; + return self; +} + +#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; + YYRowEdge 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:(YYTextLine *)line position:(YYTextPosition *)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 == YYTextAffinityBackward) { + 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:(YYTextLine *)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:(YYTextLine *)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 (!YYTextCTFontContainsColorBitmapGlyphs(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:(YYTextLine *)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. + */ +- (YYTextRange *)_correctedRangeWithEdge:(YYTextRange *)range { + NSRange visibleRange = self.visibleRange; + YYTextPosition *start = range.start; + YYTextPosition *end = range.end; + + if (start.offset == visibleRange.location && start.affinity == YYTextAffinityBackward) { + start = [YYTextPosition positionWithOffset:start.offset affinity:YYTextAffinityForward]; + } + + if (end.offset == visibleRange.location + visibleRange.length && start.affinity == YYTextAffinityForward) { + end = [YYTextPosition positionWithOffset:end.offset affinity:YYTextAffinityBackward]; + } + + if (start != range.start || end != range.end) { + range = [YYTextRange 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 ((YYTextLine *)_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 = ((YYTextLine *)_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 = ((YYTextLine *)_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; + YYTextLine *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; + YYTextLine *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 (!YYTextCTFontContainsColorBitmapGlyphs(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; +} + +- (YYTextPosition *)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; + YYTextLine *line = _lines[lineIndex]; + __block NSUInteger position = [self textPositionForPoint:point lineIndex:lineIndex]; + if (position == NSNotFound) position = line.range.location; + if (position <= _visibleRange.location) { + return [YYTextPosition positionWithOffset:_visibleRange.location affinity:YYTextAffinityForward]; + } else if (position >= _visibleRange.location + _visibleRange.length) { + return [YYTextPosition positionWithOffset:_visibleRange.location + _visibleRange.length affinity:YYTextAffinityBackward]; + } + + YYTextAffinity finalAffinity = YYTextAffinityForward; + BOOL finalAffinityDetected = NO; + + // binding range + NSRange bindingRange; + YYTextBinding *binding = [_text attribute:YYTextBindingAttributeName atIndex:position longestEffectiveRange:&bindingRange inRange:NSMakeRange(0, _text.length)]; + if (binding && bindingRange.length > 0) { + NSUInteger headLineIdx = [self lineIndexForPosition:[YYTextPosition positionWithOffset:bindingRange.location]]; + NSUInteger tailLineIdx = [self lineIndexForPosition:[YYTextPosition positionWithOffset:bindingRange.location + bindingRange.length affinity:YYTextAffinityBackward]]; + 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 = YYTextAffinityForward; + } else { + position = bindingRange.location + bindingRange.length; + finalAffinity = YYTextAffinityBackward; + } + } else { + if (fabs(point.x - left) < fabs(point.x - right)) { + position = bindingRange.location; + finalAffinity = YYTextAffinityForward; + } else { + position = bindingRange.location + bindingRange.length; + finalAffinity = YYTextAffinityBackward; + } + } + } else if (left != CGFLOAT_MAX) { + position = left; + finalAffinity = YYTextAffinityForward; + } else if (right != CGFLOAT_MAX) { + position = right; + finalAffinity = YYTextAffinityBackward; + } + finalAffinityDetected = YES; + } else if (headLineIdx == lineIndex) { + CGFloat left = [self offsetForTextPosition:bindingRange.location lineIndex:lineIndex]; + if (left != CGFLOAT_MAX) { + position = bindingRange.location; + finalAffinity = YYTextAffinityForward; + 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 = YYTextAffinityBackward; + 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 = YYTextAffinityForward; + 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 = YYTextAffinityBackward; + finalAffinityDetected = YES; + } + } + } + } + + // empty line + if (line.range.length == 0) { + BOOL behind = (_lines.count > 1 && lineIndex == _lines.count - 1); //end line + return [YYTextPosition positionWithOffset:line.range.location affinity:behind ? YYTextAffinityBackward:YYTextAffinityForward]; + } + + // detect weather the line is a linebreak token + if (line.range.length <= 2) { + NSString *str = [_text.string substringWithRange:line.range]; + if (YYTextIsLinebreakString(str)) { // an empty line ("\r", "\n", "\r\n") + return [YYTextPosition positionWithOffset:line.range.location]; + } + } + + // above whole text frame + if (lineIndex == 0 && (isVertical ? (point.x > line.right) : (point.y < line.top))) { + position = 0; + finalAffinity = YYTextAffinityForward; + 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 = YYTextAffinityBackward; + 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 (YYTextIsLinebreakChar(c1)) { + position--; + if (position > line.range.location) { + unichar c0 = [_text.string characterAtIndex:position - 1]; + if (YYTextIsLinebreakChar(c0)) { + position--; + } + } + } + } + } + if (position == line.range.location) { + return [YYTextPosition positionWithOffset:position]; + } + if (position == line.range.location + line.range.length) { + return [YYTextPosition positionWithOffset:position affinity:YYTextAffinityBackward]; + } + + [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 ? YYTextAffinityForward : YYTextAffinityBackward; + } else if (position <= line.range.location) { + finalAffinity = RTL ? YYTextAffinityBackward : YYTextAffinityForward; + } else { + finalAffinity = (ofs < (isVertical ? point.y : point.x) && !RTL) ? YYTextAffinityForward : YYTextAffinityBackward; + } + } + } + + return [YYTextPosition positionWithOffset:position affinity:finalAffinity]; +} + +- (YYTextPosition *)positionForPoint:(CGPoint)point + oldPosition:(YYTextPosition *)oldPosition + otherPosition:(YYTextPosition *)otherPosition { + if (!oldPosition || !otherPosition) { + return oldPosition; + } + YYTextPosition *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; + YYTextLine *line = _lines[lineIndex]; + YYRowEdge 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 + YYTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionUp offset:1]; + if (range) return range.start; + } else { // search forward + YYTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionDown offset:1]; + if (range) return range.end; + } + } else { + if ([oldPosition compare:otherPosition] == NSOrderedAscending) { // search backward + YYTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionLeft offset:1]; + if (range) return range.start; + } else { // search forward + YYTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionRight offset:1]; + if (range) return range.end; + } + } + + return oldPosition; +} + +- (YYTextRange *)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; + YYTextPosition *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) { + YYTextRange *range = [self textRangeByExtendingPosition:pos inDirection:(rect.origin.y >= point.y && !RTL) ? UITextLayoutDirectionUp:UITextLayoutDirectionDown offset:1]; + return range; + } else { + YYTextRange *range = [self textRangeByExtendingPosition:pos inDirection:(rect.origin.x >= point.x && !RTL) ? UITextLayoutDirectionLeft:UITextLayoutDirectionRight offset:1]; + return range; + } +} + +- (YYTextRange *)closestTextRangeAtPoint:(CGPoint)point { + YYTextPosition *pos = [self closestPositionToPoint:point]; + if (!pos) return nil; + NSUInteger lineIndex = [self lineIndexForPosition:pos]; + if (lineIndex == NSNotFound) return nil; + YYTextLine *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; + } + } + + YYTextRange *range = [self textRangeByExtendingPosition:pos inDirection:direction offset:1]; + return range; +} + +- (YYTextRange *)textRangeByExtendingPosition:(YYTextPosition *)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 [YYTextRange rangeWithRange:NSMakeRange(position.offset, 0)]; + } else if (position.offset == visibleEnd) { + return [YYTextRange rangeWithRange:NSMakeRange(position.offset, 0) affinity:YYTextAffinityBackward]; + } + + // binding range + NSRange tRange; + YYTextBinding *binding = [_text attribute:YYTextBindingAttributeName atIndex:position.offset longestEffectiveRange:&tRange inRange:_visibleRange]; + if (binding && tRange.length > 0 && tRange.location < position.offset) { + return [YYTextRange 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; + + YYTextLine *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 [YYTextRange 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 [YYTextRange rangeWithStart:[YYTextPosition positionWithOffset:position.offset - 1] end:[YYTextPosition positionWithOffset:position.offset + 1]]; + } + } + if (YYTextIsLinebreakChar(c0) && position.affinity == YYTextAffinityBackward) { + NSString *str = [_text.string substringToIndex:position.offset]; + NSUInteger len = YYTextLinebreakTailLength(str); + return [YYTextRange rangeWithStart:[YYTextPosition positionWithOffset:position.offset - len] end:[YYTextPosition positionWithOffset:position.offset]]; + } + } + + return [YYTextRange rangeWithRange:NSMakeRange(position.offset, 0) affinity:position.affinity]; +} + +- (YYTextRange *)textRangeByExtendingPosition:(YYTextPosition *)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 [YYTextRange rangeWithRange:NSMakeRange(_visibleRange.location, 0)]; + } else if (forwardMove && position.offset == visibleEnd) { + return [YYTextRange rangeWithRange:NSMakeRange(position.offset, 0) affinity:YYTextAffinityBackward]; + } + + // extend from position + YYTextRange *fromRange = [self textRangeByExtendingPosition:position]; + if (!fromRange) return nil; + YYTextRange *allForward = [YYTextRange rangeWithStart:fromRange.start end:[YYTextPosition positionWithOffset:visibleEnd]]; + YYTextRange *allBackward = [YYTextRange rangeWithStart:[YYTextPosition positionWithOffset:visibleStart] end:fromRange.end]; + + if (verticalMove) { // up/down in text layout + NSInteger lineIndex = [self lineIndexForPosition:position]; + if (lineIndex == NSNotFound) return nil; + + YYTextLine *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; + YYTextLine *mostLeftLine = nil, *mostRightLine = nil; + NSUInteger insideIndex = NSNotFound; + for (NSUInteger i = 0; i < moveToLineCount; i++) { + NSUInteger lineIndex = moveToLineFirstIndex + i; + YYTextLine *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; + } + YYTextLine *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; + YYTextPosition *extPos; + if (afinityEdge) { + if (pos == insideLine.range.location + insideLine.range.length) { + NSString *subStr = [_text.string substringWithRange:insideLine.range]; + NSUInteger lineBreakLen = YYTextLinebreakTailLength(subStr); + extPos = [YYTextPosition positionWithOffset:pos - lineBreakLen]; + } else { + extPos = [YYTextPosition positionWithOffset:pos]; + } + } else { + extPos = [YYTextPosition positionWithOffset:pos]; + } + YYTextRange *ext = [self textRangeByExtendingPosition:extPos]; + if (!ext) return nil; + if (forwardMove) { + return [YYTextRange rangeWithStart:fromRange.start end:ext.end]; + } else { + return [YYTextRange rangeWithStart:ext.start end:fromRange.end]; + } + + } else { // left/right in text layout + YYTextPosition *toPosition = [YYTextPosition positionWithOffset:position.offset + (forwardMove ? offset : -offset)]; + if (toPosition.offset <= visibleStart) return allBackward; + else if (toPosition.offset >= visibleEnd) return allForward; + + YYTextRange *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 [YYTextRange rangeWithRange:NSMakeRange(start, end - start)]; + } +} + +- (NSUInteger)lineIndexForPosition:(YYTextPosition *)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 == YYTextAffinityBackward) { + while (lo <= hi) { + mid = (lo + hi) / 2; + YYTextLine *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; + YYTextLine *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:(YYTextPosition *)position { + NSUInteger lineIndex = [self lineIndexForPosition:position]; + if (lineIndex == NSNotFound) return CGPointZero; + YYTextLine *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:(YYTextPosition *)position { + NSUInteger lineIndex = [self lineIndexForPosition:position]; + if (lineIndex == NSNotFound) return CGRectNull; + YYTextLine *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:(YYTextRange *)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; + YYTextLine *startLine = _lines[startLineIndex]; + YYTextLine *endLine = _lines[endLineIndex]; + NSMutableArray *lines = [NSMutableArray new]; + for (NSUInteger i = startLineIndex; i <= startLineIndex; i++) { + YYTextLine *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) YYTEXT_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) YYTEXT_SWAP(top, bottom); + CGRect rect = CGRectMake(startLine.left, top, startLine.width, bottom - top); + for (NSUInteger i = 1; i < lines.count; i++) { + YYTextLine *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) YYTEXT_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) YYTEXT_SWAP(left, right); + CGRect rect = CGRectMake(left, startLine.top, right - left, startLine.height); + for (NSUInteger i = 1; i < lines.count; i++) { + YYTextLine *line = lines[i]; + rect = CGRectUnion(rect, line.bounds); + } + return rect; + } + } +} + +- (CGRect)rectForRange:(YYTextRange *)range { + NSArray *rects = [self selectionRectsForRange:range]; + if (rects.count == 0) return CGRectNull; + CGRect rectUnion = ((YYTextSelectionRect *)rects.firstObject).rect; + for (NSUInteger i = 1; i < rects.count; i++) { + YYTextSelectionRect *rect = rects[i]; + rectUnion = CGRectUnion(rectUnion, rect.rect); + } + return rectUnion; +} + +- (NSArray *)selectionRectsForRange:(YYTextRange *)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) YYTEXT_SWAP(startLineIndex, endLineIndex); + YYTextLine *startLine = _lines[startLineIndex]; + YYTextLine *endLine = _lines[endLineIndex]; + CGFloat offsetStart = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; + CGFloat offsetEnd = [self offsetForTextPosition:range.end.offset lineIndex:endLineIndex]; + + YYTextSelectionRect *start = [YYTextSelectionRect 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]; + + YYTextSelectionRect *end = [YYTextSelectionRect 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) YYTEXT_SWAP(offsetStart, offsetEnd); + YYTextSelectionRect *rect = [YYTextSelectionRect 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 + YYTextSelectionRect *topRect = [YYTextSelectionRect 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 + YYTextSelectionRect *bottomRect = [YYTextSelectionRect 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++) { + YYTextLine *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; + } + + YYTextSelectionRect *rect = [YYTextSelectionRect 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:(YYTextRange *)range { + NSMutableArray *rects = [self selectionRectsForRange:range].mutableCopy; + for (NSInteger i = 0, max = rects.count; i < max; i++) { + YYTextSelectionRect *rect = rects[i]; + if (rect.containsStart || rect.containsEnd) { + [rects removeObjectAtIndex:i]; + i--; + max--; + } + } + return rects; +} + +- (NSArray *)selectionRectsWithOnlyStartAndEndForRange:(YYTextRange *)range { + NSMutableArray *rects = [self selectionRectsForRange:range].mutableCopy; + for (NSInteger i = 0, max = rects.count; i < max; i++) { + YYTextSelectionRect *rect = rects[i]; + if (!rect.containsStart && !rect.containsEnd) { + [rects removeObjectAtIndex:i]; + i--; + max--; + } + } + return rects; +} + + +#pragma mark - Draw + + +typedef NS_OPTIONS(NSUInteger, YYTextDecorationType) { + YYTextDecorationTypeUnderline = 1 << 0, + YYTextDecorationTypeStrikethrough = 1 << 1, +}; + +typedef NS_OPTIONS(NSUInteger, YYTextBorderType) { + YYTextBorderTypeBackgound = 1 << 0, + YYTextBorderTypeNormal = 1 << 1, +}; + +static CGRect YYTextMergeRectInSameLine(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 YYTextGetRunsMaxMetric(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 YYTextDrawRun(YYTextLine *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 *)(YYTextGlyphTransformAttributeName)); + 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 = YYTextGetCGColor(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 (YYTextRunGlyphRange *oneRange in runRanges) { + NSRange range = oneRange.glyphRangeInRun; + NSUInteger rangeMax = range.location + range.length; + YYTextRunGlyphDrawMode 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 == YYTextRunGlyphDrawModeVerticalRotateMove) { + x += w; + y += w; + } + CGContextSetTextPosition(context, x, y); + } else { + CGContextRotateCTM(context, YYTextDegreesToRadians(-90)); + CGContextSetTextPosition(context, + line.position.y - size.height + glyphPositions[g].x, + line.position.x + verticalOffset + glyphPositions[g].y); + } + + if (YYTextCTFontContainsColorBitmapGlyphs(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 (YYTextCTFontContainsColorBitmapGlyphs(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 (YYTextCTFontContainsColorBitmapGlyphs(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 YYTextSetLinePatternInContext(YYTextLineStyle 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 == YYTextLineStylePatternSolid) { + CGContextSetLineDash(context, phase, NULL, 0); + } else if (pattern == YYTextLineStylePatternDot) { + CGFloat lengths[2] = {width * dot, width * space}; + CGContextSetLineDash(context, phase, lengths, 2); + } else if (pattern == YYTextLineStylePatternDash) { + CGFloat lengths[2] = {width * dash, width * space}; + CGContextSetLineDash(context, phase, lengths, 2); + } else if (pattern == YYTextLineStylePatternDashDot) { + CGFloat lengths[4] = {width * dash, width * space, width * dot, width * space}; + CGContextSetLineDash(context, phase, lengths, 4); + } else if (pattern == YYTextLineStylePatternDashDotDot) { + CGFloat lengths[6] = {width * dash, width * space,width * dot, width * space, width * dot, width * space}; + CGContextSetLineDash(context, phase, lengths, 6); + } else if (pattern == YYTextLineStylePatternCircleDot) { + CGFloat lengths[2] = {width * 0, width * 3}; + CGContextSetLineDash(context, phase, lengths, 2); + CGContextSetLineCap(context, kCGLineCapRound); + CGContextSetLineJoin(context, kCGLineJoinRound); + } +} + + +static void YYTextDrawBorderRects(CGContextRef context, CGSize size, YYTextBorder *border, NSArray *rects, BOOL isVertical) { + if (rects.count == 0) return; + + YYTextShadow *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 = YYTextCGRectPixelRound(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]; + YYTextSetLinePatternInContext(border.lineStyle, border.strokeWidth, 0, context); + CGFloat inset = -border.strokeWidth * 0.5; + if ((border.lineStyle & 0xFF) == YYTextLineStyleThick) { + 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) == YYTextLineStyleDouble) { + 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); + YYTextSetLinePatternInContext(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 YYTextDrawLineStyle(CGContextRef context, CGFloat length, CGFloat lineWidth, YYTextLineStyle 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 = YYTextCGFloatPixelRound(position.y); + y2 = YYTextCGFloatPixelRound(position.y + length); + w = (styleBase == YYTextLineStyleThick ? lineWidth * 2 : lineWidth); + + CGFloat linePixel = YYTextCGFloatToPixel(w); + if (fabs(linePixel - floor(linePixel)) < 0.1) { + int iPixel = linePixel; + if (iPixel == 0 || (iPixel % 2)) { // odd line pixel + x = YYTextCGFloatPixelHalf(position.x); + } else { + x = YYTextCGFloatPixelFloor(position.x); + } + } else { + x = position.x; + } + + CGContextSetStrokeColorWithColor(context, color); + YYTextSetLinePatternInContext(style, lineWidth, position.y, context); + CGContextSetLineWidth(context, w); + if (styleBase == YYTextLineStyleSingle) { + CGContextMoveToPoint(context, x, y1); + CGContextAddLineToPoint(context, x, y2); + CGContextStrokePath(context); + } else if (styleBase == YYTextLineStyleThick) { + CGContextMoveToPoint(context, x, y1); + CGContextAddLineToPoint(context, x, y2); + CGContextStrokePath(context); + } else if (styleBase == YYTextLineStyleDouble) { + 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 = YYTextCGFloatPixelRound(position.x); + x2 = YYTextCGFloatPixelRound(position.x + length); + w = (styleBase == YYTextLineStyleThick ? lineWidth * 2 : lineWidth); + + CGFloat linePixel = YYTextCGFloatToPixel(w); + if (fabs(linePixel - floor(linePixel)) < 0.1) { + int iPixel = linePixel; + if (iPixel == 0 || (iPixel % 2)) { // odd line pixel + y = YYTextCGFloatPixelHalf(position.y); + } else { + y = YYTextCGFloatPixelFloor(position.y); + } + } else { + y = position.y; + } + + CGContextSetStrokeColorWithColor(context, color); + YYTextSetLinePatternInContext(style, lineWidth, position.x, context); + CGContextSetLineWidth(context, w); + if (styleBase == YYTextLineStyleSingle) { + CGContextMoveToPoint(context, x1, y); + CGContextAddLineToPoint(context, x2, y); + CGContextStrokePath(context); + } else if (styleBase == YYTextLineStyleThick) { + CGContextMoveToPoint(context, x1, y); + CGContextAddLineToPoint(context, x2, y); + CGContextStrokePath(context); + } else if (styleBase == YYTextLineStyleDouble) { + 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 YYTextDrawText(YYTextLayout *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++) { + YYTextLine *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); + YYTextDrawRun(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 YYTextDrawBlockBorder(YYTextLayout *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; + + YYTextLine *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); + YYTextBorder *border = attrs[YYTextBlockBorderAttributeName]; + if (!border) continue; + + NSUInteger lineStartIndex = line.index; + while (lineStartIndex > 0) { + if (((YYTextLine *)lines[lineStartIndex - 1]).row == line.row) lineStartIndex--; + else break; + } + + CGRect unionRect = CGRectZero; + NSUInteger lineStartRow = ((YYTextLine *)lines[lineStartIndex]).row; + NSUInteger lineContinueIndex = lineStartIndex; + NSUInteger lineContinueRow = lineStartRow; + do { + YYTextLine *one = lines[lineContinueIndex]; + if (lineContinueIndex == lineStartIndex) { + unionRect = one.bounds; + } else { + unionRect = CGRectUnion(unionRect, one.bounds); + } + if (lineContinueIndex + 1 == lMax) break; + YYTextLine *next = lines[lineContinueIndex + 1]; + if (next.row != lineContinueRow) { + YYTextBorder *nextBorder = [layout.text yy_attribute:YYTextBlockBorderAttributeName 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; + YYTextDrawBorderRects(context, size, border, @[[NSValue valueWithCGRect:unionRect]], isVertical); + + l = lineContinueIndex; + break; + } + } + + + CGContextRestoreGState(context); +} + +static void YYTextDrawBorder(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, YYTextBorderType 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 == YYTextBorderTypeNormal ? YYTextBorderAttributeName : YYTextBackgroundBorderAttributeName); + + BOOL needJumpRun = NO; + NSUInteger jumpRunIndex = 0; + + for (NSInteger l = 0, lMax = lines.count; l < lMax; l++) { + if (cancel && cancel()) break; + + YYTextLine *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); + YYTextBorder *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; + YYTextLine *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); + YYTextBorder *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) { + YYTEXT_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 = YYTextMergeRectInSameLine(rect, curRect, isVertical); + } else { + [drawRects addObject:[NSValue valueWithCGRect:curRect]]; + curRect = rect; + } + } else { + if (fabs(rect.origin.y - curRect.origin.y) < 1) { + curRect = YYTextMergeRectInSameLine(rect, curRect, isVertical); + } else { + [drawRects addObject:[NSValue valueWithCGRect:curRect]]; + curRect = rect; + } + } + } + if (!CGRectEqualToRect(curRect, CGRectZero)) { + [drawRects addObject:[NSValue valueWithCGRect:curRect]]; + } + + YYTextDrawBorderRects(context, size, border, drawRects, isVertical); + + if (l == endLineIndex) { + r = endRunIndex; + } else { + l = endLineIndex - 1; + needJumpRun = YES; + jumpRunIndex = endRunIndex; + break; + } + + } + } + + CGContextRestoreGState(context); +} + +static void YYTextDrawDecoration(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, YYTextDecorationType 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; + + YYTextLine *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); + YYTextDecoration *underline = attrs[YYTextUnderlineAttributeName]; + YYTextDecoration *strikethrough = attrs[YYTextStrikethroughAttributeName]; + + BOOL needDrawUnderline = NO, needDrawStrikethrough = NO; + if ((type & YYTextDecorationTypeUnderline) && underline.style > 0) { + needDrawUnderline = YES; + } + if ((type & YYTextDecorationTypeStrikethrough) && 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 (YYTextIsLinebreakString(runStr)) continue; // may need more checks... + + CGFloat xHeight, underlinePosition, lineThickness; + YYTextGetRunsMaxMetric(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 = YYTextGetCGColor(color); + } + CGFloat thickness = underline.width ? underline.width.floatValue : lineThickness; + YYTextShadow *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); + YYTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color, isVertical); + } CGContextRestoreGState(context); + } CGContextRestoreGState(context); + shadow = shadow.subShadow; + } + YYTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color, isVertical); + } + + if (needDrawStrikethrough) { + CGColorRef color = strikethrough.color.CGColor; + if (!color) { + color = (__bridge CGColorRef)(attrs[(id)kCTForegroundColorAttributeName]); + color = YYTextGetCGColor(color); + } + CGFloat thickness = strikethrough.width ? strikethrough.width.floatValue : lineThickness; + YYTextShadow *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); + YYTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color, isVertical); + } CGContextRestoreGState(context); + } CGContextRestoreGState(context); + shadow = shadow.subShadow; + } + YYTextDrawLineStyle(context, length, thickness, strikethrough.style, strikethroughStart, color, isVertical); + } + } + } + CGContextRestoreGState(context); +} + +static void YYTextDrawAttachment(YYTextLayout *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++) { + YYTextAttachment *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 = YYTextCGRectFitWithContentMode(rect, asize, a.contentMode); + rect = YYTextCGRectPixelRound(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 YYTextDrawShadow(YYTextLayout *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; + YYTextLine *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); + YYTextShadow *shadow = attrs[YYTextShadowAttributeName]; + YYTextShadow *nsShadow = [YYTextShadow 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); + YYTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset); + } CGContextRestoreGState(context); + shadow = shadow.subShadow; + } + } + } + } CGContextRestoreGState(context); +} + +static void YYTextDrawInnerShadow(YYTextLayout *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; + + YYTextLine *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); + YYTextShadow *shadow = attrs[YYTextInnerShadowAttributeName]; + 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 *)(YYTextGlyphTransformAttributeName)); + 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); { + YYTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset); + } CGContextEndTransparencyLayer(context); + } CGContextEndTransparencyLayer(context); + } CGContextEndTransparencyLayer(context); + } CGContextRestoreGState(context); + shadow = shadow.subShadow; + } + } + } + + CGContextRestoreGState(context); +} + +static void YYTextDrawDebug(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, YYTextDebugOption *op) { + UIGraphicsPushContext(context); + CGContextSaveGState(context); + CGContextTranslateCTM(context, point.x, point.y); + CGContextSetLineWidth(context, 1.0 / YYTextScreenScale()); + 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 = YYTextCGRectPixelHalf(rect); + else rect = YYTextCGRectPixelRound(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++) { + YYTextLine *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, YYTextCGRectPixelRound(lineBounds)); + CGContextFillPath(context); + } + if (op.CTLineBorderColor) { + [op.CTLineBorderColor setStroke]; + CGContextAddRect(context, YYTextCGRectPixelHalf(lineBounds)); + CGContextStrokePath(context); + } + if (op.baselineColor) { + [op.baselineColor setStroke]; + if (isVertical) { + CGFloat x = YYTextCGFloatPixelHalf(line.position.x); + CGFloat y1 = YYTextCGFloatPixelHalf(line.top); + CGFloat y2 = YYTextCGFloatPixelHalf(line.bottom); + CGContextMoveToPoint(context, x, y1); + CGContextAddLineToPoint(context, x, y2); + CGContextStrokePath(context); + } else { + CGFloat x1 = YYTextCGFloatPixelHalf(lineBounds.origin.x); + CGFloat x2 = YYTextCGFloatPixelHalf(lineBounds.origin.x + lineBounds.size.width); + CGFloat y = YYTextCGFloatPixelHalf(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.yy_color = op.CTLineNumberColor; + num.yy_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) { + YYTEXT_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, YYTextCGRectPixelRound(runTypoBounds)); + CGContextFillPath(context); + } + if (op.CTRunBorderColor) { + [op.CTRunBorderColor setStroke]; + CGContextAddRect(context, YYTextCGRectPixelHalf(runTypoBounds)); + CGContextStrokePath(context); + } + if (op.CTRunNumberColor) { + [op.CTRunNumberColor set]; + NSMutableAttributedString *num = [[NSMutableAttributedString alloc] initWithString:@(r).description]; + num.yy_color = op.CTRunNumberColor; + num.yy_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) { + YYTEXT_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, YYTextCGRectPixelRound(rect)); + CGContextFillPath(context); + } + if (op.CGGlyphBorderColor) { + [op.CGGlyphBorderColor setStroke]; + CGContextAddRect(context, YYTextCGRectPixelHalf(rect)); + CGContextStrokePath(context); + } + } + } + } + } + } + CGContextRestoreGState(context); + UIGraphicsPopContext(); +} + + +- (void)drawInContext:(CGContextRef)context + size:(CGSize)size + point:(CGPoint)point + view:(UIView *)view + layer:(CALayer *)layer + debug:(YYTextDebugOption *)debug + cancel:(BOOL (^)(void))cancel{ + @autoreleasepool { + if (self.needDrawBlockBorder && context) { + if (cancel && cancel()) return; + YYTextDrawBlockBorder(self, context, size, point, cancel); + } + if (self.needDrawBackgroundBorder && context) { + if (cancel && cancel()) return; + YYTextDrawBorder(self, context, size, point, YYTextBorderTypeBackgound, cancel); + } + if (self.needDrawShadow && context) { + if (cancel && cancel()) return; + YYTextDrawShadow(self, context, size, point, cancel); + } + if (self.needDrawUnderline && context) { + if (cancel && cancel()) return; + YYTextDrawDecoration(self, context, size, point, YYTextDecorationTypeUnderline, cancel); + } + if (self.needDrawText && context) { + if (cancel && cancel()) return; + YYTextDrawText(self, context, size, point, cancel); + } + if (self.needDrawAttachment && (context || view || layer)) { + if (cancel && cancel()) return; + YYTextDrawAttachment(self, context, size, point, view, layer, cancel); + } + if (self.needDrawInnerShadow && context) { + if (cancel && cancel()) return; + YYTextDrawInnerShadow(self, context, size, point, cancel); + } + if (self.needDrawStrikethrough && context) { + if (cancel && cancel()) return; + YYTextDrawDecoration(self, context, size, point, YYTextDecorationTypeStrikethrough, cancel); + } + if (self.needDrawBorder && context) { + if (cancel && cancel()) return; + YYTextDrawBorder(self, context, size, point, YYTextBorderTypeNormal, cancel); + } + if (debug.needDrawDebug && context) { + if (cancel && cancel()) return; + YYTextDrawDebug(self, context, size, point, debug); + } + } +} + +- (void)drawInContext:(CGContextRef)context + size:(CGSize)size + debug:(YYTextDebugOption *)debug { + [self drawInContext:context size:size point:CGPointZero view:nil layer:nil debug:debug cancel:nil]; +} + +- (void)addAttachmentToView:(UIView *)view layer:(CALayer *)layer { + NSAssert([NSThread isMainThread], @"This method must be called on the main thread"); + [self drawInContext:NULL size:CGSizeZero point:CGPointZero view:view layer:layer debug:nil cancel:nil]; +} + +- (void)removeAttachmentFromViewAndLayer { + NSAssert([NSThread isMainThread], @"This method must be called on the main thread"); + for (YYTextAttachment *a in self.attachments) { + if ([a.content isKindOfClass:[UIView class]]) { + UIView *v = a.content; + [v removeFromSuperview]; + } else if ([a.content isKindOfClass:[CALayer class]]) { + CALayer *l = a.content; + [l removeFromSuperlayer]; + } + } +} + +@end -- Gitblit v1.8.0