From 434b24a62c05b7d9651876990e9514e9403fe581 Mon Sep 17 00:00:00 2001 From: malaguenha Date: Fri, 31 Mar 2023 08:10:48 +0900 Subject: [PATCH 01/36] Avoid IME to be disabled after popups --- src/Windows/Avalonia.Win32/WindowImpl.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e066bbe5f6..9dbb7b77d8 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -150,7 +150,12 @@ namespace Avalonia.Win32 CreateWindow(); _framebuffer = new FramebufferManager(_hwnd); - UpdateInputMethod(GetKeyboardLayout(0)); + + if (this is not PopupImpl) + { + UpdateInputMethod(GetKeyboardLayout(0)); + } + if (glPlatform != null) { if (_isUsingComposition) From 6dc5eddfebfb9ce5017f6cff8b3d7022e98f7cff Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 31 Mar 2023 09:08:42 +0200 Subject: [PATCH 02/36] Fix nullability issues. --- .../VirtualizingStackPanelTests.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 7f5c0eb134..76fc31c50d 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -15,6 +15,8 @@ using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; +#nullable enable + namespace Avalonia.Controls.UnitTests { public class VirtualizingStackPanelTests @@ -483,10 +485,10 @@ namespace Avalonia.Controls.UnitTests int firstIndex, int count) { - Assert.All(target.GetRealizedContainers(), x => Assert.Same(target, x.VisualParent)); - Assert.All(target.GetRealizedContainers(), x => Assert.Same(itemsControl, x.Parent)); + Assert.All(target.GetRealizedContainers()!, x => Assert.Same(target, x.VisualParent)); + Assert.All(target.GetRealizedContainers()!, x => Assert.Same(itemsControl, x.Parent)); - var childIndexes = target.GetRealizedContainers()? + var childIndexes = target.GetRealizedContainers()! .Select(x => itemsControl.IndexFromContainer(x)) .Where(x => x >= 0) .OrderBy(x => x) @@ -500,11 +502,11 @@ namespace Avalonia.Controls.UnitTests int firstIndex, int count) { - Assert.All(target.GetRealizedContainers(), x => Assert.IsType(x)); - Assert.All(target.GetRealizedContainers(), x => Assert.Same(target, x.VisualParent)); - Assert.All(target.GetRealizedContainers(), x => Assert.Same(itemsControl, x.Parent)); + Assert.All(target.GetRealizedContainers()!, x => Assert.IsType(x)); + Assert.All(target.GetRealizedContainers()!, x => Assert.Same(target, x.VisualParent)); + Assert.All(target.GetRealizedContainers()!, x => Assert.Same(itemsControl, x.Parent)); - var childIndexes = target.GetRealizedContainers()? + var childIndexes = target.GetRealizedContainers()! .Select(x => itemsControl.IndexFromContainer(x)) .Where(x => x >= 0) .OrderBy(x => x) @@ -536,7 +538,7 @@ namespace Avalonia.Controls.UnitTests { ItemsSource = items, Template = new FuncControlTemplate((_, _) => scroll), - ItemsPanel = new FuncTemplate(() => target), + ItemsPanel = new FuncTemplate(() => target), }; if (useItemTemplate) From a08f4ac97726d51b3ba638f003b49c56099581ee Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 31 Mar 2023 12:51:16 +0200 Subject: [PATCH 03/36] Moved the realized element list outside. Class was nearly as big as `VirtualizingStackPanel` itself and getting a bit unwieldy. --- .../Utils/RealizedStackElements.cs | 448 ++++++++++++++++++ .../VirtualizingStackPanel.cs | 445 +---------------- 2 files changed, 450 insertions(+), 443 deletions(-) create mode 100644 src/Avalonia.Controls/Utils/RealizedStackElements.cs diff --git a/src/Avalonia.Controls/Utils/RealizedStackElements.cs b/src/Avalonia.Controls/Utils/RealizedStackElements.cs new file mode 100644 index 0000000000..ba1f07a632 --- /dev/null +++ b/src/Avalonia.Controls/Utils/RealizedStackElements.cs @@ -0,0 +1,448 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.Controls.Utils +{ + /// + /// Stores the realized element state for a virtualizing panel that arranges its children + /// in a stack layout, such as . + /// + internal class RealizedStackElements + { + private int _firstIndex; + private List? _elements; + private List? _sizes; + private double _startU; + private bool _startUUnstable; + + /// + /// Gets the number of realized elements. + /// + public int Count => _elements?.Count ?? 0; + + /// + /// Gets the index of the first realized element, or -1 if no elements are realized. + /// + public int FirstIndex => _elements?.Count > 0 ? _firstIndex : -1; + + /// + /// Gets the index of the last realized element, or -1 if no elements are realized. + /// + public int LastIndex => _elements?.Count > 0 ? _firstIndex + _elements.Count - 1 : -1; + + /// + /// Gets the elements. + /// + public IReadOnlyList Elements => _elements ??= new List(); + + /// + /// Gets the sizes of the elements on the primary axis. + /// + public IReadOnlyList SizeU => _sizes ??= new List(); + + /// + /// Gets the position of the first element on the primary axis. + /// + public double StartU => _startU; + + /// + /// Adds a newly realized element to the collection. + /// + /// The index of the element. + /// The element. + /// The position of the elemnt on the primary axis. + /// The size of the element on the primary axis. + public void Add(int index, Control element, double u, double sizeU) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index)); + + _elements ??= new List(); + _sizes ??= new List(); + + if (Count == 0) + { + _elements.Add(element); + _sizes.Add(sizeU); + _startU = u; + _firstIndex = index; + } + else if (index == LastIndex + 1) + { + _elements.Add(element); + _sizes.Add(sizeU); + } + else if (index == FirstIndex - 1) + { + --_firstIndex; + _elements.Insert(0, element); + _sizes.Insert(0, sizeU); + _startU = u; + } + else + { + throw new NotSupportedException("Can only add items to the beginning or end of realized elements."); + } + } + + /// + /// Gets the element at the specified index, if realized. + /// + /// The index in the source collection of the element to get. + /// The element if realized; otherwise null. + public Control? GetElement(int index) + { + var i = index - FirstIndex; + if (i >= 0 && i < _elements?.Count) + return _elements[i]; + return null; + } + + /// + /// Gets the index and start U position of the element at the specified U position. + /// + /// The U position. + /// + /// A tuple containing: + /// - The index of the item at the specified U position, or -1 if the item could not be + /// determined + /// - The U position of the start of the item, if determined + /// + public (int index, double position) GetIndexAt(double u) + { + if (_elements is null || _sizes is null || _startU > u || _startUUnstable) + return (-1, 0); + + var index = 0; + var position = _startU; + + while (index < _elements.Count) + { + var size = _sizes[index]; + if (double.IsNaN(size)) + break; + if (u >= position && u < position + size) + return (index + FirstIndex, position); + position += size; + ++index; + } + + return (-1, 0); + } + + /// + /// Gets the element at the specified position on the primary axis, if realized. + /// + /// The position. + /// + /// A tuple containing the index of the element (or -1 if not found) and the position of the element on the + /// primary axis. + /// + public (int index, double position) GetElementAt(double position) + { + if (_sizes is null || position < StartU) + return (-1, 0); + + var u = StartU; + var i = FirstIndex; + + foreach (var size in _sizes) + { + var endU = u + size; + if (position < endU) + return (i, u); + u += size; + ++i; + } + + return (-1, 0); + } + + /// + /// Estimates the average U size of all elements in the source collection based on the + /// realized elements. + /// + /// + /// The estimated U size of an element, or -1 if not enough information is present to make + /// an estimate. + /// + public double EstimateElementSizeU() + { + var total = 0.0; + var divisor = 0.0; + + // Start by averaging the size of the elements before the first realized element. + if (FirstIndex >= 0 && !_startUUnstable) + { + total += _startU; + divisor += FirstIndex; + } + + // Average the size of the realized elements. + if (_sizes is not null) + { + foreach (var size in _sizes) + { + if (double.IsNaN(size)) + continue; + total += size; + ++divisor; + } + } + + // We don't have any elements on which to base our estimate. + if (divisor == 0 || total == 0) + return -1; + + return total / divisor; + } + + /// + /// Gets the index of the specified element. + /// + /// The element. + /// The index or -1 if the element is not present in the collection. + public int GetIndex(Control element) + { + return _elements?.IndexOf(element) is int index && index >= 0 ? index + FirstIndex : -1; + } + + /// + /// Updates the elements in response to items being inserted into the source collection. + /// + /// The index in the source collection of the insert. + /// The number of items inserted. + /// A method used to update the element indexes. + public void ItemsInserted(int index, int count, Action updateElementIndex) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index)); + if (_elements is null || _elements.Count == 0) + return; + + // Get the index within the realized _elements collection. + var first = FirstIndex; + var realizedIndex = index - first; + + if (realizedIndex < Count) + { + // The insertion point affects the realized elements. Update the index of the + // elements after the insertion point. + var elementCount = _elements.Count; + var start = Math.Max(realizedIndex, 0); + var newIndex = realizedIndex + count; + + for (var i = start; i < elementCount; ++i) + { + if (_elements[i] is Control element) + updateElementIndex(element, newIndex - count, newIndex); + ++newIndex; + } + + if (realizedIndex < 0) + { + // The insertion point was before the first element, update the first index. + _firstIndex += count; + } + else + { + // The insertion point was within the realized elements, insert an empty space + // in _elements and _sizes. + _elements!.InsertMany(realizedIndex, null, count); + _sizes!.InsertMany(realizedIndex, double.NaN, count); + } + } + } + + /// + /// Updates the elements in response to items being removed from the source collection. + /// + /// The index in the source collection of the remove. + /// The number of items removed. + /// A method used to update the element indexes. + /// A method used to recycle elements. + public void ItemsRemoved( + int index, + int count, + Action updateElementIndex, + Action recycleElement) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index)); + if (_elements is null || _elements.Count == 0) + return; + + // Get the removal start and end index within the realized _elements collection. + var first = FirstIndex; + var last = LastIndex; + var startIndex = index - first; + var endIndex = (index + count) - first; + + if (endIndex < 0) + { + // The removed range was before the realized elements. Update the first index and + // the indexes of the realized elements. + _firstIndex -= count; + _startUUnstable = true; + + var newIndex = _firstIndex; + for (var i = 0; i < _elements.Count; ++i) + { + if (_elements[i] is Control element) + updateElementIndex(element, newIndex - count, newIndex); + ++newIndex; + } + } + else if (startIndex < _elements.Count) + { + // Recycle and remove the affected elements. + var start = Math.Max(startIndex, 0); + var end = Math.Min(endIndex, _elements.Count); + + for (var i = start; i < end; ++i) + { + if (_elements[i] is Control element) + recycleElement(element); + } + + _elements.RemoveRange(start, end - start); + _sizes!.RemoveRange(start, end - start); + + // If the remove started before and ended within our realized elements, then our new + // first index will be the index where the remove started. Mark StartU as unstable + // because we can't rely on it now to estimate element heights. + if (startIndex <= 0 && end < last) + { + _firstIndex = first = index; + _startUUnstable = true; + } + + // Update the indexes of the elements after the removed range. + end = _elements.Count; + var newIndex = first + start; + for (var i = start; i < end; ++i) + { + if (_elements[i] is Control element) + updateElementIndex(element, newIndex + count, newIndex); + ++newIndex; + } + } + } + + /// + /// Recycles all elements in response to the source collection being reset. + /// + /// A method used to recycle elements. + public void ItemsReset(Action recycleElement) + { + if (_elements is null || _elements.Count == 0) + return; + + foreach (var e in _elements) + { + if (e is not null) + recycleElement(e); + } + + _startU = _firstIndex = 0; + _elements?.Clear(); + _sizes?.Clear(); + + } + + /// + /// Recycles elements before a specific index. + /// + /// The index in the source collection of new first element. + /// A method used to recycle elements. + public void RecycleElementsBefore(int index, Action recycleElement) + { + if (index <= FirstIndex || _elements is null || _elements.Count == 0) + return; + + if (index > LastIndex) + { + RecycleAllElements(recycleElement); + } + else + { + var endIndex = index - FirstIndex; + + for (var i = 0; i < endIndex; ++i) + { + if (_elements[i] is Control e) + recycleElement(e, i + FirstIndex); + } + + _elements.RemoveRange(0, endIndex); + _sizes!.RemoveRange(0, endIndex); + _firstIndex = index; + } + } + + /// + /// Recycles elements after a specific index. + /// + /// The index in the source collection of new last element. + /// A method used to recycle elements. + public void RecycleElementsAfter(int index, Action recycleElement) + { + if (index >= LastIndex || _elements is null || _elements.Count == 0) + return; + + if (index < FirstIndex) + { + RecycleAllElements(recycleElement); + } + else + { + var startIndex = (index + 1) - FirstIndex; + var count = _elements.Count; + + for (var i = startIndex; i < count; ++i) + { + if (_elements[i] is Control e) + recycleElement(e, i + FirstIndex); + } + + _elements.RemoveRange(startIndex, _elements.Count - startIndex); + _sizes!.RemoveRange(startIndex, _sizes.Count - startIndex); + } + } + + /// + /// Recycles all realized elements. + /// + /// A method used to recycle elements. + public void RecycleAllElements(Action recycleElement) + { + if (_elements is null || _elements.Count == 0) + return; + + var i = FirstIndex; + + foreach (var e in _elements) + { + if (e is not null) + recycleElement(e, i); + ++i; + } + + _startU = _firstIndex = 0; + _elements?.Clear(); + _sizes?.Clear(); + } + + /// + /// Resets the element list and prepares it for reuse. + /// + public void ResetForReuse() + { + _startU = _firstIndex = 0; + _startUUnstable = false; + _elements?.Clear(); + _sizes?.Clear(); + } + } + +} diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index e86a0de657..5a766b9cd3 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -65,8 +65,8 @@ namespace Avalonia.Controls private bool _isInLayout; private bool _isWaitingForViewportUpdate; private double _lastEstimatedElementSizeU = 25; - private RealizedElementList? _measureElements; - private RealizedElementList? _realizedElements; + private RealizedStackElements? _measureElements; + private RealizedStackElements? _realizedElements; private Rect _viewport = s_invalidViewport; private Stack? _recyclePool; private Control? _unrealizedFocusedElement; @@ -853,447 +853,6 @@ namespace Avalonia.Controls return snapPoint; } - /// - /// Stores the realized element state for a . - /// - internal class RealizedElementList - { - private int _firstIndex; - private List? _elements; - private List? _sizes; - private double _startU; - private bool _startUUnstable; - - /// - /// Gets the number of realized elements. - /// - public int Count => _elements?.Count ?? 0; - - /// - /// Gets the index of the first realized element, or -1 if no elements are realized. - /// - public int FirstIndex => _elements?.Count > 0 ? _firstIndex : -1; - - /// - /// Gets the index of the last realized element, or -1 if no elements are realized. - /// - public int LastIndex => _elements?.Count > 0 ? _firstIndex + _elements.Count - 1 : -1; - - /// - /// Gets the elements. - /// - public IReadOnlyList Elements => _elements ??= new List(); - - /// - /// Gets the sizes of the elements on the primary axis. - /// - public IReadOnlyList SizeU => _sizes ??= new List(); - - /// - /// Gets the position of the first element on the primary axis. - /// - public double StartU => _startU; - - /// - /// Adds a newly realized element to the collection. - /// - /// The index of the element. - /// The element. - /// The position of the elemnt on the primary axis. - /// The size of the element on the primary axis. - public void Add(int index, Control element, double u, double sizeU) - { - if (index < 0) - throw new ArgumentOutOfRangeException(nameof(index)); - - _elements ??= new List(); - _sizes ??= new List(); - - if (Count == 0) - { - _elements.Add(element); - _sizes.Add(sizeU); - _startU = u; - _firstIndex = index; - } - else if (index == LastIndex + 1) - { - _elements.Add(element); - _sizes.Add(sizeU); - } - else if (index == FirstIndex - 1) - { - --_firstIndex; - _elements.Insert(0, element); - _sizes.Insert(0, sizeU); - _startU = u; - } - else - { - throw new NotSupportedException("Can only add items to the beginning or end of realized elements."); - } - } - - /// - /// Gets the element at the specified index, if realized. - /// - /// The index in the source collection of the element to get. - /// The element if realized; otherwise null. - public Control? GetElement(int index) - { - var i = index - FirstIndex; - if (i >= 0 && i < _elements?.Count) - return _elements[i]; - return null; - } - - /// - /// Gets the index and start U position of the element at the specified U position. - /// - /// The U position. - /// - /// A tuple containing: - /// - The index of the item at the specified U position, or -1 if the item could not be - /// determined - /// - The U position of the start of the item, if determined - /// - public (int index, double position) GetIndexAt(double u) - { - if (_elements is null || _sizes is null || _startU > u || _startUUnstable) - return (-1, 0); - - var index = 0; - var position = _startU; - - while (index < _elements.Count) - { - var size = _sizes[index]; - if (double.IsNaN(size)) - break; - if (u >= position && u < position + size) - return (index + FirstIndex, position); - position += size; - ++index; - } - - return (-1, 0); - } - - /// - /// Gets the element at the specified position on the primary axis, if realized. - /// - /// The position. - /// - /// A tuple containing the index of the element (or -1 if not found) and the position of the element on the - /// primary axis. - /// - public (int index, double position) GetElementAt(double position) - { - if (_sizes is null || position < StartU) - return (-1, 0); - - var u = StartU; - var i = FirstIndex; - - foreach (var size in _sizes) - { - var endU = u + size; - if (position < endU) - return (i, u); - u += size; - ++i; - } - - return (-1, 0); - } - - /// - /// Estimates the average U size of all elements in the source collection based on the - /// realized elements. - /// - /// - /// The estimated U size of an element, or -1 if not enough information is present to make - /// an estimate. - /// - public double EstimateElementSizeU() - { - var total = 0.0; - var divisor = 0.0; - - // Start by averaging the size of the elements before the first realized element. - if (FirstIndex >= 0 && !_startUUnstable) - { - total += _startU; - divisor += FirstIndex; - } - - // Average the size of the realized elements. - if (_sizes is not null) - { - foreach (var size in _sizes) - { - if (double.IsNaN(size)) - continue; - total += size; - ++divisor; - } - } - - // We don't have any elements on which to base our estimate. - if (divisor == 0 || total == 0) - return -1; - - return total / divisor; - } - - /// - /// Gets the index of the specified element. - /// - /// The element. - /// The index or -1 if the element is not present in the collection. - public int GetIndex(Control element) - { - return _elements?.IndexOf(element) is int index && index >= 0 ? index + FirstIndex : -1; - } - - /// - /// Updates the elements in response to items being inserted into the source collection. - /// - /// The index in the source collection of the insert. - /// The number of items inserted. - /// A method used to update the element indexes. - public void ItemsInserted(int index, int count, Action updateElementIndex) - { - if (index < 0) - throw new ArgumentOutOfRangeException(nameof(index)); - if (_elements is null || _elements.Count == 0) - return; - - // Get the index within the realized _elements collection. - var first = FirstIndex; - var realizedIndex = index - first; - - if (realizedIndex < Count) - { - // The insertion point affects the realized elements. Update the index of the - // elements after the insertion point. - var elementCount = _elements.Count; - var start = Math.Max(realizedIndex, 0); - var newIndex = realizedIndex + count; - - for (var i = start; i < elementCount; ++i) - { - if (_elements[i] is Control element) - updateElementIndex(element, newIndex - count, newIndex); - ++newIndex; - } - - if (realizedIndex < 0) - { - // The insertion point was before the first element, update the first index. - _firstIndex += count; - } - else - { - // The insertion point was within the realized elements, insert an empty space - // in _elements and _sizes. - _elements!.InsertMany(realizedIndex, null, count); - _sizes!.InsertMany(realizedIndex, double.NaN, count); - } - } - } - - /// - /// Updates the elements in response to items being removed from the source collection. - /// - /// The index in the source collection of the remove. - /// The number of items removed. - /// A method used to update the element indexes. - /// A method used to recycle elements. - public void ItemsRemoved( - int index, - int count, - Action updateElementIndex, - Action recycleElement) - { - if (index < 0) - throw new ArgumentOutOfRangeException(nameof(index)); - if (_elements is null || _elements.Count == 0) - return; - - // Get the removal start and end index within the realized _elements collection. - var first = FirstIndex; - var last = LastIndex; - var startIndex = index - first; - var endIndex = (index + count) - first; - - if (endIndex < 0) - { - // The removed range was before the realized elements. Update the first index and - // the indexes of the realized elements. - _firstIndex -= count; - _startUUnstable = true; - - var newIndex = _firstIndex; - for (var i = 0; i < _elements.Count; ++i) - { - if (_elements[i] is Control element) - updateElementIndex(element, newIndex - count, newIndex); - ++newIndex; - } - } - else if (startIndex < _elements.Count) - { - // Recycle and remove the affected elements. - var start = Math.Max(startIndex, 0); - var end = Math.Min(endIndex, _elements.Count); - - for (var i = start; i < end; ++i) - { - if (_elements[i] is Control element) - recycleElement(element); - } - - _elements.RemoveRange(start, end - start); - _sizes!.RemoveRange(start, end - start); - - // If the remove started before and ended within our realized elements, then our new - // first index will be the index where the remove started. Mark StartU as unstable - // because we can't rely on it now to estimate element heights. - if (startIndex <= 0 && end < last) - { - _firstIndex = first = index; - _startUUnstable = true; - } - - // Update the indexes of the elements after the removed range. - end = _elements.Count; - var newIndex = first + start; - for (var i = start; i < end; ++i) - { - if (_elements[i] is Control element) - updateElementIndex(element, newIndex + count, newIndex); - ++newIndex; - } - } - } - - /// - /// Recycles all elements in response to the source collection being reset. - /// - /// A method used to recycle elements. - public void ItemsReset(Action recycleElement) - { - if (_elements is null || _elements.Count == 0) - return; - - foreach (var e in _elements) - { - if (e is not null) - recycleElement(e); - } - - _startU = _firstIndex = 0; - _elements?.Clear(); - _sizes?.Clear(); - - } - - /// - /// Recycles elements before a specific index. - /// - /// The index in the source collection of new first element. - /// A method used to recycle elements. - public void RecycleElementsBefore(int index, Action recycleElement) - { - if (index <= FirstIndex || _elements is null || _elements.Count == 0) - return; - - if (index > LastIndex) - { - RecycleAllElements(recycleElement); - } - else - { - var endIndex = index - FirstIndex; - - for (var i = 0; i < endIndex; ++i) - { - if (_elements[i] is Control e) - recycleElement(e, i + FirstIndex); - } - - _elements.RemoveRange(0, endIndex); - _sizes!.RemoveRange(0, endIndex); - _firstIndex = index; - } - } - - /// - /// Recycles elements after a specific index. - /// - /// The index in the source collection of new last element. - /// A method used to recycle elements. - public void RecycleElementsAfter(int index, Action recycleElement) - { - if (index >= LastIndex || _elements is null || _elements.Count == 0) - return; - - if (index < FirstIndex) - { - RecycleAllElements(recycleElement); - } - else - { - var startIndex = (index + 1) - FirstIndex; - var count = _elements.Count; - - for (var i = startIndex; i < count; ++i) - { - if (_elements[i] is Control e) - recycleElement(e, i + FirstIndex); - } - - _elements.RemoveRange(startIndex, _elements.Count - startIndex); - _sizes!.RemoveRange(startIndex, _sizes.Count - startIndex); - } - } - - /// - /// Recycles all realized elements. - /// - /// A method used to recycle elements. - public void RecycleAllElements(Action recycleElement) - { - if (_elements is null || _elements.Count == 0) - return; - - var i = FirstIndex; - - foreach (var e in _elements) - { - if (e is not null) - recycleElement(e, i); - ++i; - } - - _startU = _firstIndex = 0; - _elements?.Clear(); - _sizes?.Clear(); - } - - /// - /// Resets the element list and prepares it for reuse. - /// - public void ResetForReuse() - { - _startU = _firstIndex = 0; - _startUUnstable = false; - _elements?.Clear(); - _sizes?.Clear(); - } - } - private struct MeasureViewport { public int firstIndex; From 5c3ce86e51e411d71d97dbf3df824c3c12d14838 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Apr 2023 11:23:08 +0600 Subject: [PATCH 04/36] Use DeferredDisplay for GLX --- src/Avalonia.X11/Glx/GlxContext.cs | 8 ++++---- src/Avalonia.X11/Glx/GlxDisplay.cs | 22 +++++++++++----------- src/Avalonia.X11/X11Window.cs | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.X11/Glx/GlxContext.cs b/src/Avalonia.X11/Glx/GlxContext.cs index b1cb330b00..035354f337 100644 --- a/src/Avalonia.X11/Glx/GlxContext.cs +++ b/src/Avalonia.X11/Glx/GlxContext.cs @@ -106,8 +106,8 @@ namespace Avalonia.X11.Glx var success = false; try { - var old = new RestoreContext(Glx, _x11.Display, _lock); - if (!Glx.MakeContextCurrent(_x11.Display, xid, xid, Handle)) + var old = new RestoreContext(Glx, _x11.DeferredDisplay, _lock); + if (!Glx.MakeContextCurrent(_x11.DeferredDisplay, xid, xid, Handle)) throw new OpenGlException("glXMakeContextCurrent failed "); success = true; @@ -124,9 +124,9 @@ namespace Avalonia.X11.Glx public void Dispose() { - Glx.DestroyContext(_x11.Display, Handle); + Glx.DestroyContext(_x11.DeferredDisplay, Handle); if (_ownsPBuffer) - Glx.DestroyPbuffer(_x11.Display, _defaultXid); + Glx.DestroyPbuffer(_x11.DeferredDisplay, _defaultXid); } public object TryGetFeature(Type featureType) diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index 7749749eaa..9bcd8f3763 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -22,7 +22,7 @@ namespace Avalonia.X11.Glx { _x11 = x11; _probeProfiles = probeProfiles.ToArray(); - _displayExtensions = Glx.GetExtensions(_x11.Display); + _displayExtensions = Glx.GetExtensions(_x11.DeferredDisplay); var baseAttribs = new[] { @@ -46,12 +46,12 @@ namespace Avalonia.X11.Glx baseAttribs, }) { - var ptr = Glx.ChooseFBConfig(_x11.Display, x11.DefaultScreen, + var ptr = Glx.ChooseFBConfig(_x11.DeferredDisplay, x11.DefaultScreen, attribs, out var count); for (var c = 0 ; c < count; c++) { - var visual = Glx.GetVisualFromFBConfig(_x11.Display, ptr[c]); + var visual = Glx.GetVisualFromFBConfig(_x11.DeferredDisplay, ptr[c]); // We prefer 32 bit visuals if (_fbconfig == IntPtr.Zero || visual->depth == 32) { @@ -71,17 +71,17 @@ namespace Avalonia.X11.Glx if (_visual == null) throw new OpenGlException("Unable to get visual info from FBConfig"); - if (Glx.GetFBConfigAttrib(_x11.Display, _fbconfig, GLX_SAMPLES, out var samples) == 0) + if (Glx.GetFBConfigAttrib(_x11.DeferredDisplay, _fbconfig, GLX_SAMPLES, out var samples) == 0) sampleCount = samples; - if (Glx.GetFBConfigAttrib(_x11.Display, _fbconfig, GLX_STENCIL_SIZE, out var stencil) == 0) + if (Glx.GetFBConfigAttrib(_x11.DeferredDisplay, _fbconfig, GLX_STENCIL_SIZE, out var stencil) == 0) stencilSize = stencil; var attributes = new[] { GLX_PBUFFER_WIDTH, 1, GLX_PBUFFER_HEIGHT, 1, 0 }; - Glx.CreatePbuffer(_x11.Display, _fbconfig, attributes); - Glx.CreatePbuffer(_x11.Display, _fbconfig, attributes); + Glx.CreatePbuffer(_x11.DeferredDisplay, _fbconfig, attributes); + Glx.CreatePbuffer(_x11.DeferredDisplay, _fbconfig, attributes); - XLib.XFlush(_x11.Display); + XLib.XFlush(_x11.DeferredDisplay); DeferredContext = CreateContext(CreatePBuffer(), null, sampleCount, stencilSize, true); @@ -108,7 +108,7 @@ namespace Avalonia.X11.Glx private IntPtr CreatePBuffer() { - return Glx.CreatePbuffer(_x11.Display, _fbconfig, new[] { GLX_PBUFFER_WIDTH, 1, GLX_PBUFFER_HEIGHT, 1, 0 }); + return Glx.CreatePbuffer(_x11.DeferredDisplay, _fbconfig, new[] { GLX_PBUFFER_WIDTH, 1, GLX_PBUFFER_HEIGHT, 1, 0 }); } public GlxContext CreateContext() => CreateContext(CreatePBuffer(), null, DeferredContext.SampleCount, @@ -139,7 +139,7 @@ namespace Avalonia.X11.Glx try { - handle = Glx.CreateContextAttribsARB(_x11.Display, _fbconfig, sharelist, true, attrs); + handle = Glx.CreateContextAttribsARB(_x11.DeferredDisplay, _fbconfig, sharelist, true, attrs); if (handle != IntPtr.Zero) { _version = profile; @@ -181,6 +181,6 @@ namespace Avalonia.X11.Glx throw new OpenGlException("Unable to create direct GLX context"); } - public void SwapBuffers(IntPtr xid) => Glx.SwapBuffers(_x11.Display, xid); + public void SwapBuffers(IntPtr xid) => Glx.SwapBuffers(_x11.DeferredDisplay, xid); } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 8bd84215ed..07561bcf05 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -183,7 +183,7 @@ namespace Avalonia.X11 surfaces.Insert(0, new EglGlPlatformSurface(new SurfaceInfo(this, _x11.DeferredDisplay, _handle, _renderHandle))); if (glx != null) - surfaces.Insert(0, new GlxGlPlatformSurface(new SurfaceInfo(this, _x11.Display, _handle, _renderHandle))); + surfaces.Insert(0, new GlxGlPlatformSurface(new SurfaceInfo(this, _x11.DeferredDisplay, _handle, _renderHandle))); surfaces.Add(Handle); From 5e961ff7bf10aff079aa332dbd0aecc88fad6415 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 3 Apr 2023 10:00:49 +0200 Subject: [PATCH 05/36] Started rewriting VirtualizationDemo. To give some more realistic examples of lists to test virtualization on. Started with a chatgpt-generated chat. --- samples/VirtualizationDemo/App.axaml | 14 ++ samples/VirtualizationDemo/App.axaml.cs | 20 ++ samples/VirtualizationDemo/App.xaml | 7 - samples/VirtualizationDemo/App.xaml.cs | 21 -- samples/VirtualizationDemo/Assets/chat.json | 190 ++++++++++++++++++ samples/VirtualizationDemo/MainWindow.axaml | 16 ++ .../VirtualizationDemo/MainWindow.axaml.cs | 15 ++ samples/VirtualizationDemo/MainWindow.xaml | 64 ------ samples/VirtualizationDemo/MainWindow.xaml.cs | 22 -- samples/VirtualizationDemo/Models/Chat.cs | 23 +++ samples/VirtualizationDemo/Program.cs | 19 +- .../ViewModels/ChatPageViewModel.cs | 16 ++ .../ViewModels/ItemViewModel.cs | 26 --- .../ViewModels/MainWindowViewModel.cs | 162 +-------------- .../Views/ChatPageView.axaml | 39 ++++ .../Views/ChatPageView.axaml.cs | 11 + .../VirtualizationDemo.csproj | 21 +- 17 files changed, 371 insertions(+), 315 deletions(-) create mode 100644 samples/VirtualizationDemo/App.axaml create mode 100644 samples/VirtualizationDemo/App.axaml.cs delete mode 100644 samples/VirtualizationDemo/App.xaml delete mode 100644 samples/VirtualizationDemo/App.xaml.cs create mode 100644 samples/VirtualizationDemo/Assets/chat.json create mode 100644 samples/VirtualizationDemo/MainWindow.axaml create mode 100644 samples/VirtualizationDemo/MainWindow.axaml.cs delete mode 100644 samples/VirtualizationDemo/MainWindow.xaml delete mode 100644 samples/VirtualizationDemo/MainWindow.xaml.cs create mode 100644 samples/VirtualizationDemo/Models/Chat.cs create mode 100644 samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs delete mode 100644 samples/VirtualizationDemo/ViewModels/ItemViewModel.cs create mode 100644 samples/VirtualizationDemo/Views/ChatPageView.axaml create mode 100644 samples/VirtualizationDemo/Views/ChatPageView.axaml.cs diff --git a/samples/VirtualizationDemo/App.axaml b/samples/VirtualizationDemo/App.axaml new file mode 100644 index 0000000000..f5f06ffb6a --- /dev/null +++ b/samples/VirtualizationDemo/App.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/App.axaml.cs b/samples/VirtualizationDemo/App.axaml.cs new file mode 100644 index 0000000000..5ac5c9a92b --- /dev/null +++ b/samples/VirtualizationDemo/App.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace VirtualizationDemo; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + desktop.MainWindow = new MainWindow(); + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/samples/VirtualizationDemo/App.xaml b/samples/VirtualizationDemo/App.xaml deleted file mode 100644 index eb5f0e4dca..0000000000 --- a/samples/VirtualizationDemo/App.xaml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/samples/VirtualizationDemo/App.xaml.cs b/samples/VirtualizationDemo/App.xaml.cs deleted file mode 100644 index 81b80c1f40..0000000000 --- a/samples/VirtualizationDemo/App.xaml.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Markup.Xaml; - -namespace VirtualizationDemo -{ - public class App : Application - { - public override void Initialize() - { - AvaloniaXamlLoader.Load(this); - } - - public override void OnFrameworkInitializationCompleted() - { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - desktop.MainWindow = new MainWindow(); - base.OnFrameworkInitializationCompleted(); - } - } -} diff --git a/samples/VirtualizationDemo/Assets/chat.json b/samples/VirtualizationDemo/Assets/chat.json new file mode 100644 index 0000000000..cc628b534a --- /dev/null +++ b/samples/VirtualizationDemo/Assets/chat.json @@ -0,0 +1,190 @@ +{ + "chat": [ + { + "sender": "Alice", + "message": "Hey Bob! How was your weekend?", + "timestamp": "2023-04-01T10:00:00" + }, + { + "sender": "Bob", + "message": "It was great, thanks for asking. I went on a camping trip with some friends. How about you?", + "timestamp": "2023-04-01T10:01:00" + }, + { + "sender": "Alice", + "message": "My weekend was pretty chill. I just stayed home and caught up on some TV shows.", + "timestamp": "2023-04-01T10:03:00" + }, + { + "sender": "Bob", + "message": "That sounds relaxing. What shows did you watch?", + "timestamp": "2023-04-01T10:05:00" + }, + { + "sender": "Alice", + "message": "I watched the new season of 'Stranger Things' and started watching 'Ozark'. Have you seen them?", + "timestamp": "2023-04-01T10:07:00" + }, + { + "sender": "Bob", + "message": "Yeah, I've seen both of those. They're really good! What do you think of them so far?", + "timestamp": "2023-04-01T10:10:00" + }, + { + "sender": "Alice", + "message": "I'm really enjoying 'Stranger Things', but 'Ozark' is a bit darker than I expected. I'm only a few episodes in though, so we'll see how it goes.", + "timestamp": "2023-04-01T10:12:00" + }, + { + "sender": "Bob", + "message": "Yeah, 'Ozark' can be intense at times, but it's really well done. Keep watching, it gets even better.", + "timestamp": "2023-04-01T10:15:00" + }, + { + "sender": "Alice", + "message": "Thanks for the recommendation, I'll definitely keep watching. So, how's work been for you lately?", + "timestamp": "2023-04-01T10:20:00" + }, + { + "sender": "Bob", + "message": "It's been pretty busy, but I'm managing. How about you?", + "timestamp": "2023-04-01T10:22:00" + }, + { + "sender": "Alice", + "message": "Same here, things have been pretty hectic. But it keeps us on our toes, right?", + "timestamp": "2023-04-01T10:25:00" + }, + { + "sender": "Bob", + "message": "Absolutely. Hey, have you heard about the new project we're starting next week?", + "timestamp": "2023-04-01T10:30:00" + }, + { + "sender": "Alice", + "message": "No, I haven't. What's it about?", + "timestamp": "2023-04-01T10:32:00" + }, + { + "sender": "Bob", + "message": "It's a big project for a new client, and it's going to require a lot of extra hours from all of us. But the pay is going to be great,so it's definitely worth the extra effort. I'll fill you in on the details later, but for now, let's just enjoy our coffee break, shall we?", + "timestamp": "2023-04-01T10:35:00" + }, + { + "sender": "Alice", + "message": "Sounds good to me. I could use a break right about now.", + "timestamp": "2023-04-01T10:40:00" + }, + { + "sender": "Bob", + "message": "Me too. So, have you tried the new caf� down the street yet?", + "timestamp": "2023-04-01T10:45:00" + }, + { + "sender": "Alice", + "message": "No, I haven't. Is it any good?", + "timestamp": "2023-04-01T10:47:00" + }, + { + "sender": "Bob", + "message": "It's really good! They have the best croissants I've ever tasted.", + "timestamp": "2023-04-01T10:50:00" + }, + { + "sender": "Alice", + "message": "Hmm, I'll have to try it out sometime. Do they have any vegan options?", + "timestamp": "2023-04-01T10:52:00" + }, + { + "sender": "Bob", + "message": "I'm not sure, but I think they do. You should ask them the next time you go there.", + "timestamp": "2023-04-01T10:55:00" + }, + { + "sender": "Alice", + "message": "Thanks for the suggestion. I'm always looking for good vegan options around here.", + "timestamp": "2023-04-01T11:00:00" + }, + { + "sender": "Bob", + "message": "No problem. So, have you made any plans for the weekend yet?", + "timestamp": "2023-04-01T11:05:00" + }, + { + "sender": "Alice", + "message": "Not yet. I was thinking of maybe going for a hike or something. What about you?", + "timestamp": "2023-04-01T11:07:00" + }, + { + "sender": "Bob", + "message": "I haven't made any plans either. Maybe we could do something together?", + "timestamp": "2023-04-01T11:10:00" + }, + { + "sender": "Alice", + "message": "That sounds like a great idea! Let's plan on it.", + "timestamp": "2023-04-01T11:12:00" + }, + { + "sender": "Bob", + "message": "Awesome. I'll check out some hiking trails and let you know which ones look good.", + "timestamp": "2023-04-01T11:15:00" + }, + { + "sender": "Alice", + "message": "Sounds good. I can't wait!", + "timestamp": "2023-04-01T11:20:00" + }, + { + "sender": "John", + "message": "Hey Lisa, how was your day?", + "timestamp": "2023-04-01T18:00:00" + }, + { + "sender": "Lisa", + "message": "It was good, thanks for asking. How about you?", + "timestamp": "2023-04-01T18:05:00" + }, + { + "sender": "John", + "message": "Eh, it was alright. Work was pretty busy, but nothing too crazy.", + "timestamp": "2023-04-01T18:10:00" + }, + { + "sender": "Lisa", + "message": "Yeah, I know what you mean. My boss has been on my case lately about meeting our deadlines.", + "timestamp": "2023-04-01T18:15:00" + }, + { + "sender": "John", + "message": "That sucks. Are you feeling stressed out?", + "timestamp": "2023-04-01T18:20:00" + }, + { + "sender": "Lisa", + "message": "A little bit, yeah. But I'm trying to stay positive and focus on getting my work done.", + "timestamp": "2023-04-01T18:25:00" + }, + { + "sender": "John", + "message": "That's a good attitude to have. Have you tried doing some meditation or other relaxation techniques?", + "timestamp": "2023-04-01T18:30:00" + }, + { + "sender": "Lisa", + "message": "I haven't, but I've been thinking about it. Do you have any suggestions?", + "timestamp": "2023-04-01T18:35:00" + }, + { + "sender": "John", + "message": "Sure, I could send you some links to guided meditations that I've found helpful. And there are also some great apps out there that can help you with relaxation.", + "timestamp": "2023-04-01T18:40:00" + }, + { + "sender": "Lisa", + "message": "That would be awesome, thanks so much!", + "timestamp": "2023-04-01T18:45:00" + } + ] +} + diff --git a/samples/VirtualizationDemo/MainWindow.axaml b/samples/VirtualizationDemo/MainWindow.axaml new file mode 100644 index 0000000000..94e7c96a76 --- /dev/null +++ b/samples/VirtualizationDemo/MainWindow.axaml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/samples/VirtualizationDemo/MainWindow.axaml.cs b/samples/VirtualizationDemo/MainWindow.axaml.cs new file mode 100644 index 0000000000..533dc00aa1 --- /dev/null +++ b/samples/VirtualizationDemo/MainWindow.axaml.cs @@ -0,0 +1,15 @@ +using Avalonia; +using Avalonia.Controls; +using VirtualizationDemo.ViewModels; + +namespace VirtualizationDemo; + +public partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + this.AttachDevTools(); + DataContext = new MainWindowViewModel(); + } +} diff --git a/samples/VirtualizationDemo/MainWindow.xaml b/samples/VirtualizationDemo/MainWindow.xaml deleted file mode 100644 index 3aee63c246..0000000000 --- a/samples/VirtualizationDemo/MainWindow.xaml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - Horiz. ScrollBar - - Vert. ScrollBar - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/VirtualizationDemo/MainWindow.xaml.cs b/samples/VirtualizationDemo/MainWindow.xaml.cs deleted file mode 100644 index cea200dcec..0000000000 --- a/samples/VirtualizationDemo/MainWindow.xaml.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using VirtualizationDemo.ViewModels; - -namespace VirtualizationDemo -{ - public class MainWindow : Window - { - public MainWindow() - { - this.InitializeComponent(); - this.AttachDevTools(); - DataContext = new MainWindowViewModel(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - } -} diff --git a/samples/VirtualizationDemo/Models/Chat.cs b/samples/VirtualizationDemo/Models/Chat.cs new file mode 100644 index 0000000000..e84760135c --- /dev/null +++ b/samples/VirtualizationDemo/Models/Chat.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace VirtualizationDemo.Models; + +public class ChatFile +{ + public ChatMessage[] Chat { get; set; } + + public static ChatFile Load(string path) + { + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + using var s = File.OpenRead(path); + return JsonSerializer.Deserialize(s, options); + } +} + +public record ChatMessage(string Sender, string Message, DateTimeOffset Timestamp); diff --git a/samples/VirtualizationDemo/Program.cs b/samples/VirtualizationDemo/Program.cs index febda46450..87212b6daa 100644 --- a/samples/VirtualizationDemo/Program.cs +++ b/samples/VirtualizationDemo/Program.cs @@ -1,15 +1,14 @@ using Avalonia; -namespace VirtualizationDemo +namespace VirtualizationDemo; + +class Program { - class Program - { - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .UsePlatformDetect() - .LogToTrace(); + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); - public static int Main(string[] args) - => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - } + public static int Main(string[] args) + => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } diff --git a/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs b/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs new file mode 100644 index 0000000000..5ade0ec9ec --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs @@ -0,0 +1,16 @@ +using System.Collections.ObjectModel; +using System.IO; +using VirtualizationDemo.Models; + +namespace VirtualizationDemo.ViewModels; + +public class ChatPageViewModel +{ + public ChatPageViewModel() + { + var chat = ChatFile.Load(Path.Combine("Assets", "chat.json")); + Messages = new(chat.Chat); + } + + public ObservableCollection Messages { get; } +} diff --git a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs deleted file mode 100644 index 9ba505ffe5..0000000000 --- a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using MiniMvvm; - -namespace VirtualizationDemo.ViewModels -{ - internal class ItemViewModel : ViewModelBase - { - private string _prefix; - private int _index; - private double _height = double.NaN; - - public ItemViewModel(int index, string prefix = "Item") - { - _prefix = prefix; - _index = index; - } - - public string Header => $"{_prefix} {_index}"; - - public double Height - { - get => _height; - set => this.RaiseAndSetIfChanged(ref _height, value); - } - } -} diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index 96dbbc1a83..6d3590307c 100644 --- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -1,160 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Layout; -using Avalonia.Controls.Selection; -using MiniMvvm; +using MiniMvvm; -namespace VirtualizationDemo.ViewModels -{ - internal class MainWindowViewModel : ViewModelBase - { - private int _itemCount = 200; - private string _newItemString = "New Item"; - private int _newItemIndex; - private AvaloniaList _items; - private string _prefix = "Item"; - private ScrollBarVisibility _horizontalScrollBarVisibility = ScrollBarVisibility.Auto; - private ScrollBarVisibility _verticalScrollBarVisibility = ScrollBarVisibility.Auto; - private Orientation _orientation = Orientation.Vertical; - - public MainWindowViewModel() - { - this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems); - RecreateCommand = MiniCommand.Create(() => Recreate()); - - AddItemCommand = MiniCommand.Create(() => AddItem()); - - RemoveItemCommand = MiniCommand.Create(() => Remove()); - - SelectFirstCommand = MiniCommand.Create(() => SelectItem(0)); - - SelectLastCommand = MiniCommand.Create(() => SelectItem(Items.Count - 1)); - } - - public string NewItemString - { - get { return _newItemString; } - set { this.RaiseAndSetIfChanged(ref _newItemString, value); } - } - - public int ItemCount - { - get { return _itemCount; } - set { this.RaiseAndSetIfChanged(ref _itemCount, value); } - } - - public SelectionModel Selection { get; } = new SelectionModel(); - - public AvaloniaList Items - { - get { return _items; } - private set { this.RaiseAndSetIfChanged(ref _items, value); } - } - - public Orientation Orientation - { - get { return _orientation; } - set { this.RaiseAndSetIfChanged(ref _orientation, value); } - } - - public IEnumerable Orientations => - Enum.GetValues(typeof(Orientation)).Cast(); - - public ScrollBarVisibility HorizontalScrollBarVisibility - { - get { return _horizontalScrollBarVisibility; } - set { this.RaiseAndSetIfChanged(ref _horizontalScrollBarVisibility, value); } - } +namespace VirtualizationDemo.ViewModels; - public ScrollBarVisibility VerticalScrollBarVisibility - { - get { return _verticalScrollBarVisibility; } - set { this.RaiseAndSetIfChanged(ref _verticalScrollBarVisibility, value); } - } - - public IEnumerable ScrollBarVisibilities => - Enum.GetValues(typeof(ScrollBarVisibility)).Cast(); - - public MiniCommand AddItemCommand { get; private set; } - public MiniCommand RecreateCommand { get; private set; } - public MiniCommand RemoveItemCommand { get; private set; } - public MiniCommand SelectFirstCommand { get; private set; } - public MiniCommand SelectLastCommand { get; private set; } - - public void RandomizeSize() - { - var random = new Random(); - - foreach (var i in Items) - { - i.Height = random.Next(240) + 10; - } - } - - public void ResetSize() - { - foreach (var i in Items) - { - i.Height = double.NaN; - } - } - - private void ResizeItems(int count) - { - if (Items == null) - { - var items = Enumerable.Range(0, count) - .Select(x => new ItemViewModel(x)); - Items = new AvaloniaList(items); - } - else if (count > Items.Count) - { - var items = Enumerable.Range(Items.Count, count - Items.Count) - .Select(x => new ItemViewModel(x)); - Items.AddRange(items); - } - else if (count < Items.Count) - { - Items.RemoveRange(count, Items.Count - count); - } - } - - private void AddItem() - { - var index = Items.Count; - - if (Selection.SelectedItems.Count > 0) - { - index = Selection.SelectedIndex; - } - - Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString)); - } - - private void Remove() - { - if (Selection.SelectedItems.Count > 0) - { - Items.RemoveAll(Selection.SelectedItems.ToList()); - } - } - - private void Recreate() - { - _prefix = _prefix == "Item" ? "Recreated" : "Item"; - var items = Enumerable.Range(0, _itemCount) - .Select(x => new ItemViewModel(x, _prefix)); - Items = new AvaloniaList(items); - } - - private void SelectItem(int index) - { - Selection.SelectedIndex = index; - } - } +internal class MainWindowViewModel : ViewModelBase +{ + public ChatPageViewModel Chat { get; } = new(); } diff --git a/samples/VirtualizationDemo/Views/ChatPageView.axaml b/samples/VirtualizationDemo/Views/ChatPageView.axaml new file mode 100644 index 0000000000..fc182f15ae --- /dev/null +++ b/samples/VirtualizationDemo/Views/ChatPageView.axaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs b/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs new file mode 100644 index 0000000000..b5c90db69c --- /dev/null +++ b/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace VirtualizationDemo.Views; + +public partial class ChatPageView : UserControl +{ + public ChatPageView() + { + InitializeComponent(); + } +} diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index 81b30c6cbe..3ac7aab589 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -1,19 +1,24 @@  - Exe + WinExe net6.0 + true + + + - - + + + + + PreserveNewest + - - - - - + + From 35d70577f48d352c9dec7ce3e9389ff2ac4bc7d4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 3 Apr 2023 10:45:45 +0200 Subject: [PATCH 06/36] Added repro for #10367 to virtualization demo. --- samples/VirtualizationDemo/MainWindow.axaml | 3 +++ .../ViewModels/ExpanderItemViewModel.cs | 21 +++++++++++++++++++ .../ViewModels/ExpanderPageViewModel.cs | 17 +++++++++++++++ .../ViewModels/MainWindowViewModel.cs | 1 + .../Views/ExpanderPageView.axaml | 18 ++++++++++++++++ .../Views/ExpanderPageView.axaml.cs | 13 ++++++++++++ 6 files changed, 73 insertions(+) create mode 100644 samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs create mode 100644 samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs create mode 100644 samples/VirtualizationDemo/Views/ExpanderPageView.axaml create mode 100644 samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs diff --git a/samples/VirtualizationDemo/MainWindow.axaml b/samples/VirtualizationDemo/MainWindow.axaml index 94e7c96a76..e064e6ab32 100644 --- a/samples/VirtualizationDemo/MainWindow.axaml +++ b/samples/VirtualizationDemo/MainWindow.axaml @@ -12,5 +12,8 @@ + + + diff --git a/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs new file mode 100644 index 0000000000..a17fc2d303 --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs @@ -0,0 +1,21 @@ +using MiniMvvm; + +namespace VirtualizationDemo.ViewModels; + +public class ExpanderItemViewModel : ViewModelBase +{ + private string? _header; + private bool _isExpanded; + + public string? Header + { + get => _header; + set => RaiseAndSetIfChanged(ref _header, value); + } + + public bool IsExpanded + { + get => _isExpanded; + set => RaiseAndSetIfChanged(ref _isExpanded, value); + } +} diff --git a/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs b/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs new file mode 100644 index 0000000000..f2807a803b --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs @@ -0,0 +1,17 @@ +using System.Collections.ObjectModel; +using System.Linq; + +namespace VirtualizationDemo.ViewModels; + +internal class ExpanderPageViewModel +{ + public ExpanderPageViewModel() + { + Items = new(Enumerable.Range(0, 100).Select(x => new ExpanderItemViewModel + { + Header = $"Item {x}", + })); + } + + public ObservableCollection Items { get; set; } +} diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index 6d3590307c..6432503595 100644 --- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -5,4 +5,5 @@ namespace VirtualizationDemo.ViewModels; internal class MainWindowViewModel : ViewModelBase { public ChatPageViewModel Chat { get; } = new(); + public ExpanderPageViewModel Expanders { get; } = new(); } diff --git a/samples/VirtualizationDemo/Views/ExpanderPageView.axaml b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml new file mode 100644 index 0000000000..972d885229 --- /dev/null +++ b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs new file mode 100644 index 0000000000..df3689cf24 --- /dev/null +++ b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace VirtualizationDemo.Views; + +public partial class ExpanderPageView : UserControl +{ + public ExpanderPageView() + { + InitializeComponent(); + } +} \ No newline at end of file From 345fb7e1d6b8aaadf01b3f7f2f722dbff433a261 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 31 Mar 2023 16:23:57 +0200 Subject: [PATCH 07/36] Register anchor candidate in panel. We need to register controls as anchor candidates in the panel instead of in `ItemsControl` because the candidate needs to be registered after arrange. Consider this scenario: - In Measure: - Container is realized and registered as an anchor candidate - Container is unrealized and unregistered - Container is recycled and registered, but it is still placed in the position from before it was recycled - In Arrange: - The container is placed in its new position - The `ScrollContentPresenter` sees it's been moved and adjusts the viewport to anchor it This is obviously incorrect, but was what was happening when `ItemsControl` was responsible for registering anchor candidates. Instead of tracking which containers have already been registered, change the list of anchor candidates in `ScrollContentPresenter` to a `HashSet` so we can just register it multiple times. --- src/Avalonia.Controls/ItemsControl.cs | 4 ---- .../Presenters/ScrollContentPresenter.cs | 5 +++-- src/Avalonia.Controls/VirtualizingStackPanel.cs | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 54ffba462f..4d71cc8d4f 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -94,7 +94,6 @@ namespace Avalonia.Controls private ItemContainerGenerator? _itemContainerGenerator; private EventHandler? _childIndexChanged; private IDataTemplate? _displayMemberItemTemplate; - private ScrollViewer? _scrollViewer; private ItemsPresenter? _itemsPresenter; /// @@ -457,7 +456,6 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - _scrollViewer = e.NameScope.Find("PART_ScrollViewer"); _itemsPresenter = e.NameScope.Find("PART_ItemsPresenter"); } @@ -629,7 +627,6 @@ namespace Avalonia.Controls internal void ItemContainerPrepared(Control container, object? item, int index) { _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index)); - _scrollViewer?.RegisterAnchorCandidate(container); ContainerPrepared?.Invoke(this, new(container, index)); } @@ -642,7 +639,6 @@ namespace Avalonia.Controls internal void ClearItemContainer(Control container) { - _scrollViewer?.UnregisterAnchorCandidate(container); ClearContainerForItemOverride(container); ContainerClearing?.Invoke(this, new(container)); } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index bc86558ab3..261b7d3533 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -97,7 +97,7 @@ namespace Avalonia.Controls.Presenters private Size _viewport; private Dictionary? _activeLogicalGestureScrolls; private Dictionary? _scrollGestureSnapPoints; - private List? _anchorCandidates; + private HashSet? _anchorCandidates; private Control? _anchorElement; private Rect _anchorElementBounds; private bool _isAnchorElementDirty; @@ -310,7 +310,7 @@ namespace Avalonia.Controls.Presenters "An anchor control must be a visual descendent of the ScrollContentPresenter."); } - _anchorCandidates ??= new List(); + _anchorCandidates ??= new(); _anchorCandidates.Add(element); _isAnchorElementDirty = true; } @@ -410,6 +410,7 @@ namespace Avalonia.Controls.Presenters try { _arranging = true; + Offset = newOffset; } finally diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 5a766b9cd3..ad2ae9278c 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -67,6 +67,7 @@ namespace Avalonia.Controls private double _lastEstimatedElementSizeU = 25; private RealizedStackElements? _measureElements; private RealizedStackElements? _realizedElements; + private ScrollViewer? _scrollViewer; private Rect _viewport = s_invalidViewport; private Stack? _recyclePool; private Control? _unrealizedFocusedElement; @@ -203,6 +204,7 @@ namespace Avalonia.Controls new Rect(u, 0, sizeU, finalSize.Height) : new Rect(0, u, finalSize.Width, sizeU); e.Arrange(rect); + _scrollViewer?.RegisterAnchorCandidate(e); u += orientation == Orientation.Horizontal ? rect.Width : rect.Height; } } @@ -217,6 +219,18 @@ namespace Avalonia.Controls } } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _scrollViewer = this.FindAncestorOfType(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _scrollViewer = null; + } + protected override void OnItemsChanged(IReadOnlyList items, NotifyCollectionChangedEventArgs e) { InvalidateMeasure(); @@ -598,6 +612,8 @@ namespace Avalonia.Controls { Debug.Assert(ItemContainerGenerator is not null); + _scrollViewer?.UnregisterAnchorCandidate(element); + if (element.IsSet(ItemIsOwnContainerProperty)) { element.IsVisible = false; From 54449850cc39d9c04c13412fc3fb9f27c35c03aa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 31 Mar 2023 22:56:13 +0200 Subject: [PATCH 08/36] Make thumb drag delta relative to parent. Prevents the viewport jumping around when scrolling a virtualized list with differing sizes. Instead the thumb jumps around. --- src/Avalonia.Controls/Primitives/Thumb.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index cb3195cf52..c205830bc2 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -2,6 +2,7 @@ using System; using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { @@ -80,20 +81,22 @@ namespace Avalonia.Controls.Primitives { if (_lastPoint.HasValue) { + var point = e.GetPosition(this.GetVisualParent()); var ev = new VectorEventArgs { RoutedEvent = DragDeltaEvent, - Vector = e.GetPosition(this) - _lastPoint.Value, + Vector = point - _lastPoint.Value, }; RaiseEvent(ev); + _lastPoint = point; } } protected override void OnPointerPressed(PointerPressedEventArgs e) { e.Handled = true; - _lastPoint = e.GetPosition(this); + _lastPoint = e.GetPosition(this.GetVisualParent()); var ev = new VectorEventArgs { @@ -116,7 +119,7 @@ namespace Avalonia.Controls.Primitives var ev = new VectorEventArgs { RoutedEvent = DragCompletedEvent, - Vector = (Vector)e.GetPosition(this), + Vector = (Vector)e.GetPosition(this.GetVisualParent()), }; RaiseEvent(ev); From bc32c061e8fe3229cd60ff4a1767e83389dd0667 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 3 Apr 2023 10:06:50 +0200 Subject: [PATCH 09/36] Added tests to detect scroll jumps. --- .../VirtualizingStackPanel.cs | 10 ++ .../VirtualizingStackPanelTests.cs | 146 +++++++++++++++++- 2 files changed, 149 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index ad2ae9278c..7cca0986ad 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -130,6 +130,16 @@ namespace Avalonia.Controls set { SetValue(AreVerticalSnapPointsRegularProperty, value); } } + /// + /// Gets the index of the first realized element, or -1 if no elements are realized. + /// + public int FirstRealizedIndex => _realizedElements?.FirstIndex ?? -1; + + /// + /// Gets the index of the last realized element, or -1 if no elements are realized. + /// + public int LastRealizedIndex => _realizedElements?.LastIndex ?? -1; + protected override Size MeasureOverride(Size availableSize) { if (!IsEffectivelyVisible) diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 76fc31c50d..2cdd4eaf95 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -37,7 +37,7 @@ namespace Avalonia.Controls.UnitTests { using var app = App(); var items = Enumerable.Range(0, 100).Select(x => new Button { Width = 25, Height = 10}); - var (target, scroll, itemsControl) = CreateTarget(items: items, useItemTemplate: false); + var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: null); Assert.Equal(1000, scroll.Extent.Height); @@ -252,7 +252,7 @@ namespace Avalonia.Controls.UnitTests { using var app = App(); var items = new ObservableCollection