[render] lay out content for the visible size on macOS, instead of under legacy scroll bars - #113
Conversation
…der legacy scroll bars
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (3)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughComposeView now accounts for visible space taken by legacy macOS scroll bars. Layout may repeat, update contexts retain view bounds, and rendering uses computed visible bounds. Tests cover indicators, offsets, resizing, and deferred passes. ChangesLegacy Scroll-Bar Layout
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Change: Bug fix · Severity of issue fixed: Medium Sequence Diagram(s)sequenceDiagram
participant ComposeView
participant ContentLayout
participant AppKitScrollView
ComposeView->>ContentLayout: Layout at the current container size
ContentLayout-->>ComposeView: Return content size and overflow
ComposeView->>AppKitScrollView: Update indicators
AppKitScrollView-->>ComposeView: Report visible size after legacy scrollers
ComposeView->>ContentLayout: Relayout when scrollers reduce available space
ComposeView->>AppKitScrollView: Apply content size and offset
Merge Risk: ⚪ Minimal · up to Legacy scroll bars now reduce the macOS content area while overlay scroll bars retain the full area. No demonstrated blocker remains before merge. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change remains within UI layout and rendering, with no identified increase in privileges or crossing of a new trust boundary. The main residual risk is client compatibility with changed viewport geometry and repeated layout callbacks. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #113 +/- ##
==========================================
+ Coverage 96.41% 96.47% +0.05%
==========================================
Files 114 114
Lines 7039 7141 +102
==========================================
+ Hits 6787 6889 +102
Misses 252 252
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ca56ab0408
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ender sizes at zero, and apply scroll indicator behavior changes in the same pass
…idesScrollers off, and stop the automatic scroll indicator update when a handler changes the behavior
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5e5255ccb8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ument the automatic scroll behaviors against the visible size Also adds the AGENTS.md lesson to refuse an invalid use where it enters, instead of correcting for it downstream.
…the last row and the document origin in the legacy scroll bar tests
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 137c55cac9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…eport the current offset to every onWillLayout call
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9dfe0e33cb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e0eeabbc3f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ze, read the scale after it, and re-render when the view's size changes
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 446a3dccfd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Summary
ComposeViewstill laid out its content for its full bounds, so the bar covered the last 17 pt of the content, and content as wide as the view could scroll sideways under it.ComposeViewnow lays out its content for its size minus the space the scroll bars take, which isvisibleSize, as AppKit apps do. Render handlers and renderable updates get that area as the render bounds. With overlay scroll bars and on UIKit, the render size equals the bounds size, so nothing changes there.What changed
renderBounds()returns the content offset withvisibleSize. A render pass made for an earlier size, because it was held or waited behind another view's pass, renders for that size minus the current scroll bar space, and a follow-up pass renders the new size (renderSize(for:)).layout(_:), formerly inline inrender(_:)): with.auto, a pass that isn't a scroll decides the scroll bars from a layout for the view's full size. If a legacy bar then takes space, the content lays out again for the smaller size, and a bar also shows for any axis the content now overflows. A scroll keeps the bars and lays out once, from the cached layout.onWillLayoutruns before each layout at a new size, so it can run more than once in a pass.ComposeViewoverridestile()to ask for a layout when that changes the visible size it rendered for.ContentUpdateContext: stores the view's bounds, the content offset with the full size, instead of the render bounds, since the render size depends on the scroll bars the pass decides.render(_:): its locals are nowrenderSizeandrenderBounds, and it re-reads the offset after the will-render handler fromcontentOffset.onWillLayout. The entry from [scroll] lock ComposeView's magnification, border and automatic content insets on macOS #109 no longer says the content always lays out for its bounds.ComposeView_LegacyScrollBarsTests(new, macOS): the layout area with a vertical, a horizontal, or both legacy bars, a bar that makes the content overflow the other axis adding the second bar, content that overflows the full size but fits the smaller one keeping its bar, overlay bars laying out once, scrolling keeping the bars, resizing deciding them again,.alwaysand.manual, and in a window: scroller style changes both ways, a manual bar toggle, and no extra layout for a style change that doesn't change the visible size, a resize, or a refresh that shows a bar.ComposeView_RenderHandlerTests: with automatic scroll indicators that don't take space,onWillLayoutruns once per pass, on macOS and iOS.ComposeView_RenderOverrideTests: a held pass renders for its earlier size minus the scroll bar.ComposeView_ScrollIndicatorBehaviorTests' sub-pixel overflow test pins overlay bars, since it depended on the system setting.laidOutSizecheck fails a test, and earlier checks did the same for the scroll bar decision and the scroll rule.Review fixes
master, so the flash shows the new range.onWillLayoutcan change: the handler can adjust things like the content offset, but not the view's size, its visible size or its scroll settings, as the doc now says. ChangingscrollBehavior,scrollIndicatorBehaviororclippingBehaviorasserts and keeps the current values. A change that resizes the visible area, such as showing a legacy scroll bar or switching the scroller style, asserts, and the pass puts back the scroll bars and the scroller style, keeping the offset. Before, a handler that picked a legacy scroll bar or the scroller style from the container size made the view render forever.contentScaleFactorafter itsonWillLayoutcall, as onmaster, so a handler that changes the scale gets a layout at the new scale.tile()override, compares it along with the render bounds. A resize that leaves the visible size the same, for example together with a scroller style switch, now renders again, and the automatic scroll bar decision no longer mistakes it for a scroll.tile()is final (macOS): a subclass that resized the clip view aftersuper.tile()left the content laid out for the old visible size. The CHANGELOG entry for this PR mentions it.onWillLayout: every call reports the current offset with the size it lays out for, so a later call reflects an offset a handler set in an earlier one, and a held update reports where the view scrolled to while it was held..auto: a scroll after an app switchesscrollIndicatorBehaviorto.autonow decides the scroll indicators.layout(_:)returns whether that clamp is needed, along with the render size. The pass clamps only when the scroll bars end different from how they started, so an offset set outside the scrollable range survives a refresh that doesn't change them.autohidesScrollersstaysfalse(macOS): AppKit hiding a legacy bar on its own changes the size the content lays out for, which could make the two alternate forever. Setting it asserts and keeps it off, like the settings [scroll] lock ComposeView's magnification, border and automatic content insets on macOS #109 locked, and the CHANGELOG entry for [scroll] lock ComposeView's magnification, border and automatic content insets on macOS #109 lists it.onWillLayoutcan run before up to three layouts, and can't change the scroll settings.ScrollBehavior.autocompares the content with the visible size. Both automatic behaviors note that legacy scroll bars take space on macOS, so a scroll indicator also shows for content that overflows only the space the other bar leaves..auto, the zero-width held pass with the document at the origin, the scroll position after an unchanged refresh, with the last row or column fully rendered, and after a refresh that hides a bar, in both directions, an offset outside the scrollable range kept across an unchanged refresh, a handler's offset, the current offset in later and held will-layout calls, clamping for shorter content and for hidden bars, theautohidesScrollerslock, and the elasticity updates.Decisions
visibleSize, AppKit's own tiling, instead of computed from the scroller thickness. AppKit can draw a legacy bar inside the content insets without shrinking the clip view. Computing it would let a pass decide the bars before applying them, without the offset undo and clamp, but it needs a rule for how insets and scroll bars share space, so that refactor is planned with #108.onWillLayoutcan't change the scroll settings or the visible size. Applying a change in the same pass took a retry and guards, and each fix exposed another case: a second change in a later callback, or indicator values set along with.manual. No caller needs these changes in the layout callback, so refusing them removes the whole class. The visible size is checked by its effect instead of a list of properties, so the check covers the scroll bars, the scroller style and anything else AppKit tiles, and never fires where a change takes no space, such as scroll indicators on iOS. The pass puts back the scroll bars and the scroller style. Content insets aren't put back, since [scroll] MakeComposeView's scrolling and fitting content account for content insets #108 defines how they share space with the scroll bars.tile()is final, instead of watching the clip view's size. Watching the clip view would have covered a subclass's tiling without an API change, but the content lays out for the visible sizetile()sets, soComposeViewkeeps the tiling to itself, like the scroll view settings it locks.lastRenderBoundsstill records the render bounds, not the full bounds. A scroller style change changes the render size without changing the bounds, and the publicpreviousRenderBoundsis compared with the current render bounds.tile()override. AppKit ignores a layout request made duringlayout(): it reads backfalse, and no extra layout follows. So the override only skips re-tiles during the view's own render pass, which renders for the size they leave.autohidesScrollersis locked instead of modeled. AppKit's auto-hiding loops with.manualindicators too, so only keeping it off covers every behavior.onWillLayoutreports the current offset in every call, not the one the update was made with. This also changes what a held update's first call reports when the view scrolled while it was held: the current offset instead of the earlier one, as theRenderTypedoc describes, and where the pass renders.Not in this PR
ComposeView's scrolling and fitting content account for content insets #108:ComposeView's scrolling and fitting content don't account for content insets. Deciding the scroll bars before applying them waits for it.borderType's doc still says borders are locked because the content lays out for the view's whole bounds, which this PR changes. Borders may work now, a follow-up.Test plan
ComposeView.swiftare existing assertion messages.make formatandmake lint.One early local full macOS run had a single failing test that I couldn't identify, because I had filtered its output to the summary line. Every full run since then has passed.
Fixes #111
Summary by CodeRabbit
ComposeViewnotes to clarify legacy scroll bar and layout behavior.