Items repeater item variance fixes#23269
Conversation
…23042) The clamp added in 08f9b15 suppressed the negative extent origin that WinUI's estimation-correction pipeline relies on, causing item overlap on fast scroll and cropped content at the top of variable-height lists. With IScrollAnchorProvider now implemented on the classic ScrollViewer, the anchor-shift path can properly compensate and the clamp is no longer needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With the StackLayout clamp removed, the repeater-internal ActualOffset of a realized item may shift while the ScrollViewer's IScrollAnchorProvider compensates to keep the anchor visually fixed. Drop the strict internal offset comparison and rely on the existing screenshot check, which is the authoritative visual invariant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two tests that guard orientation parity and extent consistency for the StackLayout.GetExtent fix: - When_FastScrollHorizontal_WithHighVarianceItems_Then_NoOverlap exercises the horizontal StackLayout path, where the clamp also applied since the fix operates on the major axis regardless of orientation. - When_ScrolledToBottom_Then_ScrollableExtentIsConsistent asserts the ScrollViewer's ExtentHeight / ScrollableHeight converge toward the real cumulative content size after scrolling through the full list, and that the last item's bottom edge aligns with the viewport bottom when scrolled to the end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The initial extent test asserted exact convergence to the real content size (VerticalOffset == ScrollableHeight after a fixed number of scroll passes). Native WinUI retains an inflated estimate during rapid scrolling so the test was flaky there. Replace with a parity-safe floor invariant: after walking through the full list, ExtentHeight must be at least as large as the true cumulative content size. This is the exact symptom the pre-fix clamp produced (under-estimated extent leaving late items unreachable) and it holds on both Skia Desktop and native WinUI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nchorProvider Ports the WinUI auto-registration hook for UIElement.CanBeScrollAnchor so an ItemsRepeater (or any other consumer that flips this property) no longer has to imperatively call IScrollAnchorProvider.Register/UnregisterAnchorCandidate on its parent scroller — the framework now does it automatically, matching WinUI's CUIElement::OnPropertyChanged dispatch (uielement.cpp:861) and CUIElement::UpdateAnchorCandidateOnParentScrollProvider (uielement.cpp:934). Move CanBeScrollAnchorProperty/CanBeScrollAnchor out of the generated UIElement.cs (which had a [Uno.NotImplemented] stub) and into UIElement.mux.cs so we can attach the PropertyChangedCallback. Also wire the OnEnter/OnLeave paths to register/unregister when an element with CanBeScrollAnchor=true enters or leaves the live tree. Also tighten ScrollViewer.IsScrollAnimationInProgress to only suppress TrimOverscroll while the wheel-driven AnchorPoint animation has more than 50ms remaining, so once the animation settles the cleanup runs and the offset is clamped to the actual ScrollableHeight rather than leaving the user overscrolled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> # Conflicts: # src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.Managed.cs
…t fluctuations (fixes #23041, #23042) Two layered fixes for the chat-style ItemsRepeater scrolling issues: 1. Track the caller-requested offset ("intent") separately from the displayed VerticalOffset/HorizontalOffset. Mirrors WinUI's m_Offset.Y vs m_ComputedOffset.Y separation in ScrollContentPresenter (ScrollContentPresenter_Partial.cpp:902 SetVerticalOffsetPrivate vs :3170 CoerceOffsets). Before this, a single ChangeView(ScrollableHeight) on a freshly-loaded high-variance repeater would land the offset in blank territory because the realization cascade clamped VO to an intermediate smaller ScrollableHeight; once extent grew back, VO had no way to follow the user's intent up. Now UpdateDimensionProperties recomputes VerticalOffset = clamp(intent, 0, ScrollableHeight) on every layout pass while a non-input-driven offset request is armed. Wheel and touch input clears the intent so user-driven scrolling never gets pushed back. The legacy TrimOverscroll only runs on actual SV viewport resize so pure realization-driven extent shrinkage no longer clamps the offset mid-scroll. 2. Auto-engage near-edge anchoring when the SV has registered anchor candidates (typically from virtualizing content that auto-flips CanBeScrollAnchor on its prepared elements) and the caller has NOT explicitly set HorizontalAnchorRatio / VerticalAnchorRatio. NaN ratios default to 0 in that case so the topmost candidate stays stable as the IR's extent.X recomputation in StackLayout.GetExtent shifts the layout origin, eliminating the visible "items reposition mid-scroll" flicker reported on high-variance content. Without this, layout-origin shifts translate directly to items moving on screen even when the user's scroll offset is steady, because no anchoring corrects for them. The default only activates when candidates are present so non-virtualizing content keeps its existing no-anchoring behavior. Adds two runtime tests under Given_ItemsRepeater_FastScroll covering the Bottom-click and wheel-monotonic scenarios. The Bottom-click test fails on Uno without these fixes and passes both with the fix and on native WinUI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
XAML Styler drift detected on
|
…rollbar drag and small wheel The auto-anchoring change in 8939f8b — defaulting NaN HorizontalAnchorRatio / VerticalAnchorRatio to 0 when the SV had registered anchor candidates — was too aggressive. Element anchoring re-picked the closest candidate on every arrange, which caused two regressions reported on the studio.live multi-template subagent markdown UI: 1. Small wheel flicks (single tick when reading content carefully) appeared to snap back to the previous scroll position. Each tick advanced the offset, but the per-arrange anchor reselection then applied a "keep anchor at the same viewport-relative position" adjustment that ate the user's small input. 2. Scrollbar thumb drag became unresponsive once the offset jumped to an unexpected position. Each Scroll event from the drag fired ChangeViewCore, but the same anchoring adjustment fought every drag tick. Restore the original IsAnchoring / ComputeAnchorPoint behavior (NaN ratios stay NaN) so anchoring only engages when the caller explicitly sets HorizontalAnchorRatio or VerticalAnchorRatio. The Bottom-click fix from 8939f8b (offset-intent tracking) is unaffected because that path doesn't depend on anchoring. Side effect: the wheel flicker on chat-style high-variance ItemsRepeater content (ItemsRepeaterVariableHeights sample) returns. That's the layout-origin recomputation in StackLayout.GetExtent producing visible item movement; tracking to be addressed in a follow-up that doesn't rely on per-arrange anchor reselection. Adds two runtime tests guarding against the regressions this revert fixes: - When_SmallWheelFlicksOnMultiTemplateContent_Then_EachAdvancesOffset - When_ScrollBarThumbDragged_Then_OffsetTracksRequestMonotonically Both use a mixed-template SUT mimicking the studio.live multi-template feed (32-200 px items cycling through 13 indices) so future regressions of this class are caught automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…licker Adds When_SlowWheelOnMultiTemplate_Then_ItemsDoNotJumpInIRLocalSpace, which tracks each realized item's IR-local Y across a series of wheel ticks. The test fails whenever StackLayout.GetExtent recomputes extent.X from the running-average element size mid-scroll (the well-defined cause of the "items reposition while scrolling slowly" flicker reported on the studio.live multi-template subagent markdown UI). Marked [Ignore] until a layout-origin stabilization fix lands that doesn't regress When_ScrolledThroughList_Then_ExtentAccommodatesRealContent — an initial stable-extent.X attempt resolved the flicker but under-estimated ExtentHeight (the unrealized leading items' running-average contribution needs to be accounted for separately when extent.X is held stable). Test left in source as the regression marker so the fix can simply remove the [Ignore] once landed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…roll flicker (fixes #23042) StackLayout.GetExtent recomputes extent.MajorStart each measure as firstY - firstIdx * averageElementSize. Because averageElementSize drifts whenever a tall item enters or leaves the 100-slot estimation buffer, the layout origin shifts by tens of pixels per measure during wheel scroll on chat-style lists with high-variance heights. The Uno SCV does not currently consume the IR's m_pendingViewportShift, so each origin shift shows up as items "jumping" in IR-local space. Hold the previously-reported MajorStart stable across measure passes and recompute extent.MajorSize against the stable origin so realized items always fit inside the IR's frame even when MakeAnchor places anchors far from the natural firstIdx*avg position. Reset to the formula value only when firstRealizedItemIndex returns to 0 (back-to-top), where the natural origin=0 invariant must be re-established to keep Source[0] flush at VerticalOffset=0. Adds FlowLayoutAlgorithm.Uno_LastMeasureDidAnchorJump so future consumers can react to MakeAnchor / disconnected-window rebuilds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the user wheel-scrolls down then back up through a high-variance list, Generate backward in FlowLayoutAlgorithm places items at algorithm-Y positions cumulatively below the previous anchor. If the anchor had been placed at the avg-estimate offset (smaller than the true cumulative height of items 0..firstIdx-1), the back-generated items end up at negative algorithm-Y. With the held stable=0 origin, those items map to IR-local Y < 0 — above the IR's frame — so the SCV cannot scroll high enough to bring them into view. VerticalOffset clamps to 0 but the first items remain off-screen, and the user has to scroll back down and try again to recover. Release the stable origin to the formula value whenever firstRealizedLayoutBounds.MajorStart drops below the held stable. The origin shifts negative, items map back to their natural IR-local positions, and scroll-to-top works on the first try. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
16b6fdd to
4bb8b8a
Compare
XAML Styler drift detected on
|
There was a problem hiding this comment.
Pull request overview
This PR targets ItemsRepeater + ScrollViewer instability with highly variable item sizes (fast scroll, scroll-to-bottom, wheel “fight”, and extent/origin drift) by improving scroll anchoring registration, offset clamping behavior, and StackLayout extent calculation, and by adding regression coverage and manual repro pages.
Changes:
- Wire
UIElement.CanBeScrollAnchorto auto-(un)register with the nearestIScrollAnchorProvideron enter/leave and property changes. - Adjust
ScrollVieweroverscroll trimming to avoid clamping against stale extents and add “offset intent” recomputation to preserve programmaticChangeViewrequests across virtualization-driven extent changes. - Stabilize StackLayout’s reported extent origin across measure passes and add targeted runtime tests + SamplesApp manual test pages for high-variance scenarios.
PR description note: the template currently says closes # without an issue number/link — please add a fully-qualified issue URL (e.g. Closes https://github.com/unoplatform/uno/issues/<id>).
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Uno.UI/UI/Xaml/UIElement.mux.cs | Implements CanBeScrollAnchor DP with callback and enter/leave registration to parent IScrollAnchorProvider. |
| src/Uno.UI/UI/Xaml/Controls/ScrollViewer/ScrollViewer.cs | Adds offset-intent tracking/recompute and refines when overscroll trimming runs. |
| src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.Managed.cs | Exposes scroll animation-in-flight detection and clears offset intents on touch/pen press. |
| src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/ScrollContentPresenter.cs | Clears offset intents on mouse wheel; updates offset intent when using direct setters. |
| src/Uno.UI/UI/Xaml/Controls/Repeater/StackLayoutState.cs | Adds state for stabilizing reported extent origin across measures. |
| src/Uno.UI/UI/Xaml/Controls/Repeater/StackLayout.cs | Uses cached/stable extent.MajorStart to prevent IR-local item jumps under high variance. |
| src/Uno.UI/UI/Xaml/Controls/Repeater/FlowLayoutAlgorithm.cs | Adds an “anchor jump happened” flag intended to coordinate extent-origin resets. |
| src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml/UIElement.cs | Removes generated CanBeScrollAnchor stubs in favor of the mux implementation. |
| src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Repeater/Given_ItemsRepeater.cs | Updates an existing repeater test to assert visual stability via screenshot rather than ActualOffset. |
| src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Repeater/Given_ItemsRepeater_FastScroll.cs | Adds new runtime regression tests for fast scroll, wheel monotonicity, extent correctness, and high-variance layouts. |
| src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Controls/Repeater/ItemsRepeaterVariableHeights.xaml(.cs) | Adds manual repro sample for high-variance item heights and scroll jumping/cropping. |
| src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Controls/Repeater/ItemsRepeaterMultiTemplate.xaml(.cs) | Adds manual repro sample for multi-template variable-height feed-like content. |
| src/SamplesApp/UITests.Shared/UITests.Shared.projitems | Includes the new sample pages in the shared SamplesApp build. |
Comments suppressed due to low confidence (1)
src/Uno.UI/UI/Xaml/Controls/ScrollViewer/ScrollViewer.cs:875
newSizeis built asnew Size(ExtentWidth, ExtentWidth), which makes the ExtentSizeChanged event report the height as the width. This should useExtentHeightfor the second component so extent-change listeners get correct dimensions (and so comparisons againstoldSizebehave correctly).
var newSize = new Size(ExtentWidth, ExtentWidth);
if (oldSize != newSize)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
WinAppSDK sync generator drift detected on
|
|
/apply-xaml-style |
|
/apply-sync-gen |
|
Pushed GitHub does not re-trigger workflows for commits pushed by the default bot, so the XAML Style Check will not automatically re-run. To turn it green, push any additional commit (for example |
|
|
|
|
WinAppSDK sync generator drift detected on
|
|
The apply workflow failed. See the workflow logs for details. |
|
🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-23269/wasm-skia-net9/index.html |
|
/apply-sync-gen |
|
|
|
Pushed GitHub does not re-trigger workflows for commits pushed by the default bot, so the WinAppSDK Sync Generator Check will not automatically re-run. To turn it green, push any additional commit (for example |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 15 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
src/Uno.UI/UI/Xaml/Controls/ScrollViewer/ScrollViewer.cs:876
UpdateDimensionPropertiesbuildsnewSizeasnew Size(ExtentWidth, ExtentWidth), which ignoresExtentHeightchanges. This will preventExtentSizeChangedfrom firing when only the height changes (common for vertical scrolling/virtualization), and can break consumers that rely on extent-size notifications. UseExtentHeightfor the height component.
var newSize = new Size(ExtentWidth, ExtentWidth);
if (oldSize != newSize)
{
|
🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-23269/wasm-skia-net9/index.html |
- Remove redundant stale-value arrange-time TrimOverscroll in ScrollViewer - Check controller.Remaining in IsScrollAnimationInProgress instead of mere AnimationController presence (stale after natural completion) - Remove dead Uno_PostAnchorJumpRefreshCountdown / Uno_LastMeasureDidAnchorJump - Exclude 3 fast-scroll repeater tests on native WinUI Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-23269/wasm-skia-net9/index.html |
GitHub Issue: Closes #23041, Closes #23042
PR Type:
What changed? 🚀
PR Checklist ✅
Screenshots Compare Test Runresults.