diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/Repeater/ItemsRepeater.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/Repeater/ItemsRepeater.cs index 42c3c2002907..2ce70062dfc7 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/Repeater/ItemsRepeater.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/Repeater/ItemsRepeater.cs @@ -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 @@ -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. @@ -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() { @@ -269,7 +271,6 @@ protected override Size MeasureOverride(Size availableSize) } m_viewportManager.SetLayoutExtent(extent); - m_lastAvailableSize = availableSize; return desiredSize; } @@ -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 @@ -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. @@ -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(); } @@ -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) diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/Repeater/UniformGridLayout/UniformGridLayout.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/Repeater/UniformGridLayout/UniformGridLayout.cs index 1c51dd6f88d9..d12d7b5e6d56 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/Repeater/UniformGridLayout/UniformGridLayout.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/Repeater/UniformGridLayout/UniformGridLayout.cs @@ -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; @@ -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); } } @@ -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 ); } @@ -200,16 +205,16 @@ 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) { @@ -217,8 +222,8 @@ Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent( 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 { }) { @@ -226,9 +231,12 @@ Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent( 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 { @@ -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); @@ -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); @@ -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()),