From 5be257b4d2a815d47cce1ca3df418410f32d5bb9 Mon Sep 17 00:00:00 2001 From: Yusuf Mohsinally <463376+yusufm@users.noreply.github.com> Date: Sun, 3 May 2026 13:18:07 -0700 Subject: [PATCH] Add transient document zoom shortcuts --- MacDown/Code/Document/MPDocument.m | 77 ++++++++++++++++++-- MacDown/Localization/Base.lproj/MainMenu.xib | 22 +++++- MacDownTests/MPPaneToggleTests.m | 38 ++++++++++ 3 files changed, 127 insertions(+), 10 deletions(-) diff --git a/MacDown/Code/Document/MPDocument.m b/MacDown/Code/Document/MPDocument.m index 75cc0fea..166a4647 100644 --- a/MacDown/Code/Document/MPDocument.m +++ b/MacDown/Code/Document/MPDocument.m @@ -182,6 +182,10 @@ - (int)rendererFlags @end +static const CGFloat MPDocumentZoomMinimum = 0.5; +static const CGFloat MPDocumentZoomMaximum = 3.0; +static const CGFloat MPDocumentZoomStep = 1.1; + @interface MPDocument () = 101100 @@ -233,6 +237,7 @@ typedef NS_ENUM(NSUInteger, MPScrollOwner) { @property (nonatomic) BOOL needsToUnregister; @property (nonatomic) BOOL alreadyRenderingInWeb; @property (nonatomic) BOOL renderToWebPending; +@property (nonatomic) CGFloat documentZoomMultiplier; @property (strong) NSArray *webViewHeaderLocations; @property (strong) NSArray *editorHeaderLocations; @property (nonatomic) MPScrollOwner scrollOwner; // Issue #342: Scroll ownership model @@ -255,6 +260,8 @@ typedef NS_ENUM(NSUInteger, MPScrollOwner) { @property (copy) NSString *loadedString; - (void)scaleWebview; +- (NSFont *)effectiveEditorBaseFont; +- (void)applyDocumentZoom; - (void)syncScrollers; - (void)syncScrollersReverse; - (void)updateHeaderLocations; @@ -437,6 +444,7 @@ - (instancetype)init return nil; self.isPreviewReady = NO; + self.documentZoomMultiplier = 1.0; _scrollOwner = MPScrollOwnerNeither; self.previousSplitRatio = -1.0; self.lastNonCollapsedRatio = -1.0; @@ -899,6 +907,18 @@ - (BOOL)validateUserInterfaceItem:(id)item return NO; } } + else if (action == @selector(zoomIn:)) + { + return result && self.documentZoomMultiplier < MPDocumentZoomMaximum; + } + else if (action == @selector(zoomOut:)) + { + return result && self.documentZoomMultiplier > MPDocumentZoomMinimum; + } + else if (action == @selector(resetZoom:)) + { + return result && fabs(self.documentZoomMultiplier - 1.0) > 0.001; + } return result; } @@ -1995,6 +2015,26 @@ - (IBAction)toggleEditorPane:(id)sender [self toggleSplitterCollapsingEditorPane:YES]; } +- (IBAction)zoomIn:(id)sender +{ + self.documentZoomMultiplier = + MIN(MPDocumentZoomMaximum, self.documentZoomMultiplier * MPDocumentZoomStep); + [self applyDocumentZoom]; +} + +- (IBAction)zoomOut:(id)sender +{ + self.documentZoomMultiplier = + MAX(MPDocumentZoomMinimum, self.documentZoomMultiplier / MPDocumentZoomStep); + [self applyDocumentZoom]; +} + +- (IBAction)resetZoom:(id)sender +{ + self.documentZoomMultiplier = 1.0; + [self applyDocumentZoom]; +} + - (IBAction)render:(id)sender { [self.renderer parseAndRenderLater]; @@ -2165,7 +2205,7 @@ - (void)setupEditor:(NSString *)changedKey style.lineSpacing = self.preferences.editorLineSpacing; // Configure tab stops to match 4-space tab width (fixes #195) - NSFont *font = [self.preferences.editorBaseFont copy]; + NSFont *font = [self effectiveEditorBaseFont]; if (font) { NSDictionary *attrs = @{NSFontAttributeName: font}; @@ -2324,17 +2364,38 @@ - (void)redrawDivider } } +- (NSFont *)effectiveEditorBaseFont +{ + NSFont *font = [self.preferences.editorBaseFont copy]; + if (!font) + return nil; + + CGFloat multiplier = self.documentZoomMultiplier > 0.0 ? + self.documentZoomMultiplier : 1.0; + CGFloat size = MAX(1.0, font.pointSize * multiplier); + return [NSFont fontWithDescriptor:font.fontDescriptor size:size] ?: font; +} + +- (void)applyDocumentZoom +{ + [self setupEditor:@"editorBaseFontInfo"]; +} + - (void)scaleWebview { - if (!self.preferences.previewZoomRelativeToBaseFontSize) - return; + CGFloat multiplier = self.documentZoomMultiplier > 0.0 ? + self.documentZoomMultiplier : 1.0; + CGFloat scale = multiplier; - CGFloat fontSize = self.preferences.editorBaseFontSize; - if (fontSize <= 0.0) - return; + if (self.preferences.previewZoomRelativeToBaseFontSize) + { + CGFloat fontSize = self.preferences.editorBaseFontSize; + if (fontSize <= 0.0) + return; - static const CGFloat defaultSize = 14.0; - CGFloat scale = fontSize / defaultSize; + static const CGFloat defaultSize = 14.0; + scale *= fontSize / defaultSize; + } #if 0 // Sadly, this doesn’t work correctly. diff --git a/MacDown/Localization/Base.lproj/MainMenu.xib b/MacDown/Localization/Base.lproj/MainMenu.xib index b5ee90f1..1060ca23 100644 --- a/MacDown/Localization/Base.lproj/MainMenu.xib +++ b/MacDown/Localization/Base.lproj/MainMenu.xib @@ -368,6 +368,22 @@ + + + + + + + + + + + + + + + + @@ -444,7 +460,8 @@ - + + @@ -509,7 +526,8 @@ - + + diff --git a/MacDownTests/MPPaneToggleTests.m b/MacDownTests/MPPaneToggleTests.m index bf36dbdd..e416e898 100644 --- a/MacDownTests/MPPaneToggleTests.m +++ b/MacDownTests/MPPaneToggleTests.m @@ -21,6 +21,9 @@ @interface MPDocument (PaneToggleTesting) @property CGFloat previousSplitRatio; - (IBAction)toggleEditorPane:(id)sender; - (IBAction)togglePreviewPane:(id)sender; +- (IBAction)zoomIn:(id)sender; +- (IBAction)zoomOut:(id)sender; +- (IBAction)resetZoom:(id)sender; - (void)applyEditorStartInPreviewModePreference; @end @@ -81,6 +84,41 @@ - (void)testTogglePreviewPaneIBActionDoesNotCrash @"togglePreviewPane: should not crash"); } +- (void)testZoomActionsUseTransientDocumentMultiplier +{ + NSDictionary *fontInfo = [self.document.preferences.editorBaseFontInfo copy]; + + [self.document zoomIn:nil]; + CGFloat zoomed = [[self.document valueForKey:@"documentZoomMultiplier"] doubleValue]; + + XCTAssertGreaterThan(zoomed, 1.0, + @"Zoom in should update the document-local multiplier"); + XCTAssertEqualObjects(self.document.preferences.editorBaseFontInfo, fontInfo, + @"Zoom should not mutate the saved editor base font preference"); + + [self.document resetZoom:nil]; + CGFloat reset = [[self.document valueForKey:@"documentZoomMultiplier"] doubleValue]; + XCTAssertEqualWithAccuracy(reset, 1.0, 0.001, + @"Reset zoom should restore actual size"); +} + +- (void)testZoomActionsClampAtConfiguredBounds +{ + for (NSUInteger i = 0; i < 100; i++) + [self.document zoomIn:nil]; + + CGFloat zoomedIn = [[self.document valueForKey:@"documentZoomMultiplier"] doubleValue]; + XCTAssertEqualWithAccuracy(zoomedIn, 3.0, 0.001, + @"Zoom in should clamp at 300 percent"); + + for (NSUInteger i = 0; i < 200; i++) + [self.document zoomOut:nil]; + + CGFloat zoomedOut = [[self.document valueForKey:@"documentZoomMultiplier"] doubleValue]; + XCTAssertEqualWithAccuracy(zoomedOut, 0.5, 0.001, + @"Zoom out should clamp at 50 percent"); +} + #pragma mark - Menu Validation Tests