mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2025-01-28 09:49:21 +01:00
316 lines
7.4 KiB
Objective-C
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
|