817 lines
26 KiB
Swift
817 lines
26 KiB
Swift
//
|
|
// MTMathListDisplay.swift
|
|
// MathRenderSwift
|
|
//
|
|
// Created by Mike Griebling on 2022-12-31.
|
|
//
|
|
|
|
import Foundation
|
|
import QuartzCore
|
|
import CoreText
|
|
import SwiftUI
|
|
|
|
func isIos6Supported() -> Bool {
|
|
if !MTDisplay.initialized {
|
|
#if os(iOS)
|
|
let reqSysVer = "6.0"
|
|
let currSysVer = UIDevice.current.systemVersion
|
|
if currSysVer.compare(reqSysVer, options: .numeric) != .orderedAscending {
|
|
MTDisplay.supported = true
|
|
}
|
|
#else
|
|
MTDisplay.supported = true
|
|
#endif
|
|
MTDisplay.initialized = true
|
|
}
|
|
return MTDisplay.supported
|
|
}
|
|
|
|
protocol DownShift {
|
|
var shiftDown:CGFloat { set get }
|
|
}
|
|
|
|
// MARK: - MTDisplay
|
|
|
|
/// The base class for rendering a math equation.
|
|
class MTDisplay {
|
|
|
|
// needed for isIos6Supported() func above
|
|
static var initialized = false
|
|
static var supported = false
|
|
|
|
/// Draws itself in the given graphics context.
|
|
func draw(_ context:CGContext) {
|
|
if (self.localBackgroundColor != nil) {
|
|
context.saveGState()
|
|
context.setBlendMode(.normal)
|
|
context.setFillColor(self.localBackgroundColor!.cgColor)
|
|
context.fill(self.displayBounds())
|
|
context.restoreGState()
|
|
}
|
|
}
|
|
|
|
|
|
/// Gets the bounding rectangle for the MTDisplay
|
|
func displayBounds() -> CGRect {
|
|
return CGRectMake(self.position.x, self.position.y - self.descent, self.width, self.ascent + self.descent)
|
|
}
|
|
|
|
/// For debugging. Shows the object in quick look in Xcode.
|
|
#if os(iOS)
|
|
func debugQuickLookObject() -> Any {
|
|
let size = CGSizeMake(self.width, self.ascent + self.descent);
|
|
UIGraphicsBeginImageContext(size);
|
|
|
|
// get a reference to that context we created
|
|
let context = UIGraphicsGetCurrentContext()!
|
|
// translate/flip the graphics context (for transforming from CG* coords to UI* coords
|
|
context.translateBy(x: 0, y: size.height);
|
|
context.scaleBy(x: 1.0, y: -1.0);
|
|
// move the position to (0,0)
|
|
context.translateBy(x: -self.position.x, y: -self.position.y);
|
|
|
|
// Move the line up by self.descent
|
|
context.translateBy(x: 0, y: self.descent);
|
|
// Draw self on context
|
|
self.draw(context)
|
|
|
|
// generate a new UIImage from the graphics context we drew onto
|
|
let img = UIGraphicsGetImageFromCurrentImageContext()
|
|
return img as Any
|
|
}
|
|
#endif
|
|
|
|
/// The distance from the axis to the top of the display
|
|
var ascent:CGFloat = 0
|
|
/// The distance from the axis to the bottom of the display
|
|
var descent:CGFloat = 0
|
|
/// The width of the display
|
|
var width:CGFloat = 0
|
|
/// Position of the display with respect to the parent view or display.
|
|
var position=CGPoint.zero
|
|
/// The range of characters supported by this item
|
|
var range:NSRange=NSMakeRange(0, 0)
|
|
/// Whether the display has a subscript/superscript following it.
|
|
var hasScript:Bool = false
|
|
/// The text color for this display
|
|
var textColor: MTColor?
|
|
/// The local color, if the color was mutated local with the color command
|
|
var localTextColor: MTColor?
|
|
/// The background color for this display
|
|
var localBackgroundColor: MTColor?
|
|
|
|
}
|
|
|
|
class MTDisplayDS : MTDisplay, DownShift {
|
|
|
|
var shiftDown: CGFloat = 0
|
|
|
|
}
|
|
|
|
// MARK: - MTCTLineDisplay
|
|
|
|
/// A rendering of a single CTLine as an MTDisplay
|
|
class MTCTLineDisplay : MTDisplay {
|
|
|
|
/// The CTLine being displayed
|
|
var line:CTLine!
|
|
/// The attributed string used to generate the CTLineRef. Note setting this does not reset the dimensions of
|
|
/// the display. So set only when
|
|
var attributedString:NSAttributedString?
|
|
|
|
/// An array of MTMathAtoms that this CTLine displays. Used for indexing back into the MTMathList
|
|
var atoms = [MTMathAtom]()
|
|
|
|
init(withString attrString:NSAttributedString?, position:CGPoint, range:NSRange, font:MTFont?, atoms:[MTMathAtom]) {
|
|
super.init()
|
|
self.position = position
|
|
self.attributedString = attrString
|
|
self.range = range
|
|
self.atoms = atoms
|
|
// We can't use typographic bounds here as the ascent and descent returned are for the font and not for the line.
|
|
self.width = CTLineGetTypographicBounds(line, nil, nil, nil);
|
|
if isIos6Supported() {
|
|
let bounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
|
|
self.ascent = max(0, CGRectGetMaxY(bounds) - 0);
|
|
self.descent = max(0, 0 - CGRectGetMinY(bounds));
|
|
// TODO: Should we use this width vs the typographic width? They are slightly different. Don't know why.
|
|
// _width = CGRectGetMaxX(bounds);
|
|
} else {
|
|
// Our own implementation of the ios6 function to get glyph path bounds.
|
|
self.computeDimensions(font)
|
|
}
|
|
}
|
|
|
|
func set(attrString: NSAttributedString?) {
|
|
attributedString = attrString
|
|
line = CTLineCreateWithAttributedString(attributedString!)
|
|
}
|
|
|
|
func set(textColor:MTColor) {
|
|
self.textColor = textColor
|
|
let attrStr = NSMutableAttributedString(attributedString: self.attributedString!)
|
|
let foregroundColor = NSAttributedString.Key(kCTForegroundColorAttributeName as String)
|
|
attrStr.addAttribute(foregroundColor, value:self.textColor!.cgColor, range:NSMakeRange(0, attrStr.length))
|
|
self.attributedString = attrStr
|
|
}
|
|
|
|
func computeDimensions(_ font:MTFont?) {
|
|
let runs = CTLineGetGlyphRuns(line) as NSArray
|
|
for obj in runs {
|
|
let run = obj as! CTRun?
|
|
let numGlyphs = CTRunGetGlyphCount(run!)
|
|
var glyphs = [CGGlyph]()
|
|
glyphs.reserveCapacity(numGlyphs)
|
|
CTRunGetGlyphs(run!, CFRangeMake(0, numGlyphs), &glyphs);
|
|
let bounds = CTFontGetBoundingRectsForGlyphs(font!.ctFont, .horizontal, glyphs, nil, numGlyphs);
|
|
let ascent = max(0, CGRectGetMaxY(bounds) - 0);
|
|
// Descent is how much the line goes below the origin. However if the line is all above the origin, then descent can't be negative.
|
|
let descent = max(0, 0 - CGRectGetMinY(bounds));
|
|
if (ascent > self.ascent) {
|
|
self.ascent = ascent;
|
|
}
|
|
if (descent > self.descent) {
|
|
self.descent = descent;
|
|
}
|
|
}
|
|
}
|
|
|
|
override func draw(_ context: CGContext) {
|
|
context.saveGState()
|
|
|
|
context.textPosition = self.position
|
|
CTLineDraw(line, context)
|
|
|
|
context.restoreGState()
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - MTMathListDisplay
|
|
|
|
/// An MTLine is a rendered form of MTMathList in one line.
|
|
/// It can render itself using the draw method.
|
|
class MTMathListDisplay : MTDisplay {
|
|
|
|
/**
|
|
@typedef MTLinePosition
|
|
@brief The type of position for a line, i.e. subscript/superscript or regular.
|
|
*/
|
|
enum LinePosition : Int {
|
|
/// Regular
|
|
case regular
|
|
/// Positioned at a subscript
|
|
case ssubscript
|
|
/// Positioned at a superscript
|
|
case superscript
|
|
}
|
|
|
|
/// Where the line is positioned
|
|
var type:LinePosition = .regular
|
|
/// An array of MTDisplays which are positioned relative to the position of the
|
|
/// the current display.
|
|
var subDisplays = [MTDisplay]()
|
|
/// If a subscript or superscript this denotes the location in the parent MTList. For a
|
|
/// regular list this is NSNotFound
|
|
var index: Int = 0
|
|
|
|
init(withDisplays displays:[MTDisplay], range:NSRange) {
|
|
super.init()
|
|
self.subDisplays = displays
|
|
self.position = CGPoint.zero
|
|
self.type = .regular //kMTLinePositionRegular;
|
|
self.index = NSNotFound
|
|
self.range = range
|
|
self.recomputeDimensions()
|
|
}
|
|
|
|
func setType(_ type:LinePosition) {
|
|
self.type = type
|
|
}
|
|
|
|
func setIndex(_ index:Int) {
|
|
self.index = index
|
|
}
|
|
|
|
func setTextColor(_ textColor:MTColor) {
|
|
// Set the color on all subdisplays
|
|
self.textColor = textColor
|
|
for displayAtom in self.subDisplays {
|
|
displayAtom.textColor = textColor
|
|
}
|
|
}
|
|
|
|
func draw(context: CGContext) {
|
|
context.saveGState()
|
|
|
|
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
|
|
context.translateBy(x: self.position.x, y: self.position.y)
|
|
context.textPosition = CGPoint.zero
|
|
|
|
// draw each atom separately
|
|
for displayAtom in self.subDisplays {
|
|
displayAtom.draw(context)
|
|
}
|
|
|
|
context.restoreGState()
|
|
}
|
|
|
|
func recomputeDimensions() {
|
|
var max_ascent:CGFloat = 0
|
|
var max_descent:CGFloat = 0
|
|
var max_width:CGFloat = 0
|
|
for atom in self.subDisplays {
|
|
let ascent = max(0, atom.position.y + atom.ascent);
|
|
if (ascent > max_ascent) {
|
|
max_ascent = ascent;
|
|
}
|
|
|
|
let descent = max(0, 0 - (atom.position.y - atom.descent));
|
|
if (descent > max_descent) {
|
|
max_descent = descent;
|
|
}
|
|
let width = atom.width + atom.position.x;
|
|
if (width > max_width) {
|
|
max_width = width;
|
|
}
|
|
}
|
|
self.ascent = max_ascent;
|
|
self.descent = max_descent;
|
|
self.width = max_width;
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - MTFractionDisplay
|
|
|
|
/// Rendering of an MTFraction as an MTDisplay
|
|
class MTFractionDisplay : MTDisplay {
|
|
|
|
/** A display representing the numerator of the fraction. Its position is relative
|
|
to the parent and is not treated as a sub-display.
|
|
*/
|
|
var numerator:MTMathListDisplay?
|
|
/** A display representing the denominator of the fraction. Its position is relative
|
|
to the parent is not treated as a sub-display.
|
|
*/
|
|
var denominator:MTMathListDisplay?
|
|
|
|
var numeratorUp:CGFloat=0 {
|
|
didSet { self.updateNumeratorPosition() }
|
|
}
|
|
var denominatorDown:CGFloat=0 {
|
|
didSet { self.updateDenominatorPosition() }
|
|
}
|
|
var linePosition:CGFloat=0
|
|
var lineThickness:CGFloat=0
|
|
|
|
init(withNumerator numerator:MTMathListDisplay?, denominator:MTMathListDisplay?, position:CGPoint, range:NSRange) {
|
|
super.init()
|
|
self.numerator = numerator;
|
|
self.denominator = denominator;
|
|
self.position = position;
|
|
self.range = range;
|
|
assert(self.range.length == 1, "Fraction range length not 1 - range (\(range.location), \(range.length)")
|
|
}
|
|
|
|
override var ascent:CGFloat {
|
|
set { super.ascent = newValue }
|
|
get { numerator!.ascent + self.numeratorUp }
|
|
}
|
|
|
|
override var descent:CGFloat {
|
|
set { super.descent = newValue }
|
|
get { denominator!.descent + self.denominatorDown }
|
|
}
|
|
|
|
override var width:CGFloat {
|
|
set { super.width = newValue }
|
|
get { max(numerator!.width, denominator!.width) }
|
|
}
|
|
|
|
func updateDenominatorPosition() {
|
|
denominator?.position = CGPointMake(self.position.x + (self.width - denominator!.width)/2, self.position.y - self.denominatorDown)
|
|
}
|
|
|
|
func updateNumeratorPosition() {
|
|
numerator?.position = CGPointMake(self.position.x + (self.width - numerator!.width)/2, self.position.y + self.numeratorUp)
|
|
}
|
|
|
|
func setPosition(_ position: CGPoint) {
|
|
super.position = position
|
|
self.updateDenominatorPosition()
|
|
self.updateNumeratorPosition()
|
|
}
|
|
|
|
func setTextColor(_ textColor:MTColor) {
|
|
super.textColor = textColor
|
|
numerator?.textColor = textColor
|
|
denominator?.textColor = textColor
|
|
}
|
|
|
|
override func draw(_ context:CGContext) {
|
|
numerator?.draw(context)
|
|
denominator?.draw(context)
|
|
|
|
context.saveGState()
|
|
|
|
self.textColor?.setStroke()
|
|
|
|
// draw the horizontal line
|
|
let path = MTBezierPath()
|
|
path.move(to: CGPointMake(self.position.x, self.position.y + self.linePosition))
|
|
path.addLine(to: CGPointMake(self.position.x + self.width, self.position.y + self.linePosition))
|
|
path.lineWidth = self.lineThickness
|
|
path.stroke()
|
|
|
|
context.restoreGState()
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - MTRadicalDisplay
|
|
|
|
/// Rendering of an MTRadical as an MTDisplay
|
|
class MTRadicalDisplay : MTDisplay {
|
|
|
|
/** A display representing the radicand of the radical. Its position is relative
|
|
to the parent is not treated as a sub-display.
|
|
*/
|
|
var radicand:MTMathListDisplay?
|
|
/** A display representing the degree of the radical. Its position is relative
|
|
to the parent is not treated as a sub-display.
|
|
*/
|
|
var degree:MTMathListDisplay?
|
|
|
|
private var _radicalGlyph:MTDisplay?
|
|
private var _radicalShift:CGFloat=0
|
|
|
|
var topKern:CGFloat=0
|
|
var lineThickness:CGFloat=0
|
|
|
|
init(withRadicand radicand:MTMathListDisplay?, glyph:MTDisplay, position:CGPoint, range:NSRange) {
|
|
super.init()
|
|
self.radicand = radicand
|
|
_radicalGlyph = glyph
|
|
_radicalShift = 0
|
|
|
|
self.position = position
|
|
self.range = range
|
|
}
|
|
|
|
func setDegree(_ degree:MTMathListDisplay?, fontMetrics:MTFontMathTable?) {
|
|
// sets up the degree of the radical
|
|
var kernBefore = fontMetrics!.radicalKernBeforeDegree;
|
|
let kernAfter = fontMetrics!.radicalKernAfterDegree;
|
|
let raise = fontMetrics!.radicalDegreeBottomRaisePercent * (self.ascent - self.descent);
|
|
|
|
// The layout is:
|
|
// kernBefore, raise, degree, kernAfter, radical
|
|
self.degree = degree;
|
|
|
|
// the radical is now shifted by kernBefore + degree.width + kernAfter
|
|
_radicalShift = kernBefore + degree!.width + kernAfter;
|
|
if _radicalShift < 0 {
|
|
// we can't have the radical shift backwards, so instead we increase the kernBefore such
|
|
// that _radicalShift will be 0.
|
|
kernBefore -= _radicalShift;
|
|
_radicalShift = 0;
|
|
}
|
|
|
|
// Note: position of degree is relative to parent.
|
|
self.degree!.position = CGPointMake(self.position.x + kernBefore, self.position.y + raise);
|
|
// Update the width by the _radicalShift
|
|
self.width = _radicalShift + _radicalGlyph!.width + self.radicand!.width;
|
|
// update the position of the radicand
|
|
self.updateRadicandPosition()
|
|
}
|
|
|
|
func setPosition(_ position:CGPoint) {
|
|
super.position = position
|
|
self.updateRadicandPosition()
|
|
}
|
|
|
|
func updateRadicandPosition() {
|
|
// The position of the radicand includes the position of the MTRadicalDisplay
|
|
// This is to make the positioning of the radical consistent with fractions and
|
|
// have the cursor position finding algorithm work correctly.
|
|
// move the radicand by the width of the radical sign
|
|
self.radicand!.position = CGPointMake(self.position.x + _radicalShift + _radicalGlyph!.width, self.position.y);
|
|
}
|
|
|
|
func setTextColor(textColor:MTColor) {
|
|
super.textColor = textColor
|
|
self.radicand!.textColor = textColor
|
|
self.degree!.textColor = textColor
|
|
}
|
|
|
|
func draw(context: CGContext) {
|
|
// draw the radicand & degree at its position
|
|
self.radicand?.draw(context)
|
|
self.degree?.draw(context)
|
|
|
|
context.saveGState();
|
|
self.textColor?.setStroke()
|
|
self.textColor?.setFill()
|
|
|
|
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
|
|
context.translateBy(x: self.position.x + _radicalShift, y: self.position.y);
|
|
context.textPosition = CGPoint.zero
|
|
|
|
// Draw the glyph.
|
|
_radicalGlyph?.draw(context)
|
|
|
|
// Draw the VBOX
|
|
// for the kern of, we don't need to draw anything.
|
|
let heightFromTop = topKern;
|
|
|
|
// draw the horizontal line with the given thickness
|
|
let path = MTBezierPath()
|
|
let lineStart = CGPointMake(_radicalGlyph!.width, self.ascent - heightFromTop - self.lineThickness / 2); // subtract half the line thickness to center the line
|
|
let lineEnd = CGPointMake(lineStart.x + self.radicand!.width, lineStart.y);
|
|
path.move(to: lineStart)
|
|
path.addLine(to: lineEnd)
|
|
path.lineWidth = lineThickness
|
|
path.lineCapStyle = .round
|
|
path.stroke()
|
|
|
|
context.restoreGState();
|
|
}
|
|
}
|
|
|
|
// MARK: - MTGlyphDisplay
|
|
|
|
/// Rendering a glyph as a display
|
|
class MTGlyphDisplay : MTDisplayDS {
|
|
|
|
var glyph:CGGlyph!
|
|
var font:MTFont?
|
|
|
|
init(withGlpyh glyph:CGGlyph, range:NSRange, font:MTFont?) {
|
|
super.init()
|
|
self.font = font
|
|
self.glyph = glyph
|
|
|
|
self.position = CGPoint.zero
|
|
self.range = range
|
|
}
|
|
|
|
func draw(context: CGContext) {
|
|
context.saveGState();
|
|
|
|
self.textColor?.setFill()
|
|
|
|
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
|
|
|
|
context.translateBy(x: self.position.x, y: self.position.y - self.shiftDown);
|
|
context.textPosition = CGPoint.zero
|
|
|
|
var pos = CGPoint.zero
|
|
CTFontDrawGlyphs(font!.ctFont, &glyph, &pos, 1, context);
|
|
|
|
context.restoreGState();
|
|
}
|
|
|
|
override var ascent:CGFloat {
|
|
set {
|
|
super.ascent = newValue
|
|
}
|
|
get {
|
|
return super.ascent - self.shiftDown;
|
|
}
|
|
}
|
|
|
|
override var descent:CGFloat {
|
|
set {
|
|
super.descent = newValue
|
|
}
|
|
get {
|
|
return super.descent + self.shiftDown;
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - MTGlyphConstructionDisplay
|
|
|
|
class MTGlyphConstructionDisplay:MTDisplayDS {
|
|
var glyphs = [CGGlyph]()
|
|
var positions = [CGPoint]()
|
|
var font:MTFont?
|
|
var numGlyphs:Int=0
|
|
|
|
init(withGlyphs glyphs:[NSNumber?], offsets:[NSNumber?], font:MTFont?) {
|
|
super.init()
|
|
assert(glyphs.count == offsets.count, "Glyphs and offsets need to match")
|
|
self.numGlyphs = glyphs.count;
|
|
self.glyphs = [CGGlyph](repeating: CGGlyph(), count: self.numGlyphs) //malloc(sizeof(CGGlyph) * _numGlyphs);
|
|
self.positions = [CGPoint](repeating: CGPoint.zero, count: self.numGlyphs) //malloc(sizeof(CGPoint) * _numGlyphs);
|
|
for i in 0 ..< self.numGlyphs {
|
|
self.glyphs[i] = glyphs[i]!.uint16Value
|
|
self.positions[i] = CGPointMake(0, CGFloat(offsets[i]!.floatValue))
|
|
}
|
|
self.font = font
|
|
self.position = CGPoint.zero
|
|
}
|
|
|
|
override func draw(_ context: CGContext) {
|
|
context.saveGState()
|
|
|
|
self.textColor?.setFill()
|
|
|
|
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
|
|
context.translateBy(x: self.position.x, y: self.position.y - self.shiftDown)
|
|
context.textPosition = CGPoint.zero
|
|
|
|
// Draw the glyphs.
|
|
CTFontDrawGlyphs(font!.ctFont, glyphs, positions, numGlyphs, context)
|
|
|
|
context.restoreGState()
|
|
}
|
|
|
|
override var ascent:CGFloat {
|
|
set {
|
|
super.ascent = newValue
|
|
}
|
|
get {
|
|
return super.ascent - self.shiftDown;
|
|
}
|
|
}
|
|
|
|
override var descent:CGFloat {
|
|
set {
|
|
super.descent = newValue
|
|
}
|
|
get {
|
|
return super.descent + self.shiftDown;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - MTLargeOpLimitsDisplay
|
|
|
|
/// Rendering a large operator with limits as an MTDisplay
|
|
class MTLargeOpLimitsDisplay : MTDisplay {
|
|
|
|
/** A display representing the upper limit of the large operator. Its position is relative
|
|
to the parent is not treated as a sub-display.
|
|
*/
|
|
var upperLimit:MTMathListDisplay?
|
|
/** A display representing the lower limit of the large operator. Its position is relative
|
|
to the parent is not treated as a sub-display.
|
|
*/
|
|
var lowerLimit:MTMathListDisplay?
|
|
|
|
var limitShift:CGFloat=0
|
|
var upperLimitGap:CGFloat=0 {
|
|
didSet {
|
|
self.updateUpperLimitPosition()
|
|
}
|
|
}
|
|
var lowerLimitGap:CGFloat=0 {
|
|
didSet {
|
|
self.updateUpperLimitPosition()
|
|
}
|
|
}
|
|
var extraPadding:CGFloat=0
|
|
|
|
var nucleus:MTDisplay?
|
|
|
|
init(withNucleus nucleus:MTDisplay?, upperLimit:MTMathListDisplay?, lowerLimit:MTMathListDisplay?, limitShift:CGFloat, extraPadding:CGFloat) {
|
|
super.init()
|
|
self.upperLimit = upperLimit;
|
|
self.lowerLimit = lowerLimit;
|
|
self.nucleus = nucleus;
|
|
|
|
var maxWidth = max(nucleus!.width, upperLimit!.width);
|
|
maxWidth = max(maxWidth, lowerLimit!.width);
|
|
|
|
self.limitShift = limitShift;
|
|
self.upperLimitGap = 0;
|
|
self.lowerLimitGap = 0;
|
|
self.extraPadding = extraPadding; // corresponds to \xi_13 in TeX
|
|
self.width = maxWidth;
|
|
}
|
|
|
|
override var ascent:CGFloat {
|
|
set {
|
|
super.ascent = newValue
|
|
}
|
|
get {
|
|
if self.upperLimit != nil {
|
|
return nucleus!.ascent + extraPadding + self.upperLimit!.ascent + upperLimitGap + self.upperLimit!.descent
|
|
} else {
|
|
return nucleus!.ascent
|
|
}
|
|
}
|
|
}
|
|
|
|
override var descent:CGFloat {
|
|
set {
|
|
super.descent = newValue
|
|
}
|
|
get {
|
|
if self.lowerLimit != nil {
|
|
return nucleus!.descent + extraPadding + lowerLimitGap + self.lowerLimit!.descent + self.lowerLimit!.ascent;
|
|
} else {
|
|
return nucleus!.descent;
|
|
}
|
|
}
|
|
}
|
|
|
|
func setPosition(_ position:CGPoint) {
|
|
super.position = position;
|
|
self.updateLowerLimitPosition()
|
|
self.updateUpperLimitPosition()
|
|
self.updateNucleusPosition()
|
|
}
|
|
|
|
func updateLowerLimitPosition() {
|
|
if self.lowerLimit != nil {
|
|
// The position of the lower limit includes the position of the MTLargeOpLimitsDisplay
|
|
// This is to make the positioning of the radical consistent with fractions and radicals
|
|
// Move the starting point to below the nucleus leaving a gap of _lowerLimitGap and subtract
|
|
// the ascent to to get the baseline. Also center and shift it to the left by _limitShift.
|
|
self.lowerLimit!.position = CGPointMake(self.position.x - limitShift + (self.width - lowerLimit!.width)/2,
|
|
self.position.y - nucleus!.descent - lowerLimitGap - self.lowerLimit!.ascent);
|
|
}
|
|
}
|
|
|
|
func updateUpperLimitPosition() {
|
|
if self.upperLimit != nil {
|
|
// The position of the upper limit includes the position of the MTLargeOpLimitsDisplay
|
|
// This is to make the positioning of the radical consistent with fractions and radicals
|
|
// Move the starting point to above the nucleus leaving a gap of _upperLimitGap and add
|
|
// the descent to to get the baseline. Also center and shift it to the right by _limitShift.
|
|
self.upperLimit!.position = CGPointMake(self.position.x + limitShift + (self.width - self.upperLimit!.width)/2,
|
|
self.position.y + nucleus!.ascent + upperLimitGap + self.upperLimit!.descent);
|
|
}
|
|
}
|
|
|
|
func updateNucleusPosition() {
|
|
// Center the nucleus
|
|
nucleus?.position = CGPointMake(self.position.x + (self.width - nucleus!.width)/2, self.position.y);
|
|
}
|
|
|
|
func setTextColor(_ textColor:MTColor) {
|
|
super.textColor = textColor
|
|
self.upperLimit?.textColor = textColor
|
|
self.lowerLimit?.textColor = textColor
|
|
nucleus?.textColor = textColor
|
|
}
|
|
|
|
override func draw(_ context:CGContext) {
|
|
// Draw the elements.
|
|
self.upperLimit?.draw(context)
|
|
self.lowerLimit?.draw(context)
|
|
nucleus?.draw(context)
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - MTLineDisplay
|
|
|
|
/// Rendering of an list with an overline or underline
|
|
class MTLineDisplay : MTDisplay {
|
|
|
|
/** A display representing the inner list that is underlined. Its position is relative
|
|
to the parent is not treated as a sub-display.
|
|
*/
|
|
var inner:MTMathListDisplay?
|
|
var lineShiftUp:CGFloat=0
|
|
var lineThickness:CGFloat=0
|
|
|
|
init(withInner inner:MTMathListDisplay?, position:CGPoint, range:NSRange) {
|
|
super.init()
|
|
self.inner = inner;
|
|
|
|
self.position = position;
|
|
self.range = range;
|
|
}
|
|
|
|
func setTextColor(_ textColor:MTColor) {
|
|
super.textColor = textColor
|
|
inner?.textColor = textColor
|
|
}
|
|
|
|
override func draw(_ context:CGContext) {
|
|
self.inner?.draw(context)
|
|
|
|
context.saveGState();
|
|
|
|
self.textColor?.setStroke()
|
|
|
|
// draw the horizontal line
|
|
let path = MTBezierPath()
|
|
let lineStart = CGPointMake(self.position.x, self.position.y + self.lineShiftUp);
|
|
let lineEnd = CGPointMake(lineStart.x + self.inner!.width, lineStart.y);
|
|
path.move(to:lineStart)
|
|
path.addLine(to: lineEnd)
|
|
path.lineWidth = self.lineThickness;
|
|
path.stroke()
|
|
|
|
context.restoreGState();
|
|
}
|
|
|
|
func setPosition(_ position:CGPoint) {
|
|
super.position = position;
|
|
self.updateInnerPosition()
|
|
}
|
|
|
|
func updateInnerPosition() {
|
|
self.inner?.position = CGPointMake(self.position.x, self.position.y);
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - MTAccentDisplay
|
|
|
|
/// Rendering an accent as a display
|
|
class MTAccentDisplay : MTDisplay {
|
|
|
|
/** A display representing the inner list that is accented. Its position is relative
|
|
to the parent is not treated as a sub-display.
|
|
*/
|
|
var accentee:MTMathListDisplay?
|
|
|
|
/** A display representing the accent. Its position is relative to the current display.
|
|
*/
|
|
var accent:MTGlyphDisplay?
|
|
|
|
init(withAccent glyph:MTGlyphDisplay?, accentee:MTMathListDisplay?, range:NSRange) {
|
|
super.init()
|
|
self.accent = glyph
|
|
self.accentee = accentee
|
|
self.accentee?.position = CGPoint.zero
|
|
self.range = range
|
|
}
|
|
|
|
func setTextColor(_ textColor:MTColor) {
|
|
super.textColor = textColor
|
|
accentee?.textColor = textColor
|
|
accent?.textColor = textColor
|
|
}
|
|
|
|
func setPosition(_ position:CGPoint) {
|
|
super.position = position
|
|
self.updateAccenteePosition()
|
|
}
|
|
|
|
func updateAccenteePosition() {
|
|
self.accentee?.position = CGPointMake(self.position.x, self.position.y);
|
|
}
|
|
|
|
override func draw(_ context:CGContext) {
|
|
self.accentee?.draw(context)
|
|
|
|
context.saveGState();
|
|
context.translateBy(x: self.position.x, y: self.position.y);
|
|
context.textPosition = CGPoint.zero
|
|
|
|
self.accent?.draw(context: context)
|
|
|
|
context.restoreGState();
|
|
}
|
|
|
|
}
|