csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
464 lines
19 KiB
464 lines
19 KiB
// This source file is adapted from the WinUI project.
|
|
// (https://github.com/microsoft/microsoft-ui-xaml)
|
|
//
|
|
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using Avalonia.Layout.Utils;
|
|
using Avalonia.Logging;
|
|
|
|
namespace Avalonia.Layout
|
|
{
|
|
internal class ElementManager
|
|
{
|
|
private readonly List<ILayoutable> _realizedElements = new List<ILayoutable>();
|
|
private readonly List<Rect> _realizedElementLayoutBounds = new List<Rect>();
|
|
private int _firstRealizedDataIndex;
|
|
private VirtualizingLayoutContext _context;
|
|
|
|
private bool IsVirtualizingContext
|
|
{
|
|
get
|
|
{
|
|
if (_context != null)
|
|
{
|
|
var rect = _context.RealizationRect;
|
|
bool hasInfiniteSize = double.IsInfinity(rect.Height) || double.IsInfinity(rect.Width);
|
|
return !hasInfiniteSize;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public void SetContext(VirtualizingLayoutContext virtualContext) => _context = virtualContext;
|
|
|
|
public void OnBeginMeasure(ScrollOrientation orientation)
|
|
{
|
|
if (_context != null)
|
|
{
|
|
if (IsVirtualizingContext)
|
|
{
|
|
// We proactively clear elements laid out outside of the realizaton
|
|
// rect so that they are available for reuse during the current
|
|
// measure pass.
|
|
// This is useful during fast panning scenarios in which the realization
|
|
// window is constantly changing and we want to reuse elements from
|
|
// the end that's opposite to the panning direction.
|
|
DiscardElementsOutsideWindow(_context.RealizationRect, orientation);
|
|
}
|
|
else
|
|
{
|
|
// If we are initialized with a non-virtualizing context, make sure that
|
|
// we have enough space to hold the bounds for all the elements.
|
|
int count = _context.ItemCount;
|
|
if (_realizedElementLayoutBounds.Count != count)
|
|
{
|
|
// Make sure there is enough space for the bounds.
|
|
// Note: We could optimize when the count becomes smaller, but keeping
|
|
// it always up to date is the simplest option for now.
|
|
_realizedElementLayoutBounds.Resize(count);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public int GetRealizedElementCount()
|
|
{
|
|
return IsVirtualizingContext ? _realizedElements.Count : _context.ItemCount;
|
|
}
|
|
|
|
public ILayoutable GetAt(int realizedIndex)
|
|
{
|
|
ILayoutable element;
|
|
|
|
if (IsVirtualizingContext)
|
|
{
|
|
if (_realizedElements[realizedIndex] == null)
|
|
{
|
|
// Sentinel. Create the element now since we need it.
|
|
int dataIndex = GetDataIndexFromRealizedRangeIndex(realizedIndex);
|
|
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "Creating element for sentinal with data index {Index}", dataIndex);
|
|
element = _context.GetOrCreateElementAt(
|
|
dataIndex,
|
|
ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
|
|
_realizedElements[realizedIndex] = element;
|
|
}
|
|
else
|
|
{
|
|
element = _realizedElements[realizedIndex];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// realizedIndex and dataIndex are the same (everything is realized)
|
|
element = _context.GetOrCreateElementAt(
|
|
realizedIndex,
|
|
ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
public void Add(ILayoutable element, int dataIndex)
|
|
{
|
|
if (_realizedElements.Count == 0)
|
|
{
|
|
_firstRealizedDataIndex = dataIndex;
|
|
}
|
|
|
|
_realizedElements.Add(element);
|
|
_realizedElementLayoutBounds.Add(default);
|
|
}
|
|
|
|
public void Insert(int realizedIndex, int dataIndex, ILayoutable element)
|
|
{
|
|
if (realizedIndex == 0)
|
|
{
|
|
_firstRealizedDataIndex = dataIndex;
|
|
}
|
|
|
|
_realizedElements.Insert(realizedIndex, element);
|
|
|
|
// Set bounds to an invalid rect since we do not know it yet.
|
|
_realizedElementLayoutBounds.Insert(realizedIndex, new Rect(-1, -1, -1, -1));
|
|
}
|
|
|
|
public void ClearRealizedRange(int realizedIndex, int count)
|
|
{
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
// Clear from the edges so that ItemsRepeater can optimize on maintaining
|
|
// realized indices without walking through all the children every time.
|
|
int index = realizedIndex == 0 ? realizedIndex + i : (realizedIndex + count - 1) - i;
|
|
var elementRef = _realizedElements[index];
|
|
|
|
if (elementRef != null)
|
|
{
|
|
_context.RecycleElement(elementRef);
|
|
}
|
|
}
|
|
|
|
int endIndex = realizedIndex + count;
|
|
_realizedElements.RemoveRange(realizedIndex, endIndex - realizedIndex);
|
|
_realizedElementLayoutBounds.RemoveRange(realizedIndex, endIndex - realizedIndex);
|
|
|
|
if (realizedIndex == 0)
|
|
{
|
|
_firstRealizedDataIndex = _realizedElements.Count == 0 ?
|
|
-1 : _firstRealizedDataIndex + count;
|
|
}
|
|
}
|
|
|
|
public void DiscardElementsOutsideWindow(bool forward, int startIndex)
|
|
{
|
|
// Remove layout elements that are outside the realized range.
|
|
if (IsDataIndexRealized(startIndex))
|
|
{
|
|
int rangeIndex = GetRealizedRangeIndexFromDataIndex(startIndex);
|
|
|
|
if (forward)
|
|
{
|
|
ClearRealizedRange(rangeIndex, GetRealizedElementCount() - rangeIndex);
|
|
}
|
|
else
|
|
{
|
|
ClearRealizedRange(0, rangeIndex + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ClearRealizedRange() => ClearRealizedRange(0, GetRealizedElementCount());
|
|
|
|
public Rect GetLayoutBoundsForDataIndex(int dataIndex)
|
|
{
|
|
int realizedIndex = GetRealizedRangeIndexFromDataIndex(dataIndex);
|
|
return _realizedElementLayoutBounds[realizedIndex];
|
|
}
|
|
|
|
public void SetLayoutBoundsForDataIndex(int dataIndex, in Rect bounds)
|
|
{
|
|
int realizedIndex = GetRealizedRangeIndexFromDataIndex(dataIndex);
|
|
_realizedElementLayoutBounds[realizedIndex] = bounds;
|
|
}
|
|
|
|
public Rect GetLayoutBoundsForRealizedIndex(int realizedIndex) => _realizedElementLayoutBounds[realizedIndex];
|
|
|
|
public void SetLayoutBoundsForRealizedIndex(int realizedIndex, in Rect bounds)
|
|
{
|
|
_realizedElementLayoutBounds[realizedIndex] = bounds;
|
|
}
|
|
|
|
public bool IsDataIndexRealized(int index)
|
|
{
|
|
if (IsVirtualizingContext)
|
|
{
|
|
int realizedCount = GetRealizedElementCount();
|
|
return
|
|
realizedCount > 0 &&
|
|
GetDataIndexFromRealizedRangeIndex(0) <= index &&
|
|
GetDataIndexFromRealizedRangeIndex(realizedCount - 1) >= index;
|
|
}
|
|
else
|
|
{
|
|
// Non virtualized - everything is realized
|
|
return index >= 0 && index < _context.ItemCount;
|
|
}
|
|
}
|
|
|
|
public bool IsIndexValidInData(int currentIndex) => currentIndex >= 0 && currentIndex < _context.ItemCount;
|
|
|
|
public ILayoutable GetRealizedElement(int dataIndex)
|
|
{
|
|
return IsVirtualizingContext ?
|
|
GetAt(GetRealizedRangeIndexFromDataIndex(dataIndex)) :
|
|
_context.GetOrCreateElementAt(
|
|
dataIndex,
|
|
ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
|
|
}
|
|
|
|
public void EnsureElementRealized(bool forward, int dataIndex, string layoutId)
|
|
{
|
|
if (IsDataIndexRealized(dataIndex) == false)
|
|
{
|
|
var element = _context.GetOrCreateElementAt(
|
|
dataIndex,
|
|
ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
|
|
|
|
if (forward)
|
|
{
|
|
Add(element, dataIndex);
|
|
}
|
|
else
|
|
{
|
|
Insert(0, dataIndex, element);
|
|
}
|
|
|
|
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Created element for index {index}", layoutId, dataIndex);
|
|
}
|
|
}
|
|
|
|
public bool IsWindowConnected(in Rect window, ScrollOrientation orientation, bool scrollOrientationSameAsFlow)
|
|
{
|
|
bool intersects = false;
|
|
|
|
if (_realizedElementLayoutBounds.Count > 0)
|
|
{
|
|
var firstElementBounds = GetLayoutBoundsForRealizedIndex(0);
|
|
var lastElementBounds = GetLayoutBoundsForRealizedIndex(GetRealizedElementCount() - 1);
|
|
|
|
var effectiveOrientation = scrollOrientationSameAsFlow ?
|
|
(orientation == ScrollOrientation.Vertical ? ScrollOrientation.Horizontal : ScrollOrientation.Vertical) :
|
|
orientation;
|
|
|
|
|
|
var windowStart = effectiveOrientation == ScrollOrientation.Vertical ? window.Y : window.X;
|
|
var windowEnd = effectiveOrientation == ScrollOrientation.Vertical ? window.Y + window.Height : window.X + window.Width;
|
|
var firstElementStart = effectiveOrientation == ScrollOrientation.Vertical ? firstElementBounds.Y : firstElementBounds.X;
|
|
var lastElementEnd = effectiveOrientation == ScrollOrientation.Vertical ? lastElementBounds.Y + lastElementBounds.Height : lastElementBounds.X + lastElementBounds.Width;
|
|
|
|
intersects =
|
|
firstElementStart <= windowEnd &&
|
|
lastElementEnd >= windowStart;
|
|
}
|
|
|
|
return intersects;
|
|
}
|
|
|
|
public void DataSourceChanged(object source, NotifyCollectionChangedEventArgs args)
|
|
{
|
|
if (_realizedElements.Count > 0)
|
|
{
|
|
switch (args.Action)
|
|
{
|
|
case NotifyCollectionChangedAction.Add:
|
|
{
|
|
OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
|
|
}
|
|
break;
|
|
|
|
case NotifyCollectionChangedAction.Replace:
|
|
{
|
|
int oldSize = args.OldItems.Count;
|
|
int newSize = args.NewItems.Count;
|
|
int oldStartIndex = args.OldStartingIndex;
|
|
int newStartIndex = args.NewStartingIndex;
|
|
|
|
if (oldSize == newSize &&
|
|
oldStartIndex == newStartIndex &&
|
|
IsDataIndexRealized(oldStartIndex) &&
|
|
IsDataIndexRealized(oldStartIndex + oldSize -1))
|
|
{
|
|
// Straight up replace of n items within the realization window.
|
|
// Removing and adding might causes us to lose the anchor causing us
|
|
// to throw away all containers and start from scratch.
|
|
// Instead, we can just clear those items and set the element to
|
|
// null (sentinel) and let the next measure get new containers for them.
|
|
var startRealizedIndex = GetRealizedRangeIndexFromDataIndex(oldStartIndex);
|
|
for (int realizedIndex = startRealizedIndex; realizedIndex < startRealizedIndex + oldSize; realizedIndex++)
|
|
{
|
|
var elementRef = _realizedElements[realizedIndex];
|
|
|
|
if (elementRef != null)
|
|
{
|
|
_context.RecycleElement(elementRef);
|
|
_realizedElements[realizedIndex] = null;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OnItemsRemoved(oldStartIndex, oldSize);
|
|
OnItemsAdded(newStartIndex, newSize);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case NotifyCollectionChangedAction.Remove:
|
|
{
|
|
OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count);
|
|
}
|
|
break;
|
|
|
|
case NotifyCollectionChangedAction.Reset:
|
|
ClearRealizedRange();
|
|
break;
|
|
|
|
case NotifyCollectionChangedAction.Move:
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
}
|
|
|
|
public int GetElementDataIndex(ILayoutable suggestedAnchor)
|
|
{
|
|
var it = _realizedElements.IndexOf(suggestedAnchor);
|
|
return it != -1 ? GetDataIndexFromRealizedRangeIndex(it) : -1;
|
|
}
|
|
|
|
public int GetDataIndexFromRealizedRangeIndex(int rangeIndex)
|
|
{
|
|
return IsVirtualizingContext ? rangeIndex + _firstRealizedDataIndex : rangeIndex;
|
|
}
|
|
|
|
private int GetRealizedRangeIndexFromDataIndex(int dataIndex)
|
|
{
|
|
return IsVirtualizingContext ? dataIndex - _firstRealizedDataIndex : dataIndex;
|
|
}
|
|
|
|
private void DiscardElementsOutsideWindow(in Rect window, ScrollOrientation orientation)
|
|
{
|
|
// The following illustration explains the cutoff indices.
|
|
// We will clear all the realized elements from both ends
|
|
// up to the corresponding cutoff index.
|
|
// '-' means the element is outside the cutoff range.
|
|
// '*' means the element is inside the cutoff range and will be cleared.
|
|
//
|
|
// Window:
|
|
// |______________________________|
|
|
// Realization range:
|
|
// |*****----------------------------------*********|
|
|
// | |
|
|
// frontCutoffIndex backCutoffIndex
|
|
//
|
|
// Note that we tolerate at most one element outside of the window
|
|
// because the FlowLayoutAlgorithm.Generate routine stops *after*
|
|
// it laid out an element outside the realization window.
|
|
// This is also convenient because it protects the anchor
|
|
// during a BringIntoView operation during which the anchor may
|
|
// not be in the realization window (in fact, the realization window
|
|
// might be empty if the BringIntoView is issued before the first
|
|
// layout pass).
|
|
|
|
int realizedRangeSize = GetRealizedElementCount();
|
|
int frontCutoffIndex = -1;
|
|
int backCutoffIndex = realizedRangeSize;
|
|
|
|
for (int i = 0;
|
|
i<realizedRangeSize &&
|
|
!Intersects(window, _realizedElementLayoutBounds[i], orientation);
|
|
++i)
|
|
{
|
|
++frontCutoffIndex;
|
|
}
|
|
|
|
for (int i = realizedRangeSize - 1;
|
|
i >= 0 &&
|
|
!Intersects(window, _realizedElementLayoutBounds[i], orientation);
|
|
--i)
|
|
{
|
|
--backCutoffIndex;
|
|
}
|
|
|
|
if (backCutoffIndex<realizedRangeSize - 1)
|
|
{
|
|
ClearRealizedRange(backCutoffIndex + 1, realizedRangeSize - backCutoffIndex - 1);
|
|
}
|
|
|
|
if (frontCutoffIndex > 0)
|
|
{
|
|
ClearRealizedRange(0, Math.Min(frontCutoffIndex, GetRealizedElementCount()));
|
|
}
|
|
}
|
|
|
|
private static bool Intersects(in Rect lhs, in Rect rhs, ScrollOrientation orientation)
|
|
{
|
|
var lhsStart = orientation == ScrollOrientation.Vertical ? lhs.Y : lhs.X;
|
|
var lhsEnd = orientation == ScrollOrientation.Vertical ? lhs.Y + lhs.Height : lhs.X + lhs.Width;
|
|
var rhsStart = orientation == ScrollOrientation.Vertical ? rhs.Y : rhs.X;
|
|
var rhsEnd = orientation == ScrollOrientation.Vertical ? rhs.Y + rhs.Height : rhs.X + rhs.Width;
|
|
|
|
return lhsEnd >= rhsStart && lhsStart <= rhsEnd;
|
|
}
|
|
|
|
private void OnItemsAdded(int index, int count)
|
|
{
|
|
// Using the old indices here (before it was updated by the collection change)
|
|
// if the insert data index is between the first and last realized data index, we need
|
|
// to insert items.
|
|
int lastRealizedDataIndex = _firstRealizedDataIndex + GetRealizedElementCount() - 1;
|
|
int newStartingIndex = index;
|
|
if (newStartingIndex > _firstRealizedDataIndex &&
|
|
newStartingIndex <= lastRealizedDataIndex)
|
|
{
|
|
// Inserted within the realized range
|
|
int insertRangeStartIndex = newStartingIndex - _firstRealizedDataIndex;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
// Insert null (sentinel) here instead of an element, that way we dont
|
|
// end up creating a lot of elements only to be thrown out in the next layout.
|
|
int insertRangeIndex = insertRangeStartIndex + i;
|
|
int dataIndex = newStartingIndex + i;
|
|
// This is to keep the contiguousness of the mapping
|
|
Insert(insertRangeIndex, dataIndex, null);
|
|
}
|
|
}
|
|
else if (index <= _firstRealizedDataIndex)
|
|
{
|
|
// Items were inserted before the realized range.
|
|
// We need to update m_firstRealizedDataIndex;
|
|
_firstRealizedDataIndex += count;
|
|
}
|
|
}
|
|
|
|
private void OnItemsRemoved(int index, int count)
|
|
{
|
|
int lastRealizedDataIndex = _firstRealizedDataIndex + _realizedElements.Count - 1;
|
|
int startIndex = Math.Max(_firstRealizedDataIndex, index);
|
|
int endIndex = Math.Min(lastRealizedDataIndex, index + count - 1);
|
|
bool removeAffectsFirstRealizedDataIndex = (index <= _firstRealizedDataIndex);
|
|
|
|
if (endIndex >= startIndex)
|
|
{
|
|
ClearRealizedRange(GetRealizedRangeIndexFromDataIndex(startIndex), endIndex - startIndex + 1);
|
|
}
|
|
|
|
if (removeAffectsFirstRealizedDataIndex &&
|
|
_firstRealizedDataIndex != -1)
|
|
{
|
|
_firstRealizedDataIndex -= count;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|