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

490 lines
12 KiB
Objective-C

//
// DTCoreTextLayoutLine.m
// WeRead
//
// Reverse-engineered from binary analysis
// Based on DTCoreText open-source library, customized by WeRead
//
// This file contains pseudo-code reconstruction of the DTCoreTextLayoutLine
// class based on ivar analysis, method signatures, and the open-source DTCoreText library.
//
#import "DTCoreTextLayoutLine.h"
#import "DTCoreTextGlyphRun.h"
#import <CoreText/CoreText.h>
#pragma mark - Private Interface
@interface DTCoreTextLayoutLine () {
// Cached glyph runs
NSArray<DTCoreTextGlyphRun *> *_cachedGlyphRuns;
// Whether glyph runs have been extracted
BOOL _glyphRunsExtracted;
// Cached typographic metrics
CGFloat _cachedAscent;
CGFloat _cachedDescent;
CGFloat _cachedLeading;
CGFloat _cachedWidth;
BOOL _metricsCached;
// Internal lock
NSLock *_lock;
}
@end
#pragma mark - DTCoreTextLayoutLine Implementation
@implementation DTCoreTextLayoutLine
#pragma mark - Lifecycle
- (instancetype)initWithCTLine:(CTLineRef)ctLine
origin:(CGPoint)origin
range:(NSRange)range
index:(NSUInteger)index {
self = [super init];
if (self) {
_lock = [[NSLock alloc] init];
// Store the CTLine with a retain
if (ctLine) {
_ctLine = (CTLineRef)CFRetain(ctLine);
}
_origin = origin;
_stringRange = range;
_lineIndex = index;
// Initialize state
_glyphRunsExtracted = NO;
_metricsCached = NO;
_baselineOffset = 0;
_isLastLineInParagraph = NO;
_isLineBreak = NO;
}
return self;
}
- (void)dealloc {
if (_ctLine) {
CFRelease(_ctLine);
_ctLine = NULL;
}
}
#pragma mark - Glyph Run Extraction
/**
* Extracts glyph runs from the CTLine.
*
* This method iterates through the CTLine's runs and creates
* DTCoreTextGlyphRun wrapper objects.
*
* Algorithm:
* 1. Get array of CTRuns from CTLine
* 2. For each CTRun:
* a. Get its string range
* b. Get glyph positions and advances
* c. Create DTCoreTextGlyphRun wrapper
* 3. Cache the results
*/
- (void)extractGlyphRuns {
if (_glyphRunsExtracted || !_ctLine) {
return;
}
[_lock lock];
// Get the runs from the CTLine
CFArrayRef ctRuns = CTLineGetGlyphRuns(_ctLine);
if (!ctRuns) {
_cachedGlyphRuns = @[];
_glyphRunsExtracted = YES;
[_lock unlock];
return;
}
CFIndex runCount = CFArrayGetCount(ctRuns);
if (runCount == 0) {
_cachedGlyphRuns = @[];
_glyphRunsExtracted = YES;
[_lock unlock];
return;
}
NSMutableArray<DTCoreTextGlyphRun *> *runs = [NSMutableArray arrayWithCapacity:runCount];
// Calculate the baseline x offset for this line
CGFloat lineXOffset = _origin.x;
for (CFIndex i = 0; i < runCount; i++) {
CTRunRef ctRun = CFArrayGetValueAtIndex(ctRuns, i);
// Get the string range for this run
CFRange cfRange = CTRunGetStringRange(ctRun);
NSRange range = NSMakeRange(cfRange.location, cfRange.length);
// Get the run's typographic bounds
CGFloat ascent, descent, leading;
double width = CTRunGetTypographicBounds(ctRun, CFRangeMake(0, 0), &ascent, &descent, &leading);
// Get the positions of the glyphs
CFIndex glyphCount = CTRunGetGlyphCount(ctRun);
CGPoint *positions = (CGPoint *)malloc(sizeof(CGPoint) * glyphCount);
CTRunGetPositions(ctRun, CFRangeMake(0, 0), positions);
// Get the attributes
NSDictionary *attributes = (__bridge NSDictionary *)CTRunGetAttributes(ctRun);
// Calculate the run's frame
CGRect runFrame;
if (glyphCount > 0) {
CGFloat minX = positions[0].x;
CGFloat maxX = positions[glyphCount - 1].x + width / glyphCount; // Approximate
// More accurate: use the actual glyph advances
CGGlyph *glyphs = (CGGlyph *)malloc(sizeof(CGGlyph) * glyphCount);
CTRunGetGlyphs(ctRun, CFRangeMake(0, 0), glyphs);
// Calculate actual bounds using the glyph advances
CGFloat *advances = (CGFloat *)malloc(sizeof(CGFloat) * glyphCount);
CTFontRef font = (__bridge CTFontRef)attributes[(__bridge NSString *)kCTFontAttributeName];
if (font) {
CTFontGetAdvancesForGlyphs(font, kCTFontOrientationHorizontal, glyphs, advances, glyphCount);
}
// Recalculate max X using actual advances
maxX = minX;
for (CFIndex j = 0; j < glyphCount; j++) {
maxX += advances[j];
}
free(glyphs);
free(advances);
runFrame = CGRectMake(
lineXOffset + minX,
_origin.y - descent,
maxX - minX,
ascent + descent
);
} else {
runFrame = CGRectZero;
}
free(positions);
// Create glyph run wrapper
DTCoreTextGlyphRun *glyphRun = [[DTCoreTextGlyphRun alloc]
initWithCTRun:ctRun
frame:runFrame
range:range
attributes:attributes
index:i];
[runs addObject:glyphRun];
}
_cachedGlyphRuns = [runs copy];
_glyphRunsExtracted = YES;
[_lock unlock];
}
/**
* Returns the array of glyph runs.
*/
- (NSArray<DTCoreTextGlyphRun *> *)glyphRuns {
[self extractGlyphRuns];
return _cachedGlyphRuns;
}
- (DTCoreTextGlyphRun *)glyphRunAtIndex:(NSUInteger)index {
NSArray *runs = [self glyphRuns];
if (index < [runs count]) {
return runs[index];
}
return nil;
}
/**
* Returns the glyph run containing the given string index.
*/
- (DTCoreTextGlyphRun *)glyphRunContainingStringIndex:(NSUInteger)index {
NSArray<DTCoreTextGlyphRun *> *allRuns = [self glyphRuns];
for (DTCoreTextGlyphRun *run in allRuns) {
NSRange range = run.stringRange;
if (index >= range.location && index < range.location + range.length) {
return run;
}
}
return nil;
}
#pragma mark - Typographic Metrics
/**
* Calculates and caches typographic metrics.
*/
- (void)calculateMetrics {
if (_metricsCached || !_ctLine) {
return;
}
[_lock lock];
// Get typographic bounds from CTLine
_cachedWidth = CTLineGetTypographicBounds(_ctLine, &_cachedAscent, &_cachedDescent, &_cachedLeading);
// Get trailing whitespace width
_trailingWhitespaceWidth = CTLineGetTrailingWhitespaceWidth(_ctLine);
_metricsCached = YES;
[_lock unlock];
}
- (CGFloat)ascent {
[self calculateMetrics];
return _cachedAscent;
}
- (CGFloat)descent {
[self calculateMetrics];
return _cachedDescent;
}
- (CGFloat)leading {
[self calculateMetrics];
return _cachedLeading;
}
- (CGFloat)height {
return [self ascent] + [self descent] + [self leading];
}
- (CGFloat)width {
[self calculateMetrics];
return _cachedWidth;
}
/**
* Returns the typographic bounds of the line.
*/
- (CGFloat)getTypographicBoundsAscent:(CGFloat *)ascent
descent:(CGFloat *)descent
leading:(CGFloat *)leading {
[self calculateMetrics];
if (ascent) *ascent = _cachedAscent;
if (descent) *descent = _cachedDescent;
if (leading) *leading = _cachedLeading;
return _cachedWidth;
}
/**
* Returns the bounds of the line (bounding box).
*/
- (CGRect)bounds {
[self calculateMetrics];
return CGRectMake(0, -_cachedDescent, _cachedWidth, [self height]);
}
/**
* Returns the frame of the line (position + bounds).
*/
- (CGRect)frame {
return CGRectMake(_origin.x, _origin.y - _cachedDescent, _cachedWidth, [self height]);
}
#pragma mark - Hit Testing
/**
* Returns the string index at the given point within this line.
*
* Uses CTLineGetStringIndexForPosition to find which character
* is at the given point.
*
* @param point The point to test (in the line's coordinate system)
* @return The string index, or NSNotFound
*/
- (NSUInteger)stringIndexAtPoint:(CGPoint)point {
if (!_ctLine) {
return NSNotFound;
}
// Convert point to line-relative coordinates
CGPoint linePoint = CGPointMake(point.x - _origin.x, point.y - _origin.y);
// Use CTLine to find the character index
CFIndex index = CTLineGetStringIndexForPosition(_ctLine, linePoint);
if (index == kCFNotFound) {
return NSNotFound;
}
return (NSUInteger)index;
}
/**
* Returns the offset for a given string index.
*
* @param index The string index
* @return The horizontal offset from the line's origin
*/
- (CGFloat)offsetForStringIndex:(NSUInteger)index {
if (!_ctLine) {
return 0;
}
// CTLineGetOffsetForStringIndex returns the offset from the line's origin
CGFloat offset = CTLineGetOffsetForStringIndex(_ctLine, index, NULL);
return offset;
}
/**
* Returns the rect for a given string index.
*/
- (CGRect)rectForStringIndex:(NSUInteger)index {
CGFloat xOffset = [self offsetForStringIndex:index];
return CGRectMake(
_origin.x + xOffset,
_origin.y - _cachedDescent,
1, // Width of 1 character
[self height]
);
}
#pragma mark - String Operations
/**
* Returns the substring represented by this line.
*/
- (NSString *)substringFromAttributedString:(NSAttributedString *)attributedString {
if (!attributedString || _stringRange.location >= [attributedString length]) {
return nil;
}
// Clamp the range to the string length
NSUInteger maxLength = [attributedString length] - _stringRange.location;
NSUInteger length = MIN(_stringRange.length, maxLength);
return [[attributedString string] substringWithRange:NSMakeRange(_stringRange.location, length)];
}
/**
* Returns the attributes at a given string index.
*/
- (NSDictionary *)attributesAtIndex:(NSUInteger)index
fromAttributedString:(NSAttributedString *)attributedString {
if (!attributedString || index >= [attributedString length]) {
return nil;
}
return [attributedString attributesAtIndex:index effectiveRange:NULL];
}
#pragma mark - Line Comparison
/**
* Compares this line to another line for ordering.
*/
- (NSComparisonResult)compareToLine:(DTCoreTextLayoutLine *)otherLine {
// Compare by line index first
if (_lineIndex < otherLine.lineIndex) {
return NSOrderedAscending;
} else if (_lineIndex > otherLine.lineIndex) {
return NSOrderedDescending;
}
// If same index, compare by string range location
if (_stringRange.location < otherLine.stringRange.location) {
return NSOrderedAscending;
} else if (_stringRange.location > otherLine.stringRange.location) {
return NSOrderedDescending;
}
return NSOrderedSame;
}
/**
* Returns whether this line contains the given string index.
*/
- (BOOL)containsStringIndex:(NSUInteger)index {
return index >= _stringRange.location &&
index < _stringRange.location + _stringRange.length;
}
/**
* Returns whether this line intersects with the given range.
*/
- (BOOL)intersectsRange:(NSRange)range {
NSRange intersection = NSIntersectionRange(_stringRange, range);
return intersection.length > 0;
}
#pragma mark - Drawing
/**
* Draws this line into a CGContext.
*
* @param context The CGContext to draw into
*/
- (void)drawInContext:(CGContextRef)context {
if (!context || !_ctLine) {
return;
}
// Save graphics state
CGContextSaveGState(context);
// Move to the line's origin
CGContextSetTextPosition(context, _origin.x, _origin.y);
// Draw the line
CTLineDraw(_ctLine, context);
// Restore graphics state
CGContextRestoreGState(context);
}
/**
* Draws this line with a specific color.
*
* @param context The CGContext to draw into
* @param color The text color
*/
- (void)drawInContext:(CGContextRef)context withColor:(UIColor *)color {
if (!context || !_ctLine || !color) {
return;
}
// Save graphics state
CGContextSaveGState(context);
// Set the text color
CGContextSetFillColorWithColor(context, color.CGColor);
// Move to the line's origin
CGContextSetTextPosition(context, _origin.x, _origin.y);
// Draw the line
CTLineDraw(_ctLine, context);
// Restore graphics state
CGContextRestoreGState(context);
}
@end