Skip to content

Items repeater item variance fixes#23269

Merged
MartinZikmund merged 18 commits into
masterfrom
dev/mazi/repeaterfixscroll
May 14, 2026
Merged

Items repeater item variance fixes#23269
MartinZikmund merged 18 commits into
masterfrom
dev/mazi/repeaterfixscroll

Conversation

@MartinZikmund
Copy link
Copy Markdown
Member

@MartinZikmund MartinZikmund commented May 14, 2026

GitHub Issue: Closes #23041, Closes #23042

PR Type:

What changed? 🚀

PR Checklist ✅

MartinZikmund and others added 10 commits May 14, 2026 10:03
…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>
Copilot AI review requested due to automatic review settings May 14, 2026 08:04
@github-actions github-actions Bot added the area/automation Categorizes an issue or PR as relevant to project automation label May 14, 2026
@github-actions
Copy link
Copy Markdown
Contributor

XAML Styler drift detected on 16b6fdd

XAML files under src/SamplesApp/ are not formatted according to src/SamplesApp/Settings.XamlStyler.

To apply automatically (PRs from branches in this repo only): comment /apply-xaml-style on this PR and a bot will push chore: Apply XAML styler to the head branch.

To apply manually (required for PRs from forks):

  1. Download the xaml-style-patch artifact from this run.
  2. In your local checkout of the PR branch:
    git apply xaml-style.patch
    git add -A
    git commit -m "chore: Apply XAML styler"
    git push
    

Or re-run the formatter locally: dotnet tool restore && dotnet xstyler -d src/SamplesApp -r.

DerekRaven and others added 5 commits May 14, 2026 10:09
…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>
@MartinZikmund MartinZikmund force-pushed the dev/mazi/repeaterfixscroll branch from 16b6fdd to 4bb8b8a Compare May 14, 2026 08:12
@github-actions
Copy link
Copy Markdown
Contributor

XAML Styler drift detected on 4bb8b8a

XAML files under src/SamplesApp/ are not formatted according to src/SamplesApp/Settings.XamlStyler.

To apply automatically (PRs from branches in this repo only): comment /apply-xaml-style on this PR and a bot will push chore: Apply XAML styler to the head branch.

To apply manually (required for PRs from forks):

  1. Download the xaml-style-patch artifact from this run.
  2. In your local checkout of the PR branch:
    git apply xaml-style.patch
    git add -A
    git commit -m "chore: Apply XAML styler"
    git push
    

Or re-run the formatter locally: dotnet tool restore && dotnet xstyler -d src/SamplesApp -r.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.CanBeScrollAnchor to auto-(un)register with the nearest IScrollAnchorProvider on enter/leave and property changes.
  • Adjust ScrollViewer overscroll trimming to avoid clamping against stale extents and add “offset intent” recomputation to preserve programmatic ChangeView requests 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

  • newSize is built as new Size(ExtentWidth, ExtentWidth), which makes the ExtentSizeChanged event report the height as the width. This should use ExtentHeight for the second component so extent-change listeners get correct dimensions (and so comparisons against oldSize behave correctly).
			var newSize = new Size(ExtentWidth, ExtentWidth);
			if (oldSize != newSize)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Uno.UI/UI/Xaml/Controls/ScrollViewer/ScrollViewer.cs Outdated
Comment thread src/Uno.UI/UI/Xaml/Controls/Repeater/StackLayoutState.cs Outdated
Comment thread src/Uno.UI/UI/Xaml/Controls/Repeater/FlowLayoutAlgorithm.cs Outdated
@github-actions
Copy link
Copy Markdown
Contributor

WinAppSDK sync generator drift detected on 16b6fdd

The Uno.WinAppSDKSyncGenerator produced changes that are not committed on this PR.

To apply automatically (PRs from branches in this repo only): comment /apply-sync-gen on this PR and a bot will push chore: Sync generator run to the head branch.

To apply manually (required for PRs from forks):

  1. Download the sync-generator-patch artifact from this run.
  2. In your local checkout of the PR branch:
    git apply sync-generator.patch
    git add -A
    git commit -m "chore: Sync generator run"
    git push
    

Or re-run the generator locally on Windows via build\run-api-sync-tool.cmd.

@MartinZikmund
Copy link
Copy Markdown
Member Author

/apply-xaml-style

@MartinZikmund
Copy link
Copy Markdown
Member Author

/apply-sync-gen

@github-actions
Copy link
Copy Markdown
Contributor

Pushed chore: Apply XAML styler to this PR branch.

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 git commit --allow-empty -m "ci: re-run") or ask a maintainer to re-run the check.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 14, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 3 committers have signed the CLA.

✅ MartinZikmund
❌ DerekRaven
❌ github-actions[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ MartinZikmund
❌ DerekRaven
You have signed the CLA already but the status is still pending? Let us recheck it.

@github-actions
Copy link
Copy Markdown
Contributor

WinAppSDK sync generator drift detected on 4bb8b8a

The Uno.WinAppSDKSyncGenerator produced changes that are not committed on this PR.

To apply automatically (PRs from branches in this repo only): comment /apply-sync-gen on this PR and a bot will push chore: Sync generator run to the head branch.

To apply manually (required for PRs from forks):

  1. Download the sync-generator-patch artifact from this run.
  2. In your local checkout of the PR branch:
    git apply sync-generator.patch
    git add -A
    git commit -m "chore: Sync generator run"
    git push
    

Or re-run the generator locally on Windows via build\run-api-sync-tool.cmd.

@github-actions
Copy link
Copy Markdown
Contributor

The apply workflow failed. See the workflow logs for details.

@unodevops
Copy link
Copy Markdown
Contributor

🤖 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

@MartinZikmund
Copy link
Copy Markdown
Member Author

/apply-sync-gen

@unodevops
Copy link
Copy Markdown
Contributor

⚠️⚠️ The build 212557 has failed on Uno.UI - CI.

@github-actions
Copy link
Copy Markdown
Contributor

Pushed chore: Sync generator run to this PR branch.

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 git commit --allow-empty -m "ci: re-run") or ask a maintainer to re-run the check.

Copilot AI review requested due to automatic review settings May 14, 2026 09:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • UpdateDimensionProperties builds newSize as new Size(ExtentWidth, ExtentWidth), which ignores ExtentHeight changes. This will prevent ExtentSizeChanged from firing when only the height changes (common for vertical scrolling/virtualization), and can break consumers that rely on extent-size notifications. Use ExtentHeight for the height component.

			var newSize = new Size(ExtentWidth, ExtentWidth);
			if (oldSize != newSize)
			{

Comment thread src/Uno.UI/UI/Xaml/Controls/Repeater/StackLayoutState.cs Outdated
Comment thread src/Uno.UI/UI/Xaml/Controls/Repeater/FlowLayoutAlgorithm.cs Outdated
Comment thread src/Uno.UI/UI/Xaml/Controls/ScrollViewer/ScrollViewer.cs
@unodevops
Copy link
Copy Markdown
Contributor

🤖 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>
@unodevops
Copy link
Copy Markdown
Contributor

🤖 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

@MartinZikmund MartinZikmund enabled auto-merge May 14, 2026 13:23
@MartinZikmund MartinZikmund merged commit 8771907 into master May 14, 2026
45 checks passed
@MartinZikmund MartinZikmund deleted the dev/mazi/repeaterfixscroll branch May 14, 2026 13:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/automation Categorizes an issue or PR as relevant to project automation

Projects

None yet

6 participants