Skip to content

Commit

Permalink
Merge pull request #19287 from unoplatform/dev/doti/uniform-grid-layo…
Browse files Browse the repository at this point in the history
…ut-winui-fixes
  • Loading branch information
MartinZikmund authored Jan 22, 2025
2 parents 7f57344 + 2489661 commit f09c944
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 45 deletions.
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

0 comments on commit f09c944

Please sign in to comment.