ReadViewSDK/Doc/WXRead/decompiled/DTCoreTextLayoutFrame.m
2026-05-21 19:40:51 +08:00

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