diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 0ba3c3c50e..125841a2c3 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.Logging; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -306,6 +307,7 @@ namespace Avalonia.Controls virtInfo.AutoRecycleCandidate && !virtInfo.KeepAlive) { + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "AutoClear - {Index}", virtInfo.Index); ClearElementImpl(element); } } diff --git a/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs index 977d9d794c..67e627b886 100644 --- a/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs +++ b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Text; using Avalonia.Layout; +using Avalonia.Logging; namespace Avalonia.Controls { @@ -58,7 +59,11 @@ namespace Avalonia.Controls protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index); - protected override void RecycleElementCore(ILayoutable element) => _owner.ClearElementImpl((IControl)element); + protected override void RecycleElementCore(ILayoutable element) + { + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "RepeaterLayout - RecycleElement: {Index}", _owner.GetElementIndex((IControl)element)); + _owner.ClearElementImpl((IControl)element); + } protected override Rect RealizationRectCore() => _owner.RealizationWindow; } diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index 4ed9cb3333..8906b9f577 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -11,6 +11,7 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; +using Avalonia.Logging; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -60,11 +61,13 @@ namespace Avalonia.Controls if (suppressAutoRecycle) { virtInfo.AutoRecycleCandidate = false; + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "GetElement: {Index} Not AutoRecycleCandidate:", virtInfo.Index); } else { virtInfo.AutoRecycleCandidate = true; virtInfo.KeepAlive = true; + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "GetElement: {Index} AutoRecycleCandidate:", virtInfo.Index); } return element; diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index e50de87877..3ba117d4e7 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Layout; +using Avalonia.Logging; using Avalonia.Threading; using Avalonia.VisualTree; @@ -184,6 +185,9 @@ namespace Avalonia.Controls // We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much. if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1) { + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Expecting viewport shift of ({Shift})", + _owner.Layout.LayoutId, _expectedViewportShift); + // There are cases where we might be expecting a shift but not get it. We will // be waiting for the effective viewport event but if the scroll viewer is not able // to perform the shift (perhaps because it cannot scroll in negative offset), @@ -293,6 +297,10 @@ namespace Avalonia.Controls // that can scroll in the direction where the shift is expected. if (_pendingViewportShift.X != 0 || _pendingViewportShift.Y != 0) { + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Layout Updated with pending shift {Shift}- invalidating measure", + _owner.Layout.LayoutId, + _pendingViewportShift); + // Assume this is never going to come. _unshiftableShift = new Point( _unshiftableShift.X + _pendingViewportShift.X, @@ -404,6 +412,7 @@ namespace Avalonia.Controls var clip = globalClip.TransformToAABB(transform); var effectiveViewport = clip.Intersect(bounds.Value.Bounds); + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: EffectiveViewportChanged event callback", _owner.Layout.LayoutId); UpdateViewport(effectiveViewport); _pendingViewportShift = default; @@ -459,11 +468,17 @@ namespace Avalonia.Controls private void UpdateViewport(Rect viewport) { var currentVisibleWindow = viewport; - var oldVisibleWindow = _visibleWindow; + var previousVisibleWindow = _visibleWindow; + + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Effective Viewport: ({Before})->({After})", + _owner.Layout.LayoutId, + previousVisibleWindow, + viewport); if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X && -currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y) { + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Viewport is invalid. visible window cleared", _owner.Layout.LayoutId); // We got cleared. _visibleWindow = default; } @@ -472,8 +487,12 @@ namespace Avalonia.Controls _visibleWindow = currentVisibleWindow; } - if (_visibleWindow != oldVisibleWindow) + if (_visibleWindow != previousVisibleWindow) { + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Used Viewport: ({Before})->({After})", + _owner.Layout.LayoutId, + previousVisibleWindow, + currentVisibleWindow); TryInvalidateMeasure(); } } @@ -494,6 +513,7 @@ namespace Avalonia.Controls // We invalidate measure instead of just invalidating arrange because // we don't invalidate measure in UpdateViewport if the view is changing to // avoid layout cycles. + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Invalidating measure due to viewport change", _owner.Layout.LayoutId); _owner.InvalidateMeasure(); } } diff --git a/src/Avalonia.Layout/AttachedLayout.cs b/src/Avalonia.Layout/AttachedLayout.cs index d22566442a..047c01343f 100644 --- a/src/Avalonia.Layout/AttachedLayout.cs +++ b/src/Avalonia.Layout/AttachedLayout.cs @@ -12,7 +12,7 @@ namespace Avalonia.Layout /// public abstract class AttachedLayout : AvaloniaObject { - internal string LayoutId { get; set; } + public string LayoutId { get; set; } /// /// Occurs when the measurement state (layout) has been invalidated. diff --git a/src/Avalonia.Layout/ElementManager.cs b/src/Avalonia.Layout/ElementManager.cs index 1748a3be03..9ccb88b625 100644 --- a/src/Avalonia.Layout/ElementManager.cs +++ b/src/Avalonia.Layout/ElementManager.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using Avalonia.Layout.Utils; +using Avalonia.Logging; namespace Avalonia.Layout { @@ -78,6 +79,7 @@ namespace Avalonia.Layout { // Sentinel. Create the element now since we need it. int dataIndex = GetDataIndexFromRealizedRangeIndex(realizedIndex); + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "Creating element for sentinal with data index {Index}", dataIndex); element = _context.GetOrCreateElementAt( dataIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); @@ -232,6 +234,8 @@ namespace Avalonia.Layout { Insert(0, dataIndex, element); } + + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Created element for index {index}", layoutId, dataIndex); } } diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs index a0699cdb7f..b70e1dca01 100644 --- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Specialized; +using Avalonia.Logging; namespace Avalonia.Layout { @@ -82,6 +83,9 @@ namespace Avalonia.Layout // If minor size is infinity, there is only one line and no need to align that line. _scrollOrientationSameAsFlow = double.IsInfinity(_orientation.Minor(availableSize)); var realizationRect = RealizationRect; + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: MeasureLayout Realization({Rect})", + layoutId, + realizationRect); var suggestedAnchorIndex = _context.RecommendedAnchorIndex; if (_elementManager.IsIndexValidInData(suggestedAnchorIndex)) @@ -100,6 +104,7 @@ namespace Avalonia.Layout Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId); if (isWrapping && IsReflowRequired()) { + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Reflow Pass", layoutId); var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); _orientation.SetMinorStart(ref firstElementBounds, 0); _elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds); @@ -121,6 +126,7 @@ namespace Avalonia.Layout LineAlignment lineAlignment, string layoutId) { + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: ArrangeLayout", layoutId); ArrangeVirtualizingLayout(finalSize, lineAlignment, isWrapping, layoutId); return new Size( @@ -184,6 +190,7 @@ namespace Avalonia.Layout if (isAnchorSuggestionValid) { + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Using suggested anchor {Anchor}", layoutId, suggestedAnchorIndex); anchorIndex = _algorithmCallbacks.Algorithm_GetAnchorForTargetElement( suggestedAnchorIndex, availableSize, @@ -223,6 +230,9 @@ namespace Avalonia.Layout } else if (needAnchorColumnRevaluation || !isRealizationWindowConnected) { + if (needAnchorColumnRevaluation) { Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: NeedAnchorColumnReevaluation", layoutId); } + if (!isRealizationWindowConnected) { Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Disconnected Window", layoutId); } + // The anchor is based on the realization window because a connected ItemsRepeater might intersect the realization window // but not the visible window. In that situation, we still need to produce a valid anchor. var anchorInfo = _algorithmCallbacks.Algorithm_GetAnchorForRealizationRect(availableSize, context); @@ -231,6 +241,7 @@ namespace Avalonia.Layout } else { + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Connected Window - picking first realized element as anchor", layoutId); // No suggestion - just pick first in realized range anchorIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0); var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); @@ -238,12 +249,14 @@ namespace Avalonia.Layout } } + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Picked anchor: {Anchor}", layoutId, anchorIndex); _firstRealizedDataIndexInsideRealizationWindow = _lastRealizedDataIndexInsideRealizationWindow = anchorIndex; if (_elementManager.IsIndexValidInData(anchorIndex)) { if (!_elementManager.IsDataIndexRealized(anchorIndex)) { // Disconnected, throw everything and create new anchor + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId} Disconnected Window - throwing away all realized elements", layoutId); _elementManager.ClearRealizedRange(); var anchor = _context.GetOrCreateElementAt(anchorIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); @@ -254,9 +267,17 @@ namespace Avalonia.Layout var desiredSize = MeasureElement(anchorElement, anchorIndex, availableSize, _context); var layoutBounds = new Rect(anchorPosition.X, anchorPosition.Y, desiredSize.Width, desiredSize.Height); _elementManager.SetLayoutBoundsForDataIndex(anchorIndex, layoutBounds); + + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Layout bounds of anchor {anchor} are ({Bounds})", + layoutId, + anchorIndex, + layoutBounds); } else { + // Throw everything away + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId} Anchor index is not valid - throwing away all realized elements", + layoutId); _elementManager.ClearRealizedRange(); } @@ -280,6 +301,12 @@ namespace Avalonia.Layout if (anchorIndex != -1) { int step = (direction == GenerateDirection.Forward) ? 1 : -1; + + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Generating {Direction} from anchor {Anchor}", + layoutId, + direction, + anchorIndex); + int previousIndex = anchorIndex; int currentIndex = anchorIndex + step; var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex); @@ -365,6 +392,10 @@ namespace Avalonia.Layout _orientation.SetMajorStart(ref bounds, previousLineOffset - lineMajorSize - lineSpacing); _orientation.SetMajorSize(ref bounds, lineMajorSize); _elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds); + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Corrected Layout bounds of element {Index} are ({Bounds})", + layoutId, + dataIndex, + bounds); } } } @@ -387,6 +418,11 @@ namespace Avalonia.Layout } _elementManager.SetLayoutBoundsForDataIndex(currentIndex, currentBounds); + + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Layout bounds of element {Index} are ({Bounds}).", + layoutId, + currentIndex, + currentBounds); previousIndex = currentIndex; currentIndex += step; } @@ -505,6 +541,7 @@ namespace Avalonia.Layout lastDataIndex, lastBounds); + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId} Extent: ({Bounds})", layoutId, extent); return extent; } @@ -673,6 +710,11 @@ namespace Avalonia.Layout } var element = _elementManager.GetAt(rangeIndex); + + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Arranging element {Index} at ({Bounds})", + layoutId, + _elementManager.GetDataIndexFromRealizedRangeIndex(rangeIndex), + bounds); element.Arrange(bounds); } } diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index 1a90d2a2e0..b0d1106ca6 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Specialized; using Avalonia.Data; +using Avalonia.Logging; namespace Avalonia.Layout { @@ -107,8 +108,15 @@ namespace Avalonia.Layout _orientation.MajorStart(extent) + (remainingItems * averageElementSize)); } + else + { + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Estimating extent with no realized elements", + LayoutId); + } } + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Extent is ({Size}). Based on average {Average}", + LayoutId, extent.Size, averageElementSize); return extent; } diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index 1698f61989..2a5a027a31 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Specialized; using Avalonia.Data; +using Avalonia.Logging; namespace Avalonia.Layout { @@ -379,8 +380,14 @@ namespace Avalonia.Layout ref extent, _orientation.MajorEnd(lastRealizedLayoutBounds) - _orientation.MajorStart(extent) + (remainingItems / itemsPerLine) * lineSize); } + else + { + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Estimating extent with no realized elements", LayoutId); + } } + Logger.TryGet(LogEventLevel.Verbose)?.Log("Repeater", this, "{LayoutId}: Extent is ({Size}). Based on lineSize {LineSize} and items per line {ItemsPerLine}", + LayoutId, extent.Size, lineSize, itemsPerLine); return extent; }