Files
swiftui-math/Sources/SwiftUIMath/Internal/Font/FontMetrics.swift
2026-01-10 06:14:44 +01:00

419 lines
9.8 KiB
Swift

@preconcurrency import CoreGraphics
@preconcurrency import CoreText
import Foundation
extension Math {
struct FontMetrics: Sendable {
struct GlyphPart: Sendable {
let glyph: CGGlyph
let fullAdvance: CGFloat
let startConnectorLength: CGFloat
let endConnectorLength: CGFloat
let isExtender: Bool
}
var mathUnit: CGFloat {
font.size / 18
}
private let font: Font
private let unitsPerEm: UInt
private let table: FontTable
init(font: Font, unitsPerEm: UInt, table: FontTable) {
self.font = font
self.unitsPerEm = unitsPerEm
self.table = table
}
func verticalVariants(forGlyph glyph: CGGlyph) -> [CGGlyph] {
guard
let graphicsFont = FontRegistry.shared.graphicsFont(named: font.name),
let glyphName = graphicsFont.name(for: glyph) as String?,
let variantGlyphs = table.vVariants[glyphName],
!variantGlyphs.isEmpty
else {
return [glyph]
}
return variantGlyphs.map {
graphicsFont.getGlyphWithGlyphName(name: $0 as CFString)
}
}
func horizontalVariants(forGlyph glyph: CGGlyph) -> [CGGlyph] {
guard
let graphicsFont = FontRegistry.shared.graphicsFont(named: font.name),
let glyphName = graphicsFont.name(for: glyph) as String?,
let variantGlyphs = table.hVariants[glyphName],
!variantGlyphs.isEmpty
else {
return [glyph]
}
return variantGlyphs.map {
graphicsFont.getGlyphWithGlyphName(name: $0 as CFString)
}
}
func largerGlyph(forGlyph glyph: CGGlyph) -> CGGlyph {
guard
let graphicsFont = FontRegistry.shared.graphicsFont(named: font.name),
let glyphName = graphicsFont.name(for: glyph) as String?,
let variantGlyphs = table.vVariants[glyphName],
!variantGlyphs.isEmpty
else {
return glyph
}
return
variantGlyphs
.first { $0 != glyphName }
.map {
graphicsFont.getGlyphWithGlyphName(name: $0 as CFString)
} ?? glyph
}
func italicCorrection(forGlyph glyph: CGGlyph) -> CGFloat {
guard
let graphicsFont = FontRegistry.shared.graphicsFont(named: font.name),
let glyphName = graphicsFont.name(for: glyph) as String?,
let value = table.italic[glyphName]
else {
return 0
}
return unitsToPoints(value)
}
func topAccentAdjustment(forGlyph glyph: CGGlyph) -> CGFloat {
guard
let graphicsFont = FontRegistry.shared.graphicsFont(named: font.name),
let glyphName = graphicsFont.name(for: glyph) as String?,
let value = table.accents[glyphName]
else {
// If no top accent is defined then it is the center of the advance width
return advance(forGlyph: glyph).width / 2
}
return unitsToPoints(value)
}
func verticalAssembly(forGlyph glyph: CGGlyph) -> [GlyphPart] {
guard
let graphicsFont = FontRegistry.shared.graphicsFont(named: font.name),
let glyphName = graphicsFont.name(for: glyph) as String?,
let assembly = table.vAssembly[glyphName]
else {
return []
}
return assembly.parts.map { part in
GlyphPart(
glyph: graphicsFont.getGlyphWithGlyphName(name: part.glyph as CFString),
fullAdvance: unitsToPoints(part.advance),
startConnectorLength: unitsToPoints(part.startConnector),
endConnectorLength: unitsToPoints(part.endConnector),
isExtender: part.extender
)
}
}
}
}
// MARK: - Fractions
extension Math.FontMetrics {
var fractionNumeratorDisplayStyleShiftUp: CGFloat {
constant(named: "FractionNumeratorDisplayStyleShiftUp")
}
var fractionNumeratorShiftUp: CGFloat {
constant(named: "FractionNumeratorShiftUp")
}
var fractionDenominatorDisplayStyleShiftDown: CGFloat {
constant(named: "FractionDenominatorDisplayStyleShiftDown")
}
var fractionDenominatorShiftDown: CGFloat {
constant(named: "FractionDenominatorShiftDown")
}
var fractionNumeratorDisplayStyleGapMin: CGFloat {
constant(named: "FractionNumDisplayStyleGapMin")
}
var fractionNumeratorGapMin: CGFloat {
constant(named: "FractionNumeratorGapMin")
}
var fractionDenominatorDisplayStyleGapMin: CGFloat {
constant(named: "FractionDenomDisplayStyleGapMin")
}
var fractionDenominatorGapMin: CGFloat {
constant(named: "FractionDenominatorGapMin")
}
var fractionRuleThickness: CGFloat {
constant(named: "FractionRuleThickness")
}
var fractionDelimiterSize: CGFloat {
1.01 * font.size
}
var fractionDelimiterDisplayStyleSize: CGFloat {
2.39 * font.size
}
}
// MARK: - Stacks
extension Math.FontMetrics {
var skewedFractionHorizonalGap: CGFloat {
constant(named: "SkewedFractionHorizontalGap")
}
var skewedFractionVerticalGap: CGFloat {
constant(named: "SkewedFractionVerticalGap")
}
var stackTopDisplayStyleShiftUp: CGFloat {
constant(named: "StackTopDisplayStyleShiftUp")
}
var stackTopShiftUp: CGFloat {
constant(named: "StackTopShiftUp")
}
var stackDisplayStyleGapMin: CGFloat {
constant(named: "StackDisplayStyleGapMin")
}
var stackGapMin: CGFloat {
constant(named: "StackGapMin")
}
var stackBottomDisplayStyleShiftDown: CGFloat {
constant(named: "StackBottomDisplayStyleShiftDown")
}
var stackBottomShiftDown: CGFloat {
constant(named: "StackBottomShiftDown")
}
}
// MARK: - Superscripts / Subscripts
extension Math.FontMetrics {
var superscriptShiftUp: CGFloat {
constant(named: "SuperscriptShiftUp")
}
var superscriptShiftUpCramped: CGFloat {
constant(named: "SuperscriptShiftUpCramped")
}
var subscriptShiftDown: CGFloat {
constant(named: "SubscriptShiftDown")
}
var superscriptBaselineDropMax: CGFloat {
constant(named: "SuperscriptBaselineDropMax")
}
var subscriptBaselineDropMin: CGFloat {
constant(named: "SubscriptBaselineDropMin")
}
var superscriptBottomMin: CGFloat {
constant(named: "SuperscriptBottomMin")
}
var subscriptTopMax: CGFloat {
constant(named: "SubscriptTopMax")
}
var subSuperscriptGapMin: CGFloat {
constant(named: "SubSuperscriptGapMin")
}
var superscriptBottomMaxWithSubscript: CGFloat {
constant(named: "SuperscriptBottomMaxWithSubscript")
}
var spaceAfterScript: CGFloat {
constant(named: "SpaceAfterScript")
}
}
// MARK: - Radicals
extension Math.FontMetrics {
var radicalExtraAscender: CGFloat {
constant(named: "RadicalExtraAscender")
}
var radicalRuleThickness: CGFloat {
constant(named: "RadicalRuleThickness")
}
var radicalDisplayStyleVerticalGap: CGFloat {
constant(named: "RadicalDisplayStyleVerticalGap")
}
var radicalVerticalGap: CGFloat {
constant(named: "RadicalVerticalGap")
}
var radicalKernBeforeDegree: CGFloat {
constant(named: "RadicalKernBeforeDegree")
}
var radicalKernAfterDegree: CGFloat {
constant(named: "RadicalKernAfterDegree")
}
var radicalDegreeBottomRaisePercent: CGFloat {
constantPercent(named: "RadicalDegreeBottomRaisePercent")
}
}
// MARK: - Limits
extension Math.FontMetrics {
var upperLimitBaselineRiseMin: CGFloat {
constant(named: "UpperLimitBaselineRiseMin")
}
var upperLimitGapMin: CGFloat {
constant(named: "UpperLimitGapMin")
}
var lowerLimitGapMin: CGFloat {
constant(named: "LowerLimitGapMin")
}
var lowerLimitBaselineDropMin: CGFloat {
constant(named: "LowerLimitBaselineDropMin")
}
var limitExtraAscenderDescender: CGFloat {
0
}
}
// MARK: - Underline
extension Math.FontMetrics {
var underbarVerticalGap: CGFloat {
constant(named: "UnderbarVerticalGap")
}
var underbarRuleThickness: CGFloat {
constant(named: "UnderbarRuleThickness")
}
var underbarExtraDescender: CGFloat {
constant(named: "UnderbarExtraDescender")
}
}
// MARK: - Overline
extension Math.FontMetrics {
var overbarVerticalGap: CGFloat {
constant(named: "OverbarVerticalGap")
}
var overbarRuleThickness: CGFloat {
constant(named: "OverbarRuleThickness")
}
var overbarExtraAscender: CGFloat {
constant(named: "OverbarExtraAscender")
}
}
// MARK: - Constants
extension Math.FontMetrics {
var axisHeight: CGFloat {
constant(named: "AxisHeight")
}
var scriptScaleDown: CGFloat {
constantPercent(named: "ScriptPercentScaleDown")
}
var scriptScriptScaleDown: CGFloat {
constantPercent(named: "ScriptScriptPercentScaleDown")
}
var mathLeading: CGFloat {
constant(named: "MathLeading")
}
var delimitedSubFormulaMinHeight: CGFloat {
constant(named: "DelimitedSubFormulaMinHeight")
}
}
// MARK: - Accent
extension Math.FontMetrics {
var accentBaseHeight: CGFloat {
constant(named: "AccentBaseHeight")
}
var flattenedAccentBaseHeight: CGFloat {
constant(named: "FlattenedAccentBaseHeight")
}
}
// MARK: - Glyph Construction
extension Math.FontMetrics {
var minConnectorOverlap: CGFloat {
constant(named: "MinConnectorOverlap")
}
}
// MARK: - Private
extension Math.FontMetrics {
private func constant(named name: String) -> CGFloat {
guard let value = table.constants[name] else {
return .zero
}
return unitsToPoints(value)
}
private func constantPercent(named name: String) -> CGFloat {
guard let value = table.constants[name] else {
return .zero
}
return CGFloat(value) / 100
}
private func unitsToPoints(_ units: Int) -> CGFloat {
CGFloat(units) * font.size / CGFloat(unitsPerEm)
}
private func advance(forGlyph glyph: CGGlyph) -> CGSize {
guard
let font = Math.FontRegistry.shared.font(named: font.name, size: font.size)
else {
return .zero
}
var glyph = glyph
var advance = CGSize.zero
CTFontGetAdvancesForGlyphs(font, .horizontal, &glyph, &advance, 1)
return advance
}
}