From e92db52a17e722043f8c2c077213c13d610a8bbf Mon Sep 17 00:00:00 2001 From: Kostub D Date: Sat, 30 May 2026 00:19:00 +0530 Subject: [PATCH 1/2] SwiftMathExample: add font switcher + LaTeX playground, fix wide-formula clipping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Playground tab to the Swift example with a live LaTeX text editor and a font switcher (Latin Modern / TeX Gyre Termes / XITS), mirroring the ObjC iosMathExample. The selected font is shared state in ContentView so switching it re-renders the Examples and Gallery tabs too. Also fix a layout bug where formulas and titles were clipped on the left in the Examples tab (only with Latin Modern). MTMathUILabel refuses to be narrower than its intrinsic width, so a single formula wider than the screen (the Rogers–Ramanujan fraction in Latin Modern) stretched the whole column past the viewport, clipping every card. The representables now implement sizeThatFits to fill the offered width without demanding more, and each Examples card wraps its formula in a horizontal ScrollView so an over-wide formula scrolls instead of breaking the layout. Center/right alignment in the Gallery is preserved. Co-Authored-By: Claude Opus 4.8 --- SwiftMathExample/ContentView.swift | 104 +++++++++++++++++++++++++++-- SwiftMathExample/MathLabel.swift | 58 +++++++++++++++- 2 files changed, 154 insertions(+), 8 deletions(-) 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..16f478f 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() + 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 + } +} From 2ba2256938f561915b408cef65910acf6ece9873 Mon Sep 17 00:00:00 2001 From: Kostub D Date: Sat, 30 May 2026 12:48:57 +0530 Subject: [PATCH 2/2] SwiftMathExample: use shared MTFontManager singleton to keep font cache Co-Authored-By: Claude Opus 4.8 --- SwiftMathExample/MathLabel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwiftMathExample/MathLabel.swift b/SwiftMathExample/MathLabel.swift index 16f478f..d16213c 100644 --- a/SwiftMathExample/MathLabel.swift +++ b/SwiftMathExample/MathLabel.swift @@ -47,7 +47,7 @@ enum MathFont: String, CaseIterable, Identifiable { var id: String { rawValue } func font(size: CGFloat) -> MTFont? { - let manager = MTFontManager() + let manager = MTFontManager.fontManager() switch self { case .latinModern: return manager.latinModernFont(withSize: size) case .termes: return manager.termesFont(withSize: size)