Files
twui/lib/UIKit/TUITextRenderer.m
2012-09-26 00:26:11 -07:00

473 lines
12 KiB
Objective-C

/*
Copyright 2011 Twitter, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this work except in compliance with the License.
You may obtain a copy of the License in the LICENSE file, or at:
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "TUITextRenderer.h"
#import "ABActiveRange.h"
#import "NSColor+TUIExtensions.h"
#import "TUIAttributedString.h"
#import "TUICGAdditions.h"
#import "TUIStringDrawing.h"
#import "TUIView.h"
NSString *const TUITextRendererDidBecomeFirstResponder = @"TUITextRendererDidBecomeFirstResponder";
NSString *const TUITextRendererDidResignFirstResponder = @"TUITextRendererDidResignFirstResponder";
@interface TUITextRenderer ()
@property (nonatomic, strong) NSMutableDictionary *lineRects;
@end
@implementation TUITextRenderer
@synthesize attributedString;
@synthesize frame;
@synthesize view;
@synthesize hitRange;
@synthesize shadowColor;
@synthesize shadowOffset;
@synthesize shadowBlur;
@synthesize verticalAlignment;
@synthesize lineRects;
- (void)_resetFrame
{
if(_ct_frame) {
CFRelease(_ct_frame);
_ct_frame = NULL;
}
if(_ct_path) {
CGPathRelease(_ct_path);
_ct_path = NULL;
}
lineRects = nil;
}
- (void)_resetFramesetter
{
if(_ct_framesetter) {
CFRelease(_ct_framesetter);
_ct_framesetter = NULL;
}
[self _resetFrame];
}
- (id)init {
if((self = [super init])) {
self.selectionColor = [NSColor selectedTextBackgroundColor];
}
return self;
}
- (void)dealloc
{
[self _resetFramesetter];
}
- (void)_buildFrameWithEffectiveFrame:(CGRect)effectiveFrame
{
[self _resetFrame];
_ct_path = CGPathCreateMutable();
CGPathAddRect((CGMutablePathRef)_ct_path, NULL, effectiveFrame);
_ct_frame = CTFramesetterCreateFrame(_ct_framesetter, CFRangeMake(0, 0), _ct_path, NULL);
}
- (void)_buildFrame
{
if(!_ct_path) {
[self _buildFrameWithEffectiveFrame:frame];
// TUITextVerticalAlignmentTop is easy since that's how Core Text always draws. For Middle and Bottom we have to shift the CTFrame down.
if(verticalAlignment != TUITextVerticalAlignmentTop) {
CGRect effectiveFrame = frame;
CGSize size = AB_CTFrameGetSize(_ct_frame);
if(verticalAlignment == TUITextVerticalAlignmentMiddle) {
effectiveFrame.origin.y = size.height/2 - frame.size.height/2;
} else if(verticalAlignment == TUITextVerticalAlignmentBottom) {
effectiveFrame.origin.y = size.height;
}
effectiveFrame = CGRectIntegral(effectiveFrame);
[self _buildFrameWithEffectiveFrame:effectiveFrame];
}
}
}
- (void)_buildFramesetter
{
if(!_ct_framesetter) {
_ct_framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)self.drawingAttributedString);
}
[self _buildFrame];
}
- (CTFramesetterRef)ctFramesetter
{
[self _buildFramesetter];
return _ct_framesetter;
}
- (CTFrameRef)ctFrame
{
[self _buildFramesetter];
return _ct_frame;
}
- (CGPathRef)ctPath
{
[self _buildFramesetter];
return _ct_path;
}
- (CFIndex)_clampToValidRange:(CFIndex)index
{
if(index < 0) return 0;
CFIndex max = [attributedString length] - 1;
if(index > max) return max;
return index;
}
- (NSRange)_wordRangeAtIndex:(CFIndex)index
{
return [attributedString doubleClickAtIndex:[self _clampToValidRange:index]];
}
- (NSRange)_lineRangeAtIndex:(CFIndex)index
{
return [[attributedString string] lineRangeForRange:NSMakeRange(index, 0)];
}
- (NSRange)_paragraphRangeAtIndex:(CFIndex)index
{
return [[attributedString string] paragraphRangeForRange:NSMakeRange(index, 0)];
}
- (CFRange)_selectedRange
{
CFIndex first, last;
if(_selectionStart <= _selectionEnd) {
first = _selectionStart;
last = _selectionEnd;
} else {
first = _selectionEnd;
last = _selectionStart;
}
if(_selectionAffinity != TUITextSelectionAffinityCharacter) {
NSRange fr = {0,0};
NSRange lr = {0,0};
switch(_selectionAffinity) {
case TUITextSelectionAffinityCharacter:
// do nothing
break;
case TUITextSelectionAffinityWord:
fr = [self _wordRangeAtIndex:first];
lr = [self _wordRangeAtIndex:last];
break;
case TUITextSelectionAffinityLine:
fr = [self _lineRangeAtIndex:first];
lr = [self _lineRangeAtIndex:last];
break;
case TUITextSelectionAffinityParagraph:
fr = [self _paragraphRangeAtIndex:first];
lr = [self _paragraphRangeAtIndex:last];
break;
}
first = fr.location;
last = lr.location + lr.length;
}
return CFRangeMake(first, last - first);
}
- (NSAttributedString*)drawingAttributedString {
return attributedString;
}
- (NSRange)selectedRange
{
return ABNSRangeFromCFRange([self _selectedRange]);
}
- (void)setSelection:(NSRange)selection
{
_selectionAffinity = TUITextSelectionAffinityCharacter;
_selectionStart = selection.location;
_selectionEnd = selection.location + selection.length;
[view setNeedsDisplay];
}
- (NSString *)selectedString
{
return [[attributedString string] substringWithRange:[self selectedRange]];
}
- (void)draw
{
[self drawInContext:TUIGraphicsGetCurrentContext()];
}
- (void)drawInContext:(CGContextRef)context
{
if(attributedString) {
CGContextSaveGState(context);
if(_flags.preDrawBlocksEnabled && !_flags.drawMaskDragSelection) {
[self.drawingAttributedString enumerateAttribute:TUIAttributedStringPreDrawBlockName inRange:NSMakeRange(0, [self.drawingAttributedString length]) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
if(value == NULL) return;
CGContextSaveGState(context);
AB_CTLineRectAggregationType aggregationType = (AB_CTLineRectAggregationType) [[self.drawingAttributedString attribute:TUIAttributedStringBackgroundFillStyleName atIndex:range.location effectiveRange:NULL] integerValue];
NSArray *rectsArray = [self rectsForCharacterRange:CFRangeMake(range.location, range.length) aggregationType:aggregationType];
CFIndex rectCount = rectsArray.count;
CGRect rects[rectCount];
for(NSUInteger i = 0; i < rectCount; i++) {
rects[i] = [[rectsArray objectAtIndex:i] rectValue];
}
TUIAttributedStringPreDrawBlock block = value;
block(self.drawingAttributedString, range, rects, rectCount);
CGContextRestoreGState(context);
}];
}
if(_flags.backgroundDrawingEnabled && !_flags.drawMaskDragSelection) {
CGContextSaveGState(context);
[self.drawingAttributedString enumerateAttribute:TUIAttributedStringBackgroundColorAttributeName inRange:NSMakeRange(0, [self.drawingAttributedString length]) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
if(value == NULL) return;
CGColorRef color = (__bridge CGColorRef) value;
CGContextSetFillColorWithColor(context, color);
AB_CTLineRectAggregationType aggregationType = (AB_CTLineRectAggregationType) [[self.drawingAttributedString attribute:TUIAttributedStringBackgroundFillStyleName atIndex:range.location effectiveRange:NULL] integerValue];
NSArray *rectsArray = [self rectsForCharacterRange:CFRangeMake(range.location, range.length) aggregationType:aggregationType];
CFIndex rectCount = rectsArray.count;
CGRect rects[rectCount];
for(NSUInteger i = 0; i < rectCount; i++) {
rects[i] = [[rectsArray objectAtIndex:i] rectValue];
}
for(CFIndex i = 0; i < rectCount; ++i) {
CGRect r = rects[i];
r = CGRectInset(r, -2, -1);
r = CGRectIntegral(r);
if(r.size.width > 1)
CGContextFillRect(context, r);
}
}];
CGContextRestoreGState(context);
}
CTFrameRef f = [self ctFrame];
if(hitRange && !_flags.drawMaskDragSelection) {
// draw highlight
CGContextSaveGState(context);
NSRange _r = [hitRange rangeValue];
CFRange r = {_r.location, _r.length};
CFIndex nRects = 10;
CGRect rects[nRects];
AB_CTFrameGetRectsForRange(f, r, rects, &nRects);
for(int i = 0; i < nRects; ++i) {
CGRect rect = rects[i];
rect = CGRectInset(rect, -2, -1);
rect.size.height -= 1;
rect = CGRectIntegral(rect);
NSColor *color = [NSColor colorWithCalibratedWhite:1.0 alpha:1.0];
[color setFill];
CGContextSetShadowWithColor(context, CGSizeMake(0, 0), 8, color.tui_CGColor);
CGContextFillRoundRect(context, rect, 10);
}
CGContextRestoreGState(context);
}
CFRange selectedRange = [self _selectedRange];
if(selectedRange.length > 0) {
[self.selectionColor set];
// draw (or mask) selection
CFIndex rectCount = 100;
CGRect rects[rectCount];
AB_CTFrameGetRectsForRange(f, selectedRange, rects, &rectCount);
if(_flags.drawMaskDragSelection) {
CGContextClipToRects(context, rects, rectCount);
} else {
[self drawSelectionWithRects:rects count:rectCount];
}
}
if(shadowColor)
CGContextSetShadowWithColor(context, shadowOffset, shadowBlur, shadowColor.tui_CGColor);
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CTFrameDraw(f, context);
CGContextRestoreGState(context);
}
}
- (void)drawSelectionWithRects:(CGRect *)rects count:(CFIndex)count {
CGContextRef context = TUIGraphicsGetCurrentContext();
for(CFIndex i = 0; i < count; ++i) {
CGRect r = rects[i];
r = CGRectIntegral(r);
if(r.size.width > 1)
CGContextFillRect(context, r);
}
}
- (CGSize)size
{
if(attributedString) {
return AB_CTFrameGetSize([self ctFrame]);
}
return CGSizeZero;
}
- (CGSize)sizeConstrainedToWidth:(CGFloat)width
{
if(attributedString) {
CGRect oldFrame = frame;
self.frame = CGRectMake(0.0f, 0.0f, width, 1000000.0f);
CGSize size = [self size];
self.frame = oldFrame;
return size;
}
return CGSizeZero;
}
- (CGSize)sizeConstrainedToWidth:(CGFloat)width numberOfLines:(NSUInteger)numberOfLines
{
NSMutableAttributedString *fake = [self.drawingAttributedString mutableCopy];
[fake replaceCharactersInRange:NSMakeRange(0, [fake length]) withString:@"M"];
CGFloat singleLineHeight = [fake ab_sizeConstrainedToWidth:width].height;
CGFloat maxHeight = singleLineHeight * numberOfLines;
CGSize size = [self sizeConstrainedToWidth:width];
return CGSizeMake(size.width, MIN(maxHeight, size.height));
}
- (void)setAttributedString:(NSAttributedString *)a
{
attributedString = a;
[self _resetFramesetter];
}
- (void)setFrame:(CGRect)f
{
frame = f;
[self _resetFrame];
}
- (void)reset
{
[self _resetFramesetter];
}
- (CGRect)firstRectForCharacterRange:(CFRange)range
{
CFIndex rectCount = 1;
CGRect rects[rectCount];
AB_CTFrameGetRectsForRange([self ctFrame], range, rects, &rectCount);
if(rectCount > 0) {
return rects[0];
}
return CGRectZero;
}
- (NSArray *)rectsForCharacterRange:(CFRange)range
{
return [self rectsForCharacterRange:range aggregationType:AB_CTLineRectAggregationTypeInline];
}
- (NSArray *)rectsForCharacterRange:(CFRange)range aggregationType:(AB_CTLineRectAggregationType)aggregationType
{
if(self.lineRects == nil) {
self.lineRects = [NSMutableDictionary dictionary];
}
NSValue *cacheKey = [NSValue valueWithRange:NSMakeRange(range.location, range.length)];
NSArray *cachedRects = [self.lineRects objectForKey:cacheKey];
if(cachedRects == nil) {
CFIndex rectCount = 100;
CGRect rects[rectCount];
AB_CTFrameGetRectsForRangeWithAggregationType([self ctFrame], range, aggregationType, rects, &rectCount);
NSMutableArray *wrappedRects = [NSMutableArray arrayWithCapacity:rectCount];
for(CFIndex i = 0; i < rectCount; i++) {
[wrappedRects addObject:[NSValue valueWithRect:rects[i]]];
}
[self.lineRects setObject:wrappedRects forKey:cacheKey];
cachedRects = wrappedRects;
}
return cachedRects;
}
- (BOOL)backgroundDrawingEnabled
{
return _flags.backgroundDrawingEnabled;
}
- (void)setBackgroundDrawingEnabled:(BOOL)enabled
{
_flags.backgroundDrawingEnabled = enabled;
}
- (BOOL)preDrawBlocksEnabled
{
return _flags.preDrawBlocksEnabled;
}
- (void)setPreDrawBlocksEnabled:(BOOL)enabled
{
_flags.preDrawBlocksEnabled = enabled;
}
- (void)setVerticalAlignment:(TUITextVerticalAlignment)alignment
{
if(verticalAlignment == alignment) return;
verticalAlignment = alignment;
[self _resetFrame];
}
- (void)setNeedsDisplay {
[self _resetFramesetter];
[view setNeedsDisplay];
}
@end