Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: UniformGridLayout and ItemsRepeater in nested layout #19287

Merged
merged 3 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 68 additions & 8 deletions src/Uno.UI/Microsoft/UI/Xaml/Controls/Repeater/ItemsRepeater.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
// ItemsRepeater.cpp, commit 1cf9f1c
// ItemsRepeater.cpp, commit 3f3e328

#pragma warning disable 105 // remove when moving to WinUI tree

Expand Down Expand Up @@ -95,17 +95,16 @@ internal Point LayoutOrigin
// Value is different from null only while we are on the OnItemsSourceChanged call stack.
NotifyCollectionChangedEventArgs m_processingItemsSourceChange;

Size m_lastAvailableSize;
bool m_isLayoutInProgress;
// The value of _layoutOrigin is expected to be set by the layout
// The value of m_layoutOrigin is expected to be set by the layout
// when it gets measured. It should not be used outside of measure.
Point m_layoutOrigin;

// Loaded events fire on the first tick after an element is put into the tree
// while unloaded is posted on the UI tree and may be processed out of sync with subsequent loaded
// events. We keep these counters to detect out-of-sync unloaded events and take action to rectify.
int _loadedCounter;
int _unloadedCounter;
int m_loadedCounter;
int m_unloadedCounter;

// Used to avoid layout cycles with StackLayout layouts where variable sized children prevent
// the ItemsRepeater's layout to settle.
Expand All @@ -122,6 +121,9 @@ internal Point LayoutOrigin
// Solution: Have flag that is only true when DataTemplate exists but it is empty.
bool m_isItemTemplateEmpty;

// Tracks the global scale factor so that children can be re-measured when
// it changes, for example when moving the app to another screen.
double m_layoutRoundFactor;

public ItemsRepeater()
{
Expand Down Expand Up @@ -269,7 +271,6 @@ protected override Size MeasureOverride(Size availableSize)
}

m_viewportManager.SetLayoutExtent(extent);
m_lastAvailableSize = availableSize;
return desiredSize;
}

Expand Down Expand Up @@ -618,7 +619,7 @@ void OnLoaded(object sender, RoutedEventArgs args)
m_viewportManager.ResetScrollers();
}

++_loadedCounter;
++m_loadedCounter;

#if HAS_UNO
// Uno specific: If the control was unloaded but is loaded again, reattach Layout and DataSource events
Expand Down Expand Up @@ -646,7 +647,7 @@ void OnLoaded(object sender, RoutedEventArgs args)
private void OnUnloaded(object sender, RoutedEventArgs args)
{
_stackLayoutMeasureCounter = 0u;
++_unloadedCounter;
++m_unloadedCounter;

#if !HAS_UNO // Avoids leak and useless as we are not validating such count in the loaded
// Only reset the scrollers if this unload event is in-sync.
Expand Down Expand Up @@ -923,6 +924,42 @@ void OnItemsSourceViewChanged(object sender, NotifyCollectionChangedEventArgs ar

void InvalidateMeasureForLayout(Layout sender, object args)
{
if (UseLayoutRounding)
{
if (XamlRoot is { } xamlRoot)

{
double layoutRoundFactor = xamlRoot.RasterizationScale;

if (layoutRoundFactor != m_layoutRoundFactor)
{
if (m_layoutRoundFactor != 0.0)
{
// Invoke InvalidateMeasure for all children owned by the layout so that they
// get re-measured using the new global scale factor.
// Otherwise they keep using their old DesiredSize based on the old factor
// which may be slightly different.
// This could have unwanted effects, like StackLayoutState::m_areElementsMeasuredRegular
// being incorrectly set to False in the StackLayout case.
InvalidateChildrenMeasure();
}

// ItemsRepeater has its own m_layoutRoundFactor field to avoid:
// - the need for a new public ItemsRepeater API,
// - the need for an internal ItemsRepeater/Layout communication.
m_layoutRoundFactor = layoutRoundFactor;
}
}
else
{
m_layoutRoundFactor = 0.0;
}
}
else
{
m_layoutRoundFactor = 0.0;
}

InvalidateMeasure();
}

Expand All @@ -931,6 +968,29 @@ void InvalidateArrangeForLayout(Layout sender, object args)
InvalidateArrange();
}

void InvalidateChildrenMeasure()
{
//ITEMSREPEATER_TRACE_INFO(*this, TRACE_MSG_METH, METH_NAME, this);

var children = Children;
var childrenCount = children.Count;

for (var childIndex = 0; childIndex < childrenCount; childIndex++)
{
if (children[childIndex] is { } element)
{
if (GetVirtualizationInfo(element) is { } virtInfo)

{
if (virtInfo.Owner == ElementOwner.Layout)
{
element.InvalidateMeasure();
}
}
}
}
}

private VirtualizingLayoutContext GetLayoutContext()
{
if (m_layoutContext == null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
// UniformGridLayout.cpp, commit 1c07867
// UniformGridLayout.cpp, commit 3f3e328

using System;
using System.Collections.Specialized;
Expand Down Expand Up @@ -139,17 +139,17 @@ FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForRealiza
var gridState = GetAsGridState(context.LayoutState);
var lastExtent = gridState.FlowAlgorithm().LastExtent;
uint itemsPerLine = GetItemsPerLine(availableSize, context);
double majorSize = (itemsCount / itemsPerLine) * (double)(GetMajorSizeWithSpacing(context));
double majorSize = GetMajorSize(itemsCount, itemsPerLine, GetMajorItemSizeWithSpacing(context));
double realizationWindowStartWithinExtent =
(double)(MajorStart(realizationRect) - MajorStart(lastExtent));
if ((realizationWindowStartWithinExtent + MajorSize(realizationRect)) >= 0 &
MajorStart(realizationRect) - MajorStart(lastExtent);
if ((realizationWindowStartWithinExtent + MajorSize(realizationRect)) >= 0.0f &
realizationWindowStartWithinExtent <= majorSize)
{
double offset = Math.Max(0.0f, MajorStart(realizationRect) - MajorStart(lastExtent));
int anchorRowIndex = (int)(offset / GetMajorSizeWithSpacing(context));
int anchorLineIndex = (int)(offset / GetMajorItemSizeWithSpacing(context));

anchorIndex = (int)Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine));
bounds = GetLayoutRectForDataIndex(availableSize, (uint)anchorIndex, lastExtent, context);
anchorIndex = (int)Math.Max(0, Math.Min(itemsCount - 1, anchorLineIndex * itemsPerLine));
bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context);
}
}

Expand All @@ -165,23 +165,28 @@ FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForTargetE
Size availableSize,
VirtualizingLayoutContext context)
{
int index = -1;
double offset = double.NaN;
int count = context.ItemCount;
if (targetIndex >= 0 & targetIndex < count)
{
uint itemsPerLine = GetItemsPerLine(availableSize, context);
var indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine;
index = (int)indexOfFirstInLine;
var state = GetAsGridState(context.LayoutState);
offset = MajorStart(GetLayoutRectForDataIndex(availableSize, (uint)index,
state.FlowAlgorithm().LastExtent, context));
// The anchor index returned is NOT the first index in the targetIndex's line. It is the targetIndex
// itself, in order to stay consistent with the ElementManager::DiscardElementsOutsideWindow method
// which keeps a single element prior to the realization window. If the first index in the targetIndex's
// line were used as the anchor, it would be discarded and re-recreated in an infinite loop.
var gridState = GetAsGridState(context.LayoutState);
double offset = MajorStart(GetLayoutRectForDataIndex(availableSize, targetIndex,
gridState.FlowAlgorithm().LastExtent, context));

return new FlowLayoutAnchorInfo
(
targetIndex,
offset
);
}

return new FlowLayoutAnchorInfo
(
index,
offset
-1,
double.NaN
);
}

Expand All @@ -200,35 +205,38 @@ Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent(
var extent = new Rect();

// Constants
uint itemsCount = (uint)context.ItemCount;
var availableSizeMinor = Minor(availableSize);
int itemsCount = context.ItemCount;
double availableSizeMinor = Minor(availableSize);

uint itemsPerLine =
Math.Min( // note use of uint s
Math.Max(1u, availableSizeMinor.IsFinite()
? (uint)((availableSizeMinor + MinItemSpacing()) / GetMinorSizeWithSpacing(context))
: itemsCount),
? (uint)((availableSizeMinor + MinItemSpacing()) / GetMinorItemSizeWithSpacing(context))
: (uint)itemsCount),
Math.Max(1u, m_maximumRowsOrColumns));
float lineSize = GetMajorSizeWithSpacing(context);
float lineSize = GetMajorItemSizeWithSpacing(context);

if (itemsCount > 0)
{
// Only use all of the space if item stretch is fill, otherwise size layout according to items placed
SetMinorSize(ref extent,
availableSizeMinor.IsFinite() && m_itemsStretch == UniformGridLayoutItemsStretch.Fill
? availableSizeMinor
: Math.Max(0.0f, itemsPerLine * GetMinorSizeWithSpacing(context) - (float)(MinItemSpacing())));
SetMajorSize(ref extent, Math.Max(0.0f, (itemsCount / itemsPerLine) * lineSize - (float)(LineSpacing())));
: Math.Max(0.0f, itemsPerLine * GetMinorItemSizeWithSpacing(context) - (float)(MinItemSpacing())));
SetMajorSize(ref extent, GetMajorSize(itemsCount, itemsPerLine, lineSize));

if (firstRealized is { })
{
_Tracing.MUX_ASSERT(lastRealized is { });

SetMajorStart(ref extent, MajorStart(firstRealizedLayoutBounds) -
(firstRealizedItemIndex / itemsPerLine) * lineSize);
var remainingItems = itemsCount - lastRealizedItemIndex - 1;
var remainingItemsOnLastRealizedLine = Math.Min(itemsCount - lastRealizedItemIndex - 1, (int)(itemsPerLine - ((lastRealizedItemIndex + 1) % itemsPerLine)));
int remainingItems = itemsCount - lastRealizedItemIndex - 1 - remainingItemsOnLastRealizedLine;
float remainingItemsMajorSize = GetMajorSize(remainingItems, itemsPerLine, lineSize);
SetMajorSize(ref extent, MajorEnd(lastRealizedLayoutBounds) - MajorStart(extent) +
(remainingItems / itemsPerLine) * lineSize);
(remainingItemsMajorSize > 0.0f ? ((float)LineSpacing() +
remainingItemsMajorSize) : 0.0f));
}
else
{
Expand Down Expand Up @@ -319,14 +327,36 @@ void OnPropertyChanged(DependencyPropertyChangedEventArgs args)

private uint GetItemsPerLine(Size availableSize, VirtualizingLayoutContext context)
{
uint itemsPerLine = Math.Min( // note use of unsigned ints
Math.Max(1u, (uint)((Minor(availableSize) + MinItemSpacing()) / GetMinorSizeWithSpacing(context))),
Math.Max(1u, m_maximumRowsOrColumns));
double availableSizeMinor = Minor(availableSize);
uint maximumRowsOrColumns = Math.Max(1u, m_maximumRowsOrColumns);

if (availableSizeMinor.IsFinite())
{
return Math.Min(
(uint)((availableSizeMinor + MinItemSpacing()) / GetMinorItemSizeWithSpacing(context)),
maximumRowsOrColumns);
}

return itemsPerLine;
return maximumRowsOrColumns;
}

float GetMinorSizeWithSpacing(VirtualizingLayoutContext context)
float GetMajorSize(int itemsCount, uint itemsPerLine, float majorItemSizeWithSpacing)
{
_Tracing.MUX_ASSERT(itemsPerLine > 0);

int fullLinesCount = itemsCount / (int)itemsPerLine;
int partialLineCount = (itemsCount % itemsPerLine) == 0 ? 0 : 1;
int totalLinesCount = fullLinesCount + partialLineCount;

if (totalLinesCount > 0)
{
return totalLinesCount * majorItemSizeWithSpacing - (float)(LineSpacing());
}

return 0.0f;
}

float GetMinorItemSizeWithSpacing(VirtualizingLayoutContext context)
{
var minItemSpacing = MinItemSpacing();
var gridState = GetAsGridState(context.LayoutState);
Expand All @@ -335,7 +365,7 @@ float GetMinorSizeWithSpacing(VirtualizingLayoutContext context)
: (float)(gridState.EffectiveItemHeight() + minItemSpacing);
}

float GetMajorSizeWithSpacing(VirtualizingLayoutContext context)
float GetMajorItemSizeWithSpacing(VirtualizingLayoutContext context)
{
var lineSpacing = LineSpacing();
var gridState = GetAsGridState(context.LayoutState);
Expand All @@ -346,18 +376,18 @@ float GetMajorSizeWithSpacing(VirtualizingLayoutContext context)

Rect GetLayoutRectForDataIndex(
Size availableSize,
uint index,
int index,
Rect lastExtent,
VirtualizingLayoutContext context)
{
uint itemsPerLine = GetItemsPerLine(availableSize, context);
uint rowIndex = index / itemsPerLine;
uint indexInRow = index - (rowIndex * itemsPerLine);
int lineIndex = index / (int)itemsPerLine;
int indexInLine = index - (lineIndex * (int)itemsPerLine);

var gridState = GetAsGridState(context.LayoutState);
Rect bounds = MinorMajorRect(
indexInRow * GetMinorSizeWithSpacing(context) + MinorStart(lastExtent),
rowIndex * GetMajorSizeWithSpacing(context) + MajorStart(lastExtent),
indexInLine * GetMinorItemSizeWithSpacing(context) + MinorStart(lastExtent),
lineIndex * GetMajorItemSizeWithSpacing(context) + MajorStart(lastExtent),
ScrollOrientation == ScrollOrientation.Vertical
? (float)(gridState.EffectiveItemWidth())
: (float)(gridState.EffectiveItemHeight()),
Expand Down
Loading