diff --git a/SwiftMathExample/ContentView.swift b/SwiftMathExample/ContentView.swift index f3b79e8..f0bbd80 100644 --- a/SwiftMathExample/ContentView.swift +++ b/SwiftMathExample/ContentView.swift @@ -4,16 +4,21 @@ // // The original single-page gallery showed only the raw rendering test suite — // useful for verifying correctness but not as a beginner reference. This file -// restructures the app into two tabs: +// restructures the app into three tabs: // // • Examples — named, curated formulae (quadratic formula, Euler's // identity, matrices, ...) that mirror the README quick-start snippets. // Each formula is displayed in a card with a human-readable title, making // the app usable as a first-look reference alongside the documentation. // +// • Playground — an interactive sandbox: type any LaTeX and pick a math font +// to see it render live. +// // • Gallery — the full rendering test suite plus visual regression cases // for parser and typesetter features. // +// The selected font is shared across all three tabs. +// // This software may be modified and distributed under the terms of the // MIT license. See the LICENSE file for details. // @@ -24,11 +29,17 @@ import iosMath // MARK: - Top-level tab container struct ContentView: View { + /// Selected math font, shared across all tabs — switching it in the + /// Playground re-renders the Examples and Gallery too, mirroring the ObjC example. + @State private var font: MathFont = .latinModern + var body: some View { TabView { - ExamplesTab() + ExamplesTab(font: font) .tabItem { Label("Examples", systemImage: "function") } - GalleryTab() + PlaygroundTab(font: $font) + .tabItem { Label("Playground", systemImage: "pencil.and.scribble") } + GalleryTab(font: font) .tabItem { Label("Gallery", systemImage: "square.grid.2x2") } } } @@ -88,12 +99,14 @@ private let namedExamples: [NamedFormula] = { /// Curated, named examples — suitable as a quick-start reference. private struct ExamplesTab: View { + let font: MathFont + var body: some View { NavigationView { ScrollView { VStack(alignment: .leading, spacing: 20) { ForEach(namedExamples.indices, id: \.self) { i in - ExampleCard(formula: namedExamples[i]) + ExampleCard(formula: namedExamples[i], font: font) } } .padding() @@ -111,14 +124,21 @@ private struct ExamplesTab: View { private struct ExampleCard: View { let formula: NamedFormula + let font: MathFont var body: some View { VStack(alignment: .leading, spacing: 6) { Text(formula.title) .font(.caption) .foregroundStyle(.secondary) - MathLabel(latex: formula.latex, fontSize: formula.fontSize, mode: formula.mode) - .frame(height: formula.height) + // Horizontal scroll so a formula wider than the screen (e.g. the + // Rogers–Ramanujan fraction in Latin Modern) scrolls within its card + // instead of stretching the whole column and clipping every card's left edge. + ScrollView(.horizontal, showsIndicators: false) { + MathLabel(latex: formula.latex, fontSize: formula.fontSize, mode: formula.mode, + font: font.font(size: formula.fontSize)) + .frame(height: formula.height) + } } .padding() .background(Color(white: 1)) @@ -127,10 +147,79 @@ private struct ExampleCard: View { } } +// MARK: - Playground tab + +/// Interactive sandbox: type any LaTeX and pick a math font to see it render +/// live. Mirrors the LaTeX text field and font switcher in the ObjC iosMathExample. +private struct PlaygroundTab: View { + @Binding var font: MathFont + @State private var latex = #"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"# + + private let fontSize: CGFloat = 24 + + var body: some View { + NavigationView { + VStack(alignment: .leading, spacing: 16) { + // Live rendering of whatever is in the editor. + ScrollView(.horizontal, showsIndicators: false) { + MathLabel( + latex: latex, + fontSize: fontSize, + mode: .display, + alignment: .center, + font: font.font(size: fontSize) + ) + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 100) + .padding(.horizontal, 12) + } + .background(Color(white: 1)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .shadow(color: .black.opacity(0.06), radius: 4, x: 0, y: 2) + + // Font switcher. + Picker("Font", selection: $font) { + ForEach(MathFont.allCases) { font in + Text(font.rawValue).tag(font) + } + } + .pickerStyle(.segmented) + + // LaTeX editor. + Text("LaTeX") + .font(.caption) + .foregroundStyle(.secondary) + TextEditor(text: $latex) + .font(.system(.body, design: .monospaced)) + .autocorrectionDisabled(true) + #if os(iOS) + .textInputAutocapitalization(.never) + #endif + .frame(minHeight: 80, maxHeight: 160) + .padding(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.secondary.opacity(0.3), lineWidth: 1) + ) + + Spacer() + } + .padding() + .navigationTitle("Playground") + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + #endif + } + #if os(iOS) + .navigationViewStyle(.stack) + #endif + } +} + // MARK: - Gallery tab /// Full typesetter test suite. Curated real-math formulae live in the Examples tab. private struct GalleryTab: View { + let font: MathFont private static let testHeights: [CGFloat] = [ 40, 40, 40, 40, 40, 60, 60, 60, 90, 30, 40, 90, 40, 60, 60, 60, @@ -156,7 +245,8 @@ private struct GalleryTab: View { alignment: testAlignment(at: i), highlighted: [0, 1, 3, 6, 7].contains(i), leftInset: i == 6 ? 20 : 0, - rightInset: i == 3 ? 20 : 0 + rightInset: i == 3 ? 20 : 0, + font: font.font(size: testFontSize(at: i)) ) .frame(height: testHeight(at: i)) .padding(.horizontal, 10) diff --git a/SwiftMathExample/MathLabel.swift b/SwiftMathExample/MathLabel.swift index f957afd..d16213c 100644 --- a/SwiftMathExample/MathLabel.swift +++ b/SwiftMathExample/MathLabel.swift @@ -21,6 +21,8 @@ struct MathLabel: View { var highlighted: Bool = false var leftInset: CGFloat = 0 var rightInset: CGFloat = 0 + /// Math font face. When nil, MTMathUILabel keeps its default (Latin Modern Math). + var font: MTFont? = nil var body: some View { _MathLabelRepresentable( @@ -30,11 +32,30 @@ struct MathLabel: View { alignment: alignment, highlighted: highlighted, leftInset: leftInset, - rightInset: rightInset + rightInset: rightInset, + font: font ) } } +/// The three math fonts bundled with iosMath, exposed for the font switcher. +enum MathFont: String, CaseIterable, Identifiable { + case latinModern = "Latin Modern" + case termes = "TeX Gyre Termes" + case xits = "XITS" + + var id: String { rawValue } + + func font(size: CGFloat) -> MTFont? { + let manager = MTFontManager.fontManager() + switch self { + case .latinModern: return manager.latinModernFont(withSize: size) + case .termes: return manager.termesFont(withSize: size) + case .xits: return manager.xitsFont(withSize: size) + } + } +} + // MARK: - Platform representables #if os(iOS) @@ -48,6 +69,7 @@ private struct _MathLabelRepresentable: UIViewRepresentable { let highlighted: Bool let leftInset: CGFloat let rightInset: CGFloat + let font: MTFont? func makeUIView(context: Context) -> MTMathUILabel { MTMathUILabel() @@ -55,6 +77,9 @@ private struct _MathLabelRepresentable: UIViewRepresentable { func updateUIView(_ label: MTMathUILabel, context: Context) { label.latex = latex + if let font = font { + label.font = font + } label.fontSize = fontSize label.mode = mode label.textAlignment = alignment @@ -63,6 +88,10 @@ private struct _MathLabelRepresentable: UIViewRepresentable { ? UIColor(hue: 0.15, saturation: 0.2, brightness: 1.0, alpha: 1.0) : .clear } + + func sizeThatFits(_ proposal: ProposedViewSize, uiView: MTMathUILabel, context: Context) -> CGSize? { + mathSizeThatFits(proposal, intrinsic: uiView.intrinsicContentSize) + } } #elseif os(macOS) @@ -76,6 +105,7 @@ private struct _MathLabelRepresentable: NSViewRepresentable { let highlighted: Bool let leftInset: CGFloat let rightInset: CGFloat + let font: MTFont? func makeNSView(context: Context) -> MTMathUILabel { MTMathUILabel() @@ -83,6 +113,9 @@ private struct _MathLabelRepresentable: NSViewRepresentable { func updateNSView(_ label: MTMathUILabel, context: Context) { label.latex = latex + if let font = font { + label.font = font + } label.fontSize = fontSize label.mode = mode label.textAlignment = alignment @@ -91,5 +124,28 @@ private struct _MathLabelRepresentable: NSViewRepresentable { ? NSColor(hue: 0.15, saturation: 0.2, brightness: 1.0, alpha: 1.0) : .clear } + + func sizeThatFits(_ proposal: ProposedViewSize, nsView: MTMathUILabel, context: Context) -> CGSize? { + mathSizeThatFits(proposal, intrinsic: nsView.intrinsicContentSize) + } } #endif + +/// Width policy shared by both platform representables. +/// +/// MTMathUILabel's default intrinsic content size is the formula's *natural* width, +/// and its high compression resistance otherwise refuses to be narrower than that. +/// A single formula wider than the viewport (e.g. the Rogers–Ramanujan fraction in +/// Latin Modern, which is wider in that font than in TeX Gyre) would then stretch the +/// whole column and clip every row. Instead: fill exactly the width we're offered when +/// that is finite (so the label still fills its row and `.center`/`.right` alignment +/// works), and only fall back to the natural width when offered unbounded space — e.g. +/// inside a horizontal ScrollView, where the formula is meant to scroll. +private func mathSizeThatFits(_ proposal: ProposedViewSize, intrinsic: CGSize) -> CGSize { + switch proposal.width { + case .some(let width) where width != .infinity: + return CGSize(width: width, height: intrinsic.height) + default: + return intrinsic + } +}