diff --git a/Sources/SwiftUIMath/Internal/Display/DisplayAccent.swift b/Sources/SwiftUIMath/Internal/Display/DisplayAccent.swift new file mode 100644 index 0000000..36a3ed3 --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Display/DisplayAccent.swift @@ -0,0 +1,24 @@ +import CoreGraphics +import Foundation + +extension Math { + final class DisplayAccent: DisplayNode { + var accentee: DisplayList? + var accent: DisplayGlyph? + + init(accent: DisplayGlyph?, accentee: DisplayList?, range: NSRange) { + self.accent = accent + self.accentee = accentee + super.init() + self.range = range + } + + override var position: CGPoint { + didSet { updateAccenteePosition() } + } + + private func updateAccenteePosition() { + accentee?.position = CGPoint(x: position.x, y: position.y) + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Display/DisplayFraction.swift b/Sources/SwiftUIMath/Internal/Display/DisplayFraction.swift new file mode 100644 index 0000000..f0c4e76 --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Display/DisplayFraction.swift @@ -0,0 +1,60 @@ +import CoreGraphics +import Foundation + +extension Math { + final class DisplayFraction: DisplayNode { + var numerator: DisplayList? + var denominator: DisplayList? + + var numeratorUp: CGFloat = 0 { didSet { updateNumeratorPosition() } } + var denominatorDown: CGFloat = 0 { didSet { updateDenominatorPosition() } } + var linePosition: CGFloat = 0 + var lineThickness: CGFloat = 0 + + init(numerator: DisplayList?, denominator: DisplayList?, position: CGPoint, range: NSRange) { + self.numerator = numerator + self.denominator = denominator + super.init() + self.position = position + self.range = range + } + + override var ascent: CGFloat { + get { (numerator?.ascent ?? 0) + numeratorUp } + set { super.ascent = newValue } + } + + override var descent: CGFloat { + get { (denominator?.descent ?? 0) + denominatorDown } + set { super.descent = newValue } + } + + override var width: CGFloat { + get { max(numerator?.width ?? 0, denominator?.width ?? 0) } + set { super.width = newValue } + } + + override var position: CGPoint { + didSet { + updateDenominatorPosition() + updateNumeratorPosition() + } + } + + private func updateDenominatorPosition() { + guard let denominator else { return } + denominator.position = CGPoint( + x: position.x + (width - denominator.width) / 2, + y: position.y - denominatorDown + ) + } + + private func updateNumeratorPosition() { + guard let numerator else { return } + numerator.position = CGPoint( + x: position.x + (width - numerator.width) / 2, + y: position.y + numeratorUp + ) + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Display/DisplayGlyph.swift b/Sources/SwiftUIMath/Internal/Display/DisplayGlyph.swift new file mode 100644 index 0000000..caa2a96 --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Display/DisplayGlyph.swift @@ -0,0 +1,27 @@ +import CoreGraphics +import Foundation + +extension Math { + final class DisplayGlyph: DisplayShiftedNode { + var glyph: UInt16 + var font: Math.Font + + init(glyph: UInt16, font: Math.Font, range: NSRange) { + self.glyph = glyph + self.font = font + super.init() + self.position = .zero + self.range = range + } + + override var ascent: CGFloat { + get { super.ascent - shiftDown } + set { super.ascent = newValue } + } + + override var descent: CGFloat { + get { super.descent + shiftDown } + set { super.descent = newValue } + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Display/DisplayGlyphRun.swift b/Sources/SwiftUIMath/Internal/Display/DisplayGlyphRun.swift new file mode 100644 index 0000000..0ede4b6 --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Display/DisplayGlyphRun.swift @@ -0,0 +1,28 @@ +import CoreGraphics +import Foundation + +extension Math { + final class DisplayGlyphRun: DisplayShiftedNode { + var glyphs: [UInt16] + var offsets: [CGFloat] + var font: Math.Font + + init(glyphs: [UInt16], offsets: [CGFloat], font: Math.Font) { + self.glyphs = glyphs + self.offsets = offsets + self.font = font + super.init() + self.position = .zero + } + + override var ascent: CGFloat { + get { super.ascent - shiftDown } + set { super.ascent = newValue } + } + + override var descent: CGFloat { + get { super.descent + shiftDown } + set { super.descent = newValue } + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Display/DisplayLargeOperator.swift b/Sources/SwiftUIMath/Internal/Display/DisplayLargeOperator.swift new file mode 100644 index 0000000..704aa4c --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Display/DisplayLargeOperator.swift @@ -0,0 +1,79 @@ +import CoreGraphics +import Foundation + +extension Math { + final class DisplayLargeOperator: DisplayNode { + var upperLimit: DisplayList? + var lowerLimit: DisplayList? + var nucleus: DisplayNode? + + var limitShift: CGFloat = 0 + var upperLimitGap: CGFloat = 0 { didSet { updateUpperLimitPosition() } } + var lowerLimitGap: CGFloat = 0 { didSet { updateLowerLimitPosition() } } + var extraPadding: CGFloat = 0 + + init(nucleus: DisplayNode?, upperLimit: DisplayList?, lowerLimit: DisplayList?, limitShift: CGFloat, extraPadding: CGFloat) { + self.upperLimit = upperLimit + self.lowerLimit = lowerLimit + self.nucleus = nucleus + self.limitShift = limitShift + self.extraPadding = extraPadding + super.init() + + var maxWidth = max(nucleus?.width ?? 0, upperLimit?.width ?? 0) + maxWidth = max(maxWidth, lowerLimit?.width ?? 0) + width = maxWidth + } + + override var ascent: CGFloat { + get { + guard let nucleus else { return 0 } + if let upperLimit { + return nucleus.ascent + extraPadding + upperLimit.ascent + upperLimitGap + upperLimit.descent + } + return nucleus.ascent + } + set { super.ascent = newValue } + } + + override var descent: CGFloat { + get { + guard let nucleus else { return 0 } + if let lowerLimit { + return nucleus.descent + extraPadding + lowerLimitGap + lowerLimit.descent + lowerLimit.ascent + } + return nucleus.descent + } + set { super.descent = newValue } + } + + override var position: CGPoint { + didSet { + updateLowerLimitPosition() + updateUpperLimitPosition() + updateNucleusPosition() + } + } + + private func updateLowerLimitPosition() { + guard let lowerLimit, let nucleus else { return } + lowerLimit.position = CGPoint( + x: position.x - limitShift + (width - lowerLimit.width) / 2, + y: position.y - nucleus.descent - lowerLimitGap - lowerLimit.ascent + ) + } + + private func updateUpperLimitPosition() { + guard let upperLimit, let nucleus else { return } + upperLimit.position = CGPoint( + x: position.x + limitShift + (width - upperLimit.width) / 2, + y: position.y + nucleus.ascent + upperLimitGap + upperLimit.descent + ) + } + + private func updateNucleusPosition() { + guard let nucleus else { return } + nucleus.position = CGPoint(x: position.x + (width - nucleus.width) / 2, y: position.y) + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Display/DisplayLine.swift b/Sources/SwiftUIMath/Internal/Display/DisplayLine.swift new file mode 100644 index 0000000..007283b --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Display/DisplayLine.swift @@ -0,0 +1,25 @@ +import CoreGraphics +import Foundation + +extension Math { + final class DisplayLine: DisplayNode { + var inner: DisplayList? + var lineShiftUp: CGFloat = 0 + var lineThickness: CGFloat = 0 + + init(inner: DisplayList?, position: CGPoint, range: NSRange) { + self.inner = inner + super.init() + self.position = position + self.range = range + } + + override var position: CGPoint { + didSet { updateInnerPosition() } + } + + private func updateInnerPosition() { + inner?.position = CGPoint(x: position.x, y: position.y) + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Display/DisplayList.swift b/Sources/SwiftUIMath/Internal/Display/DisplayList.swift new file mode 100644 index 0000000..32f725f --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Display/DisplayList.swift @@ -0,0 +1,44 @@ +import CoreGraphics +import Foundation + +extension Math { + final class DisplayList: DisplayNode { + enum LinePosition: Int { + case regular + case `subscript` + case superscript + } + + var linePosition: LinePosition = .regular + var children: [DisplayNode] = [] + var index: Int = NSNotFound + + init(children: [DisplayNode], range: NSRange) { + super.init() + self.children = children + self.position = .zero + self.index = NSNotFound + self.range = range + recomputeDimensions() + } + + func recomputeDimensions() { + var maxAscent: CGFloat = 0 + var maxDescent: CGFloat = 0 + var maxWidth: CGFloat = 0 + for node in children { + let ascent = max(0, node.position.y + node.ascent) + maxAscent = max(maxAscent, ascent) + + let descent = max(0, 0 - (node.position.y - node.descent)) + maxDescent = max(maxDescent, descent) + + let width = node.width + node.position.x + maxWidth = max(maxWidth, width) + } + self.ascent = maxAscent + self.descent = maxDescent + self.width = maxWidth + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Display/DisplayNode.swift b/Sources/SwiftUIMath/Internal/Display/DisplayNode.swift new file mode 100644 index 0000000..bd5bce5 --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Display/DisplayNode.swift @@ -0,0 +1,17 @@ +import CoreGraphics +import Foundation + +extension Math { + class DisplayNode { + var ascent: CGFloat = 0 + var descent: CGFloat = 0 + var width: CGFloat = 0 + var position: CGPoint = .zero + var range: NSRange = NSRange(location: 0, length: 0) + var hasScript: Bool = false + + func bounds() -> CGRect { + CGRect(x: position.x, y: position.y - descent, width: width, height: ascent + descent) + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Display/DisplayRadical.swift b/Sources/SwiftUIMath/Internal/Display/DisplayRadical.swift new file mode 100644 index 0000000..d798897 --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Display/DisplayRadical.swift @@ -0,0 +1,34 @@ +import CoreGraphics +import Foundation + +extension Math { + final class DisplayRadical: DisplayNode { + var radicand: DisplayList? + var degree: DisplayList? + var radicalGlyph: DisplayNode? + + var radicalShift: CGFloat = 0 + var topKern: CGFloat = 0 + var lineThickness: CGFloat = 0 + + init(radicand: DisplayList?, glyph: DisplayNode, position: CGPoint, range: NSRange) { + self.radicand = radicand + self.radicalGlyph = glyph + super.init() + self.position = position + self.range = range + } + + override var position: CGPoint { + didSet { updateRadicandPosition() } + } + + func updateRadicandPosition() { + guard let radicand, let radicalGlyph else { return } + radicand.position = CGPoint( + x: position.x + radicalShift + radicalGlyph.width, + y: position.y + ) + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Display/DisplayShiftedNode.swift b/Sources/SwiftUIMath/Internal/Display/DisplayShiftedNode.swift new file mode 100644 index 0000000..4e52950 --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Display/DisplayShiftedNode.swift @@ -0,0 +1,8 @@ +import CoreGraphics +import Foundation + +extension Math { + class DisplayShiftedNode: DisplayNode { + var shiftDown: CGFloat = 0 + } +} diff --git a/Sources/SwiftUIMath/Internal/Display/DisplayTextRun.swift b/Sources/SwiftUIMath/Internal/Display/DisplayTextRun.swift new file mode 100644 index 0000000..236ba48 --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Display/DisplayTextRun.swift @@ -0,0 +1,19 @@ +import CoreGraphics +import Foundation + +extension Math { + final class DisplayTextRun: DisplayNode { + var text: String + var font: Math.Font + var atoms: [Math.Atom] + + init(text: String, font: Math.Font, position: CGPoint = .zero, range: NSRange, atoms: [Math.Atom]) { + self.text = text + self.font = font + self.atoms = atoms + super.init() + self.position = position + self.range = range + } + } +}