Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 97 additions & 7 deletions SwiftMathExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand All @@ -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") }
}
}
Expand Down Expand Up @@ -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()
Expand All @@ -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))
Expand All @@ -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,
Expand All @@ -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)
Expand Down
58 changes: 57 additions & 1 deletion SwiftMathExample/MathLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand All @@ -48,13 +69,17 @@ private struct _MathLabelRepresentable: UIViewRepresentable {
let highlighted: Bool
let leftInset: CGFloat
let rightInset: CGFloat
let font: MTFont?

func makeUIView(context: Context) -> MTMathUILabel {
MTMathUILabel()
}

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
Expand All @@ -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)
Expand All @@ -76,13 +105,17 @@ private struct _MathLabelRepresentable: NSViewRepresentable {
let highlighted: Bool
let leftInset: CGFloat
let rightInset: CGFloat
let font: MTFont?

func makeNSView(context: Context) -> MTMathUILabel {
MTMathUILabel()
}

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
Expand All @@ -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
}
}
Loading