Add display model

This commit is contained in:
Guille Gonzalez
2026-01-03 07:45:34 +01:00
parent e9657c186e
commit 8e4db3cf0e
11 changed files with 365 additions and 0 deletions

View File

@@ -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)
}
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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 }
}
}
}

View File

@@ -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 }
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
)
}
}
}

View File

@@ -0,0 +1,8 @@
import CoreGraphics
import Foundation
extension Math {
class DisplayShiftedNode: DisplayNode {
var shiftDown: CGFloat = 0
}
}

View File

@@ -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
}
}
}