// // MPExpressionLayout.m // MathKit // // Created by Kim Wittenburg on 07.08.14. // Copyright (c) 2014 Kim Wittenburg. All rights reserved. // #import "MPExpressionLayout.h" #import "MPExpression.h" #import "MPFunction.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 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 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 { 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; } CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGFloat x = 0; NSRect lastElementBounds = NSMakeRect(0, kMPEmptyBoxYOrigin, kMPEmptyBoxWidth, kMPEmptyBoxHeight); for (NSUInteger index = 0; index < self.expression.countElements; index++) { id element = [self.expression elementAtIndex:index]; NSRect elementBounds = [self boundsOfElementAtIndex:index]; if ([element isString]) { CTLineRef line = [self lineForElementAtIndex:index]; CFRetain(line); CGContextSetTextPosition(context, x, 0); CTLineDraw(line, context); CFRelease(line); } else { 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