778 lines
20 KiB
Objective-C
778 lines
20 KiB
Objective-C
//
|
|
// DTCoreTextLayoutFrame.m
|
|
// WeRead
|
|
//
|
|
// Reverse-engineered from binary analysis
|
|
// Based on DTCoreText open-source library, customized by WeRead
|
|
//
|
|
// This file contains pseudo-code reconstruction of the DTCoreTextLayoutFrame
|
|
// class based on ivar analysis, method signatures, and the open-source DTCoreText library.
|
|
//
|
|
|
|
#import "DTCoreTextLayoutFrame.h"
|
|
#import "DTCoreTextLayoutLine.h"
|
|
#import "DTCoreTextGlyphRun.h"
|
|
#import <CoreText/CoreText.h>
|
|
|
|
#pragma mark - Private Interface
|
|
|
|
@interface DTCoreTextLayoutFrame () {
|
|
// The CoreText frame object
|
|
CTFrameRef _ctFrame;
|
|
|
|
// Cached layout lines
|
|
NSArray<DTCoreTextLayoutLine *> *_cachedLines;
|
|
|
|
// Cached glyph runs
|
|
NSArray<DTCoreTextGlyphRun *> *_cachedGlyphRuns;
|
|
|
|
// Cached line origins
|
|
NSArray<NSValue *> *_cachedLineOrigins;
|
|
|
|
// Whether lines have been extracted
|
|
BOOL _linesExtracted;
|
|
|
|
// Whether glyph runs have been extracted
|
|
BOOL _glyphRunsExtracted;
|
|
|
|
// Internal lock
|
|
NSLock *_lock;
|
|
|
|
// Cached content height
|
|
CGFloat _cachedContentHeight;
|
|
BOOL _contentHeightCached;
|
|
|
|
// Visible string range
|
|
NSRange _visibleStringRange;
|
|
|
|
// Whether the frame is truncated
|
|
BOOL _isTruncated;
|
|
|
|
// Truncation range
|
|
NSRange _truncatedRange;
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark - DTCoreTextLayoutFrame Implementation
|
|
|
|
@implementation DTCoreTextLayoutFrame
|
|
|
|
#pragma mark - Lifecycle
|
|
|
|
- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString
|
|
range:(NSRange)range
|
|
ctFrame:(CTFrameRef)ctFrame {
|
|
self = [super init];
|
|
if (self) {
|
|
_attributedString = attributedString;
|
|
_range = range;
|
|
_lock = [[NSLock alloc] init];
|
|
|
|
// Store the CTFrame
|
|
if (ctFrame) {
|
|
_ctFrame = (CTFrameRef)CFRetain(ctFrame);
|
|
}
|
|
|
|
// Initialize state
|
|
_linesExtracted = NO;
|
|
_glyphRunsExtracted = NO;
|
|
_contentHeightCached = NO;
|
|
_needsLayout = YES;
|
|
|
|
// Get the frame path bounds
|
|
if (_ctFrame) {
|
|
CGPathRef path = CTFrameGetPath(_ctFrame);
|
|
_frame = CGPathGetPathBoundingBox(path);
|
|
}
|
|
|
|
// Initialize visible range to full range
|
|
_visibleStringRange = range;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
if (_ctFrame) {
|
|
CFRelease(_ctFrame);
|
|
_ctFrame = NULL;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Line Extraction
|
|
|
|
/**
|
|
* Extracts layout lines from the CTFrame.
|
|
*
|
|
* This method iterates through the CTFrame's lines and creates
|
|
* DTCoreTextLayoutLine wrapper objects with position information.
|
|
*
|
|
* Algorithm:
|
|
* 1. Get array of CTLines from CTFrame
|
|
* 2. For each CTLine:
|
|
* a. Get its origin from CTFrameGetLineOrigins
|
|
* b. Get the string range from CTLineGetStringRange
|
|
* c. Create DTCoreTextLayoutLine wrapper
|
|
* 3. Cache the results
|
|
*/
|
|
- (void)extractLines {
|
|
if (_linesExtracted || !_ctFrame) {
|
|
return;
|
|
}
|
|
|
|
[_lock lock];
|
|
|
|
// Get the lines from the CTFrame
|
|
CFArrayRef ctLines = CTFrameGetLines(_ctFrame);
|
|
if (!ctLines) {
|
|
_cachedLines = @[];
|
|
_linesExtracted = YES;
|
|
[_lock unlock];
|
|
return;
|
|
}
|
|
|
|
CFIndex lineCount = CFArrayGetCount(ctLines);
|
|
if (lineCount == 0) {
|
|
_cachedLines = @[];
|
|
_linesExtracted = YES;
|
|
[_lock unlock];
|
|
return;
|
|
}
|
|
|
|
// Get line origins
|
|
CGPoint *origins = (CGPoint *)malloc(sizeof(CGPoint) * lineCount);
|
|
CTFrameGetLineOrigins(_ctFrame, CFRangeMake(0, 0), origins);
|
|
|
|
NSMutableArray<DTCoreTextLayoutLine *> *lines = [NSMutableArray arrayWithCapacity:lineCount];
|
|
|
|
for (CFIndex i = 0; i < lineCount; i++) {
|
|
CTLineRef ctLine = CFArrayGetValueAtIndex(ctLines, i);
|
|
|
|
// Get the string range for this line
|
|
CFRange cfRange = CTLineGetStringRange(ctLine);
|
|
NSRange range = NSMakeRange(cfRange.location, cfRange.length);
|
|
|
|
// Create layout line wrapper
|
|
DTCoreTextLayoutLine *layoutLine = [[DTCoreTextLayoutLine alloc]
|
|
initWithCTLine:ctLine
|
|
origin:origins[i]
|
|
range:range
|
|
index:i];
|
|
|
|
[lines addObject:layoutLine];
|
|
}
|
|
|
|
free(origins);
|
|
|
|
_cachedLines = [lines copy];
|
|
_cachedLineOrigins = nil; // Invalidate origin cache
|
|
_linesExtracted = YES;
|
|
|
|
[_lock unlock];
|
|
}
|
|
|
|
/**
|
|
* Returns the array of layout lines.
|
|
* Triggers extraction if not already done.
|
|
*/
|
|
- (NSArray<DTCoreTextLayoutLine *> *)lines {
|
|
[self extractLines];
|
|
return _cachedLines;
|
|
}
|
|
|
|
- (NSUInteger)numberOfLines {
|
|
return [[self lines] count];
|
|
}
|
|
|
|
- (DTCoreTextLayoutLine *)lineAtIndex:(NSUInteger)index {
|
|
NSArray *lines = [self lines];
|
|
if (index < [lines count]) {
|
|
return lines[index];
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
/**
|
|
* Returns the line that contains the given string index.
|
|
*/
|
|
- (DTCoreTextLayoutLine *)lineContainingStringIndex:(NSUInteger)index {
|
|
NSArray<DTCoreTextLayoutLine *> *allLines = [self lines];
|
|
|
|
for (DTCoreTextLayoutLine *line in allLines) {
|
|
NSRange range = line.stringRange;
|
|
|
|
if (index >= range.location && index < range.location + range.length) {
|
|
return line;
|
|
}
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
/**
|
|
* Returns the index of the line containing the given point.
|
|
*/
|
|
- (NSUInteger)lineIndexContainingPoint:(CGPoint)point {
|
|
NSArray<DTCoreTextLayoutLine *> *allLines = [self lines];
|
|
|
|
for (NSUInteger i = 0; i < [allLines count]; i++) {
|
|
DTCoreTextLayoutLine *line = allLines[i];
|
|
|
|
// Check if point is within the line's vertical bounds
|
|
CGFloat lineTop = line.origin.y + line.ascent;
|
|
CGFloat lineBottom = line.origin.y - line.descent;
|
|
|
|
if (point.y >= lineBottom && point.y <= lineTop) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return NSNotFound;
|
|
}
|
|
|
|
#pragma mark - Glyph Run Extraction
|
|
|
|
/**
|
|
* Extracts glyph runs from all lines.
|
|
*
|
|
* This method iterates through all lines and extracts their glyph runs,
|
|
* creating a flat array of all glyph runs in the frame.
|
|
*/
|
|
- (void)extractGlyphRuns {
|
|
if (_glyphRunsExtracted) {
|
|
return;
|
|
}
|
|
|
|
[_lock lock];
|
|
|
|
NSArray<DTCoreTextLayoutLine *> *allLines = [self lines];
|
|
NSMutableArray<DTCoreTextGlyphRun *> *allRuns = [NSMutableArray array];
|
|
|
|
for (DTCoreTextLayoutLine *line in allLines) {
|
|
NSArray<DTCoreTextGlyphRun *> *lineRuns = [line glyphRuns];
|
|
[allRuns addObjectsFromArray:lineRuns];
|
|
}
|
|
|
|
_cachedGlyphRuns = [allRuns copy];
|
|
_glyphRunsExtracted = YES;
|
|
|
|
[_lock unlock];
|
|
}
|
|
|
|
/**
|
|
* Returns all glyph runs in the frame.
|
|
*/
|
|
- (NSArray<DTCoreTextGlyphRun *> *)glyphRuns {
|
|
[self extractGlyphRuns];
|
|
return _cachedGlyphRuns;
|
|
}
|
|
|
|
#pragma mark - Drawing
|
|
|
|
/**
|
|
* Draws the layout frame content into a CGContext.
|
|
*
|
|
* This method draws the text content using CTFrameDraw.
|
|
*
|
|
* @param context The CGContext to draw into
|
|
*/
|
|
- (void)drawInContext:(CGContextRef)context {
|
|
if (!context || !_ctFrame) {
|
|
return;
|
|
}
|
|
|
|
[_lock lock];
|
|
|
|
// Save graphics state
|
|
CGContextSaveGState(context);
|
|
|
|
// CoreText uses bottom-left origin, UIKit uses top-left
|
|
// We need to flip the coordinate system
|
|
CGContextTranslateCTM(context, 0, _frame.size.height);
|
|
CGContextScaleCTM(context, 1.0, -1.0);
|
|
|
|
// Draw the frame
|
|
CTFrameDraw(_ctFrame, context);
|
|
|
|
// Restore graphics state
|
|
CGContextRestoreGState(context);
|
|
|
|
[_lock unlock];
|
|
}
|
|
|
|
/**
|
|
* Draws the layout frame with options.
|
|
*
|
|
* @param context The CGContext to draw into
|
|
* @param options Drawing options dictionary
|
|
*/
|
|
- (void)drawInContext:(CGContextRef)context options:(NSDictionary *)options {
|
|
if (!context || !_ctFrame) {
|
|
return;
|
|
}
|
|
|
|
[_lock lock];
|
|
|
|
// Save graphics state
|
|
CGContextSaveGState(context);
|
|
|
|
// Apply coordinate transformation
|
|
CGContextTranslateCTM(context, 0, _frame.size.height);
|
|
CGContextScaleCTM(context, 1.0, -1.0);
|
|
|
|
// Check for custom drawing options
|
|
BOOL drawImages = [options[@"drawImages"] boolValue];
|
|
BOOL drawLinks = [options[@"drawLinks"] boolValue];
|
|
BOOL drawSelection = [options[@"drawSelection"] boolValue];
|
|
|
|
// Draw the frame
|
|
CTFrameDraw(_ctFrame, context);
|
|
|
|
// Draw images if requested
|
|
if (drawImages) {
|
|
[self drawImagesInContext:context options:options];
|
|
}
|
|
|
|
// Draw links if requested
|
|
if (drawLinks) {
|
|
[self drawLinksInContext:context options:options];
|
|
}
|
|
|
|
// Draw selection if requested
|
|
if (drawSelection) {
|
|
[self drawSelectionInContext:context options:options];
|
|
}
|
|
|
|
// Restore graphics state
|
|
CGContextRestoreGState(context);
|
|
|
|
[_lock unlock];
|
|
}
|
|
|
|
/**
|
|
* Draws images in the layout frame.
|
|
*/
|
|
- (void)drawImagesInContext:(CGContextRef)context options:(NSDictionary *)options {
|
|
// Extract image attachments from attributed string
|
|
NSArray<DTCoreTextLayoutLine *> *allLines = [self lines];
|
|
|
|
for (DTCoreTextLayoutLine *line in allLines) {
|
|
NSArray<DTCoreTextGlyphRun *> *runs = [line glyphRuns];
|
|
|
|
for (DTCoreTextGlyphRun *run in runs) {
|
|
NSDictionary *attributes = run.attributes;
|
|
|
|
// Check for attachment
|
|
NSTextAttachment *attachment = attributes[@"NSAttachment"];
|
|
if (attachment && attachment.image) {
|
|
CGRect runFrame = run.frame;
|
|
|
|
// Draw the image at the run's position
|
|
CGContextDrawImage(context, runFrame, attachment.image.CGImage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draws link indicators.
|
|
*/
|
|
- (void)drawLinksInContext:(CGContextRef)context options:(NSDictionary *)options {
|
|
// Draw underlines for links
|
|
NSArray<DTCoreTextLayoutLine *> *allLines = [self lines];
|
|
|
|
for (DTCoreTextLayoutLine *line in allLines) {
|
|
NSArray<DTCoreTextGlyphRun *> *runs = [line glyphRuns];
|
|
|
|
for (DTCoreTextGlyphRun *run in runs) {
|
|
NSDictionary *attributes = run.attributes;
|
|
NSURL *linkURL = attributes[@"NSLink"];
|
|
|
|
if (linkURL) {
|
|
// Draw underline for link
|
|
CGRect runFrame = run.frame;
|
|
CGFloat underlineY = runFrame.origin.y;
|
|
|
|
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
|
|
CGContextSetLineWidth(context, 1.0);
|
|
|
|
CGContextMoveToPoint(context, runFrame.origin.x, underlineY);
|
|
CGContextAddLineToPoint(context, CGRectGetMaxX(runFrame), underlineY);
|
|
CGContextStrokePath(context);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draws selection highlight.
|
|
*/
|
|
- (void)drawSelectionInContext:(CGContextRef)context options:(NSDictionary *)options {
|
|
NSArray *selectedRanges = options[@"selectedRanges"];
|
|
UIColor *selectionColor = options[@"selectionColor"] ?: [UIColor colorWithRed:0.0
|
|
green:0.47
|
|
blue:1.0
|
|
alpha:0.2];
|
|
|
|
for (NSValue *rangeValue in selectedRanges) {
|
|
NSRange range = [rangeValue rangeValue];
|
|
|
|
// Get the rects for this range
|
|
NSArray<NSValue *> *rects = [self rectsForRange:range];
|
|
|
|
CGContextSetFillColorWithColor(context, selectionColor.CGColor);
|
|
|
|
for (NSValue *rectValue in rects) {
|
|
CGRect rect = [rectValue CGRectValue];
|
|
CGContextFillRect(context, rect);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns rects for a string range.
|
|
*/
|
|
- (NSArray<NSValue *> *)rectsForRange:(NSRange)range {
|
|
NSMutableArray<NSValue *> *rects = [NSMutableArray array];
|
|
|
|
NSArray<DTCoreTextLayoutLine *> *allLines = [self lines];
|
|
|
|
for (DTCoreTextLayoutLine *line in allLines) {
|
|
NSRange lineRange = line.stringRange;
|
|
NSRange intersection = NSIntersectionRange(range, lineRange);
|
|
|
|
if (intersection.length > 0) {
|
|
// Get the x positions for the range within this line
|
|
CGFloat startX = CTLineGetOffsetForStringIndex(
|
|
line.ctLine, intersection.location, NULL);
|
|
CGFloat endX = CTLineGetOffsetForStringIndex(
|
|
line.ctLine, intersection.location + intersection.length, NULL);
|
|
|
|
CGRect lineRect = CGRectMake(
|
|
line.origin.x + startX,
|
|
line.origin.y - line.descent,
|
|
endX - startX,
|
|
line.height
|
|
);
|
|
|
|
[rects addObject:[NSValue valueWithRect:lineRect]];
|
|
}
|
|
}
|
|
|
|
return [rects copy];
|
|
}
|
|
|
|
#pragma mark - Hit Testing
|
|
|
|
/**
|
|
* Returns the string index at the given point.
|
|
*
|
|
* Uses CTLineGetStringIndexForPosition to find which character
|
|
* is at the given point.
|
|
*
|
|
* @param point The point to test
|
|
* @return The string index, or NSNotFound
|
|
*/
|
|
- (NSUInteger)stringIndexAtPoint:(CGPoint)point {
|
|
NSArray<DTCoreTextLayoutLine *> *allLines = [self lines];
|
|
|
|
for (DTCoreTextLayoutLine *line in allLines) {
|
|
// Check if the point is within this line's vertical bounds
|
|
CGFloat lineTop = line.origin.y + line.ascent;
|
|
CGFloat lineBottom = line.origin.y - line.descent;
|
|
|
|
if (point.y >= lineBottom && point.y <= lineTop) {
|
|
// Use CTLine to find the character index
|
|
CFIndex index = CTLineGetStringIndexForPosition(line.ctLine, point);
|
|
|
|
if (index != kCFNotFound) {
|
|
return (NSUInteger)index;
|
|
}
|
|
}
|
|
}
|
|
|
|
return NSNotFound;
|
|
}
|
|
|
|
/**
|
|
* Returns the rect for the given string index.
|
|
*/
|
|
- (CGRect)rectForStringIndex:(NSUInteger)index {
|
|
NSArray<DTCoreTextLayoutLine *> *allLines = [self lines];
|
|
|
|
for (DTCoreTextLayoutLine *line in allLines) {
|
|
NSRange range = line.stringRange;
|
|
|
|
if (index >= range.location && index < range.location + range.length) {
|
|
// Get the x position for this character
|
|
CGFloat x = CTLineGetOffsetForStringIndex(line.ctLine, index, NULL);
|
|
|
|
return CGRectMake(line.origin.x + x,
|
|
line.origin.y - line.descent,
|
|
1,
|
|
line.height);
|
|
}
|
|
}
|
|
|
|
return CGRectNull;
|
|
}
|
|
|
|
/**
|
|
* Returns the cursor position for a given string index.
|
|
*/
|
|
- (CGRect)cursorRectForIndex:(NSUInteger)index {
|
|
NSArray<DTCoreTextLayoutLine *> *allLines = [self lines];
|
|
|
|
for (DTCoreTextLayoutLine *line in allLines) {
|
|
NSRange range = line.stringRange;
|
|
|
|
if (index >= range.location && index <= range.location + range.length) {
|
|
CGFloat x = CTLineGetOffsetForStringIndex(line.ctLine, index, NULL);
|
|
|
|
return CGRectMake(line.origin.x + x - 1,
|
|
line.origin.y - line.descent,
|
|
2,
|
|
line.height);
|
|
}
|
|
}
|
|
|
|
return CGRectNull;
|
|
}
|
|
|
|
#pragma mark - Text Geometry
|
|
|
|
/**
|
|
* Returns the baseline origin for a given string index.
|
|
*/
|
|
- (CGPoint)baselineOriginForStringIndex:(NSUInteger)index {
|
|
NSArray<DTCoreTextLayoutLine *> *allLines = [self lines];
|
|
|
|
for (DTCoreTextLayoutLine *line in allLines) {
|
|
NSRange range = line.stringRange;
|
|
|
|
if (index >= range.location && index < range.location + range.length) {
|
|
CGFloat x = CTLineGetOffsetForStringIndex(line.ctLine, index, NULL);
|
|
|
|
return CGPointMake(line.origin.x + x, line.origin.y);
|
|
}
|
|
}
|
|
|
|
return CGPointZero;
|
|
}
|
|
|
|
/**
|
|
* Returns the frame for a given string range.
|
|
*/
|
|
- (CGRect)frameForStringRange:(NSRange)range {
|
|
NSArray<NSValue *> *rects = [self rectsForRange:range];
|
|
|
|
if ([rects count] == 0) {
|
|
return CGRectNull;
|
|
}
|
|
|
|
// Union all rects
|
|
CGRect result = CGRectNull;
|
|
for (NSValue *rectValue in rects) {
|
|
result = CGRectUnion(result, [rectValue CGRectValue]);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Returns the string range for a given rect.
|
|
*/
|
|
- (NSRange)stringRangeForRect:(CGRect)rect {
|
|
NSArray<DTCoreTextLayoutLine *> *allLines = [self lines];
|
|
|
|
NSUInteger start = NSNotFound;
|
|
NSUInteger end = NSNotFound;
|
|
|
|
for (DTCoreTextLayoutLine *line in allLines) {
|
|
CGRect lineRect = CGRectMake(
|
|
line.origin.x,
|
|
line.origin.y - line.descent,
|
|
line.width,
|
|
line.height
|
|
);
|
|
|
|
if (CGRectIntersectsRect(rect, lineRect)) {
|
|
if (start == NSNotFound) {
|
|
start = line.stringRange.location;
|
|
}
|
|
end = line.stringRange.location + line.stringRange.length;
|
|
}
|
|
}
|
|
|
|
if (start != NSNotFound && end != NSNotFound) {
|
|
return NSMakeRange(start, end - start);
|
|
}
|
|
|
|
return NSMakeRange(NSNotFound, 0);
|
|
}
|
|
|
|
#pragma mark - Line Geometry
|
|
|
|
/**
|
|
* Returns the line origins.
|
|
*/
|
|
- (NSArray<NSValue *> *)lineOrigins {
|
|
if (_cachedLineOrigins) {
|
|
return _cachedLineOrigins;
|
|
}
|
|
|
|
[_lock lock];
|
|
|
|
NSArray<DTCoreTextLayoutLine *> *allLines = [self lines];
|
|
NSMutableArray<NSValue *> *origins = [NSMutableArray arrayWithCapacity:[allLines count]];
|
|
|
|
for (DTCoreTextLayoutLine *line in allLines) {
|
|
[origins addObject:[NSValue valueWithCGPoint:line.origin]];
|
|
}
|
|
|
|
_cachedLineOrigins = [origins copy];
|
|
|
|
[_lock unlock];
|
|
|
|
return _cachedLineOrigins;
|
|
}
|
|
|
|
/**
|
|
* Returns the line frames.
|
|
*/
|
|
- (NSArray<NSValue *> *)lineFrames {
|
|
NSArray<DTCoreTextLayoutLine *> *allLines = [self lines];
|
|
NSMutableArray<NSValue *> *frames = [NSMutableArray arrayWithCapacity:[allLines count]];
|
|
|
|
for (DTCoreTextLayoutLine *line in allLines) {
|
|
CGRect lineFrame = CGRectMake(
|
|
line.origin.x,
|
|
line.origin.y - line.descent,
|
|
line.width,
|
|
line.height
|
|
);
|
|
[frames addObject:[NSValue valueWithRect:lineFrame]];
|
|
}
|
|
|
|
return [frames copy];
|
|
}
|
|
|
|
#pragma mark - Content Height
|
|
|
|
/**
|
|
* Returns the rendered content height.
|
|
*/
|
|
- (CGFloat)contentHeight {
|
|
[_lock lock];
|
|
|
|
if (_contentHeightCached) {
|
|
[_lock unlock];
|
|
return _cachedContentHeight;
|
|
}
|
|
|
|
NSArray<DTCoreTextLayoutLine *> *allLines = [self lines];
|
|
|
|
if ([allLines count] == 0) {
|
|
_cachedContentHeight = 0;
|
|
_contentHeightCached = YES;
|
|
[_lock unlock];
|
|
return 0;
|
|
}
|
|
|
|
// Find the lowest point of the last line
|
|
DTCoreTextLayoutLine *lastLine = [allLines lastObject];
|
|
CGFloat lastLineBottom = lastLine.origin.y - lastLine.descent;
|
|
|
|
// Content height is from top of frame to bottom of last line
|
|
_cachedContentHeight = _frame.size.height - lastLineBottom;
|
|
_contentHeightCached = YES;
|
|
|
|
[_lock unlock];
|
|
|
|
return _cachedContentHeight;
|
|
}
|
|
|
|
- (CGFloat)maximumY {
|
|
NSArray<DTCoreTextLayoutLine *> *allLines = [self lines];
|
|
|
|
if ([allLines count] == 0) {
|
|
return _frame.origin.y + _frame.size.height;
|
|
}
|
|
|
|
DTCoreTextLayoutLine *firstLine = [allLines firstObject];
|
|
return firstLine.origin.y + firstLine.ascent;
|
|
}
|
|
|
|
- (CGFloat)minimumY {
|
|
NSArray<DTCoreTextLayoutLine *> *allLines = [self lines];
|
|
|
|
if ([allLines count] == 0) {
|
|
return _frame.origin.y;
|
|
}
|
|
|
|
DTCoreTextLayoutLine *lastLine = [allLines lastObject];
|
|
return lastLine.origin.y - lastLine.descent;
|
|
}
|
|
|
|
#pragma mark - Truncation
|
|
|
|
/**
|
|
* Returns whether the frame is truncated.
|
|
*/
|
|
- (BOOL)isTruncated {
|
|
// Check if the attributed string extends beyond the visible range
|
|
NSUInteger stringEnd = _range.location + _range.length;
|
|
NSUInteger visibleEnd = _visibleStringRange.location + _visibleStringRange.length;
|
|
|
|
return visibleEnd < stringEnd;
|
|
}
|
|
|
|
/**
|
|
* Returns the truncation string range.
|
|
*/
|
|
- (NSRange)truncatedStringRange {
|
|
if (![self isTruncated]) {
|
|
return NSMakeRange(NSNotFound, 0);
|
|
}
|
|
|
|
NSUInteger visibleEnd = _visibleStringRange.location + _visibleStringRange.length;
|
|
NSUInteger stringEnd = _range.location + _range.length;
|
|
|
|
return NSMakeRange(visibleEnd, stringEnd - visibleEnd);
|
|
}
|
|
|
|
#pragma mark - Layout Updates
|
|
|
|
/**
|
|
* Invalidates the layout, forcing recalculation on next access.
|
|
*/
|
|
- (void)invalidateLayout {
|
|
[_lock lock];
|
|
|
|
_linesExtracted = NO;
|
|
_glyphRunsExtracted = NO;
|
|
_contentHeightCached = NO;
|
|
_cachedLines = nil;
|
|
_cachedGlyphRuns = nil;
|
|
_cachedLineOrigins = nil;
|
|
_needsLayout = YES;
|
|
|
|
[_lock unlock];
|
|
}
|
|
|
|
/**
|
|
* Forces a layout pass.
|
|
*/
|
|
- (void)layoutIfNeeded {
|
|
if (_needsLayout) {
|
|
[self extractLines];
|
|
_needsLayout = NO;
|
|
}
|
|
}
|
|
|
|
- (NSUInteger)stringIndex {
|
|
return _range.location + _range.length;
|
|
}
|
|
|
|
@end
|