Skip to content

[scroll] lock ComposeView's magnification, border and automatic content insets on macOS - #109

Merged
honghaoz merged 2 commits into
masterfrom
scroll/lock-compose-view-settings
Oct 1, 2026
Merged

honghaoz merged 2 commits into
masterfrom
scroll/lock-compose-view-settings

Conversation

@honghaoz

@honghaoz honghaoz commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • On macOS, ComposeView now keeps automaticallyAdjustsContentInsets and allowsMagnification off, magnification, minMagnification and maxMagnification at 1, and borderType at .noBorder. Changing them asserts and keeps the value, and the animator can't magnify the view either.
  • renderBounds(), which runs on every render pass, drops the border switch and the magnification division.
  • iOS, tvOS and visionOS keep contentInsetAdjustmentBehavior adjustable, with .never as the default, as before.

What changed

  • Locks (macOS): final overrides on ComposeView for automaticallyAdjustsContentInsets, allowsMagnification, magnification, setMagnification(_:centeredAt:), magnify(toFit:) and borderType. A probe showed that AppKit never calls these setters itself, during setup, in a window, on a resize or while tiling, so the assertions fire only on a caller's change. It also showed that programmatic magnification works without allowsMagnification, and that each of the three magnification methods changes the magnification on its own, so all three are overridden.
  • Magnification range (macOS): through an NSScrollView reference, the animator's magnify(toFit:) and setMagnification(_:centeredAt:) change the magnification without calling the overrides (found in review: a 100×100 view ended at 0.25, showing a 400×400 area while its content laid out for 100×100). AppKit clamps every way of magnifying to minMagnification and maxMagnification, so both are set to 1 and locked the same way. AppKit never calls their setters itself and doesn't read them while scrolling. The animator's magnify(toFit:) still scrolls to center the rect, which lays out and renders like any other scroll.
  • renderBounds(): sizes the content from the bounds alone.
  • Tests:
    • New macOS tests check the assertion, that the value stays, and the effect: the content insets under a title bar and toolbar stay 0, the visible size keeps the view's size under each magnifying API, and the clip view still fills the view after each border. A mutation pass that let each lock change AppKit's value failed every test on the effect.
    • test_magnification_animator_staysOne calls both animator methods through an NSScrollView reference, waits for each animation to finish, and checks that the magnification stays 1, the clip view keeps the view's size, and the rendered rows are the visible ones at full width. Without the range lock, it fails the way the review described.
    • A new iOS test: with .automatic set by the caller, a compose view in a navigation controller is inset by the bar, and the rendered rows match the visible area.
    • In ComposeView+RenderBoundsTests, the three tests that existed only for borders are removed. The legacy scrollers test keeps its content inset part without the border and magnification, and the scaled-bounds test uses a fractional frame instead of a border, covering both rounding directions at 1x and 2x.
  • CHANGELOG: a breaking-change entry for the locks. The tile() entry no longer mentions the border.

Decisions

Both are recorded on #103.

  • iOS stays adjustable. UIKit keeps the caller's contentInset separate from the safe area adjustment, so ScrollView's contract holds there. On macOS, AppKit writes its adjustment into the same contentInsets the caller sets: in a probe, it replaced a caller's top inset of 10 with its own 32 when the toolbar hid. With automatic insets on iOS, layout and rendering work. The remaining gap, content that fits the bounds ignoring insets, also affects insets set by the caller on both platforms, so it's tracked in [scroll] Make ComposeView's scrolling and fitting content account for content insets #108.
  • No guards for the document view or the clip view. Removing or replacing them already fails at the next render pass. A guard would only turn that into an assertion in debug and a silent no-op in release, couldn't cover the clip view's own documentView setter, and would make contentView final. It would cost about 0.2 µs per scrolled frame.

Test plan

  • Full macOS suite: 1,398 tests, 0 failures.
  • Full iOS suite: 1,306 tests, 0 failures.
  • Coverage: every line of the locks and of renderBounds() runs under the tests.
  • make format and make lint.
  • CI on all four platforms for the latest commit: macOS, iOS, tvOS and visionOS pass, along with Lint. tvOS and visionOS aren't installed locally.

Fixes #103

Summary by CodeRabbit

  • Behavior Changes

    • On macOS, ComposeView lays out content across its full bounds; borders and magnification no longer reduce the render area.
    • Automatic content insets, magnification, and borders are unsupported on macOS. Attempts to change these settings trigger an assertion and leave them at their supported defaults.
    • Place accessory views outside ComposeView rather than within its bounds.
  • Documentation

    • Updated the breaking-change notes to describe the macOS behavior and layout boundaries.

…nt insets on macOS

ComposeView's render bounds divided by the magnification and subtracted the
border, and AppKit's automatic content insets wrote the window's title bar
and toolbar overlap into the same `contentInsets` a caller sets, replacing
the caller's value whenever the window's chrome changed. ComposeView never
needed any of them, so they're now locked on macOS:
`automaticallyAdjustsContentInsets` and `allowsMagnification` stay `false`,
`magnification` stays 1, including through `setMagnification(_:centeredAt:)`
and `magnify(toFit:)`, and `borderType` stays `.noBorder`. Changing them
asserts and keeps the value, and the overrides are final. `renderBounds()`
drops the border switch and the magnification division.

iOS, tvOS and visionOS keep `contentInsetAdjustmentBehavior` adjustable,
with `.never` as the default: UIKit keeps the caller's `contentInset`
separate from the safe area, and layout and rendering work with automatic
insets. Fitting content ignoring insets is tracked in #108.

The document view and the clip view aren't guarded: removing or replacing
them already fails at the next render pass, and a guard would only turn
that into an assertion, at a small cost on every read.

The render-bounds tests that existed only for borders are removed, and the
ones that also covered scrollers or scaled bounds keep those parts.

Fixes #103.
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: ce43301c-704e-4c9c-a08e-6ec96441783b

📥 Commits

Reviewing files that changed from the base of the PR and between d6cbf33 and aa1e03c.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • ComposeUI/Sources/ComposeUI/ComposeView/ComposeView.swift
  • ComposeUI/Tests/ComposeUITests/ComposeView/ComposeView+RenderBoundsTests.swift
  • ComposeUI/Tests/ComposeUITests/ComposeView/ComposeViewTests.swift

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

On AppKit, ComposeView fixes automatic content-inset adjustment, magnification, and borders at supported values. Render bounds now use the view bounds. Updated tests cover these settings, render sizing, and iOS automatic inset behavior.

Changes

ComposeView scroll behavior

Layer / File(s) Summary
Constrain AppKit scroll settings
ComposeUI/Sources/ComposeUI/ComposeView/ComposeView.swift, ComposeUI/Tests/ComposeUITests/ComposeView/ComposeViewTests.swift, CHANGELOG.md
AppKit ComposeView rejects attempts to enable automatic content-inset adjustment, magnification, or borders. Tests check that these settings retain their supported values, and that iOS automatic inset adjustment remains enabled. The changelog documents the settings and updated tiling bounds.
Calculate render bounds from view bounds
ComposeUI/Sources/ComposeUI/ComposeView/ComposeView.swift, ComposeUI/Tests/ComposeUITests/ComposeView/ComposeView+RenderBoundsTests.swift
AppKit render bounds use the view bounds rounded to backing pixels and use contentOffset as the origin. Updated tests cover insets, scrollers, and fractional frame sizes; the test for a view smaller than its groove border is removed.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~12 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to aa1e0

No concrete merge-blocking issue is established. The AppKit restrictions and bounds-based sizing are consistent with the documented behavior; merge after normal platform checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to aa1e0

The change restricts customization rather than granting new access or authority. The main design risks are compatibility with downstream subclasses and reliance on AppKit preserving its default geometry settings. No introduced security vulnerability was established.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated exposure is local view configuration by code running in the host application. Rejected calls affect view geometry, while the default DEBUG assertion handler can terminate the host process. The inspected paths do not demonstrate expanded tenant, service, credential or data-store authority.

Trust Boundaries and Controls

  • inferred — The changed public APIs narrow caller and subclass control over ComposeView geometry. Their examined implementations reject configuration rather than transfer identity or privileges across a trust boundary. No attacker-controlled route to a sensitive sink was established within this scope.

Resilience and Maintainability Implications

  • inferred — For already-valid superclass state, repeated rejected magnification and border calls have no forwarded mutation to roll back, and automatic-inset calls restore false after a returning assertion handler. This supports containment of ordinary invalid calls, but does not prove recovery from AppKit-internal state changes or arbitrary reentrant handlers.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning PR #109 implements the macOS locks for automatic content insets, magnification, and borders. It simplifies macOS renderBounds() and updates related tests and the changelog. Issue #103 also requires … Implement the iOS automatic-inset lock with assertion and value preservation. Add the required ScrollView document-view and clip-view guards, the ComposeView document-view replacement guard, and tests for each required assertion and pre…
Docstring Coverage ⚠️ Warning Docstring coverage is 23.53% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 3 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The reported changes stay within issue #103. They lock the specified macOS properties, simplify renderBounds(), update affected tests, document the breaking behavior, and add platform behavior cover…
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: locking macOS ComposeView magnification, border, and automatic content insets.
Full details: Linked Issues check

Explanation

PR #109 implements the macOS locks for automatic content insets, magnification, and borders. It simplifies macOS renderBounds() and updates related tests and the changelog. Issue #103 also requires locking automatic inset adjustment on iOS, but the PR leaves contentInsetAdjustmentBehavior caller-adjustable. Issue #103 requires guards for dropping or replacing the document view and replacing the clip view, plus tests for those guards. The PR does not add those guards or tests.

Resolution

Implement the iOS automatic-inset lock with assertion and value preservation. Add the required ScrollView document-view and clip-view guards, the ComposeView document-view replacement guard, and tests for each required assertion and preserved value.

Full details: Docstring Coverage

Explanation

Docstring coverage is 23.53% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 3 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.41%. Comparing base (d6cbf33) to head (9690b0f).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #109      +/-   ##
==========================================
+ Coverage   96.40%   96.41%   +0.01%     
==========================================
  Files         114      114              
  Lines        7000     7026      +26     
==========================================
+ Hits         6748     6774      +26     
  Misses        252      252              
Flag Coverage Δ
ComposeUI 96.41% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...UI/Sources/ComposeUI/ComposeView/ComposeView.swift 98.34% <100.00%> (+0.06%) ⬆️

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

… can't magnify it either

Through an `NSScrollView` reference, the animator's `magnify(toFit:)` and
`setMagnification(_:centeredAt:)` change the magnification without calling
ComposeView's overrides, so a 100×100 view ended at a magnification of 0.25,
showing a 400×400 area while its content laid out for 100×100. AppKit clamps
every way of magnifying to `minMagnification` and `maxMagnification`, so
ComposeView now sets both to 1 and locks them like the magnification:
changing either asserts and keeps 1. AppKit doesn't read either while
scrolling.

The animator's `magnify(toFit:)` still scrolls to center the rect, which
lays out and renders like any other scroll.
@honghaoz
honghaoz enabled auto-merge (squash) October 1, 2026 09:41
@honghaoz
honghaoz merged commit 946b884 into master Oct 1, 2026
6 checks passed
@honghaoz
honghaoz deleted the scroll/lock-compose-view-settings branch October 1, 2026 09:53
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.

[scroll] Lock ComposeView's automatic insets, magnification, border, and document view

1 participant