282 lines
10 KiB
Objective-C
282 lines
10 KiB
Objective-C
//
|
|
// MPExpressionLayout.m
|
|
// MathPad
|
|
//
|
|
// Created by Kim Wittenburg on 07.08.14.
|
|
// Copyright (c) 2014 Kim Wittenburg. All rights reserved.
|
|
//
|
|
|
|
#import "MPExpressionLayout.h"
|
|
#import "MPFunctionLayout.h"
|
|
|
|
#import "NSString+MPExpressionElement.h"
|
|
#import "NSIndexPath+MPAdditions.h"
|
|
|
|
#define kMPEmptyBoxWidth (self.usesSmallSize ? 2.0 : 3.0)
|
|
#define kMPEmptyBoxHeight (CTFontGetDescent((CTFontRef)self.font) + CTFontGetAscent((CTFontRef)self.font) + CTFontGetLeading((CTFontRef)self.font))
|
|
#define kMPEmptyBoxYOrigin (-(CTFontGetDescent((CTFontRef)self.font) + CTFontGetLeading((CTFontRef)self.font)))
|
|
|
|
#define kMPEmptyBoxDrawingWidth kMPEmptyBoxWidth
|
|
#define kMPEmptyBoxDrawingHeight (CTFontGetDescent((CTFontRef)self.font) + CTFontGetAscent((CTFontRef)self.font))
|
|
#define kMPEmptyBoxDrawingYOrigin (-(CTFontGetDescent((CTFontRef)self.font) + CTFontGetLeading((CTFontRef)self.font)/2))
|
|
|
|
@interface MPExpressionLayout (MPLineGeneration)
|
|
|
|
- (CTLineRef)lineForElementAtIndex:(NSUInteger)index;
|
|
|
|
@end
|
|
|
|
@implementation MPExpressionLayout (MPLineGeneration)
|
|
|
|
- (CTLineRef)lineForElementAtIndex:(NSUInteger)index
|
|
{
|
|
id element = [self.expression elementAtIndex:index];
|
|
if (![element isString]) {
|
|
return NULL;
|
|
}
|
|
NSString *string = element;
|
|
id lineObject = [self cachableObjectForIndex:index generator:^id{
|
|
CTLineRef line = [self createLineForString:string];
|
|
return CFBridgingRelease(line);
|
|
}];
|
|
return (__bridge CTLineRef)lineObject;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation MPExpressionLayout
|
|
|
|
# pragma mark Creation Methods
|
|
- (instancetype)initWithExpression:(MPExpression *)expression
|
|
parent:(MPFunctionLayout *)parent
|
|
{
|
|
self = [super initWithParent:parent];
|
|
if (self) {
|
|
_expression = expression;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
#pragma mark Cache Methods
|
|
- (MPLayout *)childLayoutAtIndex:(NSUInteger)index
|
|
{
|
|
id cachedObject = [self cachableObjectForIndex:index generator:^id{
|
|
MPFunction *function = (MPFunction *)[self.expression elementAtIndex:index];
|
|
MPFunctionLayout *layout = [MPFunctionLayout functionLayoutForFunction:function
|
|
parent:self];
|
|
layout.flipped = self.flipped;
|
|
layout.usesSmallSize = self.usesSmallSize;
|
|
return layout;
|
|
}];
|
|
if ([cachedObject isKindOfClass:[MPLayout class]]) {
|
|
return cachedObject;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (NSRect)boundsOfElementAtIndex:(NSUInteger)index
|
|
{
|
|
id symbol = [self.expression elementAtIndex:index];
|
|
if ([symbol isString]) {
|
|
CTLineRef line = [self lineForElementAtIndex:index];
|
|
CFRetain(line);
|
|
CGRect bounds = CTLineGetBoundsWithOptions(line, 0);
|
|
CFRelease(line);
|
|
return bounds;
|
|
} else {
|
|
return [self childLayoutAtIndex:index].bounds;
|
|
}
|
|
}
|
|
|
|
#pragma mark Drawing Methods
|
|
- (NSRect)generateBounds
|
|
{
|
|
if (self.expression.numberOfElements == 0) {
|
|
return NSMakeRect(0, kMPEmptyBoxYOrigin, kMPEmptyBoxWidth, kMPEmptyBoxHeight);
|
|
}
|
|
CGFloat x = 0, y = 0, width = 0, height = 0;
|
|
for (NSUInteger index = 0; index < self.expression.numberOfElements; index++) {
|
|
NSRect elementBounds = [self boundsOfElementAtIndex:index];
|
|
width += elementBounds.size.width;
|
|
height = MAX(height, elementBounds.size.height);
|
|
y = MIN(y, elementBounds.origin.y);
|
|
}
|
|
return NSMakeRect(x, y, width, height);
|
|
}
|
|
|
|
- (NSRect)boundingRectForRange:(NSRange)range
|
|
{
|
|
NSUInteger startOffset;
|
|
NSUInteger startElementIndex = [self.expression indexOfElementAtLocation:range.location
|
|
offset:&startOffset];
|
|
// Calculate x position
|
|
CGFloat x = 0, width = 0;
|
|
for (NSUInteger index = 0; index < startElementIndex; index++) {
|
|
x += [self boundsOfElementAtIndex:index].size.width;
|
|
}
|
|
|
|
if (startOffset > 0) {
|
|
CTLineRef line = [self lineForElementAtIndex:startElementIndex];
|
|
CFRetain(line);
|
|
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, startOffset, NULL);
|
|
x += xOffset;
|
|
width += CTLineGetBoundsWithOptions(line, 0).size.width - xOffset;
|
|
CFRelease(line);
|
|
} else if (startElementIndex < self.expression.numberOfElements) { // Otherwise the selection is after the last symbol
|
|
width += [self boundsOfElementAtIndex:startElementIndex].size.width;
|
|
}
|
|
|
|
// If we search the caret position we are done
|
|
if (range.length == 0) {
|
|
return NSMakeRect(x, self.bounds.origin.y, 0, self.bounds.size.height);
|
|
}
|
|
|
|
NSUInteger endOffset;
|
|
NSUInteger endElementIndex = [self.expression indexOfElementAtLocation:NSMaxRange(range)
|
|
offset:&endOffset];
|
|
|
|
// Selection is inside of one string element
|
|
if (startElementIndex == endElementIndex) {
|
|
CTLineRef line = [self lineForElementAtIndex:endElementIndex];
|
|
CFRetain(line);
|
|
CGFloat xStart = CTLineGetOffsetForStringIndex(line, startOffset, NULL);
|
|
CGFloat xEnd = CTLineGetOffsetForStringIndex(line, endOffset, NULL);
|
|
width = xEnd - xStart;
|
|
CFRelease(line);
|
|
return NSMakeRect(x, self.bounds.origin.y, width, self.bounds.size.height);
|
|
}
|
|
|
|
// Calculate width
|
|
for (NSUInteger index = startElementIndex + 1; index < endElementIndex; index++) {
|
|
width += [self boundsOfElementAtIndex:index].size.width;
|
|
}
|
|
if (endOffset > 0) {
|
|
CTLineRef line = [self lineForElementAtIndex:endElementIndex];
|
|
CFRetain(line);
|
|
width += CTLineGetOffsetForStringIndex(line, endOffset, NULL);
|
|
CFRelease(line);
|
|
}
|
|
return NSMakeRect(x, self.bounds.origin.y, width, self.bounds.size.height);
|
|
}
|
|
|
|
- (NSPoint)offsetOfChildLayoutAtIndex:(NSUInteger)index
|
|
{
|
|
CGFloat x = 0;
|
|
for (NSUInteger i = 0; i < index; i++) {
|
|
x += [self boundsOfElementAtIndex:i].size.width;
|
|
}
|
|
return NSMakePoint(x, 0);
|
|
}
|
|
|
|
- (NSIndexPath *)indexPathForMousePoint:(NSPoint)point
|
|
{
|
|
NSUInteger currentPosition = 0;
|
|
for (NSUInteger index = 0; index < self.expression.numberOfElements; index++) {
|
|
NSRect elementBounds = [self boundsOfElementAtIndex:index];
|
|
NSPoint elementOffset = [self offsetOfChildLayoutAtIndex:index];
|
|
elementBounds.origin.x += elementOffset.x;
|
|
elementBounds.origin.y += elementOffset.y;
|
|
|
|
// Only the horizontal location is to consider for hit testing
|
|
elementBounds.size.height = CGFLOAT_MAX;
|
|
|
|
id<MPExpressionElement> element = [self.expression elementAtIndex:index];
|
|
if (NSMouseInRect(point, elementBounds, self.flipped)) {
|
|
NSPoint pointInElement = NSMakePoint(point.x - elementOffset.x, point.y + elementOffset.y);
|
|
if ([element isString]) {
|
|
CTLineRef line = [self lineForElementAtIndex:index];
|
|
CFRetain(line);
|
|
CFIndex localIndex = CTLineGetStringIndexForPosition(line, pointInElement);
|
|
CFRelease(line);
|
|
return [NSIndexPath indexPathWithIndex:currentPosition+localIndex];
|
|
} else {
|
|
NSIndexPath *subPath = [[self childLayoutAtIndex:index] indexPathForMousePoint:pointInElement];
|
|
if (subPath.length == 1) {
|
|
// A single index is used to communicate back wether the
|
|
// selection should be before or after the function.
|
|
// A 0 means before, a 1 means after.
|
|
return [NSIndexPath indexPathWithIndex:currentPosition + [subPath indexAtPosition:0]];
|
|
} else {
|
|
return [subPath indexPathByPreceedingIndex:index];
|
|
}
|
|
}
|
|
}
|
|
currentPosition += element.length;
|
|
}
|
|
if (point.x < self.bounds.size.width / 2) {
|
|
return [NSIndexPath indexPathWithIndex:0];
|
|
} else {
|
|
return [NSIndexPath indexPathWithIndex:self.expression.length];
|
|
}
|
|
}
|
|
|
|
- (void)draw
|
|
{
|
|
// Get the current context
|
|
CGContextRef context =
|
|
(CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
|
|
CGContextSaveGState(context);
|
|
|
|
#ifdef MPDEBUG_DRAW_ORIGIN
|
|
[[NSColor blueColor] set];
|
|
[[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(-2, -2, 4, 4)] fill];
|
|
#endif
|
|
|
|
[[NSColor textColor] set];
|
|
|
|
#ifdef MPDEBUG_DRAW_BOUNDS
|
|
[[NSColor greenColor] set];
|
|
[[NSBezierPath bezierPathWithRect:self.bounds] stroke];
|
|
[[NSColor textColor] set];
|
|
#endif
|
|
|
|
if (self.expression.numberOfElements == 0) {
|
|
|
|
CGContextRestoreGState(context);
|
|
NSBezierPath *path = [NSBezierPath bezierPathWithRect:NSMakeRect(0, kMPEmptyBoxDrawingYOrigin, kMPEmptyBoxDrawingWidth, kMPEmptyBoxDrawingHeight)];
|
|
path.lineWidth = 0.5;
|
|
[path stroke];
|
|
return;
|
|
}
|
|
|
|
// Set the text matrix
|
|
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
|
|
|
|
// Track the x position
|
|
CGFloat x = 0;
|
|
|
|
for (NSUInteger index = 0; index < self.expression.numberOfElements; index++) {
|
|
// The current element
|
|
id element = [self.expression elementAtIndex:index];
|
|
NSRect elementBounds = [self boundsOfElementAtIndex:index];
|
|
|
|
if ([element isString]) {
|
|
// Get the line to draw
|
|
CTLineRef line = [self lineForElementAtIndex:index];
|
|
CFRetain(line);
|
|
|
|
// Move to the appropriate position
|
|
CGContextSetTextPosition(context, x, 0);
|
|
|
|
#ifdef MPDEBUG_DRAW_BASELINE
|
|
[[NSColor redColor] set];
|
|
NSRectFill(NSMakeRect(x, -1, elementBounds.size.width, 1));
|
|
[[NSColor textColor] set];
|
|
#endif
|
|
|
|
// Perform the drawing
|
|
CTLineDraw(line, context);
|
|
|
|
CFRelease(line);
|
|
} else {
|
|
// Let the child layout draw itself
|
|
MPLayout *layout = [self childLayoutAtIndex:index];
|
|
[layout drawAtPoint:NSMakePoint(x, 0)];
|
|
}
|
|
x += elementBounds.size.width;
|
|
}
|
|
CGContextRestoreGState(context);
|
|
}
|
|
|
|
@end
|