From cd0eb833e09e5236fa298e8e54c2067e87ec409f Mon Sep 17 00:00:00 2001 From: CHINBOY Date: Wed, 28 May 2025 21:37:18 +0800 Subject: [PATCH] Add DataGridDisplayData --- .../DataGridDisplayData.cs | 310 ++++++++++++++++++ 1 file changed, 310 insertions(+) diff --git a/src/AtomUI.Controls.DataGrid/DataGridDisplayData.cs b/src/AtomUI.Controls.DataGrid/DataGridDisplayData.cs index c8191ed01..daf18eafe 100644 --- a/src/AtomUI.Controls.DataGrid/DataGridDisplayData.cs +++ b/src/AtomUI.Controls.DataGrid/DataGridDisplayData.cs @@ -3,11 +3,321 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using System.Diagnostics; +using Avalonia.Controls; +using Avalonia.Media; + namespace AtomUI.Controls; internal class DataGridDisplayData { + public int FirstDisplayedScrollingCol { get; set; } + public int FirstScrollingSlot { get; set; } + public int LastScrollingSlot { get; set; } + public int LastTotallyDisplayedScrollingCol { get; set; } + public int NumDisplayedScrollingElements => _scrollingElements.Count; + public int NumTotallyDisplayedScrollingElements { get; set; } + internal double PendingVerticalScrollHeight { get; set; } + + private Stack _fullyRecycledRows; // list of Rows that have been fully recycled (Collapsed) + private int _headScrollingElements; // index of the row in _scrollingRows that is the first displayed row + private DataGrid _owner; + private Stack _recyclableRows; // list of Rows which have not been fully recycled (avoids Measure in several cases) + private List _scrollingElements; // circular list of displayed elements + private Stack _fullyRecycledGroupHeaders; // list of GroupHeaders that have been fully recycled (Collapsed) + private Stack _recyclableGroupHeaders; // list of GroupHeaders which have not been fully recycled (avoids Measure in several cases) + public DataGridDisplayData(DataGrid owner) { + _owner = owner; + FirstDisplayedScrollingCol = -1; + LastTotallyDisplayedScrollingCol = -1; + + _scrollingElements = new List(); + _recyclableRows = new Stack(); + _fullyRecycledRows = new Stack(); + _recyclableGroupHeaders = new Stack(); + _fullyRecycledGroupHeaders = new Stack(); } + + internal void AddRecyclableRow(DataGridRow row) + { + Debug.Assert(!_recyclableRows.Contains(row)); + row.DetachFromDataGrid(true); + _recyclableRows.Push(row); + } + + internal DataGridRowGroupHeader? GetUsedGroupHeader() + { + if (_recyclableGroupHeaders.Count > 0) + { + return _recyclableGroupHeaders.Pop(); + } + if (_fullyRecycledGroupHeaders.Count > 0) + { + // For fully recycled rows, we need to set the Visibility back to Visible + DataGridRowGroupHeader groupHeader = _fullyRecycledGroupHeaders.Pop(); + groupHeader.IsVisible = true; + return groupHeader; + } + return null; + } + + internal void AddRecyclableRowGroupHeader(DataGridRowGroupHeader groupHeader) + { + Debug.Assert(!_recyclableGroupHeaders.Contains(groupHeader)); + groupHeader.IsRecycled = true; + _recyclableGroupHeaders.Push(groupHeader); + } + + internal void ClearElements(bool recycle) + { + ResetSlotIndexes(); + if (recycle) + { + foreach (Control element in _scrollingElements) + { + if (element is DataGridRow row) + { + if (row.IsRecyclable) + { + AddRecyclableRow(row); + } + else + { + row.Clip = new RectangleGeometry(); + } + } + else if (element is DataGridRowGroupHeader groupHeader) + { + AddRecyclableRowGroupHeader(groupHeader); + } + } + } + else + { + _recyclableRows.Clear(); + _fullyRecycledRows.Clear(); + _recyclableGroupHeaders.Clear(); + _fullyRecycledGroupHeaders.Clear(); + } + _scrollingElements.Clear(); + } + + internal void CorrectSlotsAfterDeletion(int slot, bool wasCollapsed) + { + if (wasCollapsed) + { + if (slot > FirstScrollingSlot) + { + LastScrollingSlot--; + } + } + else if (_owner.IsSlotVisible(slot)) + { + UnloadScrollingElement(slot, true /*updateSlotInformation*/, true /*wasDeleted*/); + } + // This cannot be an else condition because if there are 2 rows left, and you delete the first one + // then these indexes need to be updated as well + if (slot < FirstScrollingSlot) + { + FirstScrollingSlot--; + LastScrollingSlot--; + } + } + + internal void CorrectSlotsAfterInsertion(int slot, Control? element, bool isCollapsed) + { + if (slot < FirstScrollingSlot) + { + // The row was inserted above our viewport, just update our indexes + FirstScrollingSlot++; + LastScrollingSlot++; + } + else if (isCollapsed && (slot <= LastScrollingSlot)) + { + LastScrollingSlot++; + } + else if ((_owner.GetPreviousVisibleSlot(slot) <= LastScrollingSlot) || (LastScrollingSlot == -1)) + { + if (element != null) + { + // The row was inserted in our viewport, add it as a scrolling row + LoadScrollingSlot(slot, element, true /*updateSlotInformation*/); + } + } + } + + private int GetCircularListIndex(int slot, bool wrap) + { + int index = slot - FirstScrollingSlot - _headScrollingElements - _owner.GetCollapsedSlotCount(FirstScrollingSlot, slot); + return wrap ? index % _scrollingElements.Count : index; + } + + internal void FullyRecycleElements() + { + // Fully recycle Recyclable rows and transfer them to Recycled rows + while (_recyclableRows.Count > 0) + { + DataGridRow row = _recyclableRows.Pop(); + row.IsVisible = false; + Debug.Assert(!_fullyRecycledRows.Contains(row)); + _fullyRecycledRows.Push(row); + } + // Fully recycle Recyclable GroupHeaders and transfer them to Recycled GroupHeaders + while (_recyclableGroupHeaders.Count > 0) + { + DataGridRowGroupHeader groupHeader = _recyclableGroupHeaders.Pop(); + groupHeader.IsVisible = false; + Debug.Assert(!_fullyRecycledGroupHeaders.Contains(groupHeader)); + _fullyRecycledGroupHeaders.Push(groupHeader); + } + } + + internal Control GetDisplayedElement(int slot) + { + Debug.Assert(slot >= FirstScrollingSlot); + Debug.Assert(slot <= LastScrollingSlot); + + return _scrollingElements[GetCircularListIndex(slot, true /*wrap*/)]; + } + + internal DataGridRow? GetDisplayedRow(int rowIndex) + { + return GetDisplayedElement(_owner.SlotFromRowIndex(rowIndex)) as DataGridRow; + } + + // Returns an enumeration of the displayed scrolling rows in order starting with the FirstDisplayedScrollingRow + internal IEnumerable GetScrollingElements() + { + return GetScrollingElements(null); + } + + internal IEnumerable GetScrollingElements(Predicate? filter) + { + for (int i = 0; i < _scrollingElements.Count; i++) + { + Control element = _scrollingElements[(_headScrollingElements + i) % _scrollingElements.Count]; + if (filter == null || filter(element)) + { + // _scrollingRows is a circular list that wraps + yield return element; + } + } + } + + internal IEnumerable GetScrollingRows() + { + return GetScrollingElements(element => element is DataGridRow); + } + + internal DataGridRow? GetUsedRow() + { + if (_recyclableRows.Count > 0) + { + return _recyclableRows.Pop(); + } + if (_fullyRecycledRows.Count > 0) + { + // For fully recycled rows, we need to set the Visibility back to Visible + DataGridRow row = _fullyRecycledRows.Pop(); + row.IsVisible = true; + return row; + } + return null; + } + + // Tracks the row at index rowIndex as a scrolling row + internal void LoadScrollingSlot(int slot, Control element, bool updateSlotInformation) + { + if (_scrollingElements.Count == 0) + { + SetScrollingSlots(slot); + _scrollingElements.Add(element); + } + else + { + // The slot should be adjacent to the other slots being displayed + Debug.Assert(slot >= _owner.GetPreviousVisibleSlot(FirstScrollingSlot) && slot <= _owner.GetNextVisibleSlot(LastScrollingSlot)); + if (updateSlotInformation) + { + if (slot < FirstScrollingSlot) + { + FirstScrollingSlot = slot; + } + else + { + LastScrollingSlot = _owner.GetNextVisibleSlot(LastScrollingSlot); + } + } + int insertIndex = GetCircularListIndex(slot, false /*wrap*/); + if (insertIndex > _scrollingElements.Count) + { + // We need to wrap around from the bottom to the top of our circular list; as a result the head of the list moves forward + insertIndex -= _scrollingElements.Count; + _headScrollingElements++; + } + _scrollingElements.Insert(insertIndex, element); + } + } + + private void ResetSlotIndexes() + { + SetScrollingSlots(-1); + NumTotallyDisplayedScrollingElements = 0; + _headScrollingElements = 0; + } + + private void SetScrollingSlots(int newValue) + { + FirstScrollingSlot = newValue; + LastScrollingSlot = newValue; + } + + // Stops tracking the element at the given slot as a scrolling element + internal void UnloadScrollingElement(int slot, bool updateSlotInformation, bool wasDeleted) + { + Debug.Assert(_owner.IsSlotVisible(slot)); + int elementIndex = GetCircularListIndex(slot, false /*wrap*/); + if (elementIndex > _scrollingElements.Count) + { + // We need to wrap around from the top to the bottom of our circular list + elementIndex -= _scrollingElements.Count; + _headScrollingElements--; + } + _scrollingElements.RemoveAt(elementIndex); + + if (updateSlotInformation) + { + if (slot == FirstScrollingSlot && !wasDeleted) + { + FirstScrollingSlot = _owner.GetNextVisibleSlot(FirstScrollingSlot); + } + else + { + LastScrollingSlot = _owner.GetPreviousVisibleSlot(LastScrollingSlot); + } + if (LastScrollingSlot < FirstScrollingSlot) + { + ResetSlotIndexes(); + } + } + } + +#if DEBUG + internal void PrintDisplay() + { + foreach (Control element in GetScrollingElements()) + { + if (element is DataGridRow row) + { + Debug.WriteLine(String.Format(System.Globalization.CultureInfo.InvariantCulture, "Slot: {0} Row: {1} ", row.Slot, row.Index)); + } + else if (element is DataGridRowGroupHeader groupHeader) + { + Debug.WriteLine(String.Format(System.Globalization.CultureInfo.InvariantCulture, "Slot: {0} GroupHeader: {1}", groupHeader.RowGroupInfo?.Slot, groupHeader.RowGroupInfo?.CollectionViewGroup?.Key)); + } + } + } +#endif } \ No newline at end of file