Skip to content

Example apps — expose all 8 fonts + font-size slider#211

Merged
kostub merged 7 commits into
masterfrom
feature/add-math-fonts-pr3
May 31, 2026
Merged

Example apps — expose all 8 fonts + font-size slider#211
kostub merged 7 commits into
masterfrom
feature/add-math-fonts-pr3

Conversation

@kostub
Copy link
Copy Markdown
Owner

@kostub kostub commented May 30, 2026

Goal

All three example apps list all 8 fonts and gain a font-size slider (range 10–40, default 15) that applies one global override size to every label.

Plan: docs/plans/2026-05-30-add-math-fonts.md (items 15–17)
LLD: docs/lld/2026-05-30-add-math-fonts.md

Stack

Commits

  • [item 15] iOS example: 8-font wheel + global font-size slider
  • [item 16] SwiftMathExample: 8 fonts (.menu) + shared font-size slider
  • [item 17] macOS example: code-built font popup + font-size slider

Changes per app

iOS (iosMathExample): fontNames/kFontKeys extended to 8 entries; UISlider (10–40pt, default 15) added in code above the scroll view; sizeChanged: action loops all labels; per-label labels[8].fontSize=30 / labels[9].fontSize=10 overrides removed.

Swift (SwiftMathExample): 5 new MathFont enum cases; Picker style changed from .segmented to .menu; @State fontSize: CGFloat = 15 shared across all tabs; Slider(value: $fontSize, in: 10...40) in PlaygroundTab; ExampleCard and GalleryTab use shared fontSize.

macOS (MacOSMathExample): NSPopUpButton + NSSlider added as a code-built 36pt header above the scroll view; changeFont: / changeSize: actions wired; per-label size overrides removed.

Testing

All three apps build (BUILD SUCCEEDED). No storyboard/XIB edits.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a global font-size slider and expands the available math fonts (adding New Computer Modern, TeX Gyre Pagella, STIX Two, Fira Math, and Noto Sans Math) across the macOS, iOS, and SwiftMath examples. While these additions improve customization, several layout issues were identified. Specifically, in the iOS ViewController, the logic for finding the render panel constraint is too broad and could match the safe area layout guide, leading to layout conflicts. Additionally, across all platforms (macOS, iOS, and SwiftUI), changing the font size dynamically causes vertical clipping because the label heights and content view heights remain fixed at their startup values instead of scaling proportionally with the font size.

Comment thread iosMathExample/example/ViewController.m Outdated
Comment on lines +93 to +103
for (NSLayoutConstraint* c in self.view.constraints) {
if (c.firstItem == self.scrollView && c.firstAttribute == NSLayoutAttributeTop) {
renderPanelRef = c.secondItem;
c.priority = UILayoutPriorityDefaultLow;
break;
} else if (c.secondItem == self.scrollView && c.secondAttribute == NSLayoutAttributeTop) {
renderPanelRef = c.firstItem;
c.priority = UILayoutPriorityDefaultLow;
break;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current logic for finding the render panel constraint is too broad. If the scroll view's top constraint is anchored to the safe area layout guide (which is common in modern iOS layouts), renderPanelRef will be resolved to self.view.safeAreaLayoutGuide. Pinning the top of the size slider to the bottom of the safe area layout guide (which is at the bottom of the screen) will break the layout and cause constraint conflicts. Restrict the search to constraints where the second item is anchored via NSLayoutAttributeBottom to ensure we only match the actual render panel view.

    for (NSLayoutConstraint* c in self.view.constraints) {
        if (c.firstItem == self.scrollView && c.firstAttribute == NSLayoutAttributeTop) {
            if (c.secondAttribute == NSLayoutAttributeBottom) {
                renderPanelRef = c.secondItem;
                c.priority = UILayoutPriorityDefaultLow;
                break;
            }
        } else if (c.secondItem == self.scrollView && c.secondAttribute == NSLayoutAttributeTop) {
            if (c.firstAttribute == NSLayoutAttributeBottom) {
                renderPanelRef = c.firstItem;
                c.priority = UILayoutPriorityDefaultLow;
                break;
            }
        }
    }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 090db84. The search now also requires the other end of the constraint to be NSLayoutAttributeBottom, so it only matches scrollView.top == renderPanel.bottom and can no longer latch onto a scrollView.top-to-safe-area constraint.

Comment thread iosMathExample/example/ViewController.m Outdated
Comment on lines +293 to +298
- (void)sizeChanged:(UISlider *)sender
{
CGFloat size = (CGFloat)sender.value;
for (MTMathUILabel* label in self.demoLabels) { label.fontSize = size; }
for (MTMathUILabel* label in self.labels) { label.fontSize = size; }
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When the font size is changed dynamically via the slider, the labels' font sizes are updated, but their height constraints (which are fixed at startup) and the content view's total height are not updated. This causes the math formulas to be vertically clipped when the font size is increased, and the scrollable area will not adjust correctly. Consider dynamically scaling the height constraints of the labels and the content view's height constraint proportionally to the font size ratio (size / 15.0), or transition to a fully dynamic Auto Layout system without fixed height constraints.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 090db84. Each label now keeps a reference to its height constraint; sizeChanged: rescales it from the startup baseline font size (demo labels 15, test labels 20) and recomputes the content view height constraint, so formulas grow with the slider instead of clipping.

Comment on lines +201 to +206
- (void)changeSize:(NSSlider *)sender
{
CGFloat size = (CGFloat)sender.doubleValue;
for (MTMathUILabel* label in self.demoLabels) { label.fontSize = size; }
for (MTMathUILabel* label in self.labels) { label.fontSize = size; }
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When the font size is changed dynamically via the slider, the labels' font sizes are updated, but their height constraints (which are fixed at startup) and the content view's frame height are not updated. This causes the math formulas to be vertically clipped when the font size is increased, and the scrollable area will not adjust correctly. Consider dynamically scaling the height constraints of the labels and the content view's frame height proportionally to the font size ratio (size / 15.0), or transition to a fully dynamic Auto Layout system without fixed height constraints.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 090db84. changeSize: now rescales each label height constraint from its baseline (demo 15, test 20) and grows the document view frame height to match.

Comment thread SwiftMathExample/ContentView.swift Outdated
Comment on lines 142 to 144
MathLabel(latex: formula.latex, fontSize: fontSize, mode: formula.mode,
font: font.font(size: fontSize))
.frame(height: formula.height)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The height of the MathLabel frame is fixed to formula.height, which was designed for the default font size of 20. When the global fontSize is increased (up to 40), the formula will be vertically clipped. Scale the frame height proportionally to the font size ratio to prevent clipping.

Suggested change
MathLabel(latex: formula.latex, fontSize: fontSize, mode: formula.mode,
font: font.font(size: fontSize))
.frame(height: formula.height)
MathLabel(latex: formula.latex, fontSize: fontSize, mode: formula.mode,
font: font.font(size: fontSize))
.frame(height: formula.height * (fontSize / formula.fontSize))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 090db84 — the card height is now formula.height * (fontSize / formula.fontSize).

Comment thread SwiftMathExample/ContentView.swift Outdated
font: font.font(size: testFontSize(at: i))
font: font.font(size: fontSize)
)
.frame(height: testHeight(at: i))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The height of the MathLabel frame in the gallery is fixed to testHeight(at: i), which was designed for the original font sizes (15 for most, 30 for index 8, 10 for index 9). When the global fontSize is increased, the formulas will be vertically clipped. Scale the frame height proportionally to the font size ratio to prevent clipping.

Suggested change
.frame(height: testHeight(at: i))
.frame(height: testHeight(at: i) * (fontSize / (i == 8 ? 30 : (i == 9 ? 10 : 15))))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 090db84. Restored the per-entry baseline (30 at index 8, 10 at index 9, else 15) as testBaselineFontSize(at:) and scale the row height by fontSize / that baseline.

kostub added 4 commits May 31, 2026 12:46
…match

Addresses gemini-code-assist review on PR #211:

- iOS: restrict the render-panel constraint search to scrollView.top ==
  <panel>.bottom (checking the other end's attribute) so it can't match a
  scrollView.top-to-safe-area-top constraint and pin the slider off-screen.
- iOS/macOS/SwiftUI: the size slider only changed fontSize, leaving the
  fixed startup label/content heights, which clipped formulas at larger
  sizes. Track each label's height constraint and rescale it (and the
  content/document height) from its startup baseline font size — demo
  labels 15, test labels 20 on iOS/macOS; per-entry baselines in SwiftUI.

All three example apps build.
@kostub kostub force-pushed the feature/add-math-fonts-pr3 branch from d2a99a6 to 090db84 Compare May 31, 2026 08:31
@kostub
Copy link
Copy Markdown
Owner Author

kostub commented May 31, 2026

/gemini review this PR

@kostub kostub changed the base branch from feature/add-math-fonts-pr2 to master May 31, 2026 08:33
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces breaking API changes to MTFontManager by removing per-font convenience methods in favor of a generic fontWithName:size: accessor and exposing new public font name constants for eight bundled fonts. It also refactors the manager to enforce singleton usage and updates the iOS, macOS, and Swift example apps with dynamic font size sliders that scale label heights to prevent clipping. Feedback on these changes highlights a critical crash risk in ViewController.m caused by mutating a required constraint's priority at runtime, which should be resolved by deactivating the constraint instead. Additionally, it is recommended to use the newly introduced public font constants in AppDelegate.m rather than duplicating raw string literals.

Comment thread iosMathExample/example/ViewController.m Outdated
Comment on lines +104 to +120
UIView* renderPanelRef = nil;
for (NSLayoutConstraint* c in self.view.constraints) {
// Match only scrollView.top == <renderPanel>.bottom. Checking the other
// end's attribute avoids accidentally matching scrollView.top anchored to
// the safe area layout guide's top, which would pin the slider off-screen.
if (c.firstItem == self.scrollView && c.firstAttribute == NSLayoutAttributeTop
&& c.secondAttribute == NSLayoutAttributeBottom) {
renderPanelRef = c.secondItem;
c.priority = UILayoutPriorityDefaultLow;
break;
} else if (c.secondItem == self.scrollView && c.secondAttribute == NSLayoutAttributeTop
&& c.firstAttribute == NSLayoutAttributeBottom) {
renderPanelRef = c.firstItem;
c.priority = UILayoutPriorityDefaultLow;
break;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Mutating the priority of an active constraint after it has been added to a view is only supported if the priority remains in the non-required range (i.e., less than 1000). Since constraints created in Interface Builder/XIBs default to a priority of 1000 (Required), attempting to change c.priority to UILayoutPriorityDefaultLow at runtime will throw an exception and crash the application. Deactivating the constraint by setting c.active = NO; is the safe and standard way to replace a constraint at runtime.

    UIView* renderPanelRef = nil;
    for (NSLayoutConstraint* c in self.view.constraints) {
        // Match only scrollView.top == <renderPanel>.bottom. Checking the other
        // end's attribute avoids accidentally matching scrollView.top anchored to
        // the safe area layout guide's top, which would pin the slider off-screen.
        if (c.firstItem == self.scrollView && c.firstAttribute == NSLayoutAttributeTop
            && c.secondAttribute == NSLayoutAttributeBottom) {
            renderPanelRef = c.secondItem;
            c.active = NO;
            break;
        } else if (c.secondItem == self.scrollView && c.secondAttribute == NSLayoutAttributeTop
                   && c.firstAttribute == NSLayoutAttributeBottom) {
            renderPanelRef = c.firstItem;
            c.active = NO;
            break;
        }
    }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cb98a7d. XIB constraints are required (1000), and mutating an active constraint's priority to/from required throws — so the original scrollView.top==renderPanel.bottom constraint is now deactivated (c.active = NO) instead. The new required scrollView.top==slider.bottom constraint replaces it.

Comment on lines +45 to +55
static NSString *const kMacFontNames[] = {
@"Latin Modern Math", @"TeX Gyre Termes", @"XITS Math",
@"New Computer Modern", @"TeX Gyre Pagella", @"STIX Two",
@"Fira Math", @"Noto Sans Math",
};
static NSString *const kMacFontKeys[] = {
@"latinmodern-math", @"texgyretermes-math", @"xits-math",
@"newcm-math", @"texgyrepagella-math", @"stixtwo-math",
@"firamath", @"notosansmath",
};
static const NSUInteger kMacFontCount = 8;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of duplicating the raw string literals for the font keys in a global static array, we can define kMacFontKeys locally inside the changeFont: method using the newly exposed public constants (MTFontNameLatinModern, etc.) from MTFontManager.h. This improves maintainability and keeps the implementation consistent with ViewController.m.

static NSString *const kMacFontNames[] = {
    @"Latin Modern Math", @"TeX Gyre Termes", @"XITS Math",
    @"New Computer Modern", @"TeX Gyre Pagella", @"STIX Two",
    @"Fira Math", @"Noto Sans Math",
};
static const NSUInteger kMacFontCount = 8;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cb98a7d, using the public MTFontName* constants to match the iOS example. The array had to move local to changeFont: rather than stay a file-scope static — the constants are runtime extern NSString *const values, not compile-time constants, so they can't initialize a static array.

Comment on lines +208 to +219
- (void)changeFont:(NSPopUpButton *)sender
{
NSInteger i = sender.indexOfSelectedItem;
if (i < 0 || (NSUInteger)i >= kMacFontCount) return;
NSString* key = kMacFontKeys[i];
for (MTMathUILabel* label in self.demoLabels) {
label.font = [[MTFontManager fontManager] fontWithName:key size:label.font.fontSize];
}
for (MTMathUILabel* label in self.labels) {
label.font = [[MTFontManager fontManager] fontWithName:key size:label.font.fontSize];
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Define the kMacFontKeys array locally using the public constants from MTFontManager.h to avoid hardcoded string literals.

- (void)changeFont:(NSPopUpButton *)sender
{
    NSInteger i = sender.indexOfSelectedItem;
    if (i < 0 || (NSUInteger)i >= kMacFontCount) return;
    NSString *const kMacFontKeys[] = {
        MTFontNameLatinModern, MTFontNameTermes, MTFontNameXITS,
        MTFontNameNewComputerModern, MTFontNamePagella, MTFontNameSTIXTwo,
        MTFontNameFiraMath, MTFontNameNotoSansMath,
    };
    NSString* key = kMacFontKeys[i];
    for (MTMathUILabel* label in self.demoLabels) {
        label.font = [[MTFontManager fontManager] fontWithName:key size:label.font.fontSize];
    }
    for (MTMathUILabel* label in self.labels) {
        label.font = [[MTFontManager fontManager] fontWithName:key size:label.font.fontSize];
    }
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in cb98a7d — kMacFontKeys is now a local array in changeFont: built from MTFontNameLatinModern, MTFontNameTermes, etc.

kostub and others added 3 commits May 31, 2026 14:21
…S font keys via public constants

iOS: changing an active required (1000) XIB constraint's priority to/from
required throws at runtime; deactivate it instead.

macOS: replace hardcoded font-key string literals with the public
MTFontName* constants (local array, since they're runtime extern consts),
matching the iOS example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ight cards

Testing feedback on the Playground/Examples tabs:
- Size control drifted as the font picker's width changed; replace the
  Slider with a Stepper pinned to the trailing edge (Spacer + fixedSize).
- The stepper also shows the current size ("Size: NN"), which the slider did not.
- Examples cards used a guessed fixed-height table that clipped tall formulae
  (e.g. Rogers-Ramanujan) at every font size; drop the fixed frame and let the
  label size to its intrinsic content height. Removes the now-unused per-formula
  height/fontSize metadata.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…: drop orphan XIB views

iOS testing feedback:
- Replaced the font-size slider with a stepper in the top row beside the font
  and colour pickers, showing the current point size ("15pt"). The font field's
  fixed width was removed so it absorbs the row's slack and the stepper stays
  fully on-screen and tappable at any width (incl. iPhone 16 Pro). Removed the
  redundant "Font" label to make room.
- Formulae no longer clip horizontally when the font size grows: the content
  view may now exceed the viewport width (driven by each label's intrinsic
  width) so wide formulae scroll horizontally instead of being cut off.

macOS testing feedback:
- Removed the stray blue box: an orphaned MTMathUILabel (plus an old "Update!"
  button and text field) left in MainMenu.xib from the original template, which
  AppDelegate never used. Also removed the matching stale outlet/action
  connections (screen, inputTextField, clickUpdateButton:).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kostub
Copy link
Copy Markdown
Owner Author

kostub commented May 31, 2026

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces global font size controls and expands the supported math fonts across the macOS, iOS, and SwiftMath examples. It refactors the layout logic to dynamically scale label heights and content views, enabling horizontal scrolling for wide formulas and preventing clipping. The feedback highlights a potential issue where arrays in ViewController.m are not initialized if instantiated via initWithCoder:, which could lead to silent failures. Additionally, the test formulas at indices 8 and 9 in both the macOS and iOS examples should be scaled using their specific baseline font sizes (30 and 10) rather than a hardcoded baseline of 20.0 to ensure correct layout heights.

Comment on lines +68 to +71
self.demoHeightConstraints = [[NSMutableArray alloc] init];
self.testHeightConstraints = [[NSMutableArray alloc] init];
self.demoBaseHeights = [[NSMutableArray alloc] init];
self.testBaseHeights = [[NSMutableArray alloc] init];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since this View Controller is defined in View.xib (and potentially used in storyboards), it can be instantiated via initWithCoder: instead of initWithNibName:bundle:. If that happens, these arrays will remain nil, causing silent failures when trying to add or update labels. Consider implementing initWithCoder: or moving the initialization of these arrays to a common initializer method (e.g., commonInit) called by both initWithNibName:bundle: and initWithCoder:.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This VC is created in exactly one place — AppDelegate.m: [[ViewController alloc] initWithNibName:@"View" bundle:nil]. There is no storyboard (no UIMainStoryboardFile/NSMainNibFile in Info.plist), and View.xib archives the view hierarchy with File's Owner = ViewController, not the controller object itself, so initWithCoder: is never invoked here. The nil-array path the comment guards against does not exist in this app, so I'm leaving the single designated initializer as-is rather than adding a commonInit for an unreachable instantiation path (YAGNI).

documentHeight += 30; // gap between sections
for (NSUInteger i = 0; i < self.labels.count; i++) {
self.labels[i].fontSize = size;
CGFloat h = self.testBaseHeights[i].doubleValue * (size / 20.0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The test formulas at indices 8 and 9 have baseline font sizes of 30 and 10 respectively, rather than the default 20. Scaling their heights using a hardcoded baseline of 20.0 will cause incorrect layout heights (potentially clipping at smaller sizes or leaving excessive empty space). Consider scaling them using their correct baseline font sizes, similar to the logic implemented in the Swift example.

        CGFloat baseline = 20.0;
        if (i == 8) baseline = 30.0;
        else if (i == 9) baseline = 10.0;
        CGFloat h = self.testBaseHeights[i].doubleValue * (size / baseline);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't apply to the macOS example as it currently stands. The per-label labels[8].fontSize = 30 / labels[9].fontSize = 10 overrides were removed in this PR (see the PR description) — current code sets only textAlignment for those indices. Every test label now renders at the default MTMathUILabel.fontSize of 20 (MTMathUILabel.m:44), so size / 20.0 is correct for all of them. Special-casing 30/10 here would scale indices 8/9 against a baseline they no longer use and clip/over-size them. The per-entry baselines still legitimately exist in the Swift gallery because those formula structs carry their own fontSize; iOS/macOS no longer do.

total += 30; // gap between sections
for (NSUInteger i = 0; i < self.labels.count; i++) {
self.labels[i].fontSize = size;
CGFloat h = self.testBaseHeights[i].doubleValue * (size / 20.0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The test formulas at indices 8 and 9 have baseline font sizes of 30 and 10 respectively, rather than the default 20. Scaling their heights using a hardcoded baseline of 20.0 will cause incorrect layout heights (potentially clipping at smaller sizes or leaving excessive empty space). Consider scaling them using their correct baseline font sizes, similar to the logic implemented in the Swift example.

        CGFloat baseline = 20.0;
        if (i == 8) baseline = 30.0;
        else if (i == 9) baseline = 10.0;
        CGFloat h = self.testBaseHeights[i].doubleValue * (size / baseline);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the macOS comment: the labels[8].fontSize = 30 / labels[9].fontSize = 10 overrides were removed in this PR, so all test labels render at the default MTMathUILabel.fontSize of 20 (MTMathUILabel.m:44). size / 20.0 is correct for every test label; reintroducing a 30/10 baseline for indices 8/9 would mis-scale them since those labels no longer render at 30/10.

@kostub kostub merged commit 9d337c0 into master May 31, 2026
1 check passed
@kostub kostub deleted the feature/add-math-fonts-pr3 branch May 31, 2026 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant