Fix viewport blanking after large programmatic scroll (setFrameSize re-entrance)#110
Merged
krzyzanowskim merged 1 commit intokrzyzanowskim:mainfrom Apr 28, 2026
Conversation
…e-entrance) When `relocateViewport(to: docStart)` calls `setFrameSize(width, lineHeight)` from a scrolled-down position, AppKit must synchronously retract the clip view to fit the new bounds. That fires `prepareContent` from inside `super.setFrameSize`, which calls `updateContentSizeIfNeeded` and re-enters `setFrameSize` with the full document height. The recursive call correctly resizes `contentView`. When control returns to the outer call, `newSize` is still the original (small) value, and `contentView.frame.size = newSize` stomps the recursive call's result. End state: textView at full height, contentView and contentViewportView at one-line height, fragment views past line 1 clipped. Manifests as Cmd-End → Cmd-Home blanking the editor on a multi-screen word-wrapped document. Resizing the window or toggling word-wrap restores rendering because both go through a fresh non-re-entrant `setFrameSize`. Fix: after `super.setFrameSize`, read `frame.size` (which reflects any recursive resize) rather than the stale `newSize` parameter. When no re-entrance occurred, `frame.size == newSize`, so the common path is unchanged.
114b7bb to
29844db
Compare
krzyzanowskim
approved these changes
Apr 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Cmd-End → Cmd-Home on a document taller than one viewport leaves the editor blank except for the first wrapped fragment. Viewport layout is correct; rendering is clipped because
contentViewandcontentViewportViewend up pinned to ~one-line tall while the textView itself is at full document height.Root cause
When
relocateViewport(to: docStart)callssetFrameSize(width, lineHeight)from a scrolled-down position, AppKit must synchronously retract the clip view to fit the new bounds. That firesprepareContentfrom insidesuper.setFrameSize, which callsupdateContentSizeIfNeededand re-enterssetFrameSizewith the full document height. The recursive call correctly resizescontentView. When control returns to the outer call,newSizeis still the original (small) value, andcontentView.frame.size = newSizestomps the recursive call's result.End state: textView at full height,
contentViewandcontentViewportViewat one-line height, fragment views past line 1 clipped.Repro
Open a multi-screen word-wrapped document, Cmd-End, then Cmd-Home. The editor renders only the first wrapped fragment of paragraph 1. Resizing the window or toggling word-wrap restores rendering because both go through a fresh non-re-entrant
setFrameSize.Verified in the bundled `TextEdit.SwiftUI` example with a ~24KB document; reverting the change reproduces the blanking.
Fix
After `super.setFrameSize`, read `frame.size` (which reflects any recursive resize) rather than the stale `newSize` parameter. When no re-entrance occurred, `frame.size == newSize`, so the common path is unchanged.