Skip to content

fix(desktop): give the tutorial illustration a fixed size so Lottie stops a constraints loop - #3418

Merged
janicduplessis merged 1 commit into
mainfrom
fix/3405-desktop-constraints-loop
Oct 10, 2026
Merged

janicduplessis merged 1 commit into
mainfrom
fix/3405-desktop-constraints-loop

Conversation

@janicduplessis

@janicduplessis janicduplessis commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Stim Desktop 0.1.23 crashed (EXC_BREAKPOINT, uncaught NSException) after the main thread blocked for over 2 s. The tutorial panel was open on a tutorial workspace page. The tutorial record's step times put the stalls in the Live View step and the crash in the Agent Actions step. AppKit raised from -[NSWindow(NSDisplayCycle) _postWindowNeedsUpdateConstraints], called from NSHostingView.updateConstraints. That is the exception AppKit throws when the window keeps asking for another Update Constraints pass within one display cycle, so some view's size kept depending on its own layout pass.

The crashed build does not contain #3397. Of the Desktop changes since 0.1.21, the last build that ran without this crash, only #3383's tutorial illustration puts a LottieView whose size follows the panel's layout into the main window; every other LottieView has a fixed frame.

TutorialIllustration sized its LottieView with .resizable() inside .aspectRatio(288:168, .fit), so the panel's layout decided its size: the panel's 296 pt content width gives a 172.67 pt height. Every other LottieView in the app has a fixed integral frame. Lottie 4.6.1's SwiftUIMeasurementContainer makes that combination self-referential on macOS:

  • With the .proposed strategy (.resizable()), measureView() answers an unspecified proposal with its current bounds (L257-L305), and .aspectRatio(.fit) likely asks for that ideal size when it resolves the ratio. The view's ideal size is therefore its own last layout result.
  • layout() calls invalidateIntrinsicContentSize() whenever bounds.size differs at all from the last measured bounds (L99-L108). That makes the hosting view request another constraints pass.

airbnb/lottie-ios#2678 (open, unmerged) describes the iOS form of the same loop: repeated re-measurement from exact-equality bounds checks hangs layout. It proposes a 1 pt threshold.

Solution

Not reproduced: the crash's tutorial workspaces were already removed, and idle tests at the default frame and at the maintainer's 1920x968 frame showed no difference between old and new code. The fix rests on the mechanism above, the release diff, and the crash evidence. There is no pure decision to unit test.

Give the illustration a fixed 288 x 168 frame, the artwork's own size, in place of the aspect-fit sizing. SwiftUI then proposes a constant size, the container never reports its own bounds as its ideal size, and its bounds stop changing with the panel's layout. The panel has 296 pt available, so the art is now 8 pt narrower than before and centered.

Fixes #3405

Test plan

  • stim macos from apps/desktop with the tutorial seeded at step 1 in the dev build's own defaults: the panel opens and the illustration renders and animates at 288 x 168.

    Tutorial panel with the fixed-size illustration

  • scripts/format.sh --check, node scripts/check-ascii.mjs, swift build -c release -Xswiftc -warnings-as-errors, swift test -Xswiftc -warnings-as-errors.

@janicduplessis janicduplessis added the area: desktop apps/desktop label Oct 10, 2026
@janicduplessis

Copy link
Copy Markdown
Collaborator Author

Fresh review of a28c757 against #3405: clear, no blocking findings. Labelled review: ai-approved. No review: needs-human category is touched.

Correctness

  • .frame(width: 288, height: 168) fits the panel. TutorialPanel is pinned to WorkspaceDetail.inspectorWidth (320), and the illustration has Space.lg (12) horizontal padding, which leaves 296. The outer VStack is center-aligned, so the art is centered as the description says. The static fixture path (Image.resizable()) and TutorialArt.still both render at Self.size, so they now match the frame exactly.
  • The Lottie 4.6.1 references check out. In SwiftUIMeasurementContainer.layout() (L99-L108), bounds.size != latestMeasurementBoundsSize is an exact equality check that calls invalidateIntrinsicContentSize(). Under .proposed, measureView() returns proposedSize.replacingNoIntrinsicMetric(with: bounds.size). The function is declared at L256, so the linked range starts one line into the body, which does not matter. With a fixed frame, the proposal is a constant 288x168 and the bounds stop following the panel's layout.
  • Another mechanism supports the same fix. The old aspect-fit height was 296 * 168/288 = 172.67 pt, which is not integral. Pixel alignment can make bounds.size differ from the last measured size on each pass even without the ideal-size feedback. An integral fixed frame removes both paths. I did not verify whether SwiftUI's .aspectRatio(.fit) asks the child for its ideal size under a width-only proposal, so treat the "asks for exactly that ideal size" sentence as likely rather than confirmed.

Release diff 0.1.21..0.1.23 (other candidates)

  • Every other LottieView has a fixed integral frame: Jar 128x206, Components 111x180, SetupIllustrations 84x136, TutorialTrack 40x40 (new in this range), and StimBuildAnimation 56x84, which its caller in DeviceTile sets.
  • The other new layout code is either an overlay or Text constrained by width. Overlays do not feed size back into their parent. That covers TutorialViewerCard (fixed 260 pt width, inside an overlay), TutorialGlow (TimelineView, inside an overlayPreferenceValue, gone after about 4.6 s), and the .fixedSize(horizontal: false, vertical: true) on Text in TutorialCompletionCard. I found no other self-sizing AppKit view.

Nits (non-blocking, description only)

Checks

  • The diff is one ASCII line with no new comments.
  • CI is pending at review time, so merge waits for green.

@janicduplessis janicduplessis added the review: ai-approved A fresh AI review found nothing left to fix label Oct 10, 2026
@janicduplessis
janicduplessis marked this pull request as ready for review October 10, 2026 23:00
@janicduplessis
janicduplessis merged commit 027a9b7 into main Oct 10, 2026
4 checks passed
@janicduplessis
janicduplessis deleted the fix/3405-desktop-constraints-loop branch October 10, 2026 23:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: desktop apps/desktop review: ai-approved A fresh AI review found nothing left to fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Desktop crashes with an Update Constraints loop on a tutorial workspace page

1 participant