Archived
1
This repository has been archived on 2022-08-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
mathpad/MathKit/MPExpressionLayout.m
2014-12-17 22:04:49 +01:00

298 lines
11 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 "MPExpression.h"
#import "MPExpression.h"
#import "MPPowerFunction.h"
#import "MPFunctionLayout.h"
#import "MPPowerFunctionLayout.h"
#import "MPToken.h"
#import "NSIndexPath+MPAdditions.h"
@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;
}
NSRange tokensRange = [self.expression convertRange:NSMakeRange(index, 1)
fromReferenceFrame:MPElementReferenceFrame
toReferenceFrame:MPTokenReferenceFrame];
NSArray *tokens = [self.expression itemsInRange:tokensRange
referenceFrame:MPTokenReferenceFrame];
id lineObject = [self cachableObjectForIndex:index generator:^id{
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] init];
for (id<MPToken> token in tokens) {
NSFont *font;
if (token.tokenType == MPElementaryFunctionToken) {
font = [self specialFontWithSize:self.contextInferredFontSize];
} else {
font = [self normalFontWithSize:self.contextInferredFontSize];
}
NSAttributedString *tokenText = [[NSAttributedString alloc] initWithString:token.stringValue
attributes:@{NSFontAttributeName: font}];
[text appendAttributedString:tokenText];
}
CFAttributedStringRef attributedString = CFBridgingRetain(text);
CTLineRef line = CTLineCreateWithAttributedString(attributedString);
CFRelease(attributedString);
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
- (NSUInteger)numberOfChildren
{
return self.expression.countElements;
}
- (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.countElements == 0) {
return NSMakeRect(0, kMPEmptyBoxYOrigin, kMPEmptyBoxWidth, kMPEmptyBoxHeight);
}
CGFloat x = 0, y = 0, width = 0, height = 0;
for (NSUInteger index = 0; index < self.expression.countElements; 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 convertIndex:range.location
fromReferenceFrame:MPSymbolReferenceFrame
toReferenceFrame:MPElementReferenceFrame
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.countElements) { // 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 convertIndex:NSMaxRange(range)
fromReferenceFrame:MPSymbolReferenceFrame
toReferenceFrame:MPElementReferenceFrame
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.countElements; 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.countSymbols];
}
}
- (BOOL)drawsChildrenManually
{
return YES;
}
- (void)draw
{
// Get the current context
CGContextRef context =
(CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
CGContextSaveGState(context);
[[NSColor textColor] set];
if (self.expression.countElements == 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 and the bounds of the last element
CGFloat x = 0;
NSRect lastElementBounds = NSMakeRect(0, kMPEmptyBoxYOrigin, kMPEmptyBoxWidth, kMPEmptyBoxHeight);
for (NSUInteger index = 0; index < self.expression.countElements; 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);
// Perform the drawing
CTLineDraw(line, context);
CFRelease(line);
} else {
// Let the child layout draw itself
MPLayout *layout = [self childLayoutAtIndex:index];
if ([layout isKindOfClass:[MPPowerFunctionLayout class]]) {
((MPPowerFunctionLayout *)layout).baseBounds = lastElementBounds;
}
[layout drawAtPoint:NSMakePoint(x, 0)];
}
x += elementBounds.size.width;
lastElementBounds = elementBounds;
}
CGContextRestoreGState(context);
}
@end