316 lines
7.4 KiB
Objective-C

//
// RSMultiLineRenderer.m
// RSTextDrawing
//
// Created by Brent Simmons on 3/3/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
#import "RSMultiLineRenderer.h"
#import "RSMultiLineRendererMeasurements.h"
@interface RSMultiLineRenderer ()
@property (nonatomic, readonly) NSAttributedString *title;
@property (nonatomic) NSRect rect;
@property (nonatomic, readonly) CTFramesetterRef framesetter;
@property (nonatomic) CTFrameRef frameref;
@property (nonatomic, readonly) NSMutableDictionary *measurementCache;
@end
static NSMutableDictionary *rendererCache = nil;
static NSUInteger kMaximumNumberOfLines = 2;
@implementation RSMultiLineRenderer
#pragma mark - Class Methods
+ (instancetype)rendererWithAttributedTitle:(NSAttributedString *)title {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
rendererCache = [NSMutableDictionary new];
});
RSMultiLineRenderer *cachedRenderer = rendererCache[title];
if (cachedRenderer != nil) {
return cachedRenderer;
}
RSMultiLineRenderer *renderer = [[RSMultiLineRenderer alloc] initWithAttributedTitle:title];
rendererCache[title] = renderer;
return renderer;
}
+ (void)emptyCache {
for (RSMultiLineRenderer *oneRenderer in rendererCache.allValues) {
[oneRenderer emptyCache];
[oneRenderer releaseFrameref];
}
rendererCache = [NSMutableDictionary new];
}
#pragma mark Init
- (instancetype)initWithAttributedTitle:(NSAttributedString *)title {
self = [super init];
if (self == nil) {
return nil;
}
_title = title;
_measurementCache = [NSMutableDictionary new];
_framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)title);
_backgroundColor = NSColor.whiteColor;
return self;
}
#pragma mark Dealloc
- (void)dealloc {
if (_framesetter) {
CFRelease(_framesetter);
_framesetter = nil;
}
if (_frameref) {
CFRelease(_frameref);
_frameref = nil;
}
}
#pragma mark Accessors
- (void)setRect:(NSRect)r {
r.origin.y = floor(r.origin.y);
r.origin.x = floor(r.origin.x);
r.size.height = floor(r.size.height);
r.size.width = floor(r.size.width);
if (!NSEqualRects(r, _rect)) {
_rect = r;
[self releaseFrameref];
}
}
- (void)releaseFrameref {
if (_frameref) {
CFRelease(_frameref);
_frameref = nil;
}
}
#pragma mark - Measurements
- (NSInteger)integerForWidth:(CGFloat)width {
return (NSInteger)(floor(width));
}
- (NSNumber *)keyForWidth:(CGFloat)width {
return @([self integerForWidth:width]);
}
- (RSMultiLineRendererMeasurements *)measurementsByRegardingNarrowerNeighborsInCache:(CGFloat)width {
/*If width==30, and cached measurements for width==15 indicate that it's a single line of text, then return those measurements.*/
NSInteger w = [self integerForWidth:width];
static const NSInteger kSingleLineHeightWithSlop = 20;
for (NSNumber *oneKey in self.measurementCache.allKeys) {
NSInteger oneWidth = oneKey.integerValue;
if (oneWidth < w) {
RSMultiLineRendererMeasurements *oneMeasurements = self.measurementCache[oneKey];
if (oneMeasurements.height <= kSingleLineHeightWithSlop) {
return oneMeasurements;
}
}
}
return nil;
}
- (RSMultiLineRendererMeasurements *)measurementsByRegardingNeighborsInCache:(CGFloat)width {
/*If width==30, and the cached measurements for width==15 and width==42 are equal, then we can use one of those for width==30.*/
if (self.measurementCache.count < 2) {
return nil;
}
NSInteger w = [self integerForWidth:width];
NSInteger lessThanNeighbor = NSNotFound;
NSInteger greaterThanNeighbor = NSNotFound;
for (NSNumber *oneKey in self.measurementCache.allKeys) {
NSInteger oneWidth = oneKey.integerValue;
if (oneWidth < w) {
if (lessThanNeighbor == NSNotFound) {
lessThanNeighbor = oneWidth;
}
else if (lessThanNeighbor < oneWidth) {
lessThanNeighbor = oneWidth;
}
}
if (oneWidth > w) {
if (greaterThanNeighbor == NSNotFound) {
greaterThanNeighbor = oneWidth;
}
else if (greaterThanNeighbor > oneWidth) {
greaterThanNeighbor = oneWidth;
}
}
}
if (lessThanNeighbor == NSNotFound || greaterThanNeighbor == NSNotFound) {
return nil;
}
RSMultiLineRendererMeasurements *lessThanMeasurements = self.measurementCache[@(lessThanNeighbor)];
RSMultiLineRendererMeasurements *greaterThanMeasurements = self.measurementCache[@(greaterThanNeighbor)];
if ([lessThanMeasurements isEqual:greaterThanMeasurements]) {
return lessThanMeasurements;
}
return nil;
}
- (RSMultiLineRendererMeasurements *)measurementsForWidth:(CGFloat)width {
NSNumber *key = [self keyForWidth:width];
RSMultiLineRendererMeasurements *cachedMeasurements = self.measurementCache[key];
if (cachedMeasurements) {
return cachedMeasurements;
}
RSMultiLineRendererMeasurements *measurements = [self measurementsByRegardingNarrowerNeighborsInCache:width];
if (measurements) {
return measurements;
}
measurements = [self measurementsByRegardingNeighborsInCache:width];
if (measurements) {
return measurements;
}
measurements = [self calculatedMeasurementsForWidth:width];
self.measurementCache[key] = measurements;
return measurements;
}
#pragma mark - Cache
- (void)emptyCache {
[self.measurementCache removeAllObjects];
}
#pragma mark Rendering
static const CGFloat kMaxHeight = 10000.0;
- (RSMultiLineRendererMeasurements *)calculatedMeasurementsForWidth:(CGFloat)width {
NSInteger height = 0;
NSInteger heightOfFirstLine = 0;
width = floor(width);
@autoreleasepool {
CGRect r = CGRectMake(0.0f, 0.0f, width, kMaxHeight);
CGPathRef path = CGPathCreateWithRect(r, NULL);
CTFrameRef frameref = CTFramesetterCreateFrame(self.framesetter, CFRangeMake(0, (CFIndex)(self.title.length)), path, NULL);
NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frameref);
if (lines.count > 0) {
NSUInteger indexOfLastLine = MIN(kMaximumNumberOfLines - 1, lines.count - 1);
CGPoint origins[indexOfLastLine + 1];
CTFrameGetLineOrigins(frameref, CFRangeMake(0, (CFIndex)indexOfLastLine + 1), origins);
CTLineRef lastLine = (__bridge CTLineRef)lines[indexOfLastLine];
CGPoint lastOrigin = origins[indexOfLastLine];
CGFloat descent;
CTLineGetTypographicBounds(lastLine, NULL, &descent, NULL);
height = r.size.height - (ceil(lastOrigin.y) - ceil(descent));
height = (NSInteger)ceil(height);
CTLineRef firstLine = (__bridge CTLineRef)lines[0];
CGRect firstLineRect = CTLineGetBoundsWithOptions(firstLine, 0);
heightOfFirstLine = (NSInteger)ceil(NSHeight(firstLineRect));
}
CFRelease(path);
CFRelease(frameref);
}
RSMultiLineRendererMeasurements *measurements = [RSMultiLineRendererMeasurements measurementsWithHeight:height heightOfFirstLine:heightOfFirstLine];
return measurements;
}
- (void)renderTextInRect:(CGRect)r {
self.rect = r;
CGContextRef context = [NSGraphicsContext currentContext].CGContext;
CGContextSaveGState(context);
CGContextSetFillColorWithColor(context, self.backgroundColor.CGColor);
CGContextFillRect(context, r);
CGContextSetShouldSmoothFonts(context, true);
CTFrameDraw(self.frameref, context);
CGContextRestoreGState(context);
}
- (CTFrameRef)frameref {
if (_frameref) {
return _frameref;
}
CGPathRef path = CGPathCreateWithRect(self.rect, NULL);
_frameref = CTFramesetterCreateFrame(self.framesetter, CFRangeMake(0, (CFIndex)(self.title.length)), path, NULL);
CFRelease(path);
return _frameref;
}
@end