Files
swiftui-math/Sources/SwiftMathRender/MathRender/MTMathUILabel.swift
2023-01-07 07:53:46 -05:00

251 lines
8.3 KiB
Swift

//
// MTMathUILabel.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2023-01-01.
//
import Foundation
import CoreText
/**
Different display styles supported by the `MTMathUILabel`.
The only significant difference between the two modes is how fractions
and limits on large operators are displayed.
*/
public enum MTMathUILabelMode {
/// Display mode. Equivalent to $$ in TeX
case display
/// Text mode. Equivalent to $ in TeX.
case text
}
/**
@typedef MTTextAlignment
@brief Horizontal text alignment for `MTMathUILabel`.
*/
public enum MTTextAlignment : UInt {
/// Align left.
case left
/// Align center.
case center
/// Align right.
case right
}
/** The main view for rendering math.
`MTMathLabel` accepts either a string in LaTeX or an `MTMathList` to display. Use
`MTMathList` directly only if you are building it programmatically (e.g. using an
editor), otherwise using LaTeX is the preferable method.
The math display is centered vertically in the label. The default horizontal alignment is
is left. This can be changed by setting `textAlignment`. The math is default displayed in
*Display* mode. This can be changed using `labelMode`.
When created it uses `[MTFontManager defaultFont]` as its font. This can be changed using
the `font` parameter.
*/
class MTMathUILabel : MTView {
/** The `MTMathList` to render. Setting this will remove any
`latex` that has already been set. If `latex` has been set, this will
return the parsed `MTMathList` if the `latex` parses successfully. Use this
setting if the `MTMathList` has been programmatically constructed, otherwise it
is preferred to use `latex`.
*/
var mathList:MTMathList? {
didSet {
self.error = nil
self.latex = MTMathListBuilder.mathListToString(mathList)
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
}
/** The latex string to be displayed. Setting this will remove any `mathList` that
has been set. If latex has not been set, this will return the latex output for the
`mathList` that is set.
@see error */
var latex = "" {
didSet {
self.error = nil
var error = NSError()
self.mathList = MTMathListBuilder.build(fromString: latex, error: &error)
if error != NSError() {
self.mathList = nil
self.error = error
self.errorLabel?.text = error.localizedDescription
self.errorLabel?.frame = self.bounds
self.errorLabel?.isHidden = !self.displayErrorInline
} else {
self.errorLabel?.isHidden = true
}
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
}
/** This contains any error that occurred when parsing the latex. */
var error:NSError?
/** If true, if there is an error it displays the error message inline. Default true. */
var displayErrorInline = true
/** The MTFont to use for rendering. */
var font = MTFontManager.fontManager.defaultFont {
didSet {
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
}
/** Convenience method to just set the size of the font without changing the fontface. */
var fontSize = MTFontManager.fontManager.kDefaultFontSize {
didSet {
self.font = font?.copy(withSize: fontSize)
}
}
/** This sets the text color of the rendered math formula. The default color is black. */
var textColor:MTColor? = MTColor.black {
didSet {
self.displayList?.textColor = textColor
self.setNeedsDisplay()
}
}
/** The minimum distance from the margin of the view to the rendered math. This value is
`UIEdgeInsetsZero` by default. This is useful if you need some padding between the math and
the border/background color. sizeThatFits: will have its returned size increased by these insets.
*/
var contentInsets = MTEdgeInsetsZero {
didSet {
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
}
/** The Label mode for the label. The default mode is Display */
var labelMode = MTMathUILabelMode.display {
didSet {
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
}
/** Horizontal alignment for the text. The default is align left. */
var textAlignment = MTTextAlignment.left {
didSet {
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
}
/** The internal display of the MTMathUILabel. This is for advanced use only. */
var displayList: MTMathListDisplay? = nil
var currentStyle:MTLineStyle {
switch labelMode {
case .display: return .display
case .text: return .text
}
}
var errorLabel: MTLabel?
override init(frame: CGRect) {
super.init(frame: frame)
self.initCommon()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.initCommon()
}
func initCommon() {
#if os(macOS)
self.layer?.isGeometryFlipped = true
errorLabel?.layer?.isGeometryFlipped = true
#else
self.layer.isGeometryFlipped = true
errorLabel?.layer.isGeometryFlipped = true
#endif
self.backgroundColor = MTColor.clear
errorLabel = MTLabel()
errorLabel?.isHidden = true
errorLabel?.textColor = MTColor.red
self.addSubview(errorLabel!)
}
override func draw(_ dirtyRect: MTRect) {
super.draw(dirtyRect)
if self.mathList == nil { return }
// drawing code
let context = MTGraphicsGetCurrentContext()!
context.saveGState()
displayList!.draw(context)
context.restoreGState()
}
func _layoutSubviews() {
if mathList != nil {
displayList = MTTypesetter.createLineForMathList(mathList, font: font, style: currentStyle)
displayList?.textColor = textColor
var textX = CGFloat(0)
switch self.textAlignment {
case .left:
textX = self.contentInsets.left
case .center:
textX = (bounds.size.width - contentInsets.left - contentInsets.right - displayList!.width) / 2 +
contentInsets.left
case .right:
textX = bounds.size.width - displayList!.width - contentInsets.right
}
let availableHeight = bounds.size.height - contentInsets.bottom - contentInsets.top
// center things vertically
var height = displayList!.ascent + displayList!.descent
if height < fontSize/2 {
height = fontSize/2 // set height to half the font size
}
let textY = (availableHeight - height) / 2 + displayList!.descent + contentInsets.bottom
displayList?.position = CGPointMake(textX, textY)
} else {
displayList = nil
}
errorLabel?.frame = self.bounds
self.setNeedsDisplay()
}
func _sizeThatFits(_ size:CGSize) -> CGSize {
var size = size
var displayList:MTMathListDisplay? = nil
if mathList != nil {
displayList = MTTypesetter.createLineForMathList(mathList, font: font, style: currentStyle)
}
size.width = displayList!.width + contentInsets.left + contentInsets.right
size.height = displayList!.ascent + displayList!.descent + contentInsets.top + contentInsets.bottom
return size
}
override var intrinsicContentSize: CGSize { _sizeThatFits(CGSizeZero) }
#if os(macOS)
override var isFlipped: Bool { false }
func setNeedsDisplay() { self.needsDisplay = true }
func setNeedsLayout() { self.needsLayout = true }
override func layout() {
self._layoutSubviews()
super.layout()
}
#else
override func layoutSubviews() { self._layoutSubviews() }
override func sizeThatFits(_ size: CGSize) -> CGSize { self._sizeThatFits(size) }
#endif
}