From 24f208bab1fce4a7ed9403703a0b7f9ff07d2c2b Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 25 Mar 2019 17:45:42 +0100 Subject: [PATCH 001/138] !R Rework deferred setter for better performance. --- src/Avalonia.Base/AvaloniaObject.cs | 91 +++++++++-- .../Utilities/AvaloniaPropertyCollection.cs | 143 ++++++++++++++++ .../Utilities/DeferredSetterOptimized.cs | 84 ++++++++++ src/Avalonia.Base/ValueStore.cs | 153 ++++-------------- .../Base/DirectPropertyBenchmark.cs | 82 ++++++++++ tests/Avalonia.Benchmarks/Base/Properties.cs | 3 +- 6 files changed, 420 insertions(+), 136 deletions(-) create mode 100644 src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs create mode 100644 src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs create mode 100644 tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 7601b64ce9..22aeccc3fb 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -561,6 +561,7 @@ namespace Avalonia protected bool SetAndRaise(AvaloniaProperty property, ref T field, T value) { VerifyAccess(); + return SetAndRaise( property, ref field, @@ -568,19 +569,89 @@ namespace Avalonia => SetAndRaiseCore(property, ref backing, val, notifyWrapper), value); } + + /// + /// Default setter handler that will set backing field and raise notification. + /// + private sealed class DefaultSetterHandler : DeferredSetterOptimized.ISetterHandler + { + public static readonly DefaultSetterHandler Instance = new DefaultSetterHandler(); + + public bool Update(AvaloniaObject source, AvaloniaProperty property, ref T backing, T value) + { + var old = backing; + backing = value; + + source.RaisePropertyChanged(property, old, value); + + return true; + } + } /// - /// Default assignment logic for SetAndRaise. + /// Setter handler that will run custom user callback. /// - /// The type of the property. - /// The property. - /// The backing field. - /// The value. - /// A wrapper for the property-changed notification. - /// - /// True if the value changed, otherwise false. - /// - private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) + private sealed class CallbackSetterHandler : DeferredSetterOptimized.ISetterHandler + { + private readonly SetAndRaiseCallback _callback; + + public CallbackSetterHandler(SetAndRaiseCallback callback) + { + _callback = callback; + } + + public bool Update(AvaloniaObject source, AvaloniaProperty property, ref T backing, T value) + { + _callback(value, ref backing, notification => notification()); + + return true; + } + } + + protected bool SetAndRaiseOptimized(AvaloniaProperty property, ref T field, T value) + { + VerifyAccess(); + + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + DeferredSetterOptimized setter = Values.GetDeferredSetter(property); + + return setter.SetAndNotify(this, property, DefaultSetterHandler.Instance, ref field, value); + } + + protected bool SetAndRaiseOptimized( + AvaloniaProperty property, + ref T field, + SetAndRaiseCallback setterCallback, + T value) + { + VerifyAccess(); + + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + DeferredSetterOptimized setter = Values.GetDeferredSetter(property); + + return setter.SetAndNotify(this, property, new CallbackSetterHandler(setterCallback) , ref field, value); + } + + /// + /// Default assignment logic for SetAndRaise. + /// + /// The type of the property. + /// The property. + /// The backing field. + /// The value. + /// A wrapper for the property-changed notification. + /// + /// True if the value changed, otherwise false. + /// + private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) { var old = field; field = value; diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs new file mode 100644 index 0000000000..5b1492a7a8 --- /dev/null +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.Utilities +{ + internal sealed class AvaloniaPropertyCollection + { + private Entry[] _entries; + + public AvaloniaPropertyCollection() + { + // The last item in the list is always int.MaxValue + _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } }; + } + + private (int, bool) TryFindEntry(int propertyId) + { + if (_entries.Length <= 12) + { + // For small lists, we use an optimized linear search. Since the last item in the list + // is always int.MaxValue, we can skip a conditional branch in each iteration. + // By unrolling the loop, we can skip another unconditional branch in each iteration. + + if (_entries[0].PropertyId >= propertyId) + return (0, _entries[0].PropertyId == propertyId); + if (_entries[1].PropertyId >= propertyId) + return (1, _entries[1].PropertyId == propertyId); + if (_entries[2].PropertyId >= propertyId) + return (2, _entries[2].PropertyId == propertyId); + if (_entries[3].PropertyId >= propertyId) + return (3, _entries[3].PropertyId == propertyId); + if (_entries[4].PropertyId >= propertyId) + return (4, _entries[4].PropertyId == propertyId); + if (_entries[5].PropertyId >= propertyId) + return (5, _entries[5].PropertyId == propertyId); + if (_entries[6].PropertyId >= propertyId) + return (6, _entries[6].PropertyId == propertyId); + if (_entries[7].PropertyId >= propertyId) + return (7, _entries[7].PropertyId == propertyId); + if (_entries[8].PropertyId >= propertyId) + return (8, _entries[8].PropertyId == propertyId); + if (_entries[9].PropertyId >= propertyId) + return (9, _entries[9].PropertyId == propertyId); + if (_entries[10].PropertyId >= propertyId) + return (10, _entries[10].PropertyId == propertyId); + } + else + { + int low = 0; + int high = _entries.Length; + int id; + + while (high - low > 3) + { + int pivot = (high + low) / 2; + id = _entries[pivot].PropertyId; + + if (propertyId == id) + return (pivot, true); + + if (propertyId <= id) + high = pivot; + else + low = pivot + 1; + } + + do + { + id = _entries[low].PropertyId; + + if (id == propertyId) + return (low, true); + + if (id > propertyId) + break; + + ++low; + } + while (low < high); + } + + return (0, false); + } + + public bool TryGetValue(AvaloniaProperty property, out TValue value) + { + (int index, bool found) = TryFindEntry(property.Id); + if (!found) + { + value = default; + return false; + } + + value = _entries[index].Value; + return true; + } + + public void AddValueInternal(AvaloniaProperty property, TValue value) + { + Entry[] entries = new Entry[_entries.Length + 1]; + + for (int i = 0; i < _entries.Length; ++i) + { + if (_entries[i].PropertyId > property.Id) + { + if (i > 0) + { + Array.Copy(_entries, 0, entries, 0, i); + } + + entries[i] = new Entry { PropertyId = property.Id, Value = value }; + Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); + break; + } + } + + _entries = entries; + } + + public void SetValueInternal(AvaloniaProperty property, TValue value) + { + _entries[TryFindEntry(property.Id).Item1].Value = value; + } + + public Dictionary ToDictionary() + { + var dict = new Dictionary(_entries.Length - 1); + + for (int i = 0; i < _entries.Length - 1; ++i) + { + dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); + } + + return dict; + } + + private struct Entry + { + internal int PropertyId; + internal TValue Value; + } + } +} diff --git a/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs b/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs new file mode 100644 index 0000000000..e3bed90498 --- /dev/null +++ b/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs @@ -0,0 +1,84 @@ +using System; + +namespace Avalonia.Utilities +{ + /// + /// A utility class to enable deferring assignment until after property-changed notifications are sent. + /// Used to fix #855. + /// + /// The type of value with which to track the delayed assignment. + internal sealed class DeferredSetterOptimized + { + private bool _isNotifying; + private readonly SingleOrQueue _pendingValues; + + public DeferredSetterOptimized() + { + _pendingValues = new SingleOrQueue(); + } + + public bool SetAndNotify( + AvaloniaObject source, + AvaloniaProperty property, + ISetterHandler handler, + ref TSetRecord backing, + TSetRecord value) + { + if (!_isNotifying) + { + bool updated; + + using (new NotifyDisposable(this)) + { + updated = handler.Update(source, property, ref backing, value); + } + + if (!_pendingValues.Empty) + { + using (new NotifyDisposable(this)) + { + while (!_pendingValues.Empty) + { + updated = handler.Update(source, property, ref backing, _pendingValues.Dequeue()); + } + } + } + + return updated; + } + + _pendingValues.Enqueue(value); + + return false; + } + + /// + /// Disposable that marks the property as currently notifying. + /// When disposed, marks the property as done notifying. + /// + private readonly struct NotifyDisposable : IDisposable + { + private readonly DeferredSetterOptimized _setter; + + internal NotifyDisposable(DeferredSetterOptimized setter) + { + _setter = setter; + _setter._isNotifying = true; + } + + public void Dispose() + { + _setter._isNotifying = false; + } + } + + public interface ISetterHandler + { + bool Update( + AvaloniaObject source, + AvaloniaProperty property, + ref TSetRecord backing, + TSetRecord value); + } + } +} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 24f85ea6b1..b2c4c68f9c 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -7,21 +7,15 @@ namespace Avalonia { internal class ValueStore : IPriorityValueOwner { - private struct Entry - { - internal int PropertyId; - internal object Value; - } - + private readonly AvaloniaPropertyCollection _propertyValues; + private readonly AvaloniaPropertyCollection _deferredSetters; private readonly AvaloniaObject _owner; - private Entry[] _entries; public ValueStore(AvaloniaObject owner) { _owner = owner; - - // The last item in the list is always int.MaxValue - _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = null } }; + _propertyValues = new AvaloniaPropertyCollection(); + _deferredSetters = new AvaloniaPropertyCollection(); } public IDisposable AddBinding( @@ -31,7 +25,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (TryGetValue(property, out var v)) + if (_propertyValues.TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -39,13 +33,13 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - SetValueInternal(property, priorityValue); + _propertyValues.SetValueInternal(property, priorityValue); } } else { priorityValue = CreatePriorityValue(property); - AddValueInternal(property, priorityValue); + _propertyValues.AddValueInternal(property, priorityValue); } return priorityValue.Add(source, (int)priority); @@ -55,7 +49,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (TryGetValue(property, out var v)) + if (_propertyValues.TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -63,7 +57,7 @@ namespace Avalonia { if (priority == (int)BindingPriority.LocalValue) { - SetValueInternal(property, Validate(property, value)); + _propertyValues.SetValueInternal(property, Validate(property, value)); Changed(property, priority, v, value); return; } @@ -71,7 +65,7 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - SetValueInternal(property, priorityValue); + _propertyValues.SetValueInternal(property, priorityValue); } } } @@ -84,14 +78,14 @@ namespace Avalonia if (priority == (int)BindingPriority.LocalValue) { - AddValueInternal(property, Validate(property, value)); + _propertyValues.AddValueInternal(property, Validate(property, value)); Changed(property, priority, AvaloniaProperty.UnsetValue, value); return; } else { priorityValue = CreatePriorityValue(property); - AddValueInternal(property, priorityValue); + _propertyValues.AddValueInternal(property, priorityValue); } } @@ -110,14 +104,9 @@ namespace Avalonia public IDictionary GetSetValues() { - var dict = new Dictionary(_entries.Length - 1); - for (int i = 0; i < _entries.Length - 1; ++i) - { - dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); - } - - return dict; + return _propertyValues.ToDictionary(); } + public void LogError(AvaloniaProperty property, Exception e) { _owner.LogBindingError(property, e); @@ -127,7 +116,7 @@ namespace Avalonia { var result = AvaloniaProperty.UnsetValue; - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { result = (value is PriorityValue priorityValue) ? priorityValue.Value : value; } @@ -137,12 +126,12 @@ namespace Avalonia public bool IsAnimating(AvaloniaProperty property) { - return TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; + return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; } public bool IsSet(AvaloniaProperty property) { - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue; } @@ -152,7 +141,7 @@ namespace Avalonia public void Revalidate(AvaloniaProperty property) { - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { (value as PriorityValue)?.Revalidate(); } @@ -189,113 +178,29 @@ namespace Avalonia return value; } - private DeferredSetter _deferredSetter; - - public DeferredSetter Setter - { - get - { - return _deferredSetter ?? - (_deferredSetter = new DeferredSetter()); - } - } - - private bool TryGetValue(AvaloniaProperty property, out object value) + public DeferredSetterOptimized GetDeferredSetter(AvaloniaProperty property) { - (int index, bool found) = TryFindEntry(property.Id); - if (!found) + if (_deferredSetters.TryGetValue(property, out var deferredSetter)) { - value = null; - return false; + return (DeferredSetterOptimized)deferredSetter; } - value = _entries[index].Value; - return true; - } - - private void AddValueInternal(AvaloniaProperty property, object value) - { - Entry[] entries = new Entry[_entries.Length + 1]; - - for (int i = 0; i < _entries.Length; ++i) - { - if (_entries[i].PropertyId > property.Id) - { - if (i > 0) - { - Array.Copy(_entries, 0, entries, 0, i); - } + var newDeferredSetter = new DeferredSetterOptimized(); - entries[i] = new Entry { PropertyId = property.Id, Value = value }; - Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); - break; - } - } + _deferredSetters.AddValueInternal(property, newDeferredSetter); - _entries = entries; + return newDeferredSetter; } - private void SetValueInternal(AvaloniaProperty property, object value) - { - _entries[TryFindEntry(property.Id).Item1].Value = value; - } + private DeferredSetter _deferredSetter; - private (int, bool) TryFindEntry(int propertyId) + public DeferredSetter Setter { - if (_entries.Length <= 12) - { - // For small lists, we use an optimized linear search. Since the last item in the list - // is always int.MaxValue, we can skip a conditional branch in each iteration. - // By unrolling the loop, we can skip another unconditional branch in each iteration. - - if (_entries[0].PropertyId >= propertyId) return (0, _entries[0].PropertyId == propertyId); - if (_entries[1].PropertyId >= propertyId) return (1, _entries[1].PropertyId == propertyId); - if (_entries[2].PropertyId >= propertyId) return (2, _entries[2].PropertyId == propertyId); - if (_entries[3].PropertyId >= propertyId) return (3, _entries[3].PropertyId == propertyId); - if (_entries[4].PropertyId >= propertyId) return (4, _entries[4].PropertyId == propertyId); - if (_entries[5].PropertyId >= propertyId) return (5, _entries[5].PropertyId == propertyId); - if (_entries[6].PropertyId >= propertyId) return (6, _entries[6].PropertyId == propertyId); - if (_entries[7].PropertyId >= propertyId) return (7, _entries[7].PropertyId == propertyId); - if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId); - if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId); - if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId); - } - else + get { - int low = 0; - int high = _entries.Length; - int id; - - while (high - low > 3) - { - int pivot = (high + low) / 2; - id = _entries[pivot].PropertyId; - - if (propertyId == id) - return (pivot, true); - - if (propertyId <= id) - high = pivot; - else - low = pivot + 1; - } - - do - { - id = _entries[low].PropertyId; - - if (id == propertyId) - return (low, true); - - if (id > propertyId) - break; - - ++low; - } - while (low < high); + return _deferredSetter ?? + (_deferredSetter = new DeferredSetter()); } - - return (0, false); } } } diff --git a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs new file mode 100644 index 0000000000..4f36b54414 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs @@ -0,0 +1,82 @@ +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Base +{ + [MemoryDiagnoser] + public class DirectPropertyBenchmark + { + [Benchmark(Baseline = true)] + public void SetAndRaiseOriginal() + { + var obj = new DirectClass(); + + for (var i = 0; i < 100; ++i) + { + obj.IntValue += 1; + } + } + + [Benchmark] + public void SetAndRaiseOptimized() + { + var obj = new DirectClass(); + + for (var i = 0; i < 100; ++i) + { + obj.IntValueOptimized += 1; + } + } + + [Benchmark] + public void SetAndRaiseSimple() + { + var obj = new DirectClass(); + + for (var i = 0; i < 100; ++i) + { + obj.IntValueSimple += 1; + } + } + + class DirectClass : AvaloniaObject + { + private int _intValue; + + public static readonly DirectProperty IntValueProperty = + AvaloniaProperty.RegisterDirect(nameof(IntValue), + o => o.IntValue, + (o, v) => o.IntValue = v); + + public int IntValue + { + get => _intValue; + set => SetAndRaise(IntValueProperty, ref _intValue, value); + } + + public int IntValueOptimized + { + get => _intValue; + set => SetAndRaiseOptimized(IntValueProperty, ref _intValue, value); + } + + public int IntValueSimple + { + get => _intValue; + set + { + VerifyAccess(); + + if (_intValue == value) + { + return; + } + + var old = _intValue; + _intValue = value; + + RaisePropertyChanged(IntValueProperty, old, _intValue); + } + } + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Benchmarks/Base/Properties.cs b/tests/Avalonia.Benchmarks/Base/Properties.cs index 0a020961d5..45fc68ac96 100644 --- a/tests/Avalonia.Benchmarks/Base/Properties.cs +++ b/tests/Avalonia.Benchmarks/Base/Properties.cs @@ -1,5 +1,4 @@ -using System; -using System.Reactive.Subjects; +using System.Reactive.Subjects; using BenchmarkDotNet.Attributes; namespace Avalonia.Benchmarks.Base From ece1b3e334c4c77acb1092e64604e4d285a7ce8a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 31 May 2019 22:45:14 +0200 Subject: [PATCH 002/138] Initial import of ItemsRepeater from the WinUI library. https://github.com/microsoft/microsoft-ui-xaml --- samples/ControlCatalog/MainView.xaml | 1 + .../Pages/ItemsRepeaterPage.xaml | 7 + .../Pages/ItemsRepeaterPage.xaml.cs | 25 + .../Properties/AssemblyInfo.cs | 1 + .../Repeaters/ElementFactoryGetArgs.cs | 11 + .../Repeaters/ElementFactoryRecycleArgs.cs | 12 + .../Repeaters/ElementManager.cs | 426 ++++++++++ .../Repeaters/FlowLayoutAlgorithm.cs | 689 +++++++++++++++ .../IFlowLayoutAlgorithmDelegates.cs | 43 + .../Repeaters/ITrackerHandleManager.cs | 10 + .../Repeaters/ItemTemplateWrapper.cs | 53 ++ .../Repeaters/ItemsRepeater.cs | 631 ++++++++++++++ .../ItemsRepeaterElementClearingEventArgs.cs | 11 + ...emsRepeaterElementIndexChangedEventArgs.cs | 27 + .../ItemsRepeaterElementPreparedEventArgs.cs | 25 + .../Repeaters/ItemsSourceView.cs | 91 ++ src/Avalonia.Controls/Repeaters/Layout.cs | 24 + .../Repeaters/LayoutContext.cs | 13 + .../Repeaters/NonVirtualizingLayout.cs | 29 + .../Repeaters/OrientationBasedMeasures.cs | 78 ++ .../Repeaters/RecyclePool.cs | 101 +++ .../Repeaters/RepeaterLayoutContext.cs | 59 ++ .../Repeaters/StackLayout.cs | 298 +++++++ .../Repeaters/StackLayoutState.cs | 53 ++ .../Repeaters/UniqueIdElementPool.cs | 54 ++ .../Repeaters/ViewManager.cs | 787 ++++++++++++++++++ .../Repeaters/ViewportManager.cs | 463 +++++++++++ .../Repeaters/VirtualizationInfo.cs | 121 +++ .../Repeaters/VirtualizingLayout.cs | 47 ++ .../Repeaters/VirtualizingLayoutContext.cs | 190 +++++ src/Avalonia.Controls/StackPanel.cs | 4 +- src/Avalonia.Controls/Utils/ListUtils.cs | 32 + .../Rendering/DeferredRenderer.cs | 1 - 33 files changed, 4414 insertions(+), 3 deletions(-) create mode 100644 samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml create mode 100644 samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs create mode 100644 src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs create mode 100644 src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs create mode 100644 src/Avalonia.Controls/Repeaters/ElementManager.cs create mode 100644 src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs create mode 100644 src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs create mode 100644 src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs create mode 100644 src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs create mode 100644 src/Avalonia.Controls/Repeaters/ItemsRepeater.cs create mode 100644 src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs create mode 100644 src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs create mode 100644 src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs create mode 100644 src/Avalonia.Controls/Repeaters/ItemsSourceView.cs create mode 100644 src/Avalonia.Controls/Repeaters/Layout.cs create mode 100644 src/Avalonia.Controls/Repeaters/LayoutContext.cs create mode 100644 src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs create mode 100644 src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs create mode 100644 src/Avalonia.Controls/Repeaters/RecyclePool.cs create mode 100644 src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs create mode 100644 src/Avalonia.Controls/Repeaters/StackLayout.cs create mode 100644 src/Avalonia.Controls/Repeaters/StackLayoutState.cs create mode 100644 src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs create mode 100644 src/Avalonia.Controls/Repeaters/ViewManager.cs create mode 100644 src/Avalonia.Controls/Repeaters/ViewportManager.cs create mode 100644 src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs create mode 100644 src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs create mode 100644 src/Avalonia.Controls/Repeaters/VirtualizingLayoutContext.cs create mode 100644 src/Avalonia.Controls/Utils/ListUtils.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 1cddb9d295..4c9b4eaed5 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -26,6 +26,7 @@ + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml new file mode 100644 index 0000000000..a0cbefa4d9 --- /dev/null +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -0,0 +1,7 @@ + + + + + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs new file mode 100644 index 0000000000..a6ca27cb67 --- /dev/null +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class ItemsRepeaterPage : UserControl + { + public ItemsRepeaterPage() + { + this.InitializeComponent(); + DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" ) + .ToArray(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + } +} diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs index c04c66a77f..8fa80e55d2 100644 --- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls/Properties/AssemblyInfo.cs @@ -13,6 +13,7 @@ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Embedding")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Presenters")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Repeaters")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Shapes")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Templates")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Notifications")] diff --git a/src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs b/src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs new file mode 100644 index 0000000000..173388b68e --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs @@ -0,0 +1,11 @@ +using System; + +namespace Avalonia.Controls.Repeaters +{ + public sealed class ElementFactoryGetArgs : EventArgs + { + public object Data { get; set; } + public IControl Parent { get; set; } + internal int Index { get; set; } + } +} diff --git a/src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs b/src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs new file mode 100644 index 0000000000..3025546f67 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + public sealed class ElementFactoryRecycleArgs : EventArgs + { + public IControl Element { get; set; } + public IControl Parent { get; set; } + } +} diff --git a/src/Avalonia.Controls/Repeaters/ElementManager.cs b/src/Avalonia.Controls/Repeaters/ElementManager.cs new file mode 100644 index 0000000000..2ce0ad24fa --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ElementManager.cs @@ -0,0 +1,426 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Text; +using Avalonia.Controls.Utils; + +namespace Avalonia.Controls.Repeaters +{ + internal class ElementManager + { + private readonly List _realizedElements = new List(); + private readonly List _realizedElementLayoutBounds = new List(); + 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(Orientation 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 IControl GetAt(int realizedIndex) + { + IControl element; + + if (IsVirtualizingContext) + { + if (_realizedElements[realizedIndex] == null) + { + // Sentinel. Create the element now since we need it. + int dataIndex = GetDataIndexFromRealizedRangeIndex(realizedIndex); + 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(IControl element, int dataIndex) + { + if (_realizedElements.Count == 0) + { + _firstRealizedDataIndex = dataIndex; + } + + _realizedElements.Add(element); + _realizedElementLayoutBounds.Add(default); + } + + public void Insert(int realizedIndex, int dataIndex, IControl 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 IControl 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); + } + } + } + + public bool IsWindowConnected(in Rect window, Orientation orientation, bool scrollOrientationSameAsFlow) + { + bool intersects = false; + + if (_realizedElementLayoutBounds.Count > 0) + { + var firstElementBounds = GetLayoutBoundsForRealizedIndex(0); + var lastElementBounds = GetLayoutBoundsForRealizedIndex(GetRealizedElementCount() - 1); + + var effectiveOrientation = scrollOrientationSameAsFlow ? + (orientation == Orientation.Vertical ? Orientation.Horizontal : Orientation.Vertical) : + orientation; + + + var windowStart = effectiveOrientation == Orientation.Vertical ? window.Y : window.X; + var windowEnd = effectiveOrientation == Orientation.Vertical ? window.Y + window.Height : window.X + window.Width; + var firstElementStart = effectiveOrientation == Orientation.Vertical ? firstElementBounds.Y : firstElementBounds.X; + var lastElementEnd = effectiveOrientation == Orientation.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: + { + OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count); + OnItemsAdded(args.NewStartingIndex, args.NewItems.Count); + } + 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(IControl 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, Orientation 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= 0 && + !Intersects(window, _realizedElementLayoutBounds[i], orientation); + --i) + { + --backCutoffIndex; + } + + if (backCutoffIndex 0) + { + ClearRealizedRange(0, Math.Min(frontCutoffIndex, GetRealizedElementCount())); + } + } + + private static bool Intersects(in Rect lhs, in Rect rhs, Orientation orientation) + { + var lhsStart = orientation == Orientation.Vertical ? lhs.Y : lhs.X; + var lhsEnd = orientation == Orientation.Vertical ? lhs.Y + lhs.Height : lhs.X + lhs.Width; + var rhsStart = orientation == Orientation.Vertical ? rhs.Y : rhs.X; + var rhsEnd = orientation == Orientation.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; + } + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs new file mode 100644 index 0000000000..a0ced65076 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs @@ -0,0 +1,689 @@ +using System; +using System.Collections.Specialized; + +namespace Avalonia.Controls.Repeaters +{ + internal class FlowLayoutAlgorithm + { + private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); + private readonly ElementManager _elementManager = new ElementManager(); + private Size _lastAvailableSize; + private double _lastItemSpacing; + private bool _collectionChangePending; + private VirtualizingLayoutContext _context; + private IFlowLayoutAlgorithmDelegates _algorithmCallbacks; + private Rect _lastExtent; + private int _firstRealizedDataIndexInsideRealizationWindow = -1; + private int _lastRealizedDataIndexInsideRealizationWindow = -1; + + // If the scroll orientation is the same as the folow orientation + // we will only have one line since we will never wrap. In that case + // we do not want to align the line. We could potentially switch the + // meaning of line alignment in this case, but I'll hold off on that + // feature until someone asks for it - This is not a common scenario + // anyway. + private bool _scrollOrientationSameAsFlow; + + public Rect LastExtent => _lastExtent; + + 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; + } + } + + private Rect RealizationRect => IsVirtualizingContext ? _context.RealizationRect : new Rect(Size.Infinity); + + public void InitializeForContext(VirtualizingLayoutContext context, IFlowLayoutAlgorithmDelegates callbacks) + { + _algorithmCallbacks = callbacks; + _context = context; + _elementManager.SetContext(context); + } + + public void UninitializeForContext(VirtualizingLayoutContext context) + { + if (IsVirtualizingContext) + { + // This layout is about to be detached. Let go of all elements + // being held and remove the layout state from the context. + _elementManager.ClearRealizedRange(); + } + + context.LayoutState = null; + } + + public Size Measure( + Size availableSize, + VirtualizingLayoutContext context, + bool isWrapping, + double minItemSpacing, + double lineSpacing, + Orientation orientation, + string layoutId) + { + _orientation.ScrollOrientation = orientation; + + // 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; + + var suggestedAnchorIndex = _context.RecommendedAnchorIndex; + if (_elementManager.IsIndexValidInData(suggestedAnchorIndex)) + { + var anchorRealized = _elementManager.IsDataIndexRealized(suggestedAnchorIndex); + if (!anchorRealized) + { + MakeAnchor(_context, suggestedAnchorIndex, availableSize); + } + } + + _elementManager.OnBeginMeasure(orientation); + + int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId); + Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId); + Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId); + if (isWrapping && IsReflowRequired()) + { + var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); + _orientation.SetMinorStart(ref firstElementBounds, 0); + _elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds); + Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, layoutId); + } + + RaiseLineArranged(); + _collectionChangePending = false; + _lastExtent = EstimateExtent(availableSize, layoutId); + SetLayoutOrigin(); + + return new Size(_lastExtent.Width, _lastExtent.Height); + } + + public Size Arrange( + Size finalSize, + VirtualizingLayoutContext context, + LineAlignment lineAlignment, + string layoutId) + { + ArrangeVirtualizingLayout(finalSize, lineAlignment, layoutId); + + return new Size( + Math.Max(finalSize.Width, _lastExtent.Width), + Math.Max(finalSize.Height, _lastExtent.Height)); + } + + public void OnItemsSourceChanged( + object source, + NotifyCollectionChangedEventArgs args, + VirtualizingLayoutContext context) + { + _elementManager.DataSourceChanged(source, args); + _collectionChangePending = true; + } + + public Size MeasureElement( + IControl element, + int index, + Size availableSize, + VirtualizingLayoutContext context) + { + var measureSize = _algorithmCallbacks.Algorithm_GetMeasureSize(index, availableSize, context); + element.Measure(measureSize); + var provisionalArrangeSize = _algorithmCallbacks.Algorithm_GetProvisionalArrangeSize(index, measureSize, element.DesiredSize, context); + _algorithmCallbacks.Algorithm_OnElementMeasured(element, index, availableSize, measureSize, element.DesiredSize, provisionalArrangeSize, context); + + return provisionalArrangeSize; + } + + private int GetAnchorIndex( + Size availableSize, + bool isWrapping, + double minItemSpacing, + string layoutId) + { + int anchorIndex = -1; + var anchorPosition= new Point(); + var context = _context; + + if (!IsVirtualizingContext) + { + // Non virtualizing host, start generating from the element 0 + anchorIndex = context.ItemCount > 0 ? 0 : -1; + } + else + { + bool isRealizationWindowConnected = _elementManager.IsWindowConnected(RealizationRect, _orientation.ScrollOrientation, _scrollOrientationSameAsFlow); + // Item spacing and size in non-virtualizing direction change can cause elements to reflow + // and get a new column position. In that case we need the anchor to be positioned in the + // correct column. + bool needAnchorColumnRevaluation = isWrapping && ( + _orientation.Minor(_lastAvailableSize) != _orientation.Minor(availableSize) || + _lastItemSpacing != minItemSpacing || + _collectionChangePending); + + var suggestedAnchorIndex = _context.RecommendedAnchorIndex; + + var isAnchorSuggestionValid = suggestedAnchorIndex >= 0 && + _elementManager.IsDataIndexRealized(suggestedAnchorIndex); + + if (isAnchorSuggestionValid) + { + anchorIndex = _algorithmCallbacks.Algorithm_GetAnchorForTargetElement( + suggestedAnchorIndex, + availableSize, + context).Index; + + if (_elementManager.IsDataIndexRealized(anchorIndex)) + { + var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex); + if (needAnchorColumnRevaluation) + { + // We were provided a valid anchor, but its position might be incorrect because for example it is in + // the wrong column. We do know that the anchor is the first element in the row, so we can force the minor position + // to start at 0. + anchorPosition = _orientation.MinorMajorPoint(0, _orientation.MajorStart(anchorBounds)); + } + else + { + anchorPosition = new Point(anchorBounds.X, anchorBounds.Y); + } + } + else + { + // It is possible to end up in a situation during a collection change where GetAnchorForTargetElement returns an index + // which is not in the realized range. Eg. insert one item at index 0 for a grid layout. + // SuggestedAnchor will be 1 (used to be 0) and GetAnchorForTargetElement will return 0 (left most item in row). However 0 is not in the + // realized range yet. In this case we realize the gap between the target anchor and the suggested anchor. + int firstRealizedDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0); + + for (int i = firstRealizedDataIndex - 1; i >= anchorIndex; --i) + { + _elementManager.EnsureElementRealized(false /*forward*/, i, layoutId); + } + + var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(suggestedAnchorIndex); + anchorPosition = _orientation.MinorMajorPoint(0, _orientation.MajorStart(anchorBounds)); + } + } + else if (needAnchorColumnRevaluation || !isRealizationWindowConnected) + { + // 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); + anchorIndex = anchorInfo.Index; + anchorPosition = _orientation.MinorMajorPoint(0, anchorInfo.Offset); + } + else + { + // No suggestion - just pick first in realized range + anchorIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0); + var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); + anchorPosition = new Point(firstElementBounds.X, firstElementBounds.Y); + } + } + + _firstRealizedDataIndexInsideRealizationWindow = _lastRealizedDataIndexInsideRealizationWindow = anchorIndex; + if (_elementManager.IsIndexValidInData(anchorIndex)) + { + if (!_elementManager.IsDataIndexRealized(anchorIndex)) + { + // Disconnected, throw everything and create new anchor + _elementManager.ClearRealizedRange(); + + var anchor = _context.GetOrCreateElementAt(anchorIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); + _elementManager.Add(anchor, anchorIndex); + } + + var anchorElement = _elementManager.GetRealizedElement(anchorIndex); + var desiredSize = MeasureElement(anchorElement, anchorIndex, availableSize, _context); + var layoutBounds = new Rect(anchorPosition.X, anchorPosition.Y, desiredSize.Width, desiredSize.Height); + _elementManager.SetLayoutBoundsForDataIndex(anchorIndex, layoutBounds); + } + else + { + _elementManager.ClearRealizedRange(); + } + + // TODO: Perhaps we can track changes in the property setter + _lastAvailableSize = availableSize; + _lastItemSpacing = minItemSpacing; + + return anchorIndex; + } + + private void Generate( + GenerateDirection direction, + int anchorIndex, + Size availableSize, + double minItemSpacing, + double lineSpacing, + string layoutId) + { + if (anchorIndex != -1) + { + int step = (direction == GenerateDirection.Forward) ? 1 : -1; + int previousIndex = anchorIndex; + int currentIndex = anchorIndex + step; + var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex); + var lineOffset = _orientation.MajorStart(anchorBounds); + var lineMajorSize = _orientation.MajorSize(anchorBounds); + int countInLine = 1; + bool lineNeedsReposition = false; + + while (_elementManager.IsIndexValidInData(currentIndex) && + ShouldContinueFillingUpSpace(previousIndex, direction)) + { + // Ensure layout element. + _elementManager.EnsureElementRealized(direction == GenerateDirection.Forward, currentIndex, layoutId); + var currentElement = _elementManager.GetRealizedElement(currentIndex); + var desiredSize = MeasureElement(currentElement, currentIndex, availableSize, _context); + + // Lay it out. + var previousElement = _elementManager.GetRealizedElement(previousIndex); + var currentBounds = new Rect(0, 0, desiredSize.Width, desiredSize.Height); + var previousElementBounds = _elementManager.GetLayoutBoundsForDataIndex(previousIndex); + + if (direction == GenerateDirection.Forward) + { + double remainingSpace = _orientation.Minor(availableSize) - (_orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing + _orientation.Minor(desiredSize)); + if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) + { + // No more space in this row. wrap to next row. + _orientation.SetMinorStart(ref currentBounds, 0); + _orientation.SetMajorStart(ref currentBounds, _orientation.MajorStart(previousElementBounds) + lineMajorSize + lineSpacing); + + if (lineNeedsReposition) + { + // reposition the previous line (countInLine items) + for (int i = 0; i < countInLine; i++) + { + var dataIndex = currentIndex - 1 - i; + var bounds = _elementManager.GetLayoutBoundsForDataIndex(dataIndex); + _orientation.SetMajorSize(ref bounds, lineMajorSize); + _elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds); + } + } + + // Setup for next line. + lineMajorSize = _orientation.MajorSize(currentBounds); + lineOffset = _orientation.MajorStart(currentBounds); + lineNeedsReposition = false; + countInLine = 1; + } + else + { + // More space is available in this row. + _orientation.SetMinorStart(ref currentBounds, _orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing); + _orientation.SetMajorStart(ref currentBounds, lineOffset); + lineMajorSize = Math.Max(lineMajorSize, _orientation.MajorSize(currentBounds)); + lineNeedsReposition = _orientation.MajorSize(previousElementBounds) != _orientation.MajorSize(currentBounds); + countInLine++; + } + } + else + { + // Backward + double remainingSpace = _orientation.MinorStart(previousElementBounds) - (_orientation.Minor(desiredSize) + minItemSpacing); + if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) + { + // Does not fit, wrap to the previous row + var availableSizeMinor = _orientation.Minor(availableSize); + + _orientation.SetMinorStart(ref currentBounds, double.IsInfinity(availableSizeMinor) ? availableSizeMinor - _orientation.Minor(desiredSize) : 0); + _orientation.SetMajorStart(ref currentBounds, lineOffset - _orientation.Major(desiredSize) - lineSpacing); + + if (lineNeedsReposition) + { + var previousLineOffset = _orientation.MajorStart(_elementManager.GetLayoutBoundsForDataIndex(currentIndex + countInLine + 1)); + // reposition the previous line (countInLine items) + for (int i = 0; i < countInLine; i++) + { + var dataIndex = currentIndex + 1 + i; + if (dataIndex != anchorIndex) + { + var bounds = _elementManager.GetLayoutBoundsForDataIndex(dataIndex); + _orientation.SetMajorStart(ref bounds, previousLineOffset - lineMajorSize - lineSpacing); + _orientation.SetMajorSize(ref bounds, lineMajorSize); + _elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds); + } + } + } + + // Setup for next line. + lineMajorSize = _orientation.MajorSize(currentBounds); + lineOffset = _orientation.MajorStart(currentBounds); + lineNeedsReposition = false; + countInLine = 1; + } + else + { + // Fits in this row. put it in the previous position + _orientation.SetMinorStart(ref currentBounds, _orientation.MinorStart(previousElementBounds) - _orientation.Minor(desiredSize) - minItemSpacing); + _orientation.SetMajorStart(ref currentBounds, lineOffset); + lineMajorSize = Math.Max(lineMajorSize, _orientation.MajorSize(currentBounds)); + lineNeedsReposition = _orientation.MajorSize(previousElementBounds) != _orientation.MajorSize(currentBounds); + countInLine++; + } + } + + _elementManager.SetLayoutBoundsForDataIndex(currentIndex, currentBounds); + previousIndex = currentIndex; + currentIndex += step; + } + + // If we did not reach the top or bottom of the extent, we realized one + // extra item before we knew we were outside the realization window. Do not + // account for that element in the indicies inside the realization window. + if (direction == GenerateDirection.Forward) + { + int dataCount = _context.ItemCount; + _lastRealizedDataIndexInsideRealizationWindow = previousIndex == dataCount - 1 ? dataCount - 1 : previousIndex - 1; + _lastRealizedDataIndexInsideRealizationWindow = Math.Max(0, _lastRealizedDataIndexInsideRealizationWindow); + } + else + { + int dataCount = _context.ItemCount; + _firstRealizedDataIndexInsideRealizationWindow = previousIndex == 0 ? 0 : previousIndex + 1; + _firstRealizedDataIndexInsideRealizationWindow = Math.Min(dataCount - 1, _firstRealizedDataIndexInsideRealizationWindow); + } + + _elementManager.DiscardElementsOutsideWindow(direction == GenerateDirection.Forward, currentIndex); + } + } + + private void MakeAnchor( + VirtualizingLayoutContext context, + int index, + Size availableSize) + { + _elementManager.ClearRealizedRange(); + // FlowLayout requires that the anchor is the first element in the row. + var internalAnchor = _algorithmCallbacks.Algorithm_GetAnchorForTargetElement(index, availableSize, context); + //MUX_ASSERT(internalAnchor.Index <= index); + + // No need to set the position of the anchor. + // (0,0) is fine for now since the extent can + // grow in any direction. + for (int dataIndex = internalAnchor.Index; dataIndex < index + 1; ++dataIndex) + { + var element = context.GetOrCreateElementAt(dataIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); + element.Measure(_algorithmCallbacks.Algorithm_GetMeasureSize(dataIndex, availableSize, context)); + _elementManager.Add(element, dataIndex); + } + } + + private bool IsReflowRequired() + { + // If first element is realized and is not at the very beginning we need to reflow. + return + _elementManager.GetRealizedElementCount() > 0 && + _elementManager.GetDataIndexFromRealizedRangeIndex(0) == 0 && + _orientation.MinorStart(_elementManager.GetLayoutBoundsForRealizedIndex(0)) != 0; + } + + private bool ShouldContinueFillingUpSpace( + int index, + GenerateDirection direction) + { + bool shouldContinue = false; + if (!IsVirtualizingContext) + { + shouldContinue = true; + } + else + { + var realizationRect = _context.RealizationRect; + var elementBounds = _elementManager.GetLayoutBoundsForDataIndex(index); + + var elementMajorStart = _orientation.MajorStart(elementBounds); + var elementMajorEnd = _orientation.MajorEnd(elementBounds); + var rectMajorStart = _orientation.MajorStart(realizationRect); + var rectMajorEnd = _orientation.MajorEnd(realizationRect); + + var elementMinorStart = _orientation.MinorStart(elementBounds); + var elementMinorEnd = _orientation.MinorEnd(elementBounds); + var rectMinorStart = _orientation.MinorStart(realizationRect); + var rectMinorEnd = _orientation.MinorEnd(realizationRect); + + // Ensure that both minor and major directions are taken into consideration so that if the scrolling direction + // is the same as the flow direction we still stop at the end of the viewport rectangle. + shouldContinue = + (direction == GenerateDirection.Forward && elementMajorStart < rectMajorEnd && elementMinorStart < rectMinorEnd) || + (direction == GenerateDirection.Backward && elementMajorEnd > rectMajorStart && elementMinorEnd > rectMinorStart); + } + + return shouldContinue; + } + + private Rect EstimateExtent(Size availableSize, string layoutId) + { + IControl firstRealizedElement = null; + Rect firstBounds = new Rect(); + IControl lastRealizedElement = null; + Rect lastBounds = new Rect(); + int firstDataIndex = -1; + int lastDataIndex = -1; + + if (_elementManager.GetRealizedElementCount() > 0) + { + firstRealizedElement = _elementManager.GetAt(0); + firstBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); + firstDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);; + + int last = _elementManager.GetRealizedElementCount() - 1; + lastRealizedElement = _elementManager.GetAt(last); + lastDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(last); + lastBounds = _elementManager.GetLayoutBoundsForRealizedIndex(last); + } + + Rect extent = _algorithmCallbacks.Algorithm_GetExtent( + availableSize, + _context, + firstRealizedElement, + firstDataIndex, + firstBounds, + lastRealizedElement, + lastDataIndex, + lastBounds); + + return extent; + } + + private void RaiseLineArranged() + { + var realizationRect = RealizationRect; + if (realizationRect.Width != 0.0f || realizationRect.Height != 0.0f) + { + int realizedElementCount = _elementManager.GetRealizedElementCount(); + if (realizedElementCount > 0) + { + //MUX_ASSERT(_firstRealizedDataIndexInsideRealizationWindow != -1 && _lastRealizedDataIndexInsideRealizationWindow != -1); + int countInLine = 0; + var previousElementBounds = _elementManager.GetLayoutBoundsForDataIndex(_firstRealizedDataIndexInsideRealizationWindow); + var currentLineOffset = _orientation.MajorStart(previousElementBounds); + var currentLineSize = _orientation.MajorSize(previousElementBounds); + for (int currentDataIndex = _firstRealizedDataIndexInsideRealizationWindow; currentDataIndex <= _lastRealizedDataIndexInsideRealizationWindow; currentDataIndex++) + { + var currentBounds = _elementManager.GetLayoutBoundsForDataIndex(currentDataIndex); + if (_orientation.MajorStart(currentBounds) != currentLineOffset) + { + // Staring a new line + _algorithmCallbacks.Algorithm_OnLineArranged(currentDataIndex - countInLine, countInLine, currentLineSize, _context); + countInLine = 0; + currentLineOffset = _orientation.MajorStart(currentBounds); + currentLineSize = 0; + } + + currentLineSize = Math.Max(currentLineSize, _orientation.MajorSize(currentBounds)); + countInLine++; + previousElementBounds = currentBounds; + } + + // Raise for the last line. + _algorithmCallbacks.Algorithm_OnLineArranged(_lastRealizedDataIndexInsideRealizationWindow - countInLine + 1, countInLine, currentLineSize, _context); + } + } + } + + private void ArrangeVirtualizingLayout( + Size finalSize, + LineAlignment lineAlignment, + string layoutId) + { + // Walk through the realized elements one line at a time and + // align them, Then call element.Arrange with the arranged bounds. + int realizedElementCount = _elementManager.GetRealizedElementCount(); + if (realizedElementCount > 0) + { + var countInLine = 1; + var previousElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); + var currentLineOffset = _orientation.MajorStart(previousElementBounds); + var spaceAtLineStart = _orientation.MinorStart(previousElementBounds); + var spaceAtLineEnd = 0.0; + var currentLineSize = _orientation.MajorSize(previousElementBounds); + for (int i = 1; i < realizedElementCount; i++) + { + var currentBounds = _elementManager.GetLayoutBoundsForRealizedIndex(i); + if (_orientation.MajorStart(currentBounds) != currentLineOffset) + { + spaceAtLineEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds); + PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, layoutId); + spaceAtLineStart = _orientation.MinorStart(currentBounds); + countInLine = 0; + currentLineOffset = _orientation.MajorStart(currentBounds); + currentLineSize = 0; + } + + countInLine++; // for current element + currentLineSize = Math.Max(currentLineSize, _orientation.MajorSize(currentBounds)); + previousElementBounds = currentBounds; + } + + // Last line - potentially have a property to customize + // aligning the last line or not. + if (countInLine > 0) + { + var spaceAtEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds); + PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, layoutId); + } + } + } + + // Align elements within a line. Note that this does not modify LayoutBounds. So if we get + // repeated measures, the LayoutBounds remain the same in each layout. + private void PerformLineAlignment( + int lineStartIndex, + int countInLine, + double spaceAtLineStart, + double spaceAtLineEnd, + double lineSize, + LineAlignment lineAlignment, + string layoutId) + { + for (int rangeIndex = lineStartIndex; rangeIndex < lineStartIndex + countInLine; ++rangeIndex) + { + var bounds = _elementManager.GetLayoutBoundsForRealizedIndex(rangeIndex); + _orientation.SetMajorSize(ref bounds, lineSize); + + if (!_scrollOrientationSameAsFlow) + { + // Note: Space at start could potentially be negative + if (spaceAtLineStart != 0 || spaceAtLineEnd != 0) + { + var totalSpace = spaceAtLineStart + spaceAtLineEnd; + var minorStart = _orientation.MinorStart(bounds); + switch (lineAlignment) + { + case LineAlignment.Start: + { + _orientation.SetMinorStart(ref bounds, minorStart - spaceAtLineStart); + break; + } + + case LineAlignment.End: + { + _orientation.SetMinorStart(ref bounds, minorStart + spaceAtLineEnd); + break; + } + + case LineAlignment.Center: + { + _orientation.SetMinorStart(ref bounds, (minorStart - spaceAtLineStart) + (totalSpace / 2)); + break; + } + + case LineAlignment.SpaceAround: + { + var interItemSpace = countInLine >= 1 ? totalSpace / (countInLine * 2) : 0; + _orientation.SetMinorStart( + ref bounds, + (minorStart - spaceAtLineStart) + (interItemSpace * ((rangeIndex - lineStartIndex + 1) * 2 - 1))); + break; + } + + case LineAlignment.SpaceBetween: + { + var interItemSpace = countInLine > 1 ? totalSpace / (countInLine - 1) : 0; + _orientation.SetMinorStart( + ref bounds, + (minorStart - spaceAtLineStart) + (interItemSpace * (rangeIndex - lineStartIndex))); + break; + } + + case LineAlignment.SpaceEvenly: + { + var interItemSpace = countInLine >= 1 ? totalSpace / (countInLine + 1) : 0; + _orientation.SetMinorStart( + ref bounds, + (minorStart - spaceAtLineStart) + (interItemSpace * (rangeIndex - lineStartIndex + 1))); + break; + } + } + } + } + + bounds = bounds.Translate(-_lastExtent.Position); + var element = _elementManager.GetAt(rangeIndex); + element.Arrange(bounds); + } + } + + void SetLayoutOrigin() + { + if (IsVirtualizingContext) + { + _context.LayoutOrigin = new Point(_lastExtent.X, _lastExtent.Y); + } + else + { + // Should have 0 origin for non-virtualizing layout since we always start from + // the first item + //MUX_ASSERT(m_lastExtent.X == 0 && m_lastExtent.Y == 0); + } + } + + public enum LineAlignment + { + Start, + Center, + End, + SpaceAround, + SpaceBetween, + SpaceEvenly, + } + + private enum GenerateDirection + { + Forward, + Backward, + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs b/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs new file mode 100644 index 0000000000..e27f00540c --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + internal struct FlowLayoutAnchorInfo + { + public int Index { get; set; } + public double Offset { get; set; } + } + + internal interface IFlowLayoutAlgorithmDelegates + { + Size Algorithm_GetMeasureSize(int index, Size availableSize, VirtualizingLayoutContext context); + Size Algorithm_GetProvisionalArrangeSize(int index, Size measureSize, Size desiredSize, VirtualizingLayoutContext context); + bool Algorithm_ShouldBreakLine(int index, double remainingSpace); + FlowLayoutAnchorInfo Algorithm_GetAnchorForRealizationRect(Size availableSize, VirtualizingLayoutContext context); + FlowLayoutAnchorInfo Algorithm_GetAnchorForTargetElement(int targetIndex, Size availableSize, VirtualizingLayoutContext context); + Rect Algorithm_GetExtent( + Size availableSize, + VirtualizingLayoutContext context, + IControl firstRealized, + int firstRealizedItemIndex, + Rect firstRealizedLayoutBounds, + IControl lastRealized, + int lastRealizedItemIndex, + Rect lastRealizedLayoutBounds); + void Algorithm_OnElementMeasured( + IControl element, + int index, + Size availableSize, + Size measureSize, + Size desiredSize, + Size provisionalArrangeSize, + VirtualizingLayoutContext context); + void Algorithm_OnLineArranged( + int startIndex, + int countInLine, + double lineSize, + VirtualizingLayoutContext context); + } +} diff --git a/src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs b/src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs new file mode 100644 index 0000000000..55ce6c28ca --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + internal interface ITrackerHandleManager + { + } +} diff --git a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs new file mode 100644 index 0000000000..c26d73c02c --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Controls.Templates; + +namespace Avalonia.Controls.Repeaters +{ + internal class ItemTemplateWrapper + { + private readonly IDataTemplate _dataTemplate; + + public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate; + + public IControl GetElement(ElementFactoryGetArgs args) + { + var selectedTemplate = _dataTemplate; + var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); + IControl element = null; + + if (recyclePool != null) + { + // try to get an element from the recycle pool. + element = recyclePool.TryGetElement(string.Empty, args.Parent); + } + + if (element == null) + { + // no element was found in recycle pool, create a new element + element = selectedTemplate.Build(args.Data); + + // Associate template with element + element.SetValue(RecyclePool.OriginTemplateProperty, selectedTemplate); + } + + return element; + } + + public void RecycleElement(ElementFactoryRecycleArgs args) + { + var element = args.Element; + var selectedTemplate = _dataTemplate; + var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); + if (recyclePool == null) + { + // No Recycle pool in the template, create one. + recyclePool = new RecyclePool(); + RecyclePool.SetPoolInstance(selectedTemplate, recyclePool); + } + + recyclePool.PutElement(args.Element, "" /* key */, args.Parent); + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs new file mode 100644 index 0000000000..9d71685b11 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs @@ -0,0 +1,631 @@ +using System; +using System.Collections; +using System.Collections.Specialized; +using Avalonia.Controls.Templates; +using Avalonia.Input; + +namespace Avalonia.Controls.Repeaters +{ + public class ItemsRepeater : Panel + { + public static readonly AvaloniaProperty HorizontalCacheLengthProperty = + AvaloniaProperty.Register(nameof(HorizontalCacheLength), 2.0); + public static readonly StyledProperty ItemTemplateProperty = + ItemsControl.ItemTemplateProperty.AddOwner(); + public static readonly DirectProperty ItemsProperty = + ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v); + public static readonly AvaloniaProperty LayoutProperty = + AvaloniaProperty.Register(nameof(Layout), new StackLayout()); + public static readonly AvaloniaProperty VerticalCacheLengthProperty = + AvaloniaProperty.Register(nameof(VerticalCacheLength), 2.0); + private static readonly AttachedProperty VirtualizationInfoProperty = + AvaloniaProperty.RegisterAttached("VirtualizationInfo"); + + internal static readonly Rect InvalidRect = new Rect(-1, -1, -1, -1); + internal static readonly Point ClearedElementsArrangePosition = new Point(-10000.0, -10000.0); + + private readonly ViewManager _viewManager; + private readonly ViewportManager _viewportManager; + private IEnumerable _items; + private VirtualizingLayoutContext _layoutContext; + private NotifyCollectionChangedEventArgs _processingItemsSourceChange; + private Size _lastAvailableSize; + private bool _isLayoutInProgress; + private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs; + private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; + private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs; + + public ItemsRepeater() + { + _viewManager = new ViewManager(this); + _viewportManager = new ViewportManager(this); + KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once); + OnLayoutChanged(null, Layout); + } + + static ItemsRepeater() + { + ClipToBoundsProperty.OverrideDefaultValue(true); + } + + public Layout Layout + { + get => GetValue(LayoutProperty); + set => SetValue(LayoutProperty, value); + } + + public IEnumerable Items + { + get => _items; + set => SetAndRaise(ItemsProperty, ref _items, value); + } + + public IDataTemplate ItemTemplate + { + get => GetValue(ItemTemplateProperty); + set => SetValue(ItemTemplateProperty, value); + } + + public double HorizontalCacheLength + { + get => GetValue(HorizontalCacheLengthProperty); + set => SetValue(HorizontalCacheLengthProperty, value); + } + + public double VerticalCacheLength + { + get => GetValue(VerticalCacheLengthProperty); + set => SetValue(VerticalCacheLengthProperty, value); + } + + public ItemsSourceView ItemsSourceView { get; private set; } + + internal ItemTemplateWrapper ItemTemplateShim { get; set; } + internal Point LayoutOrigin { get; set; } + internal object LayoutState { get; set; } + internal IControl MadeAnchor => _viewportManager.MadeAnchor; + internal Rect RealizationWindow => _viewportManager.GetLayoutRealizationWindow(); + internal IControl SuggestedAnchor => _viewportManager.SuggestedAnchor; + + private bool IsProcessingCollectionChange => _processingItemsSourceChange != null; + + private LayoutContext LayoutContext + { + get + { + if (_layoutContext == null) + { + _layoutContext = new RepeaterLayoutContext(this); + } + + return _layoutContext; + } + } + + public event EventHandler ElementClearing; + public event EventHandler ElementIndexChanged; + public event EventHandler ElementPrepared; + + public int GetElementIndex(IControl element) => GetElementIndexImpl(element); + + public IControl TryGetElement(int index) => GetElementFromIndexImpl(index); + + public void PinElement(IControl element) => _viewManager.UpdatePin(element, true); + + public void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false); + + public IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index); + + internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element) + { + var value = element.GetValue(VirtualizationInfoProperty); + return value; + } + + internal static VirtualizationInfo CreateAndInitializeVirtualizationInfo(IControl element) + { + if (TryGetVirtualizationInfo(element) != null) + { + throw new InvalidOperationException("VirtualizationInfo already created."); + } + + var result = new VirtualizationInfo(); + element.SetValue(VirtualizationInfoProperty, result); + return result; + } + + internal static VirtualizationInfo GetVirtualizationInfo(IControl element) + { + var result = element.GetValue(VirtualizationInfoProperty); + + if (result == null) + { + result = new VirtualizationInfo(); + element.SetValue(VirtualizationInfoProperty, result); + } + + return result; + } + + protected override Size MeasureOverride(Size availableSize) + { + if (_isLayoutInProgress) + { + throw new AvaloniaInternalException("Reentrancy detected during layout."); + } + + if (IsProcessingCollectionChange) + { + throw new NotSupportedException("Cannot run layout in the middle of a collection change."); + } + + _viewportManager.OnOwnerMeasuring(); + + _isLayoutInProgress = true; + + try + { + _viewManager.PrunePinnedElements(); + var extent = new Rect(); + var desiredSize = new Size(); + var layout = Layout; + + if (layout != null) + { + var layoutContext = GetLayoutContext(); + + desiredSize = layout.Measure(layoutContext, availableSize); + extent = new Rect(LayoutOrigin.X, LayoutOrigin.Y, desiredSize.Width, desiredSize.Height); + + // Clear auto recycle candidate elements that have not been kept alive by layout - i.e layout did not + // call GetElementAt(index). + foreach (var element in Children) + { + var virtInfo = GetVirtualizationInfo(element); + + if (virtInfo.Owner == ElementOwner.Layout && + virtInfo.AutoRecycleCandidate && + !virtInfo.KeepAlive) + { + ClearElementImpl(element); + } + } + } + + _viewportManager.SetLayoutExtent(extent); + _lastAvailableSize = availableSize; + return desiredSize; + } + finally + { + _isLayoutInProgress = false; + } + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (_isLayoutInProgress) + { + throw new AvaloniaInternalException("Reentrancy detected during layout."); + } + + if (IsProcessingCollectionChange) + { + throw new NotSupportedException("Cannot run layout in the middle of a collection change."); + } + + _isLayoutInProgress = true; + + try + { + var arrangeSize = Layout?.Arrange(GetLayoutContext(), finalSize) ?? default; + + // The view manager might clear elements during this call. + // That's why we call it before arranging cleared elements + // off screen. + _viewManager.OnOwnerArranged(); + + foreach (var element in Children) + { + var virtInfo = GetVirtualizationInfo(element); + virtInfo.KeepAlive = false; + + if (virtInfo.Owner == ElementOwner.ElementFactory || + virtInfo.Owner == ElementOwner.PinnedPool) + { + // Toss it away. And arrange it with size 0 so that XYFocus won't use it. + element.Arrange(new Rect( + ClearedElementsArrangePosition.X - element.DesiredSize.Width, + ClearedElementsArrangePosition.Y - element.DesiredSize.Height, + 0, + 0)); + } + else + { + var newBounds = element.Bounds; + + //if (virtInfo.ArrangeBounds != ItemsRepeater.InvalidRect && + // newBounds != virtInfo.ArrangeBounds) + //{ + // _animationManager.OnElementBoundsChanged(element, virtInfo.ArrangeBounds, newBounds); + //} + + virtInfo.ArrangeBounds = newBounds; + } + } + + _viewportManager.OnOwnerArranged(); + //_animationManager.OnOwnerArranged(); + + return arrangeSize; + } + finally + { + _isLayoutInProgress = false; + } + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + InvalidateMeasure(); + _viewportManager.ResetScrollers(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _viewportManager.ResetScrollers(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args) + { + var property = args.Property; + + if (property == ItemsProperty) + { + var newValue = (IEnumerable)args.NewValue; + var newDataSource = newValue as ItemsSourceView; + if (newValue != null && newDataSource == null) + { + newDataSource = new ItemsSourceView(newValue); + } + + OnDataSourcePropertyChanged(ItemsSourceView, newDataSource); + } + else if (property == ItemTemplateProperty) + { + OnItemTemplateChanged((IDataTemplate)args.OldValue, (IDataTemplate)args.NewValue); + } + else if (property == LayoutProperty) + { + OnLayoutChanged((Layout)args.OldValue, (Layout)args.NewValue); + } + //else if (property == AnimatorProperty) + //{ + // OnAnimatorChanged((ElementAnimator)args.OldValue, (ElementAnimator)args.NewValue); + //} + else if (property == HorizontalCacheLengthProperty) + { + _viewportManager.HorizontalCacheLength = (double)args.NewValue; + } + else if (property == VerticalCacheLengthProperty) + { + _viewportManager.VerticalCacheLength = (double)args.NewValue; + } + else + { + base.OnPropertyChanged(args); + } + } + + internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle) + { + var element = _viewManager.GetElement(index, forceCreate, supressAutoRecycle); + return element; + } + + internal void ClearElementImpl(IControl element) + { + // Clearing an element due to a collection change + // is more strict in that pinned elements will be forcibly + // unpinned and sent back to the view generator. + var isClearedDueToCollectionChange = + _processingItemsSourceChange != null && + (_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Remove || + _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Replace || + _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Reset); + + _viewManager.ClearElement(element, isClearedDueToCollectionChange); + _viewportManager.OnElementCleared(element); + } + + private int GetElementIndexImpl(IControl element) + { + var virtInfo = TryGetVirtualizationInfo(element); + return _viewManager.GetElementIndex(virtInfo); + } + + private IControl GetElementFromIndexImpl(int index) + { + IControl result = null; + + var children = Children; + for (var i = 0; i < children.Count && result == null; ++i) + { + var element = children[i]; + var virtInfo = TryGetVirtualizationInfo(element); + if (virtInfo?.IsRealized == true && virtInfo.Index == index) + { + result = element; + } + } + + return result; + } + + private IControl GetOrCreateElementImpl(int index) + { + if (index >= 0 && index >= ItemsSourceView.Count) + { + throw new ArgumentException("Argument index is invalid.", "index"); + } + + if (_isLayoutInProgress) + { + throw new NotSupportedException("GetOrCreateElement invocation is not allowed during layout."); + } + + var element = GetElementFromIndexImpl(index); + bool isAnchorOutsideRealizedRange = element == null; + + if (isAnchorOutsideRealizedRange) + { + if (Layout == null) + { + throw new InvalidOperationException("Cannot make an Anchor when there is no attached layout."); + } + + element = GetLayoutContext().GetOrCreateElementAt(index); + element.Measure(Size.Infinity); + } + + _viewportManager.OnMakeAnchor(element, isAnchorOutsideRealizedRange); + InvalidateMeasure(); + + return element; + } + + internal void OnElementPrepared(IControl element, int index) + { + _viewportManager.OnElementPrepared(element); + if (ElementPrepared != null) + { + if (_elementPreparedArgs == null) + { + _elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index); + } + else + { + _elementPreparedArgs.Update(element, index); + } + + ElementPrepared(this, _elementPreparedArgs); + } + } + + internal void OnElementClearing(IControl element) + { + if (ElementClearing != null) + { + if (_elementClearingArgs == null) + { + _elementClearingArgs = new ItemsRepeaterElementClearingEventArgs(element); + } + else + { + _elementClearingArgs.Update(element); + } + + ElementClearing(this, _elementClearingArgs); + } + } + + internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex) + { + if (ElementIndexChanged != null) + { + if (_elementIndexChangedArgs == null) + { + _elementIndexChangedArgs = new ItemsRepeaterElementIndexChangedEventArgs(element, oldIndex, newIndex); + } + else + { + _elementIndexChangedArgs.Update(element, oldIndex, newIndex); + } + + ElementIndexChanged(this, _elementIndexChangedArgs); + } + } + + private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue) + { + if (_isLayoutInProgress) + { + throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout."); + } + + ItemsSourceView?.Dispose(); + ItemsSourceView = newValue; + + if (oldValue != null) + { + oldValue.CollectionChanged -= OnItemsSourceViewChanged; + } + + if (newValue != null) + { + newValue.CollectionChanged += OnItemsSourceViewChanged; + } + + if (Layout != null) + { + if (Layout is VirtualizingLayout virtualLayout) + { + var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + virtualLayout.OnItemsChangedCore(GetLayoutContext(), newValue, args); + } + else if (Layout is NonVirtualizingLayout nonVirtualLayout) + { + // Walk through all the elements and make sure they are cleared for + // non-virtualizing layouts. + foreach (var element in Children) + { + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } + } + } + + InvalidateMeasure(); + } + } + + private void OnItemTemplateChanged(IDataTemplate oldValue, IDataTemplate newValue) + { + if (_isLayoutInProgress && oldValue != null) + { + throw new AvaloniaInternalException("ItemTemplate cannot be changed during layout."); + } + + // Since the ItemTemplate has changed, we need to re-evaluate all the items that + // have already been created and are now in the tree. The easiest way to do that + // would be to do a reset.. Note that this has to be done before we change the template + // so that the cleared elements go back into the old template. + if (Layout != null) + { + if (Layout is VirtualizingLayout virtualLayout) + { + var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + _processingItemsSourceChange = args; + + try + { + virtualLayout.OnItemsChangedCore(GetLayoutContext(), newValue, args); + } + finally + { + _processingItemsSourceChange = null; + } + } + else if (Layout is NonVirtualizingLayout nonVirtualLayout) + { + // Walk through all the elements and make sure they are cleared for + // non-virtualizing layouts. + foreach (var element in Children) + { + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } + } + } + } + + ItemTemplateShim = new ItemTemplateWrapper(newValue); + + InvalidateMeasure(); + } + + private void OnLayoutChanged(Layout oldValue, Layout newValue) + { + if (_isLayoutInProgress) + { + throw new InvalidOperationException("Layout cannot be changed during layout."); + } + + _viewManager.OnLayoutChanging(); + + if (oldValue != null) + { + oldValue.UninitializeForContext(LayoutContext); + oldValue.MeasureInvalidated -= InvalidateMeasureForLayout; + oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout; + + // Walk through all the elements and make sure they are cleared + foreach (var element in Children) + { + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } + } + + LayoutState = null; + } + + if (newValue != null) + { + newValue.InitializeForContext(LayoutContext); + newValue.MeasureInvalidated += InvalidateMeasureForLayout; + newValue.ArrangeInvalidated += InvalidateArrangeForLayout; + } + + bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; + _viewportManager.OnLayoutChanged(isVirtualizingLayout); + InvalidateMeasure(); + } + + private void OnItemsSourceViewChanged(object sender, NotifyCollectionChangedEventArgs args) + { + if (_isLayoutInProgress) + { + // Bad things will follow if the data changes while we are in the middle of a layout pass. + throw new InvalidOperationException("Changes in data source are not allowed during layout."); + } + + if (IsProcessingCollectionChange) + { + throw new InvalidOperationException("Changes in the data source are not allowed during another change in the data source."); + } + + _processingItemsSourceChange = args; + + try + { + //_animationManager.OnItemsSourceChanged(sender, args); + _viewManager.OnItemsSourceChanged(sender, args); + + if (Layout != null) + { + if (Layout is VirtualizingLayout virtualLayout) + { + virtualLayout.OnItemsChangedCore(GetLayoutContext(), sender, args); + } + else + { + // NonVirtualizingLayout + InvalidateMeasure(); + } + } + } + finally + { + _processingItemsSourceChange = null; + } + } + + private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateMeasure(); + + private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateArrange(); + + private VirtualizingLayoutContext GetLayoutContext() + { + if (_layoutContext == null) + { + _layoutContext = new RepeaterLayoutContext(this); + } + + return _layoutContext; + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs new file mode 100644 index 0000000000..20e62bc3b8 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs @@ -0,0 +1,11 @@ +using System; + +namespace Avalonia.Controls.Repeaters +{ + public class ItemsRepeaterElementClearingEventArgs : EventArgs + { + internal ItemsRepeaterElementClearingEventArgs(IControl element) => Element = element; + public IControl Element { get; private set; } + internal void Update(IControl element) => Element = element; + } +} diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs new file mode 100644 index 0000000000..c81c997bd4 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs @@ -0,0 +1,27 @@ +using System; + +namespace Avalonia.Controls.Repeaters +{ + public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs + { + internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int newIndex, int oldIndex) + { + Element = element; + NewIndex = newIndex; + OldIndex = oldIndex; + } + + public IControl Element { get; private set; } + + public int NewIndex { get; private set; } + + public int OldIndex { get; private set; } + + internal void Update(IControl element, int newIndex, int oldIndex) + { + Element = element; + NewIndex = newIndex; + OldIndex = oldIndex; + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs new file mode 100644 index 0000000000..3180058fae --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + public class ItemsRepeaterElementPreparedEventArgs + { + internal ItemsRepeaterElementPreparedEventArgs(IControl element, int index) + { + Element = element; + Index = index; + } + + public IControl Element { get; private set; } + + public int Index { get; private set; } + + internal void Update(IControl element, int index) + { + Element = element; + Index = index; + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs b/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs new file mode 100644 index 0000000000..0d21af0975 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; + +namespace Avalonia.Controls.Repeaters +{ + public class ItemsSourceView : INotifyCollectionChanged, IDisposable + { + private readonly IList _inner; + private INotifyCollectionChanged _notifyCollectionChanged; + private int _cachedSize = -1; + + public ItemsSourceView(IEnumerable source) + { + Contract.Requires(source != null); + + _inner = source as IList; + + if (_inner == null && source is IEnumerable objectEnumerable) + { + _inner = new List(objectEnumerable); + } + else + { + _inner = new List(source.Cast()); + } + + ListenToCollectionChanges(); + } + + public int Count + { + get + { + if (_cachedSize == -1) + { + _cachedSize = _inner.Count; + } + + return _cachedSize; + } + } + + public bool HasKeyIndexMapping => false; + + + public event NotifyCollectionChangedEventHandler CollectionChanged; + + public void Dispose() + { + if (_notifyCollectionChanged != null) + { + _notifyCollectionChanged.CollectionChanged -= OnCollectionChanged; + } + } + + public object GetAt(int index) => _inner[index]; + + public string KeyFromIndex(int index) + { + throw new NotImplementedException(); + } + + public int IndexFromKey(string key) + { + throw new NotImplementedException(); + } + + protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args) + { + _cachedSize = _inner.Count; + CollectionChanged?.Invoke(this, args); + } + + private void ListenToCollectionChanges() + { + if (_inner is INotifyCollectionChanged incc) + { + incc.CollectionChanged += OnCollectionChanged; + _notifyCollectionChanged = incc; + } + } + + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + OnItemsSourceChanged(e); + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/Layout.cs b/src/Avalonia.Controls/Repeaters/Layout.cs new file mode 100644 index 0000000000..a03e881616 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/Layout.cs @@ -0,0 +1,24 @@ +using System; + +namespace Avalonia.Controls.Repeaters +{ + public abstract class Layout : AvaloniaObject + { + public string LayoutId { get; set; } + + public event EventHandler MeasureInvalidated; + public event EventHandler ArrangeInvalidated; + + public abstract void InitializeForContext(LayoutContext context); + + public abstract void UninitializeForContext(LayoutContext context); + + public abstract Size Measure(LayoutContext context, Size availableSize); + + public abstract Size Arrange(LayoutContext context, Size finalSize); + + protected void InvalidateMeasure() => MeasureInvalidated?.Invoke(this, EventArgs.Empty); + + protected void InvalidateArrange() => ArrangeInvalidated?.Invoke(this, EventArgs.Empty); + } +} diff --git a/src/Avalonia.Controls/Repeaters/LayoutContext.cs b/src/Avalonia.Controls/Repeaters/LayoutContext.cs new file mode 100644 index 0000000000..ce462f6a16 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/LayoutContext.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + public class LayoutContext : AvaloniaObject + { + public object LayoutState { get; set; } + + protected virtual object LayoutStateCore { get; set; } + } +} diff --git a/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs b/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs new file mode 100644 index 0000000000..c5ab27fabb --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + public class NonVirtualizingLayout : Layout + { + public override Size Arrange(LayoutContext context, Size finalSize) + { + throw new NotImplementedException(); + } + + public override void InitializeForContext(LayoutContext context) + { + throw new NotImplementedException(); + } + + public override Size Measure(LayoutContext context, Size availableSize) + { + throw new NotImplementedException(); + } + + public override void UninitializeForContext(LayoutContext context) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs b/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs new file mode 100644 index 0000000000..173b62842b --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs @@ -0,0 +1,78 @@ +namespace Avalonia.Controls.Repeaters +{ + internal class OrientationBasedMeasures + { + public Orientation ScrollOrientation { get; set; } = Orientation.Vertical; + + public double Major(in Size size) => ScrollOrientation == Orientation.Vertical ? size.Height : size.Width; + public double Minor(in Size size) => ScrollOrientation == Orientation.Vertical ? size.Width : size.Height; + public double MajorSize(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Height : rect.Width; + public double MinorSize(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Width : rect.Height; + public double MajorStart(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Y : rect.X; + public double MinorStart(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.X : rect.Y; + public double MajorEnd(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Bottom : rect.Right; + public double MinorEnd(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Right : rect.Bottom; + + public void SetMajorSize(ref Rect rect, double value) + { + if (ScrollOrientation == Orientation.Vertical) + { + rect = rect.WithHeight(value); + } + else + { + rect = rect.WithWidth(value); + } + } + + public void SetMinorSize(ref Rect rect, double value) + { + if (ScrollOrientation == Orientation.Vertical) + { + rect = rect.WithWidth(value); + } + else + { + rect = rect.WithHeight(value); + } + } + + public void SetMajorStart(ref Rect rect, double value) + { + if (ScrollOrientation == Orientation.Vertical) + { + rect = rect.WithY(value); + } + else + { + rect = rect.WithX(value); + } + } + + public void SetMinorStart(ref Rect rect, double value) + { + if (ScrollOrientation == Orientation.Vertical) + { + rect = rect.WithX(value); + } + else + { + rect = rect.WithY(value); + } + } + + public Point MinorMajorPoint(double minor, double major) + { + return ScrollOrientation == Orientation.Vertical ? + new Point(minor, major) : + new Point(major, minor); + } + + public Size MinorMajorSize(double minor, double major) + { + return ScrollOrientation == Orientation.Vertical ? + new Size(minor, major) : + new Size(major, minor); + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/RecyclePool.cs b/src/Avalonia.Controls/Repeaters/RecyclePool.cs new file mode 100644 index 0000000000..e602a6b674 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/RecyclePool.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using Avalonia.Controls.Templates; + +namespace Avalonia.Controls.Repeaters +{ + internal class RecyclePool + { + public static readonly AttachedProperty OriginTemplateProperty = + AvaloniaProperty.RegisterAttached("OriginTemplate", typeof(RecyclePool)); + + private static ConditionalWeakTable s_pools = new ConditionalWeakTable(); + private readonly Dictionary> _elements = new Dictionary>(); + + public static RecyclePool GetPoolInstance(IDataTemplate dataTemplate) + { + s_pools.TryGetValue(dataTemplate, out var result); + return result; + } + + public static void SetPoolInstance(IDataTemplate dataTemplate, RecyclePool value) => s_pools.Add(dataTemplate, value); + + public void PutElement(IControl element, string key, IControl owner) + { + var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner); + var elementInfo = new ElementInfo(element, ownerAsPanel); + + if (!_elements.TryGetValue(key, out var pool)) + { + pool = new List(); + _elements.Add(key, pool); + } + + pool.Add(elementInfo); + } + + public IControl TryGetElement(string key, IControl owner) + { + if (_elements.TryGetValue(key, out var elements)) + { + if (elements.Count > 0) + { + // Prefer an element from the same owner or with no owner so that we don't incur + // the enter/leave cost during recycling. + // TODO: prioritize elements with the same owner to those without an owner. + var elementInfo = elements.FirstOrDefault(x => x.Owner == owner) ?? elements.LastOrDefault(); + elements.Remove(elementInfo); + + var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner); + if (elementInfo.Owner != null && elementInfo.Owner != ownerAsPanel) + { + // Element is still under its parent. remove it from its parent. + var panel = elementInfo.Owner; + if (panel != null) + { + int childIndex = panel.Children.IndexOf(elementInfo.Element); + if (childIndex == -1) + { + throw new KeyNotFoundException("ItemsRepeater's child not found in its Children collection."); + } + + panel.Children.RemoveAt(childIndex); + } + } + + return elementInfo.Element; + } + } + + return null; + } + + private IPanel EnsureOwnerIsPanelOrNull(IControl owner) + { + if (owner is IPanel panel) + { + return panel; + } + else if (owner != null) + { + throw new InvalidOperationException("Owner must be IPanel or null."); + } + + return null; + } + + private class ElementInfo + { + public ElementInfo(IControl element, IPanel owner) + { + Element = element; + Owner = owner; + } + + public IControl Element { get; } + public IPanel Owner { get;} + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs b/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs new file mode 100644 index 0000000000..3f002c3121 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + internal class RepeaterLayoutContext : VirtualizingLayoutContext + { + private readonly ItemsRepeater _owner; + + public RepeaterLayoutContext(ItemsRepeater owner) + { + _owner = owner; + } + + protected override Point LayoutOriginCore + { + get => _owner.LayoutOrigin; + set => _owner.LayoutOrigin = value; + } + + protected override object LayoutStateCore + { + get => _owner.LayoutState; + set => _owner.LayoutState = value; + } + + protected override int RecommendedAnchorIndexCore + { + get + { + int anchorIndex = -1; + var anchor = _owner.SuggestedAnchor; + if (anchor != null) + { + anchorIndex = _owner.GetElementIndex(anchor); + } + + return anchorIndex; + } + } + + protected override int ItemCountCore() => _owner.ItemsSourceView?.Count ?? 0; + + protected override IControl GetOrCreateElementAtCore(int index, ElementRealizationOptions options) + { + return _owner.GetElementImpl( + index, + (options & ElementRealizationOptions.ForceCreate) != 0, + (options & ElementRealizationOptions.SuppressAutoRecycle) != 0); + } + + protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index); + + protected override void RecycleElementCore(IControl element) => _owner.ClearElementImpl(element); + + protected override Rect RealizationRectCore() => _owner.RealizationWindow; + } +} diff --git a/src/Avalonia.Controls/Repeaters/StackLayout.cs b/src/Avalonia.Controls/Repeaters/StackLayout.cs new file mode 100644 index 0000000000..79ebcef645 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/StackLayout.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Specialized; + +namespace Avalonia.Controls.Repeaters +{ + public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates + { + public static readonly AvaloniaProperty OrientationProperty + = StackPanel.OrientationProperty.AddOwner(); + + public static readonly AvaloniaProperty SpacingProperty + = StackPanel.SpacingProperty.AddOwner(); + + private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); + + public Orientation Orientation + { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + public double Spacing + { + get => GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } + + public Rect GetExtent( + Size availableSize, + VirtualizingLayoutContext context, + IControl firstRealized, + int firstRealizedItemIndex, + Rect firstRealizedLayoutBounds, + IControl lastRealized, + int lastRealizedItemIndex, + Rect lastRealizedLayoutBounds) + { + var extent = new Rect(); + + // Constants + int itemsCount = context.ItemCount; + var stackState = (StackLayoutState)context.LayoutState; + double averageElementSize = GetAverageElementSize(availableSize, context, stackState) + Spacing; + + _orientation.SetMinorSize(ref extent, stackState.MaxArrangeBounds); + _orientation.SetMajorSize(ref extent, Math.Max(0.0f, itemsCount * averageElementSize - Spacing)); + if (itemsCount > 0) + { + if (firstRealized != null) + { + //MUX_ASSERT(lastRealized); + _orientation.SetMajorStart( + ref extent, + _orientation.MajorStart(firstRealizedLayoutBounds) - firstRealizedItemIndex * averageElementSize); + var remainingItems = itemsCount - lastRealizedItemIndex - 1; + _orientation.SetMajorSize( + ref extent, + _orientation.MajorEnd(lastRealizedLayoutBounds) - + _orientation.MajorStart(extent) + + (remainingItems * averageElementSize)); + } + } + + return extent; + } + + public void OnElementMeasured( + IControl element, + int index, + Size availableSize, + Size measureSize, + Size desiredSize, + Size provisionalArrangeSize, + VirtualizingLayoutContext context) + { + if (context is VirtualizingLayoutContext virtualContext) + { + var stackState = (StackLayoutState)virtualContext.LayoutState; + var provisionalArrangeSizeWinRt = provisionalArrangeSize; + stackState.OnElementMeasured( + index, + _orientation.Major(provisionalArrangeSizeWinRt), + _orientation.Minor(provisionalArrangeSizeWinRt)); + } + } + + Size IFlowLayoutAlgorithmDelegates.Algorithm_GetMeasureSize( + int index, + Size availableSize, + VirtualizingLayoutContext context) => availableSize; + + Size IFlowLayoutAlgorithmDelegates.Algorithm_GetProvisionalArrangeSize( + int index, + Size measureSize, + Size desiredSize, + VirtualizingLayoutContext context) + { + var measureSizeMinor = _orientation.Minor(measureSize); + return _orientation.MinorMajorSize( + double.IsInfinity(measureSizeMinor) ? + Math.Max(measureSizeMinor, _orientation.Minor(desiredSize)) : + _orientation.Minor(desiredSize), + _orientation.Major(desiredSize)); + } + + bool IFlowLayoutAlgorithmDelegates.Algorithm_ShouldBreakLine(int index, double remainingSpace) => true; + + FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForRealizationRect( + Size availableSize, + VirtualizingLayoutContext context) => GetAnchorForRealizationRect(availableSize, context); + + FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForTargetElement( + int targetIndex, + Size availableSize, + VirtualizingLayoutContext context) + { + double offset = double.NaN; + int index = -1; + int itemsCount = context.ItemCount; + + if (targetIndex >= 0 && targetIndex < itemsCount) + { + index = targetIndex; + var state = (StackLayoutState)context.LayoutState; + double averageElementSize = GetAverageElementSize(availableSize, context, state) + Spacing; + offset = index * averageElementSize + _orientation.MajorStart(state.FlowAlgorithm.LastExtent); + } + + return new FlowLayoutAnchorInfo { Index = index, Offset = offset }; + } + + Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent( + Size availableSize, + VirtualizingLayoutContext context, + IControl firstRealized, + int firstRealizedItemIndex, + Rect firstRealizedLayoutBounds, + IControl lastRealized, + int lastRealizedItemIndex, + Rect lastRealizedLayoutBounds) + { + return GetExtent( + availableSize, + context, + firstRealized, + firstRealizedItemIndex, + firstRealizedLayoutBounds, + lastRealized, + lastRealizedItemIndex, + lastRealizedLayoutBounds); + } + + void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(IControl element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context) + { + OnElementMeasured( + element, + index, + availableSize, + measureSize, + desiredSize, + provisionalArrangeSize, + context); + } + + void IFlowLayoutAlgorithmDelegates.Algorithm_OnLineArranged(int startIndex, int countInLine, double lineSize, VirtualizingLayoutContext context) + { + } + + internal FlowLayoutAnchorInfo GetAnchorForRealizationRect( + Size availableSize, + VirtualizingLayoutContext context) + { + int anchorIndex = -1; + double offset = double.NaN; + + // Constants + int itemsCount = context.ItemCount; + if (itemsCount > 0) + { + var realizationRect = context.RealizationRect; + var state = (StackLayoutState)context.LayoutState; + var lastExtent = state.FlowAlgorithm.LastExtent; + + double averageElementSize = GetAverageElementSize(availableSize, context, state) + Spacing; + double realizationWindowOffsetInExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); + double majorSize = _orientation.MajorSize(lastExtent) == 0 ? Math.Max(0.0, averageElementSize * itemsCount - Spacing) : _orientation.MajorSize(lastExtent); + if (itemsCount > 0 && + _orientation.MajorSize(realizationRect) > 0 && + realizationWindowOffsetInExtent + _orientation.MajorSize(realizationRect) >= 0 && realizationWindowOffsetInExtent <= majorSize) + { + anchorIndex = (int) (realizationWindowOffsetInExtent / averageElementSize); + offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent); + anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorIndex)); + } + } + + return new FlowLayoutAnchorInfo { Index = anchorIndex, Offset = offset, }; + } + + protected override void InitializeForContextCore(VirtualizingLayoutContext context) + { + var state = context.LayoutState; + var stackState = state as StackLayoutState; + + if (stackState == null) + { + if (state != null) + { + throw new InvalidOperationException("LayoutState must derive from StackLayoutState."); + } + + // Custom deriving layouts could potentially be stateful. + // If that is the case, we will just create the base state required by UniformGridLayout ourselves. + stackState = new StackLayoutState(); + } + + stackState.InitializeForContext(context, this); + } + + protected override void UninitializeForContextCore(VirtualizingLayoutContext context) + { + var stackState = (StackLayoutState)context.LayoutState; + stackState.UninitializeForContext(context); + } + + protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) + { + var desiredSize = GetFlowAlgorithm(context).Measure( + availableSize, + context, + false, + 0, + Spacing, + _orientation.ScrollOrientation, + LayoutId); + + return new Size(desiredSize.Width, desiredSize.Height); + } + + protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) + { + var value = GetFlowAlgorithm(context).Arrange( + finalSize, + context, + FlowLayoutAlgorithm.LineAlignment.Start, + LayoutId); + + ((StackLayoutState)context.LayoutState).OnArrangeLayoutEnd(); + + return new Size(value.Width, value.Height); + } + + protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args) + { + GetFlowAlgorithm(context).OnItemsSourceChanged(source, args, context); + // Always invalidate layout to keep the view accurate. + InvalidateLayout(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == OrientationProperty) + { + //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation. + //Horizontal Orientation means we have a Horizontal ScrollOrientation. + _orientation.ScrollOrientation = (Orientation)e.NewValue; + } + + InvalidateLayout(); + } + + private double GetAverageElementSize( + Size availableSize, + VirtualizingLayoutContext context, + StackLayoutState stackLayoutState) + { + double averageElementSize = 0; + + if (context.ItemCount > 0) + { + if (stackLayoutState.TotalElementsMeasured == 0) + { + var tmpElement = context.GetOrCreateElementAt(0, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); + stackLayoutState.FlowAlgorithm.MeasureElement(tmpElement, 0, availableSize, context); + context.RecycleElement(tmpElement); + } + + averageElementSize = Math.Round(stackLayoutState.TotalElementSize / stackLayoutState.TotalElementsMeasured); + } + + return averageElementSize; + } + + private void InvalidateLayout() => InvalidateMeasure(); + + private FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((StackLayoutState)context.LayoutState).FlowAlgorithm; + } +} diff --git a/src/Avalonia.Controls/Repeaters/StackLayoutState.cs b/src/Avalonia.Controls/Repeaters/StackLayoutState.cs new file mode 100644 index 0000000000..cefaf38f3a --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/StackLayoutState.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Controls.Repeaters +{ + public class StackLayoutState + { + private const int BufferSize = 100; + private readonly List _estimationBuffer = new List(); + + internal FlowLayoutAlgorithm FlowAlgorithm { get; } = new FlowLayoutAlgorithm(); + internal double MaxArrangeBounds { get; private set; } + internal int TotalElementsMeasured { get; private set; } + internal double TotalElementSize { get; private set; } + + internal void InitializeForContext(VirtualizingLayoutContext context, IFlowLayoutAlgorithmDelegates callbacks) + { + FlowAlgorithm.InitializeForContext(context, callbacks); + + if (_estimationBuffer.Count == 0) + { + _estimationBuffer.AddRange(Enumerable.Repeat(0.0, BufferSize)); + } + + context.LayoutState = this; + } + + internal void UninitializeForContext(VirtualizingLayoutContext context) + { + FlowAlgorithm.UninitializeForContext(context); + } + + internal void OnElementMeasured(int elementIndex, double majorSize, double minorSize) + { + int estimationBufferIndex = elementIndex % _estimationBuffer.Count; + bool alreadyMeasured = _estimationBuffer[estimationBufferIndex] != 0; + + if (!alreadyMeasured) + { + TotalElementsMeasured++; + } + + TotalElementSize -= _estimationBuffer[estimationBufferIndex]; + TotalElementSize += majorSize; + _estimationBuffer[estimationBufferIndex] = majorSize; + + MaxArrangeBounds = Math.Max(MaxArrangeBounds, minorSize); + } + + internal void OnArrangeLayoutEnd() => MaxArrangeBounds = 0; + } +} diff --git a/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs b/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs new file mode 100644 index 0000000000..4b3922b9c3 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + internal class UniqueIdElementPool : IEnumerable> + { + private readonly Dictionary _elementMap = new Dictionary(); + private readonly ItemsRepeater _owner; + + public UniqueIdElementPool(ItemsRepeater owner) => _owner = owner; + + public void Add(IControl element) + { + //MUX_ASSERT(_owner.ItemsSourceView.HasKeyIndexMapping); + + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var key = virtInfo.UniqueId; + + if (_elementMap.ContainsKey(key)) + { + throw new InvalidOperationException($"The unique id provided ({key}) is not unique."); + } + + _elementMap.Add(key, element); + } + + public IControl Remove(int index) + { + //MUX_ASSERT(_owner.ItemsSourceView.HasKeyIndexMapping); + + // Check if there is already a element in the mapping and if so, use it. + string key = _owner.ItemsSourceView.KeyFromIndex(index); + + if (_elementMap.TryGetValue(key, out var element)) + { + _elementMap.Remove(key); + } + + return element; + } + + public void Clear() + { + //MUX_ASSERT(_owner.ItemsSourceView.HasKeyIndexMapping); + _elementMap.Clear(); + } + + public IEnumerator> GetEnumerator() => _elementMap.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Avalonia.Controls/Repeaters/ViewManager.cs b/src/Avalonia.Controls/Repeaters/ViewManager.cs new file mode 100644 index 0000000000..d7f8f224b8 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ViewManager.cs @@ -0,0 +1,787 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Repeaters +{ + internal sealed class ViewManager + { + private const int FirstRealizedElementIndexDefault = int.MaxValue; + private const int LastRealizedElementIndexDefault = int.MinValue; + + private readonly ItemsRepeater _owner; + private readonly List _pinnedPool = new List(); + private readonly UniqueIdElementPool _resetPool; + private IControl _lastFocusedElement; + private bool _isDataSourceStableResetPending; + private ElementFactoryGetArgs _elementFactoryGetArgs; + private ElementFactoryRecycleArgs _elementFactoryRecycleArgs; + private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault; + private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault; + private bool _eventsSubscribed; + + public ViewManager(ItemsRepeater owner) + { + _owner = owner; + _resetPool = new UniqueIdElementPool(owner); + } + + public IControl GetElement(int index, bool forceCreate, bool suppressAutoRecycle) + { + var element = forceCreate ? null : GetElementIfAlreadyHeldByLayout(index); + if (element == null) + { + // check if this is the anchor made through repeater in preparation + // for a bring into view. + var madeAnchor = _owner.MadeAnchor; + if (madeAnchor != null) + { + var anchorVirtInfo = ItemsRepeater.TryGetVirtualizationInfo(madeAnchor); + if (anchorVirtInfo.Index == index) + { + element = madeAnchor; + } + } + } + if (element == null) { element = GetElementFromUniqueIdResetPool(index); }; + if (element == null) { element = GetElementFromPinnedElements(index); } + if (element == null) { element = GetElementFromElementFactory(index); } + + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); + if (suppressAutoRecycle) + { + virtInfo.AutoRecycleCandidate = false; + } + else + { + virtInfo.AutoRecycleCandidate = true; + virtInfo.KeepAlive = true; + } + + return element; + } + + public void ClearElement(IControl element, bool isClearedDueToCollectionChange) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var index = virtInfo.Index; + bool cleared = + ClearElementToUniqueIdResetPool(element, virtInfo) || + ClearElementToPinnedPool(element, virtInfo, isClearedDueToCollectionChange); + + if (!cleared) + { + ClearElementToElementFactory(element); + } + + // Both First and Last indices need to be valid or default. + if (index == _firstRealizedElementIndexHeldByLayout && index == _lastRealizedElementIndexHeldByLayout) + { + // First and last were pointing to the same element and that is going away. + InvalidateRealizedIndicesHeldByLayout(); + } + else if (index == _firstRealizedElementIndexHeldByLayout) + { + // The FirstElement is going away, shrink the range by one. + ++_firstRealizedElementIndexHeldByLayout; + } + else if (index == _lastRealizedElementIndexHeldByLayout) + { + // Last element is going away, shrink the range by one at the end. + --_lastRealizedElementIndexHeldByLayout; + } + else + { + // Index is either outside the range we are keeping track of or inside the range. + // In both these cases, we just keep the range we have. If this clear was due to + // a collection change, then in the CollectionChanged event, we will invalidate these guys. + } + } + + public void ClearElementToElementFactory(IControl element) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var clearedIndex = virtInfo.Index; + _owner.OnElementClearing(element); + + if (_elementFactoryRecycleArgs == null) + { + // Create one. + _elementFactoryRecycleArgs = new ElementFactoryRecycleArgs(); + } + + var context = _elementFactoryRecycleArgs; + context.Element = element; + context.Parent = _owner; + + _owner.ItemTemplateShim.RecycleElement(context); + + context.Element = null; + context.Parent = null; + + virtInfo.MoveOwnershipToElementFactory(); + //_phaser.StopPhasing(element, virtInfo); + if (_lastFocusedElement == element) + { + // Focused element is going away. Remove the tracked last focused element + // and pick a reasonable next focus if we can find one within the layout + // realized elements. + MoveFocusFromClearedIndex(clearedIndex); + } + + } + + private void MoveFocusFromClearedIndex(int clearedIndex) + { + IControl focusedChild = null; + var focusCandidate = FindFocusCandidate(clearedIndex, focusedChild); + if (focusCandidate != null) + { + //var focusState = _lastFocusedElement?.FocusState ?? FocusState.Programmatic; + + // If the last focused element has focus, use its focus state, if not use programmatic. + //focusState = focusState == FocusState.Unfocused ? FocusState.Programmatic : focusState; + focusCandidate.Focus(); + + _lastFocusedElement = focusedChild; + // Add pin to hold the focused element. + UpdatePin(focusedChild, true /* addPin */); + } + else + { + // We could not find a candiate. + _lastFocusedElement = null; + } + } + + IControl FindFocusCandidate(int clearedIndex, IControl focusedChild) + { + // Walk through all the children and find elements with index before and after the cleared index. + // Note that during a delete the next element would now have the same index. + int previousIndex = int.MinValue; + int nextIndex = int.MaxValue; + IControl nextElement = null; + IControl previousElement = null; + + foreach (var child in _owner.Children) + { + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child); + if (virtInfo?.IsHeldByLayout == true) + { + int currentIndex = virtInfo.Index; + if (currentIndex < clearedIndex) + { + if (currentIndex > previousIndex) + { + previousIndex = currentIndex; + previousElement = child; + } + } + else if (currentIndex >= clearedIndex) + { + // Note that we use >= above because if we deleted the focused element, + // the next element would have the same index now. + if (currentIndex < nextIndex) + { + nextIndex = currentIndex; + nextElement = child; + } + } + } + } + + // Find the next element if one exists, if not use the previous element. + // If the container itself is not focusable, find a descendent that is. + IControl focusCandidate = null; + if (nextElement != null) + { + focusCandidate = nextElement as IControl; + if (focusCandidate != null) + { + ////var firstFocus = FocusManager.FindFirstFocusableElement(nextElement); + + ////if (firstFocus != null) + ////{ + //// focusCandidate = firstFocus as IControl; + ////} + } + } + + if (focusCandidate == null && previousElement != null) + { + focusCandidate = previousElement as IControl; + if (previousElement != null) + { + ////var lastFocus = FocusManager.FindLastFocusableElement(previousElement); + + ////if (lastFocus != null) + ////{ + //// focusCandidate = lastFocus as IControl; + ////} + } + } + + return focusCandidate; + } + + public int GetElementIndex(VirtualizationInfo virtInfo) + { + if (virtInfo == null) + { + throw new ArgumentException("Element is not a child of this ItemsRepeater."); + } + + return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1; + } + + public void PrunePinnedElements() + { + EnsureEventSubscriptions(); + + // Go through pinned elements and make sure they still have + // a reason to be pinned. + for (var i = 0; i < _pinnedPool.Count; ++i) + { + var elementInfo = _pinnedPool[i]; + var virtInfo = elementInfo.VirtualizationInfo; + + //MUX_ASSERT(virtInfo.Owner() == ElementOwner.PinnedPool); + + if (!virtInfo.IsPinned) + { + _pinnedPool.RemoveAt(i); + --i; + + // Pinning was the only thing keeping this element alive. + ClearElementToElementFactory(elementInfo.PinnedElement); + } + } + } + + public void UpdatePin(IControl element, bool addPin) + { + var parent = element.VisualParent; + var child = (IVisual)element; + + while (parent != null) + { + if (parent is ItemsRepeater repeater) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo((IControl)child); + if (virtInfo.IsRealized) + { + if (addPin) + { + virtInfo.AddPin(); + } + else if (virtInfo.IsPinned) + { + if (virtInfo.RemovePin() == 0) + { + // ElementFactory is invoked during the measure pass. + // We will clear the element then. + repeater.InvalidateMeasure(); + } + } + } + } + + child = parent; + parent = child.VisualParent; + } + } + + public void OnItemsSourceChanged(object sender, NotifyCollectionChangedEventArgs args) + { + // Note: For items that have been removed, the index will not be touched. It will hold + // the old index before it was removed. It is not valid anymore. + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + { + var newIndex = args.NewStartingIndex; + var newCount = args.NewItems.Count; + EnsureFirstLastRealizedIndices(); + if (newIndex <= _lastRealizedElementIndexHeldByLayout) + { + _lastRealizedElementIndexHeldByLayout += newCount; + foreach (var element in _owner.Children) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var dataIndex = virtInfo.Index; + + if (virtInfo.IsRealized && dataIndex >= newIndex) + { + UpdateElementIndex(element, virtInfo, dataIndex + newCount); + } + } + } + else + { + // Indices held by layout are not affected + // We could still have items in the pinned elements that need updates. This is usually a very small vector. + for (var i = 0; i < _pinnedPool.Count; ++i) + { + var elementInfo = _pinnedPool[i]; + var virtInfo = elementInfo.VirtualizationInfo; + var dataIndex = virtInfo.Index; + + if (virtInfo.IsRealized && dataIndex >= newIndex) + { + var element = elementInfo.PinnedElement; + UpdateElementIndex(element, virtInfo, dataIndex + newCount); + } + } + } + break; + } + + case NotifyCollectionChangedAction.Replace: + { + // Requirement: oldStartIndex == newStartIndex. It is not a replace if this is not true. + // Two cases here + // case 1: oldCount == newCount + // indices are not affected. nothing to do here. + // case 2: oldCount != newCount + // Replaced with less or more items. This is like an insert or remove + // depending on the counts. + var oldStartIndex = args.OldStartingIndex; + var newStartingIndex = args.NewStartingIndex; + var oldCount = args.OldItems.Count; + var newCount = args.NewItems.Count; + if (oldStartIndex != newStartingIndex) + { + throw new NotSupportedException("Replace is only allowed with OldStartingIndex equals to NewStartingIndex."); + } + + if (oldCount == 0) + { + throw new NotSupportedException("Replace notification with args.OldItemsCount value of 0 is not allowed. Use Insert action instead."); + } + + if (newCount == 0) + { + throw new NotSupportedException("Replace notification with args.NewItemCount value of 0 is not allowed. Use Remove action instead."); + } + + int countChange = newCount - oldCount; + if (countChange != 0) + { + // countChange > 0 : countChange items were added + // countChange < 0 : -countChange items were removed + foreach (var element in _owner.Children) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var dataIndex = virtInfo.Index; + + if (virtInfo.IsRealized) + { + if (dataIndex >= oldStartIndex + oldCount) + { + UpdateElementIndex(element, virtInfo, dataIndex + countChange); + } + } + } + + EnsureFirstLastRealizedIndices(); + _lastRealizedElementIndexHeldByLayout += countChange; + } + break; + } + + case NotifyCollectionChangedAction.Remove: + { + var oldStartIndex = args.OldStartingIndex; + var oldCount = args.OldItems.Count; + foreach (var element in _owner.Children) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var dataIndex = virtInfo.Index; + + if (virtInfo.IsRealized) + { + if (virtInfo.AutoRecycleCandidate && oldStartIndex <= dataIndex && dataIndex < oldStartIndex + oldCount) + { + // If we are doing the mapping, remove the element who's data was removed. + _owner.ClearElementImpl(element); + } + else if (dataIndex >= (oldStartIndex + oldCount)) + { + UpdateElementIndex(element, virtInfo, dataIndex - oldCount); + } + } + } + + InvalidateRealizedIndicesHeldByLayout(); + break; + } + + case NotifyCollectionChangedAction.Reset: + if (_owner.ItemsSourceView.HasKeyIndexMapping) + { + _isDataSourceStableResetPending = true; + } + + // Walk through all the elements and make sure they are cleared, they will go into + // the stable id reset pool. + foreach (var element in _owner.Children) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate) + { + _owner.ClearElementImpl(element); + } + } + + InvalidateRealizedIndicesHeldByLayout(); + break; + } + } + + private void EnsureFirstLastRealizedIndices() + { + if (_firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault) + { + // This will ensure that the indexes are updated. + GetElementIfAlreadyHeldByLayout(0); + } + } + + public void OnLayoutChanging() + { + if (_owner.ItemsSourceView?.HasKeyIndexMapping == true) + { + _isDataSourceStableResetPending = true; + } + } + + public void OnOwnerArranged() + { + if (_isDataSourceStableResetPending) + { + _isDataSourceStableResetPending = false; + + foreach (var entry in _resetPool) + { + // TODO: Task 14204306: ItemsRepeater: Find better focus candidate when focused element is deleted in the ItemsSource. + // Focused element is getting cleared. Need to figure out semantics on where + // focus should go when the focused element is removed from the data collection. + ClearElement(entry.Value, true /* isClearedDueToCollectionChange */); + } + + _resetPool.Clear(); + } + } + + // We optimize for the case where index is not realized to return null as quickly as we can. + // Flow layouts manage containers on their own and will never ask for an index that is already realized. + // If an index that is realized is requested by the layout, we unfortunately have to walk the + // children. Not ideal, but a reasonable default to provide consistent behavior between virtualizing + // and non-virtualizing hosts. + private IControl GetElementIfAlreadyHeldByLayout(int index) + { + IControl element = null; + + bool cachedFirstLastIndicesInvalid = _firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault; + //MUX_ASSERT(!cachedFirstLastIndicesInvalid || m_lastRealizedElementIndexHeldByLayout == LastRealizedElementIndexDefault); + + bool isRequestedIndexInRealizedRange = (_firstRealizedElementIndexHeldByLayout <= index && index <= _lastRealizedElementIndexHeldByLayout); + + if (cachedFirstLastIndicesInvalid || isRequestedIndexInRealizedRange) + { + // Both First and Last indices need to be valid or default. + //MUX_ASSERT((m_firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault && m_lastRealizedElementIndexHeldByLayout == LastRealizedElementIndexDefault) || + // (m_firstRealizedElementIndexHeldByLayout != FirstRealizedElementIndexDefault && m_lastRealizedElementIndexHeldByLayout != LastRealizedElementIndexDefault)); + + foreach (var child in _owner.Children) + { + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child); + if (virtInfo?.IsHeldByLayout == true) + { + // Only give back elements held by layout. If someone else is holding it, they will be served by other methods. + int childIndex = virtInfo.Index; + _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, childIndex); + _lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, childIndex); + if (virtInfo.Index == index) + { + element = child; + // If we have valid first/last indices, we don't have to walk the rest, but if we + // do not, then we keep walking through the entire children collection to get accurate + // indices once. + if (!cachedFirstLastIndicesInvalid) + { + break; + } + } + } + } + } + + return element; + } + + private IControl GetElementFromUniqueIdResetPool(int index) + { + IControl element = null; + // See if you can get it from the reset pool. + if (_isDataSourceStableResetPending) + { + element = _resetPool.Remove(index); + if (element != null) + { + // Make sure that the index is updated to the current one + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool(); + UpdateElementIndex(element, virtInfo, index); + } + } + + return element; + } + + private IControl GetElementFromPinnedElements(int index) + { + IControl element = null; + + // See if you can find something among the pinned elements. + for (var i = 0; i < _pinnedPool.Count; ++i) + { + var elementInfo = _pinnedPool[i]; + var virtInfo = elementInfo.VirtualizationInfo; + + if (virtInfo.Index == index) + { + _pinnedPool.RemoveAt(i); + element = elementInfo.PinnedElement; + elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool(); + break; + } + } + + return element; + } + + private IControl GetElementFromElementFactory(int index) + { + // The view generator is the provider of last resort. + + var itemTemplateFactory = _owner.ItemTemplateShim; + if (itemTemplateFactory == null) + { + // If no ItemTemplate was provided, use a default + var factory = FuncDataTemplate.Default; + _owner.ItemTemplate = factory; + itemTemplateFactory = _owner.ItemTemplateShim; + } + + var data = _owner.ItemsSourceView.GetAt(index); + + if (_elementFactoryGetArgs == null) + { + // Create one. + _elementFactoryGetArgs = new ElementFactoryGetArgs(); + } + + var args = _elementFactoryGetArgs; + args.Data = data; + args.Parent = _owner; + args.Index= index; + + var element = itemTemplateFactory.GetElement(args); + + args.Data = null; + args.Parent = null; + + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); + if (virtInfo == null) + { + virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); + } + + // Prepare the element + // If we are phasing, run phase 0 before setting DataContext. If phase 0 is not + // run before setting DataContext, when setting DataContext all the phases will be + // run in the OnDataContextChanged handler in code generated by the xaml compiler (code-gen). + var extension = false; ////CachedVisualTreeHelpers.GetDataTemplateComponent(element); + if (extension) + { + ////// Clear out old data. + ////extension.Recycle(); + ////int nextPhase = VirtualizationInfo.PhaseReachedEnd; + ////// Run Phase 0 + ////extension.ProcessBindings(data, index, 0 /* currentPhase */, nextPhase); + + ////// Setup phasing information, so that Phaser can pick up any pending phases left. + ////// Update phase on virtInfo. Set data and templateComponent only if x:Phase was used. + ////virtInfo.UpdatePhasingInfo(nextPhase, nextPhase > 0 ? data : null, nextPhase > 0 ? extension : null); + } + else + { + // Set data context only if no x:Bind was used. ie. No data template component on the root. + element.DataContext = data; + } + + virtInfo.MoveOwnershipToLayoutFromElementFactory( + index, + /* uniqueId: */ + _owner.ItemsSourceView.HasKeyIndexMapping ? + _owner.ItemsSourceView.KeyFromIndex(index) : + string.Empty); + + // The view generator is the only provider that prepares the element. + var repeater = _owner; + + // Add the element to the children collection here before raising OnElementPrepared so + // that handlers can walk up the tree in case they want to find their IndexPath in the + // nested case. + var children = repeater.Children; + if (element.VisualParent != repeater) + { + children.Add(element); + } + + ////repeater.AnimationManager.OnElementPrepared(element); + repeater.OnElementPrepared(element, index); + ////_phaser.PhaseElement(element, virtInfo); + + // Update realized indices + _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index); + _lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index); + + return element; + } + + private bool ClearElementToUniqueIdResetPool(IControl element, VirtualizationInfo virtInfo) + { + if (_isDataSourceStableResetPending) + { + _resetPool.Add(element); + virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout(); + } + + return _isDataSourceStableResetPending; + } + + private bool ClearElementToAnimator(IControl element, VirtualizationInfo virtInfo) + { + return false; + ////bool cleared = _owner.AnimationManager.ClearElement(element); + ////if (cleared) + ////{ + //// int clearedIndex = virtInfo.Index; + //// virtInfo.MoveOwnershipToAnimator(); + //// if (_lastFocusedElement == element) + //// { + //// // Focused element is going away. Remove the tracked last focused element + //// // and pick a reasonable next focus if we can find one within the layout + //// // realized elements. + //// MoveFocusFromClearedIndex(clearedIndex); + //// } + ////} + ////return cleared; + } + + private bool ClearElementToPinnedPool(IControl element, VirtualizationInfo virtInfo, bool isClearedDueToCollectionChange) + { + if (_isDataSourceStableResetPending) + { + _resetPool.Add(element); + virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout(); + } + + return _isDataSourceStableResetPending; + } + + private void UpdateFocusedElement() + { + IControl focusedElement = null; + + var child = FocusManager.Instance.Current; + + if (child != null) + { + var parent = child.VisualParent; + var owner = _owner; + + // Find out if the focused element belongs to one of our direct + // children. + while (parent != null) + { + if (parent is ItemsRepeater repeater) + { + var element = child as IControl; + if (repeater == owner && ItemsRepeater.GetVirtualizationInfo(element).IsRealized) + { + focusedElement = element; + } + + break; + } + + child = parent as IInputElement; + parent = child.VisualParent; + } + } + + // If the focused element has changed, + // we need to unpin the old one and pin the new one. + if (_lastFocusedElement != focusedElement) + { + if (_lastFocusedElement != null) + { + UpdatePin(_lastFocusedElement, false /* addPin */); + } + + if (focusedElement != null) + { + UpdatePin(focusedElement, true /* addPin */); + } + + _lastFocusedElement = focusedElement; + } + } + + private void OnFocusChanged(object sender, RoutedEventArgs e) => UpdateFocusedElement(); + + private void EnsureEventSubscriptions() + { + if (!_eventsSubscribed) + { + _owner.GotFocus += OnFocusChanged; + _owner.LostFocus += OnFocusChanged; + } + } + + private void UpdateElementIndex(IControl element, VirtualizationInfo virtInfo, int index) + { + var oldIndex = virtInfo.Index; + if (oldIndex != index) + { + virtInfo.UpdateIndex(index); + _owner.OnElementIndexChanged(element, oldIndex, index); + } + } + + private void InvalidateRealizedIndicesHeldByLayout() + { + _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault; + _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault; + } + + private struct PinnedElementInfo + { + public PinnedElementInfo(IControl element) + { + PinnedElement = element; + VirtualizationInfo = ItemsRepeater.GetVirtualizationInfo(element); + } + + public IControl PinnedElement { get; } + public VirtualizationInfo VirtualizationInfo { get; } + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/ViewportManager.cs b/src/Avalonia.Controls/Repeaters/ViewportManager.cs new file mode 100644 index 0000000000..5986c75579 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ViewportManager.cs @@ -0,0 +1,463 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Avalonia.Layout; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Repeaters +{ + internal class ViewportManager + { + private const double CacheBufferPerSideInflationPixelDelta = 40.0; + private readonly ItemsRepeater _owner; + private IControl _makeAnchorElement; + private bool _isAnchorOutsideRealizedRange; + private Task _cacheBuildAction; + private Rect _visibleWindow; + private Rect _layoutExtent; + // This is the expected shift by the layout. + private Point _expectedViewportShift; + // This is what is pending and not been accounted for. + // Sometimes the scrolling surface cannot service a shift (for example + // it is already at the top and cannot shift anymore.) + private Point _pendingViewportShift; + // Unshiftable shift amount that this view manager can + // handle on its own to fake it to the layout as if the shift + // actually happened. This can happen in cases where no scrollviewer + // in the parent chain can scroll in the shift direction. + private Point _unshiftableShift; + private double _maximumHorizontalCacheLength = 2.0; + private double _maximumVerticalCacheLength = 2.0; + private double _horizontalCacheBufferPerSide; + private double _verticalCacheBufferPerSide; + private bool _isBringIntoViewInProgress; + // For non-virtualizing layouts, we do not need to keep + // updating viewports and invalidating measure often. So when + // a non virtualizing layout is used, we stop doing all that work. + bool _managingViewportDisabled; + private IDisposable _effectiveViewportChangedRevoker; + private bool _layoutUpdatedSubscribed; + + public ViewportManager(ItemsRepeater owner) + { + _owner = owner; + } + + // TODO: Implement + public IControl SuggestedAnchor => null; + + // TODO: Implement + public bool HasScroller => false; + + public IControl MadeAnchor => _makeAnchorElement; + + public double HorizontalCacheLength + { + get => _maximumHorizontalCacheLength; + set + { + if (_maximumHorizontalCacheLength != value) + { + ValidateCacheLength(value); + _maximumHorizontalCacheLength = value; + ResetCacheBuffer(); + } + } + } + + public double VerticalCacheLength + { + get => _maximumVerticalCacheLength; + set + { + if (_maximumVerticalCacheLength != value) + { + ValidateCacheLength(value); + _maximumVerticalCacheLength = value; + ResetCacheBuffer(); + } + } + } + + private Rect GetLayoutVisibleWindowDiscardAnchor() + { + var visibleWindow = _visibleWindow; + + if (HasScroller) + { + visibleWindow = new Rect( + visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X, + visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y, + visibleWindow.Width, + visibleWindow.Height); + } + + return visibleWindow; + } + + public Rect GetLayoutVisibleWindow() + { + var visibleWindow = _visibleWindow; + + if (_makeAnchorElement != null) + { + // The anchor is not necessarily laid out yet. Its position should default + // to zero and the layout origin is expected to change once layout is done. + // Until then, we need a window that's going to protect the anchor from + // getting recycled. + visibleWindow = visibleWindow.WithX(0).WithY(0); + } + else if (HasScroller) + { + visibleWindow = new Rect( + visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X, + visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y, + visibleWindow.Width, + visibleWindow.Height); + } + + return visibleWindow; + } + + public Rect GetLayoutRealizationWindow() + { + var realizationWindow = GetLayoutVisibleWindow(); + if (HasScroller) + { + realizationWindow = new Rect( + realizationWindow.X - _horizontalCacheBufferPerSide, + realizationWindow.Y - _verticalCacheBufferPerSide, + realizationWindow.Width + _horizontalCacheBufferPerSide * 2.0, + realizationWindow.Height + _verticalCacheBufferPerSide * 2.0); + } + + return realizationWindow; + } + + public void SetLayoutExtent(Rect extent) + { + _expectedViewportShift = new Point( + _expectedViewportShift.X + _layoutExtent.X - extent.X, + _expectedViewportShift.Y + _layoutExtent.Y - extent.Y); + + // 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) + { + // 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), + // then we will end up not realizing elements in the visible + // window. To avoid this, we register to layout updated for this layout pass. If we + // get an effective viewport, we know we have a new viewport and we unregister from + // layout updated. If we get the layout updated handler, then we know that the + // scroller was unable to perform the shift and we invalidate measure and unregister + // from the layout updated event. + if (!_layoutUpdatedSubscribed) + { + _owner.LayoutUpdated += OnLayoutUpdated; + _layoutUpdatedSubscribed = true; + } + } + + _layoutExtent = extent; + _pendingViewportShift = _expectedViewportShift; + + // We just finished a measure pass and have a new extent. + // Let's make sure the scrollers will run its arrange so that they track the anchor. + ////if (_scroller != null) + ////{ + //// ((IControl)_scroller).InvalidateArrange(); + ////} + } + + public Point GetOrigin() => throw new NotImplementedException(); + + public void OnLayoutChanged(bool isVirtualizing) + { + _managingViewportDisabled = !isVirtualizing; + + _layoutExtent = default; + _expectedViewportShift = default; + _pendingViewportShift = default; + _unshiftableShift = default; + ResetCacheBuffer(); + + _effectiveViewportChangedRevoker?.Dispose(); + + if (!_managingViewportDisabled) + { + _effectiveViewportChangedRevoker = _owner.GetObservable(Visual.TransformedBoundsProperty) + .Skip(1) + .Subscribe(OnEffectiveViewportChanged); + } + } + + public void OnElementPrepared(IControl element) + { + // If we have an anchor element, we do not want the + // scroll anchor provider to start anchoring some other element. + ////element.CanBeScrollAnchor(true); + } + + public void OnElementCleared(IControl element) + { + ////element.CanBeScrollAnchor(false); + } + + public void OnOwnerMeasuring() + { + // This is because of a bug that causes effective viewport to not + // fire if you register during arrange. + // Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport + //EnsureScroller(); + } + + public void OnOwnerArranged() + { + _expectedViewportShift = default; + + if (!_managingViewportDisabled) + { + // This is because of a bug that causes effective viewport to not + // fire if you register during arrange. + // Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport + // EnsureScroller(); + + if (HasScroller) + { + double maximumHorizontalCacheBufferPerSide = _maximumHorizontalCacheLength * _visibleWindow.Width / 2.0; + double maximumVerticalCacheBufferPerSide = _maximumVerticalCacheLength * _visibleWindow.Height / 2.0; + + bool continueBuildingCache = + _horizontalCacheBufferPerSide < maximumHorizontalCacheBufferPerSide || + _verticalCacheBufferPerSide < maximumVerticalCacheBufferPerSide; + + if (continueBuildingCache) + { + _horizontalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta; + _verticalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta; + + _horizontalCacheBufferPerSide = Math.Min(_horizontalCacheBufferPerSide, maximumHorizontalCacheBufferPerSide); + _verticalCacheBufferPerSide = Math.Min(_verticalCacheBufferPerSide, maximumVerticalCacheBufferPerSide); + + // Since we grow the cache buffer at the end of the arrange pass, + // we need to register work even if we just reached cache potential. + RegisterCacheBuildWork(); + } + } + } + } + + private void OnLayoutUpdated(object sender, EventArgs args) + { + _owner.LayoutUpdated -= OnLayoutUpdated; + if (_managingViewportDisabled) + { + return; + } + + // We were expecting a viewport shift but we never got one and we are not going to in this + // layout pass. We likely will never get this shift, so lets assume that we are never going to get it and + // adjust our expected shift to track that. One case where this can happen is when there is no scrollviewer + // that can scroll in the direction where the shift is expected. + if (_pendingViewportShift.X != 0 || _pendingViewportShift.Y != 0) + { + // Assume this is never going to come. + _unshiftableShift = new Point( + _unshiftableShift.X + _pendingViewportShift.X, + _unshiftableShift.Y + _pendingViewportShift.Y); + _pendingViewportShift = default; + _expectedViewportShift = default; + + TryInvalidateMeasure(); + } + } + + public void OnMakeAnchor(IControl anchor, bool isAnchorOutsideRealizedRange) + { + _makeAnchorElement = anchor; + _isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange; + } + + public void OnBringIntoViewRequested(RequestBringIntoViewEventArgs args) + { + if (!_managingViewportDisabled) + { + // We do not animate bring-into-view operations where the anchor is disconnected because + // it doesn't look good (the blank space is obvious because the layout can't keep track + // of two realized ranges while the animation is going on). + if (_isAnchorOutsideRealizedRange) + { + ////args.AnimationDesired(false); + } + + // During the time between a bring into view request and the element coming into view we do not + // want the anchor provider to pick some anchor and jump to it. Instead we want to anchor on the + // element that is being brought into view. We can do this by making just that element as a potential + // anchor candidate and ensure no other element of this repeater is an anchor candidate. + // Once the layout pass is done and we render the frame, the element will be in frame and we can + // switch back to letting the anchor provider pick a suitable anchor. + + // get the targetChild - i.e the immediate child of this repeater that is being brought into view. + // Note that the element being brought into view could be a descendant. + var targetChild = GetImmediateChildOfRepeater((IControl)args.TargetObject); + + // Make sure that only the target child can be the anchor during the bring into view operation. + foreach (var child in _owner.Children) + { + ////if (child.CanBeScrollAnchor && child != targetChild) + ////{ + //// child.CanBeScrollAnchor = false; + ////} + } + + // Register to rendering event to go back to how things were before where any child can be the anchor. + _isBringIntoViewInProgress = true; + ////if (!m_renderingToken) + ////{ + //// winrt::Windows::UI::Xaml::Media::CompositionTarget compositionTarget{ nullptr }; + //// m_renderingToken = compositionTarget.Rendering(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnCompositionTargetRendering }); + ////} + } + } + + private IControl GetImmediateChildOfRepeater(IControl descendant) + { + var targetChild = descendant; + var parent = descendant.Parent; + while (parent != null && parent != _owner) + { + targetChild = parent; + parent = (IControl)parent.VisualParent; + } + + if (parent == null) + { + throw new InvalidOperationException("OnBringIntoViewRequested called with args.target element not under the ItemsRepeater that recieved the call"); + } + + return targetChild; + } + + public void ResetScrollers() + { + ////_scroller = null; + ////_effectiveViewportChangedRevoker.Dispose(); + ////m_ensuredScroller = false; + } + + private void OnEffectiveViewportChanged(TransformedBounds? bounds) + { + if (!bounds.HasValue) + { + return; + } + + var globalClip = bounds.Value.Clip; + var transform = _owner.GetVisualRoot().TransformToVisual(_owner).Value; + var clip = globalClip.TransformToAABB(transform); + var effectiveViewport = clip.Intersect(bounds.Value.Bounds); + + UpdateViewport(effectiveViewport); + + _pendingViewportShift = default; + _unshiftableShift = default; + if (_visibleWindow.IsEmpty) + { + // We got cleared. + _layoutExtent = default; + } + + // We got a new viewport, we dont need to wait for layout updated anymore to + // see if our request for a pending shift was handled. + if (_layoutUpdatedSubscribed) + { + _owner.LayoutUpdated -= OnLayoutUpdated; + } + } + + private void UpdateViewport(Rect viewport) + { + //assert(!m_managingViewportDisabled); + var previousVisibleWindow = _visibleWindow; + var currentVisibleWindow = viewport; + + if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X && + -currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y) + { + // We got cleared. + _visibleWindow = default; + } + else + { + _visibleWindow = currentVisibleWindow; + } + + TryInvalidateMeasure(); + } + + private void ResetCacheBuffer() + { + _horizontalCacheBufferPerSide = 0.0; + _verticalCacheBufferPerSide = 0.0; + + if (!_managingViewportDisabled) + { + // We need to start building the realization buffer again. + RegisterCacheBuildWork(); + } + } + + private static void ValidateCacheLength(double cacheLength) + { + if (cacheLength < 0.0 || double.IsInfinity(cacheLength) || double.IsNaN(cacheLength)) + { + throw new ArgumentException("The maximum cache length must be equal or superior to zero."); + } + } + + private void RegisterCacheBuildWork() + { + ////assert(!m_managingViewportDisabled); + if (_owner.Layout != null && + _cacheBuildAction == null) + { + // We capture 'owner' (a strong refernce on ItemsRepeater) to make sure ItemsRepeater is still around + // when the async action completes. By protecting ItemsRepeater, we also ensure that this instance + // of ViewportManager (referenced by 'this' pointer) is valid because the lifetime of ItemsRepeater + // and ViewportManager is the same (see ItemsRepeater::m_viewportManager). + // We can't simply hold a strong reference on ViewportManager because it's not a COM object. + ////auto strongOwner = m_owner->get_strong(); + ////m_cacheBuildAction.set( + //// m_owner->Dispatcher().RunIdleAsync([this, strongOwner](const winrt::IdleDispatchedHandlerArgs&) + ////{ + //// OnCacheBuildActionCompleted(); + ////})); + } + } + + private void TryInvalidateMeasure() + { + // Don't invalidate measure if we have an invalid window. + if (!_visibleWindow.IsEmpty) + { + // 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. + _owner.InvalidateMeasure(); + } + } + + private class ScrollerInfo + { + public ScrollerInfo(ScrollViewer scroller) + { + Scroller = scroller; + } + + public ScrollViewer Scroller { get; } + } + }; +} diff --git a/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs new file mode 100644 index 0000000000..5c6554be81 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs @@ -0,0 +1,121 @@ +using System; + +namespace Avalonia.Controls.Repeaters +{ + internal enum ElementOwner + { + // All elements are originally owned by the view generator. + ElementFactory, + // Ownership is transferred to the layout when it calls GetElement. + Layout, + // Ownership is transferred to the pinned pool if the element is cleared (outside of + // a 'remove' collection change of course). + PinnedPool, + // Ownership is transfered to the reset pool if the element is cleared by a reset and + // the data source supports unique ids. + UniqueIdResetPool, + // Ownership is transfered to the animator if the element is cleared due to a + // 'remove'-like collection change. + Animator + } + + internal class VirtualizationInfo + { + private int _pinCounter; + private object _data; + + public Rect ArrangeBounds { get; set; } + public bool AutoRecycleCandidate { get; set; } + public int Index { get; private set; } + public bool IsPinned => _pinCounter > 0; + public bool IsHeldByLayout => Owner == ElementOwner.Layout; + public bool IsRealized => IsHeldByLayout || Owner == ElementOwner.PinnedPool; + public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool; + public bool KeepAlive { get; set; } + public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory; + public string UniqueId { get; private set; } + + public void MoveOwnershipToLayoutFromElementFactory(int index, string uniqueId) + { + //MUX_ASSERT(_owner == ElementOwner.ElementFactory); + Owner = ElementOwner.Layout; + Index = index; + UniqueId = uniqueId; + } + + public void MoveOwnershipToLayoutFromUniqueIdResetPool() + { + //MUX_ASSERT(_owner == ElementOwner.UniqueIdResetPool); + Owner = ElementOwner.Layout; + } + + public void MoveOwnershipToLayoutFromPinnedPool() + { + //MUX_ASSERT(_owner == ElementOwner.PinnedPool); + //MUX_ASSERT(IsPinned()); + Owner = ElementOwner.Layout; + } + + public void MoveOwnershipToElementFactory() + { + //MUX_ASSERT(_owner != ElementOwner.ElementFactory); + Owner = ElementOwner.ElementFactory; + _pinCounter = 0; + Index = -1; + UniqueId = string.Empty; + ArrangeBounds = ItemsRepeater.InvalidRect; + } + + public void MoveOwnershipToUniqueIdResetPoolFromLayout() + { + //MUX_ASSERT(_owner == ElementOwner.Layout); + Owner = ElementOwner.UniqueIdResetPool; + // Keep the pinCounter the same. If the container survives the reset + // it can go on being pinned as if nothing happened. + } + + public void MoveOwnershipToAnimator() + { + // During a unique id reset, some elements might get removed. + // Their ownership will go from the UniqueIdResetPool to the Animator. + // The common path though is for ownership to go from Layout to Animator. + //MUX_ASSERT(_owner == ElementOwner.Layout || _owner == ElementOwner.UniqueIdResetPool); + Owner = ElementOwner.Animator; + Index = -1; + _pinCounter = 0; + } + + public void MoveOwnershipToPinnedPool() + { + //MUX_ASSERT(_owner == ElementOwner.Layout); + Owner = ElementOwner.PinnedPool; + } + + public int AddPin() + { + if (!IsRealized) + { + throw new InvalidOperationException("You can't pin an unrealized element."); + } + + return ++_pinCounter; + } + + public int RemovePin() + { + if (!IsRealized) + { + throw new InvalidOperationException("You can't unpin an unrealized element."); + } + + if (!IsPinned) + { + throw new InvalidOperationException("UnpinElement was called more often than PinElement."); + } + + return --_pinCounter; + } + + public void UpdateIndex(int newIndex) => Index = newIndex; + } +} diff --git a/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs b/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs new file mode 100644 index 0000000000..fa31095f9e --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + public abstract class VirtualizingLayout : Layout + { + public sealed override void InitializeForContext(LayoutContext context) + { + InitializeForContextCore((VirtualizingLayoutContext)context); + } + + public sealed override void UninitializeForContext(LayoutContext context) + { + UninitializeForContextCore((VirtualizingLayoutContext)context); + } + + public sealed override Size Measure(LayoutContext context, Size availableSize) + { + return MeasureOverride((VirtualizingLayoutContext)context, availableSize); + } + + public sealed override Size Arrange(LayoutContext context, Size finalSize) + { + return ArrangeOverride((VirtualizingLayoutContext)context, finalSize); + } + + protected virtual void InitializeForContextCore(VirtualizingLayoutContext context) + { + } + + protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context) + { + } + + protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize); + + protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize; + + protected internal virtual void OnItemsChangedCore( + VirtualizingLayoutContext context, + object source, + NotifyCollectionChangedEventArgs args) => InvalidateMeasure(); + } +} diff --git a/src/Avalonia.Controls/Repeaters/VirtualizingLayoutContext.cs b/src/Avalonia.Controls/Repeaters/VirtualizingLayoutContext.cs new file mode 100644 index 0000000000..da8d6f0f03 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/VirtualizingLayoutContext.cs @@ -0,0 +1,190 @@ +// 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; + +namespace Avalonia.Controls.Repeaters +{ + /// + /// Defines constants that specify whether to suppress automatic recycling of the retrieved + /// element or force creation of a new element. + /// + /// + /// When you call , + /// you can specify whether to suppress automatic recycling of the retrieved element or force + /// creation of a new element. Elements retrieved with automatic recycling suppressed + /// (SuppressAutoRecycle) are ignored by the automatic recycling logic that clears realized + /// elements that were not retrieved as part of the current layout pass. You must explicitly + /// recycle these elements by passing them to the RecycleElement method to avoid memory leaks. + /// + [Flags] + public enum ElementRealizationOptions + { + /// + /// No option is specified. + /// + None = 0x0, + + /// + /// Creation of a new element is forced. + /// + ForceCreate = 0x1, + + /// + /// The element is ignored by the automatic recycling logic. + /// + SuppressAutoRecycle = 0x2, + }; + + /// + /// Represents the base class for layout context types that support virtualization. + /// + public abstract class VirtualizingLayoutContext : LayoutContext + { + /// + /// Gets the number of items in the data. + /// + /// + /// This property gets the value returned by ItemCountCore, which must be implemented in + /// a derived class. + /// + public int ItemCount => ItemCountCore(); + + /// + /// Gets or sets the origin point for the estimated content size. + /// + /// + /// LayoutOrigin is used by virtualizing layouts that rely on estimations when determining + /// the size and position of content. It allows the layout to fix-up the estimated origin + /// of the content as it changes due to on-going estimation or potentially identifying the + /// actual size to use. For example, it’s possible that as a user is scrolling back to the + /// top of the content that the layout's estimates for the content size that it reports as + /// part of its MeasureOverride become increasingly accurate. If the predicted position of + /// the content does not already match the previously predicted position (for example, if + /// the size of the elements ends up being smaller than previously thought), then the + /// layout can indicate a new origin. The viewport provided to the layout on subsequent + /// passes will take into account the adjusted origin. + /// + public Point LayoutOrigin { get => LayoutOriginCore; set => LayoutOriginCore = value; } + + /// + /// Gets an area that represents the viewport and buffer that the layout should fill with + /// realized elements. + /// + public Rect RealizationRect => RealizationRectCore(); + + /// + /// Gets the recommended index from which to start the generation and layout of elements. + /// + /// + /// The recommended index might be the result of programmatically realizing an element and + /// requesting that it be brought into view. Or, it may be that a user drags the scrollbar + /// thumb so quickly that the new viewport and the viewport and buffer previously given to + /// the layout do not intersect, so a new index is suggested as the anchor from which to + /// generate and layout other elements. + /// + public int RecommendedAnchorIndex => RecommendedAnchorIndexCore; + + /// + /// Implements the behavior of LayoutOrigin in a derived or custom VirtualizingLayoutContext. + /// + protected abstract Point LayoutOriginCore { get; set; } + + /// + /// Implements the behavior for getting the return value of RecommendedAnchorIndex in a + /// derived or custom . + /// + protected virtual int RecommendedAnchorIndexCore { get; } + + /// + /// Retrieves the data item in the source found at the specified index. + /// + /// The index of the data item to retrieve. + public object GetItemAt(int index) => GetItemAtCore(index); + + /// + /// Retrieves a UIElement that represents the data item in the source found at the + /// specified index. By default, if an element already exists, it is returned; otherwise, + /// a new element is created. + /// + /// The index of the data item to retrieve a UIElement for. + /// + /// This method calls + /// with options set to None. GetElementAtCore must be implemented in a derived class. + /// + public IControl GetOrCreateElementAt(int index) + => GetOrCreateElementAtCore(index, ElementRealizationOptions.None); + + /// + /// Retrieves a UIElement that represents the data item in the source found at the + /// specified index using the specified options. + /// + /// The index of the data item to retrieve a UIElement for. + /// + /// A value of that specifies whether to suppress + /// automatic recycling of the retrieved element or force creation of a new element. + /// + /// + /// This method calls , + /// which must be implemented in a derived class. When you request an element for the + /// specified index, you can optionally specify whether to suppress automatic recycling of + /// the retrieved element or force creation of a new element.Elements retrieved with + /// automatic recycling suppressed(SuppressAutoRecycle) are ignored by the automatic + /// recycling logic that clears realized elements that were not retrieved as part of the + /// current layout pass.You must explicitly recycle these elements by passing them to the + /// RecycleElement method to avoid memory leaks. These options are intended for more + /// advanced layouts that choose to explicitly manage the realization and recycling of + /// elements as a performance optimization. + /// + public IControl GetOrCreateElementAt(int index, ElementRealizationOptions options) + => GetOrCreateElementAtCore(index, options); + + /// + /// Clears the specified UIElement and allows it to be either re-used or released. + /// + /// The element to clear. + /// + /// This method calls , which must be implemented + /// in a derived class. + /// + public void RecycleElement(IControl element) => RecycleElementCore(element); + + /// + /// When implemented in a derived class, retrieves the number of items in the data. + /// + protected abstract int ItemCountCore(); + + /// + /// When implemented in a derived class, retrieves the data item in the source found at the + /// specified index. + /// + /// The index of the data item to retrieve. + protected abstract object GetItemAtCore(int index); + + /// + /// When implemented in a derived class, retrieves an area that represents the viewport and + /// buffer that the layout should fill with realized elements. + /// + protected abstract Rect RealizationRectCore(); + + /// + /// When implemented in a derived class, retrieves a UIElement that represents the data item + /// in the source found at the specified index using the specified options. + /// + /// The index of the data item to retrieve a UIElement for. + /// + /// A value of that specifies whether to suppress + /// automatic recycling of the retrieved element or force creation of a new element. + /// + protected abstract IControl GetOrCreateElementAtCore(int index, ElementRealizationOptions options); + + /// + /// When implemented in a derived class, clears the specified UIElement and allows it to be + /// either re-used or released. + /// + /// The element to clear. + protected abstract void RecycleElementCore(IControl element); + } +} diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index c29faa1b4d..b391ff061d 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -161,8 +161,8 @@ namespace Avalonia.Controls /// The desired size of the control. protected override Size MeasureOverride(Size availableSize) { - double childAvailableWidth = double.PositiveInfinity; - double childAvailableHeight = double.PositiveInfinity; + double childAvailableWidth = availableSize.Width; + double childAvailableHeight = availableSize.Height; if (Orientation == Orientation.Vertical) { diff --git a/src/Avalonia.Controls/Utils/ListUtils.cs b/src/Avalonia.Controls/Utils/ListUtils.cs new file mode 100644 index 0000000000..8599c53057 --- /dev/null +++ b/src/Avalonia.Controls/Utils/ListUtils.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Controls.Utils +{ + internal static class ListUtils + { + public static void Resize(this List list, int size, T value) + { + int cur = list.Count; + + if (size < cur) + { + list.RemoveRange(size, cur - size); + } + else if (size > cur) + { + if (size > list.Capacity) + { + list.Capacity = size; + } + + list.AddRange(Enumerable.Repeat(value, size - cur)); + } + } + + public static void Resize(this List list, int count) + { + Resize(list, count, default); + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 0d077d2a3a..ebaf62b2c0 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -547,7 +547,6 @@ namespace Avalonia.Rendering } } - System.Diagnostics.Debug.WriteLine("Invalidated " + rect); SceneInvalidated(this, new SceneInvalidatedEventArgs((IRenderRoot)_root, rect)); } } From 4850571a90d3de5f09738d7978a33fea76a4c570 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 31 May 2019 22:37:07 +0200 Subject: [PATCH 003/138] Fix quick scrolling. With a bit of a hack... --- src/Avalonia.Controls/Repeaters/ViewportManager.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Avalonia.Controls/Repeaters/ViewportManager.cs b/src/Avalonia.Controls/Repeaters/ViewportManager.cs index 5986c75579..cf69f9beb6 100644 --- a/src/Avalonia.Controls/Repeaters/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewportManager.cs @@ -188,7 +188,16 @@ namespace Avalonia.Controls.Repeaters if (!_managingViewportDisabled) { + // HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater - + // we can get this from TransformedBounds, but this property is updated after layout has + // run, resulting in the UI being updated too late when scrolling quickly. We can + // partially remedey this by triggering also on Bounds changes, but this won't work so + // well for nested ItemsRepeaters. + // + // UWP uses the EffectiveBoundsChanged event (which I think was implemented specially + // for this case): we need to implement that in Avalonia. _effectiveViewportChangedRevoker = _owner.GetObservable(Visual.TransformedBoundsProperty) + .Merge(_owner.GetObservable(Visual.BoundsProperty).Select(_ => _owner.TransformedBounds)) .Skip(1) .Subscribe(OnEffectiveViewportChanged); } From 20a49c713d1f6e729981774b9bc4a805bcc6c923 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 1 Jun 2019 00:06:36 +0200 Subject: [PATCH 004/138] Fix infinity checks. Misread `std::isfinite` in C++ code as `std::infinite`, ha. --- src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs | 2 +- src/Avalonia.Controls/Repeaters/StackLayout.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs index a0ced65076..f8da998201 100644 --- a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs @@ -337,7 +337,7 @@ namespace Avalonia.Controls.Repeaters // Does not fit, wrap to the previous row var availableSizeMinor = _orientation.Minor(availableSize); - _orientation.SetMinorStart(ref currentBounds, double.IsInfinity(availableSizeMinor) ? availableSizeMinor - _orientation.Minor(desiredSize) : 0); + _orientation.SetMinorStart(ref currentBounds, !double.IsInfinity(availableSizeMinor) ? availableSizeMinor - _orientation.Minor(desiredSize) : 0); _orientation.SetMajorStart(ref currentBounds, lineOffset - _orientation.Major(desiredSize) - lineSpacing); if (lineNeedsReposition) diff --git a/src/Avalonia.Controls/Repeaters/StackLayout.cs b/src/Avalonia.Controls/Repeaters/StackLayout.cs index 79ebcef645..379eab8675 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayout.cs +++ b/src/Avalonia.Controls/Repeaters/StackLayout.cs @@ -97,7 +97,7 @@ namespace Avalonia.Controls.Repeaters { var measureSizeMinor = _orientation.Minor(measureSize); return _orientation.MinorMajorSize( - double.IsInfinity(measureSizeMinor) ? + !double.IsInfinity(measureSizeMinor) ? Math.Max(measureSizeMinor, _orientation.Minor(desiredSize)) : _orientation.Minor(desiredSize), _orientation.Major(desiredSize)); From aef3a2c9bd6c66d4c1e69eda8bb7f46c837cca28 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 1 Jun 2019 16:20:38 +0200 Subject: [PATCH 005/138] Added UniformGridLayout. --- .../Repeaters/FlowLayoutAlgorithm.cs | 23 +- .../Repeaters/OrientationBasedMeasures.cs | 7 + .../Repeaters/StackLayout.cs | 13 +- .../Repeaters/UniformGridLayout.cs | 389 ++++++++++++++++++ .../Repeaters/UniformGridLayoutState.cs | 176 ++++++++ 5 files changed, 603 insertions(+), 5 deletions(-) create mode 100644 src/Avalonia.Controls/Repeaters/UniformGridLayout.cs create mode 100644 src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs diff --git a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs index f8da998201..6aaad0bdb5 100644 --- a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs @@ -656,7 +656,7 @@ namespace Avalonia.Controls.Repeaters } } - void SetLayoutOrigin() + private void SetLayoutOrigin() { if (IsVirtualizingContext) { @@ -670,6 +670,27 @@ namespace Avalonia.Controls.Repeaters } } + public IControl GetElementIfRealized(int dataIndex) + { + if (_elementManager.IsDataIndexRealized(dataIndex)) + { + return _elementManager.GetRealizedElement(dataIndex); + } + + return null; + } + + public bool TryAddElement0(IControl element) + { + if (_elementManager.GetRealizedElementCount() == 0) + { + _elementManager.Add(element, 0); + return true; + } + + return false; + } + public enum LineAlignment { Start, diff --git a/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs b/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs index 173b62842b..2f30f4c551 100644 --- a/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs +++ b/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs @@ -61,6 +61,13 @@ } } + public Rect MinorMajorRect(double minor, double major, double minorSize, double majorSize) + { + return ScrollOrientation == Orientation.Vertical ? + new Rect(minor, major, minorSize, majorSize) : + new Rect(major, minor, majorSize, minorSize); + } + public Point MinorMajorPoint(double minor, double major) { return ScrollOrientation == Orientation.Vertical ? diff --git a/src/Avalonia.Controls/Repeaters/StackLayout.cs b/src/Avalonia.Controls/Repeaters/StackLayout.cs index 379eab8675..db1900d03b 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayout.cs +++ b/src/Avalonia.Controls/Repeaters/StackLayout.cs @@ -5,14 +5,19 @@ namespace Avalonia.Controls.Repeaters { public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates { - public static readonly AvaloniaProperty OrientationProperty - = StackPanel.OrientationProperty.AddOwner(); + public static readonly StyledProperty OrientationProperty = + StackPanel.OrientationProperty.AddOwner(); - public static readonly AvaloniaProperty SpacingProperty - = StackPanel.SpacingProperty.AddOwner(); + public static readonly StyledProperty SpacingProperty = + StackPanel.SpacingProperty.AddOwner(); private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); + public StackLayout() + { + LayoutId = "StackLayout"; + } + public Orientation Orientation { get => GetValue(OrientationProperty); diff --git a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs b/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs new file mode 100644 index 0000000000..4d062e23ab --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs @@ -0,0 +1,389 @@ +using System; +using System.Collections.Specialized; + +namespace Avalonia.Controls.Repeaters +{ + public enum UniformGridLayoutItemsJustification + { + Start = 0, + Center = 1, + End = 2, + SpaceAround = 3, + SpaceBetween = 4, + SpaceEvenly = 5, + }; + + public enum UniformGridLayoutItemsStretch + { + None = 0, + Fill = 1, + Uniform = 2, + }; + + public class UniformGridLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates + { + public static readonly StyledProperty ItemsJustificationProperty = + AvaloniaProperty.Register(nameof(ItemsJustification)); + + public static readonly StyledProperty ItemsStretchProperty = + AvaloniaProperty.Register(nameof(ItemsStretch)); + + public static readonly StyledProperty MinColumnSpacingProperty = + AvaloniaProperty.Register(nameof(MinColumnSpacing)); + + public static readonly StyledProperty MinItemHeightProperty = + AvaloniaProperty.Register(nameof(MinItemHeight)); + + public static readonly StyledProperty MinItemWidthProperty = + AvaloniaProperty.Register(nameof(MinItemWidth)); + + public static readonly StyledProperty MinRowSpacingProperty = + AvaloniaProperty.Register(nameof(MinRowSpacing)); + + public static readonly StyledProperty OrientationProperty = + StackPanel.OrientationProperty.AddOwner(); + + private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); + private double _minItemWidth = double.NaN; + private double _minItemHeight = double.NaN; + private double _minRowSpacing; + private double _minColumnSpacing; + private UniformGridLayoutItemsJustification _itemsJustification; + private UniformGridLayoutItemsStretch _itemsStretch; + + public UniformGridLayout() + { + LayoutId = "UniformGridLayout"; + } + + static UniformGridLayout() + { + OrientationProperty.OverrideDefaultValue(Orientation.Horizontal); + } + + public UniformGridLayoutItemsJustification ItemsJustification + { + get => GetValue(ItemsJustificationProperty); + set => SetValue(ItemsJustificationProperty, value); + } + + public UniformGridLayoutItemsStretch ItemsStretch + { + get => GetValue(ItemsStretchProperty); + set => SetValue(ItemsStretchProperty, value); + } + + public double MinColumnSpacing + { + get => GetValue(MinColumnSpacingProperty); + set => SetValue(MinColumnSpacingProperty, value); + } + + public double MinItemHeight + { + get => GetValue(MinItemHeightProperty); + set => SetValue(MinItemHeightProperty, value); + } + + public double MinItemWidth + { + get => GetValue(MinItemWidthProperty); + set => SetValue(MinItemWidthProperty, value); + } + + public double MinRowSpacing + { + get => GetValue(MinRowSpacingProperty); + set => SetValue(MinRowSpacingProperty, value); + } + + public Orientation Orientation + { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + internal double LineSpacing => Orientation == Orientation.Horizontal ? _minRowSpacing : _minColumnSpacing; + internal double MinItemSpacing => Orientation == Orientation.Horizontal ? _minColumnSpacing : _minRowSpacing; + + Size IFlowLayoutAlgorithmDelegates.Algorithm_GetMeasureSize( + int index, + Size availableSize, + VirtualizingLayoutContext context) + { + var gridState = (UniformGridLayoutState)context.LayoutState; + return new Size(gridState.EffectiveItemWidth, gridState.EffectiveItemHeight); + } + + Size IFlowLayoutAlgorithmDelegates.Algorithm_GetProvisionalArrangeSize( + int index, + Size measureSize, + Size desiredSize, + VirtualizingLayoutContext context) + { + var gridState = (UniformGridLayoutState)context.LayoutState; + return new Size(gridState.EffectiveItemWidth, gridState.EffectiveItemHeight); + } + + bool IFlowLayoutAlgorithmDelegates.Algorithm_ShouldBreakLine(int index, double remainingSpace) => remainingSpace < 0; + + FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForRealizationRect( + Size availableSize, + VirtualizingLayoutContext context) + { + Rect bounds = new Rect(double.NaN, double.NaN, double.NaN, double.NaN); + int anchorIndex = -1; + + int itemsCount = context.ItemCount; + var realizationRect = context.RealizationRect; + if (itemsCount > 0 && _orientation.MajorSize(realizationRect) > 0) + { + var gridState = (UniformGridLayoutState)context.LayoutState; + var lastExtent = gridState.FlowAlgorithm.LastExtent; + int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); + double majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context); + double realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); + if ((realizationWindowStartWithinExtent + _orientation.MajorSize(realizationRect)) >= 0 && realizationWindowStartWithinExtent <= majorSize) + { + double offset = Math.Max(0.0, _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent)); + int anchorRowIndex = (int)(offset / GetMajorSizeWithSpacing(context)); + + anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine)); + bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context); + } + } + + return new FlowLayoutAnchorInfo + { + Index = anchorIndex, + Offset = _orientation.MajorStart(bounds) + }; + } + + FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForTargetElement( + int targetIndex, + Size availableSize, + VirtualizingLayoutContext context) + { + int index = -1; + double offset = double.NaN; + int count = context.ItemCount; + if (targetIndex >= 0 && targetIndex < count) + { + int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); + int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine; + index = indexOfFirstInLine; + var state = context.LayoutState as UniformGridLayoutState; + offset = _orientation.MajorStart(GetLayoutRectForDataIndex(availableSize, indexOfFirstInLine, state.FlowAlgorithm.LastExtent, context)); + } + + return new FlowLayoutAnchorInfo + { + Index = index, + Offset = offset + }; + } + + Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent( + Size availableSize, + VirtualizingLayoutContext context, + IControl firstRealized, + int firstRealizedItemIndex, + Rect firstRealizedLayoutBounds, + IControl lastRealized, + int lastRealizedItemIndex, + Rect lastRealizedLayoutBounds) + { + var extent = new Rect(); + + + // Constants + int itemsCount = context.ItemCount; + double availableSizeMinor = _orientation.Minor(availableSize); + int itemsPerLine = Math.Max(1, !double.IsInfinity(availableSizeMinor) ? + (int)(availableSizeMinor / GetMinorSizeWithSpacing(context)) : itemsCount); + double lineSize = GetMajorSizeWithSpacing(context); + + if (itemsCount > 0) + { + _orientation.SetMinorSize( + ref extent, + !double.IsInfinity(availableSizeMinor) ? + availableSizeMinor : + Math.Max(0.0, itemsCount * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing)); + _orientation.SetMajorSize( + ref extent, + Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing)); + + if (firstRealized != null) + { + ////MUX_ASSERT(lastRealized); + + _orientation.SetMajorStart( + ref extent, + _orientation.MajorStart(firstRealizedLayoutBounds) - (firstRealizedItemIndex / itemsPerLine) * lineSize); + int remainingItems = itemsCount - lastRealizedItemIndex - 1; + _orientation.SetMajorSize( + ref extent, + _orientation.MajorEnd(lastRealizedLayoutBounds) - _orientation.MajorStart(extent) + (remainingItems / itemsPerLine) * lineSize); + } + } + + return extent; + } + + void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(IControl element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context) + { + } + + void IFlowLayoutAlgorithmDelegates.Algorithm_OnLineArranged(int startIndex, int countInLine, double lineSize, VirtualizingLayoutContext context) + { + } + + protected override void InitializeForContextCore(VirtualizingLayoutContext context) + { + var state = context.LayoutState; + var gridState = state as UniformGridLayoutState; + + if (gridState == null) + { + if (state != null) + { + throw new InvalidOperationException("LayoutState must derive from UniformGridLayoutState."); + } + + // Custom deriving layouts could potentially be stateful. + // If that is the case, we will just create the base state required by UniformGridLayout ourselves. + gridState = new UniformGridLayoutState(); + } + + gridState.InitializeForContext(context, this); + } + + protected override void UninitializeForContextCore(VirtualizingLayoutContext context) + { + var gridState = (UniformGridLayoutState)context.LayoutState; + gridState.UninitializeForContext(context); + } + + protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) + { + // Set the width and height on the grid state. If the user already set them then use the preset. + // If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items. + var gridState = (UniformGridLayoutState)context.LayoutState; + gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing); + + var desiredSize = GetFlowAlgorithm(context).Measure( + availableSize, + context, + true, + MinItemSpacing, + LineSpacing, + _orientation.ScrollOrientation, + LayoutId); + + return new Size(desiredSize.Width, desiredSize.Height); + } + + protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) + { + var value = GetFlowAlgorithm(context).Arrange( + finalSize, + context, + (FlowLayoutAlgorithm.LineAlignment)_itemsJustification, + LayoutId); + return new Size(value.Width, value.Height); + } + + protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args) + { + GetFlowAlgorithm(context).OnItemsSourceChanged(source, args, context); + // Always invalidate layout to keep the view accurate. + InvalidateLayout(); + + var gridState = (UniformGridLayoutState)context.LayoutState; + gridState.ClearElementOnDataSourceChange(context, args); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args) + { + if (args.Property == OrientationProperty) + { + var orientation = (Orientation)args.NewValue; + + //Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation. + //i.e. the properties are the inverse of each other. + var scrollOrientation = (orientation == Orientation.Horizontal) ? Orientation.Vertical : Orientation.Horizontal; + _orientation.ScrollOrientation = scrollOrientation; + } + else if (args.Property == MinColumnSpacingProperty) + { + _minColumnSpacing = (double)args.NewValue; + } + else if (args.Property == MinRowSpacingProperty) + { + _minRowSpacing = (double)args.NewValue; + } + else if (args.Property == ItemsJustificationProperty) + { + _itemsJustification = (UniformGridLayoutItemsJustification)args.NewValue; + } + else if (args.Property == ItemsStretchProperty) + { + _itemsStretch = (UniformGridLayoutItemsStretch)args.NewValue; + } + else if (args.Property == MinItemWidthProperty) + { + _minItemWidth = (double)args.NewValue; + } + else if (args.Property == MinItemHeightProperty) + { + _minItemHeight = (double)args.NewValue; + } + + InvalidateLayout(); + } + + private double GetMinorSizeWithSpacing(VirtualizingLayoutContext context) + { + var minItemSpacing = MinItemSpacing; + var gridState = (UniformGridLayoutState)context.LayoutState; + return _orientation.ScrollOrientation == Orientation.Vertical? + gridState.EffectiveItemWidth + minItemSpacing : + gridState.EffectiveItemHeight + minItemSpacing; + } + + private double GetMajorSizeWithSpacing(VirtualizingLayoutContext context) + { + var lineSpacing = LineSpacing; + var gridState = (UniformGridLayoutState)context.LayoutState; + return _orientation.ScrollOrientation == Orientation.Vertical ? + gridState.EffectiveItemHeight + lineSpacing : + gridState.EffectiveItemWidth + lineSpacing; + } + + Rect GetLayoutRectForDataIndex( + Size availableSize, + int index, + Rect lastExtent, + VirtualizingLayoutContext context) + { + int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); + int rowIndex = (int)(index / itemsPerLine); + int indexInRow = index - (rowIndex * itemsPerLine); + + var gridState = (UniformGridLayoutState)context.LayoutState; + Rect bounds = _orientation.MinorMajorRect( + indexInRow * GetMinorSizeWithSpacing(context) + _orientation.MinorStart(lastExtent), + rowIndex * GetMajorSizeWithSpacing(context) + _orientation.MajorStart(lastExtent), + _orientation.ScrollOrientation == Orientation.Vertical ? gridState.EffectiveItemWidth : gridState.EffectiveItemHeight, + _orientation.ScrollOrientation == Orientation.Vertical ? gridState.EffectiveItemHeight : gridState.EffectiveItemWidth); + + return bounds; + } + + private void InvalidateLayout() => InvalidateMeasure(); + + private FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((UniformGridLayoutState)context.LayoutState).FlowAlgorithm; + } +} diff --git a/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs b/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs new file mode 100644 index 0000000000..e62f64192d --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + public class UniformGridLayoutState + { + // We need to measure the element at index 0 to know what size to measure all other items. + // If FlowlayoutAlgorithm has already realized element 0 then we can use that. + // If it does not, then we need to do context.GetElement(0) at which point we have requested an element and are on point to clear it. + // If we are responsible for clearing element 0 we keep m_cachedFirstElement valid. + // If we are not (because FlowLayoutAlgorithm is holding it for us) then we just null out this field and use the one from FlowLayoutAlgorithm. + private IControl _cachedFirstElement; + + internal FlowLayoutAlgorithm FlowAlgorithm { get; } = new FlowLayoutAlgorithm(); + internal double EffectiveItemWidth { get; private set; } + internal double EffectiveItemHeight { get; private set; } + + internal void InitializeForContext(VirtualizingLayoutContext context, IFlowLayoutAlgorithmDelegates callbacks) + { + FlowAlgorithm.InitializeForContext(context, callbacks); + context.LayoutState = this; + } + + internal void UninitializeForContext(VirtualizingLayoutContext context) + { + FlowAlgorithm.UninitializeForContext(context); + + if (_cachedFirstElement != null) + { + context.RecycleElement(_cachedFirstElement); + } + } + + internal void EnsureElementSize( + Size availableSize, + VirtualizingLayoutContext context, + double layoutItemWidth, + double LayoutItemHeight, + UniformGridLayoutItemsStretch stretch, + Orientation orientation, + double minRowSpacing, + double minColumnSpacing) + { + if (context.ItemCount > 0) + { + // If the first element is realized we don't need to cache it or to get it from the context + var realizedElement = FlowAlgorithm.GetElementIfRealized(0); + if (realizedElement != null) + { + realizedElement.Measure(availableSize); + SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing); + _cachedFirstElement = null; + } + else + { + if (_cachedFirstElement == null) + { + // we only cache if we aren't realizing it + _cachedFirstElement = context.GetOrCreateElementAt( + 0, + ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); // expensive + } + + _cachedFirstElement.Measure(availableSize); + SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing); + + // See if we can move ownership to the flow algorithm. If we can, we do not need a local cache. + bool added = FlowAlgorithm.TryAddElement0(_cachedFirstElement); + if (added) + { + _cachedFirstElement = null; + } + } + } + } + + private void SetSize( + IControl element, + double layoutItemWidth, + double LayoutItemHeight, + Size availableSize, + UniformGridLayoutItemsStretch stretch, + Orientation orientation, + double minRowSpacing, + double minColumnSpacing) + { + EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth); + EffectiveItemHeight = (double.IsNaN(LayoutItemHeight) ? element.DesiredSize.Height : LayoutItemHeight); + + var availableSizeMinor = orientation == Orientation.Horizontal ? availableSize.Width : availableSize.Height; + var minorItemSpacing = orientation == Orientation.Vertical ? minRowSpacing : minColumnSpacing; + + var itemSizeMinor = orientation == Orientation.Horizontal ? EffectiveItemWidth : EffectiveItemHeight; + itemSizeMinor += minorItemSpacing; + + var numItemsPerColumn = (int)(Math.Max(1.0, availableSizeMinor / itemSizeMinor)); + var remainingSpace = ((int)availableSizeMinor) % ((int)itemSizeMinor); + var extraMinorPixelsForEachItem = remainingSpace / numItemsPerColumn; + + if (stretch == UniformGridLayoutItemsStretch.Fill) + { + if (orientation == Orientation.Horizontal) + { + EffectiveItemWidth += extraMinorPixelsForEachItem; + } + else + { + EffectiveItemHeight += extraMinorPixelsForEachItem; + } + } + else if (stretch == UniformGridLayoutItemsStretch.Uniform) + { + var itemSizeMajor = orientation == Orientation.Horizontal ? EffectiveItemHeight : EffectiveItemWidth; + var extraMajorPixelsForEachItem = itemSizeMajor * (extraMinorPixelsForEachItem / itemSizeMinor); + if (orientation == Orientation.Horizontal) + { + EffectiveItemWidth += extraMinorPixelsForEachItem; + EffectiveItemHeight += extraMajorPixelsForEachItem; + } + else + { + EffectiveItemHeight += extraMinorPixelsForEachItem; + EffectiveItemWidth += extraMajorPixelsForEachItem; + } + } + } + + internal void EnsureFirstElementOwnership() + { + if (FlowAlgorithm.GetElementIfRealized(0) != null) + { + _cachedFirstElement = null; + } + } + + internal void ClearElementOnDataSourceChange( + VirtualizingLayoutContext context, + NotifyCollectionChangedEventArgs args) + { + if (_cachedFirstElement != null) + { + bool shouldClear = false; + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + shouldClear = args.NewStartingIndex == 0; + break; + + case NotifyCollectionChangedAction.Replace: + shouldClear = args.NewStartingIndex == 0 || args.OldStartingIndex == 0; + break; + + case NotifyCollectionChangedAction.Remove: + shouldClear = args.OldStartingIndex == 0; + break; + + case NotifyCollectionChangedAction.Reset: + shouldClear = true; + break; + + case NotifyCollectionChangedAction.Move: + throw new NotImplementedException(); + } + + if (shouldClear) + { + context.RecycleElement(_cachedFirstElement); + _cachedFirstElement = null; + } + } + } + } +} From 89268e9df66da5fb6c71c805964a9a95717f8dd8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 12:21:28 +0200 Subject: [PATCH 006/138] Allow switching ItemsRepeater layout in ControlCatalog. --- .../Pages/ItemsRepeaterPage.xaml | 24 +++++++-- .../Pages/ItemsRepeaterPage.xaml.cs | 53 ++++++++++++++++++- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index a0cbefa4d9..dfe8be2cec 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -1,7 +1,25 @@ - - - + + + ItemsRepeater + A data-driven collection control that incorporates a flexible layout system, custom views, and virtualization. + + + + Stack - Vertical + Stack - Horizontal + UniformGrid - Vertical + UniformGrid - Horizontal + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index a6ca27cb67..dccb242a78 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -3,17 +3,23 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Repeaters; using Avalonia.Markup.Xaml; namespace ControlCatalog.Pages { public class ItemsRepeaterPage : UserControl { + private ItemsRepeater _repeater; + private ScrollViewer _scroller; + public ItemsRepeaterPage() { this.InitializeComponent(); - DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" ) - .ToArray(); + _repeater = this.FindControl("repeater"); + _scroller = this.FindControl("scroller"); + DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" ).ToArray(); } private void InitializeComponent() @@ -21,5 +27,48 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } + private void LayoutChanged(object sender, SelectionChangedEventArgs e) + { + if (_repeater == null) + { + return; + } + + var comboBox = (ComboBox)sender; + + switch (comboBox.SelectedIndex) + { + case 0: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + _repeater.Layout = new StackLayout { Orientation = Orientation.Vertical }; + break; + case 1: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + _repeater.Layout = new StackLayout { Orientation = Orientation.Horizontal }; + break; + case 2: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled; + _repeater.Layout = new UniformGridLayout + { + Orientation = Orientation.Vertical, + MinItemWidth = 200, + MinItemHeight = 200, + }; + break; + case 3: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + _repeater.Layout = new UniformGridLayout + { + Orientation = Orientation.Horizontal, + MinItemWidth = 200, + MinItemHeight = 200, + }; + break; + } + } } } From e09b1e4bea92da015954d3af2f3ccc7ff64f3b4b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 12:59:40 +0200 Subject: [PATCH 007/138] Removed the ElementFactory args stuff. We're just using `IDataTemplate` so it's not needed. --- .../Repeaters/ElementFactoryGetArgs.cs | 11 ------ .../Repeaters/ElementFactoryRecycleArgs.cs | 12 ------- .../Repeaters/ItemTemplateWrapper.cs | 11 +++--- .../Repeaters/ViewManager.cs | 35 ++----------------- 4 files changed, 7 insertions(+), 62 deletions(-) delete mode 100644 src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs delete mode 100644 src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs diff --git a/src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs b/src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs deleted file mode 100644 index 173388b68e..0000000000 --- a/src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Avalonia.Controls.Repeaters -{ - public sealed class ElementFactoryGetArgs : EventArgs - { - public object Data { get; set; } - public IControl Parent { get; set; } - internal int Index { get; set; } - } -} diff --git a/src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs b/src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs deleted file mode 100644 index 3025546f67..0000000000 --- a/src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Avalonia.Controls.Repeaters -{ - public sealed class ElementFactoryRecycleArgs : EventArgs - { - public IControl Element { get; set; } - public IControl Parent { get; set; } - } -} diff --git a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs index c26d73c02c..300fd11d1c 100644 --- a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs +++ b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs @@ -11,7 +11,7 @@ namespace Avalonia.Controls.Repeaters public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate; - public IControl GetElement(ElementFactoryGetArgs args) + public IControl GetElement(IControl parent, object data) { var selectedTemplate = _dataTemplate; var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); @@ -20,13 +20,13 @@ namespace Avalonia.Controls.Repeaters if (recyclePool != null) { // try to get an element from the recycle pool. - element = recyclePool.TryGetElement(string.Empty, args.Parent); + element = recyclePool.TryGetElement(string.Empty, parent); } if (element == null) { // no element was found in recycle pool, create a new element - element = selectedTemplate.Build(args.Data); + element = selectedTemplate.Build(data); // Associate template with element element.SetValue(RecyclePool.OriginTemplateProperty, selectedTemplate); @@ -35,9 +35,8 @@ namespace Avalonia.Controls.Repeaters return element; } - public void RecycleElement(ElementFactoryRecycleArgs args) + public void RecycleElement(IControl parent, IControl element) { - var element = args.Element; var selectedTemplate = _dataTemplate; var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); if (recyclePool == null) @@ -47,7 +46,7 @@ namespace Avalonia.Controls.Repeaters RecyclePool.SetPoolInstance(selectedTemplate, recyclePool); } - recyclePool.PutElement(args.Element, "" /* key */, args.Parent); + recyclePool.PutElement(element, "" /* key */, parent); } } } diff --git a/src/Avalonia.Controls/Repeaters/ViewManager.cs b/src/Avalonia.Controls/Repeaters/ViewManager.cs index d7f8f224b8..5fccba5412 100644 --- a/src/Avalonia.Controls/Repeaters/ViewManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewManager.cs @@ -18,8 +18,6 @@ namespace Avalonia.Controls.Repeaters private readonly UniqueIdElementPool _resetPool; private IControl _lastFocusedElement; private bool _isDataSourceStableResetPending; - private ElementFactoryGetArgs _elementFactoryGetArgs; - private ElementFactoryRecycleArgs _elementFactoryRecycleArgs; private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault; private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault; private bool _eventsSubscribed; @@ -107,21 +105,7 @@ namespace Avalonia.Controls.Repeaters var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); var clearedIndex = virtInfo.Index; _owner.OnElementClearing(element); - - if (_elementFactoryRecycleArgs == null) - { - // Create one. - _elementFactoryRecycleArgs = new ElementFactoryRecycleArgs(); - } - - var context = _elementFactoryRecycleArgs; - context.Element = element; - context.Parent = _owner; - - _owner.ItemTemplateShim.RecycleElement(context); - - context.Element = null; - context.Parent = null; + _owner.ItemTemplateShim.RecycleElement(_owner, element); virtInfo.MoveOwnershipToElementFactory(); //_phaser.StopPhasing(element, virtInfo); @@ -579,22 +563,7 @@ namespace Avalonia.Controls.Repeaters } var data = _owner.ItemsSourceView.GetAt(index); - - if (_elementFactoryGetArgs == null) - { - // Create one. - _elementFactoryGetArgs = new ElementFactoryGetArgs(); - } - - var args = _elementFactoryGetArgs; - args.Data = data; - args.Parent = _owner; - args.Index= index; - - var element = itemTemplateFactory.GetElement(args); - - args.Data = null; - args.Parent = null; + var element = itemTemplateFactory.GetElement(_owner, data); var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); if (virtInfo == null) From 66c813c420b330d135a6c08598ff0556201bcc1e Mon Sep 17 00:00:00 2001 From: wieslawsoltes Date: Tue, 25 Jun 2019 13:33:53 +0200 Subject: [PATCH 008/138] Initial port of WPF StackPanel --- src/Avalonia.Controls/StackPanel.cs | 148 ++++++++++++++++------------ 1 file changed, 83 insertions(+), 65 deletions(-) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index c29faa1b4d..59c3c33942 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -1,10 +1,10 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. +// This source file is adapted from the Windows Presentation Foundation project. +// (https://github.com/dotnet/wpf/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; -using System.Linq; using Avalonia.Input; -using Avalonia.Layout; namespace Avalonia.Controls { @@ -155,106 +155,124 @@ namespace Avalonia.Controls } /// - /// Measures the control. + /// General StackPanel layout behavior is to grow unbounded in the "stacking" direction (Size To Content). + /// Children in this dimension are encouraged to be as large as they like. In the other dimension, + /// StackPanel will assume the maximum size of its children. /// - /// The available size. - /// The desired size of the control. + /// Constraint + /// Desired size protected override Size MeasureOverride(Size availableSize) { - double childAvailableWidth = double.PositiveInfinity; - double childAvailableHeight = double.PositiveInfinity; + Size stackDesiredSize = new Size(); + var children = Children; + Size layoutSlotSize = availableSize; + bool fHorizontal = (Orientation == Orientation.Horizontal); + double spacing = Spacing; + bool hasVisibleChild = false; - if (Orientation == Orientation.Vertical) + // + // Initialize child sizing and iterator data + // Allow children as much size as they want along the stack. + // + if (fHorizontal) { - childAvailableWidth = availableSize.Width; - - if (!double.IsNaN(Width)) - { - childAvailableWidth = Width; - } - - childAvailableWidth = Math.Min(childAvailableWidth, MaxWidth); - childAvailableWidth = Math.Max(childAvailableWidth, MinWidth); + layoutSlotSize = layoutSlotSize.WithWidth(Double.PositiveInfinity); } else { - childAvailableHeight = availableSize.Height; + layoutSlotSize = layoutSlotSize.WithHeight(Double.PositiveInfinity); + } - if (!double.IsNaN(Height)) - { - childAvailableHeight = Height; - } + // + // Iterate through children. + // While we still supported virtualization, this was hidden in a child iterator (see source history). + // + for (int i = 0, count = children.Count; i < count; ++i) + { + // Get next child. + var child = children[i]; - childAvailableHeight = Math.Min(childAvailableHeight, MaxHeight); - childAvailableHeight = Math.Max(childAvailableHeight, MinHeight); - } + if (child == null) + { continue; } - double measuredWidth = 0; - double measuredHeight = 0; - double spacing = Spacing; - bool hasVisibleChild = Children.Any(c => c.IsVisible); + bool isVisible = child.IsVisible; - foreach (Control child in Children) - { - child.Measure(new Size(childAvailableWidth, childAvailableHeight)); - Size size = child.DesiredSize; + if (isVisible && !hasVisibleChild) + { + hasVisibleChild = true; + } - if (Orientation == Orientation.Vertical) + // Measure the child. + child.Measure(layoutSlotSize); + Size childDesiredSize = child.DesiredSize; + + // Accumulate child size. + if (fHorizontal) { - measuredHeight += size.Height + (child.IsVisible ? spacing : 0); - measuredWidth = Math.Max(measuredWidth, size.Width); + stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width + (isVisible ? spacing : 0) + childDesiredSize.Width); + stackDesiredSize = stackDesiredSize.WithHeight(Math.Max(stackDesiredSize.Height, childDesiredSize.Height)); } else { - measuredWidth += size.Width + (child.IsVisible ? spacing : 0); - measuredHeight = Math.Max(measuredHeight, size.Height); + stackDesiredSize = stackDesiredSize.WithWidth(Math.Max(stackDesiredSize.Width, childDesiredSize.Width)); + stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height + (isVisible ? spacing : 0) + childDesiredSize.Height); } } - if (Orientation == Orientation.Vertical) + if (fHorizontal) { - measuredHeight -= (hasVisibleChild ? spacing : 0); + stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width - (hasVisibleChild ? spacing : 0)); } else - { - measuredWidth -= (hasVisibleChild ? spacing : 0); + { + stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height - (hasVisibleChild ? spacing : 0)); } - return new Size(measuredWidth, measuredHeight).Constrain(availableSize); + // TODO: In WPF `.Constrain(availableSize)` is not used. + //return stackDesiredSize; + return stackDesiredSize.Constrain(availableSize); } - /// + /// + /// Content arrangement. + /// + /// Arrange size protected override Size ArrangeOverride(Size finalSize) { - var orientation = Orientation; + var children = Children; + bool fHorizontal = (Orientation == Orientation.Horizontal); + Rect rcChild = new Rect(finalSize); + double previousChildSize = 0.0; var spacing = Spacing; - var finalRect = new Rect(finalSize); - var pos = 0.0; - foreach (Control child in Children) + // + // Arrange and Position Children. + // + for (int i = 0, count = children.Count; i < count; ++i) { - if (!child.IsVisible) - { - continue; - } + var child = children[i]; - double childWidth = child.DesiredSize.Width; - double childHeight = child.DesiredSize.Height; + if (child == null) + { continue; } - if (orientation == Orientation.Vertical) + if (fHorizontal) { - var rect = new Rect(0, pos, childWidth, childHeight) - .Align(finalRect, child.HorizontalAlignment, VerticalAlignment.Top); - ArrangeChild(child, rect, finalSize, orientation); - pos += childHeight + spacing; + rcChild = rcChild.WithX(rcChild.X + previousChildSize); + previousChildSize = child.DesiredSize.Width; + rcChild = rcChild.WithWidth(previousChildSize); + rcChild = rcChild.WithHeight(Math.Max(finalSize.Height, child.DesiredSize.Height)); + previousChildSize += spacing; } else { - var rect = new Rect(pos, 0, childWidth, childHeight) - .Align(finalRect, HorizontalAlignment.Left, child.VerticalAlignment); - ArrangeChild(child, rect, finalSize, orientation); - pos += childWidth + spacing; + rcChild = rcChild.WithY(rcChild.Y + previousChildSize); + previousChildSize = child.DesiredSize.Height; + rcChild = rcChild.WithHeight(previousChildSize); + rcChild = rcChild.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width)); + previousChildSize += spacing; } + + child.Arrange(rcChild); } return finalSize; From acfa8329672d23db92b8e9f6e7afc54cdbc77ad9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 13:34:28 +0200 Subject: [PATCH 009/138] Try up code. Remove unused code and implement some focus stuff. --- .../Repeaters/FlowLayoutAlgorithm.cs | 8 -- .../Repeaters/ITrackerHandleManager.cs | 10 --- .../Repeaters/ItemsRepeater.cs | 13 --- .../Repeaters/NonVirtualizingLayout.cs | 21 +---- .../Repeaters/StackLayout.cs | 1 - .../Repeaters/UniformGridLayout.cs | 2 - .../Repeaters/UniqueIdElementPool.cs | 5 -- .../Repeaters/ViewManager.cs | 79 +++---------------- .../Repeaters/ViewportManager.cs | 49 ------------ .../Repeaters/VirtualizationInfo.cs | 8 -- 10 files changed, 14 insertions(+), 182 deletions(-) delete mode 100644 src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs diff --git a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs index 6aaad0bdb5..f605bf72c8 100644 --- a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs @@ -407,7 +407,6 @@ namespace Avalonia.Controls.Repeaters _elementManager.ClearRealizedRange(); // FlowLayout requires that the anchor is the first element in the row. var internalAnchor = _algorithmCallbacks.Algorithm_GetAnchorForTargetElement(index, availableSize, context); - //MUX_ASSERT(internalAnchor.Index <= index); // No need to set the position of the anchor. // (0,0) is fine for now since the extent can @@ -505,7 +504,6 @@ namespace Avalonia.Controls.Repeaters int realizedElementCount = _elementManager.GetRealizedElementCount(); if (realizedElementCount > 0) { - //MUX_ASSERT(_firstRealizedDataIndexInsideRealizationWindow != -1 && _lastRealizedDataIndexInsideRealizationWindow != -1); int countInLine = 0; var previousElementBounds = _elementManager.GetLayoutBoundsForDataIndex(_firstRealizedDataIndexInsideRealizationWindow); var currentLineOffset = _orientation.MajorStart(previousElementBounds); @@ -662,12 +660,6 @@ namespace Avalonia.Controls.Repeaters { _context.LayoutOrigin = new Point(_lastExtent.X, _lastExtent.Y); } - else - { - // Should have 0 origin for non-virtualizing layout since we always start from - // the first item - //MUX_ASSERT(m_lastExtent.X == 0 && m_lastExtent.Y == 0); - } } public IControl GetElementIfRealized(int dataIndex) diff --git a/src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs b/src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs deleted file mode 100644 index 55ce6c28ca..0000000000 --- a/src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Avalonia.Controls.Repeaters -{ - internal interface ITrackerHandleManager - { - } -} diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs index 9d71685b11..b0e9842d57 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs @@ -243,19 +243,11 @@ namespace Avalonia.Controls.Repeaters else { var newBounds = element.Bounds; - - //if (virtInfo.ArrangeBounds != ItemsRepeater.InvalidRect && - // newBounds != virtInfo.ArrangeBounds) - //{ - // _animationManager.OnElementBoundsChanged(element, virtInfo.ArrangeBounds, newBounds); - //} - virtInfo.ArrangeBounds = newBounds; } } _viewportManager.OnOwnerArranged(); - //_animationManager.OnOwnerArranged(); return arrangeSize; } @@ -299,10 +291,6 @@ namespace Avalonia.Controls.Repeaters { OnLayoutChanged((Layout)args.OldValue, (Layout)args.NewValue); } - //else if (property == AnimatorProperty) - //{ - // OnAnimatorChanged((ElementAnimator)args.OldValue, (ElementAnimator)args.NewValue); - //} else if (property == HorizontalCacheLengthProperty) { _viewportManager.HorizontalCacheLength = (double)args.NewValue; @@ -592,7 +580,6 @@ namespace Avalonia.Controls.Repeaters try { - //_animationManager.OnItemsSourceChanged(sender, args); _viewManager.OnItemsSourceChanged(sender, args); if (Layout != null) diff --git a/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs b/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs index c5ab27fabb..fb8d329d7f 100644 --- a/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs +++ b/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs @@ -4,26 +4,7 @@ using System.Text; namespace Avalonia.Controls.Repeaters { - public class NonVirtualizingLayout : Layout + public abstract class NonVirtualizingLayout : Layout { - public override Size Arrange(LayoutContext context, Size finalSize) - { - throw new NotImplementedException(); - } - - public override void InitializeForContext(LayoutContext context) - { - throw new NotImplementedException(); - } - - public override Size Measure(LayoutContext context, Size availableSize) - { - throw new NotImplementedException(); - } - - public override void UninitializeForContext(LayoutContext context) - { - throw new NotImplementedException(); - } } } diff --git a/src/Avalonia.Controls/Repeaters/StackLayout.cs b/src/Avalonia.Controls/Repeaters/StackLayout.cs index db1900d03b..66b0be5b92 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayout.cs +++ b/src/Avalonia.Controls/Repeaters/StackLayout.cs @@ -53,7 +53,6 @@ namespace Avalonia.Controls.Repeaters { if (firstRealized != null) { - //MUX_ASSERT(lastRealized); _orientation.SetMajorStart( ref extent, _orientation.MajorStart(firstRealizedLayoutBounds) - firstRealizedItemIndex * averageElementSize); diff --git a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs b/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs index 4d062e23ab..624e477354 100644 --- a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs +++ b/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs @@ -217,8 +217,6 @@ namespace Avalonia.Controls.Repeaters if (firstRealized != null) { - ////MUX_ASSERT(lastRealized); - _orientation.SetMajorStart( ref extent, _orientation.MajorStart(firstRealizedLayoutBounds) - (firstRealizedItemIndex / itemsPerLine) * lineSize); diff --git a/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs b/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs index 4b3922b9c3..3ad883c04b 100644 --- a/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs +++ b/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs @@ -14,8 +14,6 @@ namespace Avalonia.Controls.Repeaters public void Add(IControl element) { - //MUX_ASSERT(_owner.ItemsSourceView.HasKeyIndexMapping); - var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); var key = virtInfo.UniqueId; @@ -29,8 +27,6 @@ namespace Avalonia.Controls.Repeaters public IControl Remove(int index) { - //MUX_ASSERT(_owner.ItemsSourceView.HasKeyIndexMapping); - // Check if there is already a element in the mapping and if so, use it. string key = _owner.ItemsSourceView.KeyFromIndex(index); @@ -44,7 +40,6 @@ namespace Avalonia.Controls.Repeaters public void Clear() { - //MUX_ASSERT(_owner.ItemsSourceView.HasKeyIndexMapping); _elementMap.Clear(); } diff --git a/src/Avalonia.Controls/Repeaters/ViewManager.cs b/src/Avalonia.Controls/Repeaters/ViewManager.cs index 5fccba5412..1167705b5b 100644 --- a/src/Avalonia.Controls/Repeaters/ViewManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewManager.cs @@ -108,7 +108,7 @@ namespace Avalonia.Controls.Repeaters _owner.ItemTemplateShim.RecycleElement(_owner, element); virtInfo.MoveOwnershipToElementFactory(); - //_phaser.StopPhasing(element, virtInfo); + if (_lastFocusedElement == element) { // Focused element is going away. Remove the tracked last focused element @@ -125,13 +125,9 @@ namespace Avalonia.Controls.Repeaters var focusCandidate = FindFocusCandidate(clearedIndex, focusedChild); if (focusCandidate != null) { - //var focusState = _lastFocusedElement?.FocusState ?? FocusState.Programmatic; - - // If the last focused element has focus, use its focus state, if not use programmatic. - //focusState = focusState == FocusState.Unfocused ? FocusState.Programmatic : focusState; focusCandidate.Focus(); - _lastFocusedElement = focusedChild; + // Add pin to hold the focused element. UpdatePin(focusedChild, true /* addPin */); } @@ -186,12 +182,12 @@ namespace Avalonia.Controls.Repeaters focusCandidate = nextElement as IControl; if (focusCandidate != null) { - ////var firstFocus = FocusManager.FindFirstFocusableElement(nextElement); + var firstFocus = KeyboardNavigationHandler.GetNext(nextElement, NavigationDirection.First); - ////if (firstFocus != null) - ////{ - //// focusCandidate = firstFocus as IControl; - ////} + if (firstFocus != null) + { + focusCandidate = firstFocus as IControl; + } } } @@ -200,12 +196,12 @@ namespace Avalonia.Controls.Repeaters focusCandidate = previousElement as IControl; if (previousElement != null) { - ////var lastFocus = FocusManager.FindLastFocusableElement(previousElement); + var lastFocus = KeyboardNavigationHandler.GetNext(previousElement, NavigationDirection.Last); - ////if (lastFocus != null) - ////{ - //// focusCandidate = lastFocus as IControl; - ////} + if (lastFocus != null) + { + focusCandidate = lastFocus as IControl; + } } } @@ -233,8 +229,6 @@ namespace Avalonia.Controls.Repeaters var elementInfo = _pinnedPool[i]; var virtInfo = elementInfo.VirtualizationInfo; - //MUX_ASSERT(virtInfo.Owner() == ElementOwner.PinnedPool); - if (!virtInfo.IsPinned) { _pinnedPool.RemoveAt(i); @@ -471,16 +465,10 @@ namespace Avalonia.Controls.Repeaters IControl element = null; bool cachedFirstLastIndicesInvalid = _firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault; - //MUX_ASSERT(!cachedFirstLastIndicesInvalid || m_lastRealizedElementIndexHeldByLayout == LastRealizedElementIndexDefault); - bool isRequestedIndexInRealizedRange = (_firstRealizedElementIndexHeldByLayout <= index && index <= _lastRealizedElementIndexHeldByLayout); if (cachedFirstLastIndicesInvalid || isRequestedIndexInRealizedRange) { - // Both First and Last indices need to be valid or default. - //MUX_ASSERT((m_firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault && m_lastRealizedElementIndexHeldByLayout == LastRealizedElementIndexDefault) || - // (m_firstRealizedElementIndexHeldByLayout != FirstRealizedElementIndexDefault && m_lastRealizedElementIndexHeldByLayout != LastRealizedElementIndexDefault)); - foreach (var child in _owner.Children) { var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child); @@ -572,27 +560,7 @@ namespace Avalonia.Controls.Repeaters } // Prepare the element - // If we are phasing, run phase 0 before setting DataContext. If phase 0 is not - // run before setting DataContext, when setting DataContext all the phases will be - // run in the OnDataContextChanged handler in code generated by the xaml compiler (code-gen). - var extension = false; ////CachedVisualTreeHelpers.GetDataTemplateComponent(element); - if (extension) - { - ////// Clear out old data. - ////extension.Recycle(); - ////int nextPhase = VirtualizationInfo.PhaseReachedEnd; - ////// Run Phase 0 - ////extension.ProcessBindings(data, index, 0 /* currentPhase */, nextPhase); - - ////// Setup phasing information, so that Phaser can pick up any pending phases left. - ////// Update phase on virtInfo. Set data and templateComponent only if x:Phase was used. - ////virtInfo.UpdatePhasingInfo(nextPhase, nextPhase > 0 ? data : null, nextPhase > 0 ? extension : null); - } - else - { - // Set data context only if no x:Bind was used. ie. No data template component on the root. - element.DataContext = data; - } + element.DataContext = data; virtInfo.MoveOwnershipToLayoutFromElementFactory( index, @@ -613,9 +581,7 @@ namespace Avalonia.Controls.Repeaters children.Add(element); } - ////repeater.AnimationManager.OnElementPrepared(element); repeater.OnElementPrepared(element, index); - ////_phaser.PhaseElement(element, virtInfo); // Update realized indices _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index); @@ -635,25 +601,6 @@ namespace Avalonia.Controls.Repeaters return _isDataSourceStableResetPending; } - private bool ClearElementToAnimator(IControl element, VirtualizationInfo virtInfo) - { - return false; - ////bool cleared = _owner.AnimationManager.ClearElement(element); - ////if (cleared) - ////{ - //// int clearedIndex = virtInfo.Index; - //// virtInfo.MoveOwnershipToAnimator(); - //// if (_lastFocusedElement == element) - //// { - //// // Focused element is going away. Remove the tracked last focused element - //// // and pick a reasonable next focus if we can find one within the layout - //// // realized elements. - //// MoveFocusFromClearedIndex(clearedIndex); - //// } - ////} - ////return cleared; - } - private bool ClearElementToPinnedPool(IControl element, VirtualizationInfo virtInfo, bool isClearedDueToCollectionChange) { if (_isDataSourceStableResetPending) diff --git a/src/Avalonia.Controls/Repeaters/ViewportManager.cs b/src/Avalonia.Controls/Repeaters/ViewportManager.cs index cf69f9beb6..238870644f 100644 --- a/src/Avalonia.Controls/Repeaters/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewportManager.cs @@ -62,7 +62,6 @@ namespace Avalonia.Controls.Repeaters { ValidateCacheLength(value); _maximumHorizontalCacheLength = value; - ResetCacheBuffer(); } } } @@ -76,7 +75,6 @@ namespace Avalonia.Controls.Repeaters { ValidateCacheLength(value); _maximumVerticalCacheLength = value; - ResetCacheBuffer(); } } } @@ -182,7 +180,6 @@ namespace Avalonia.Controls.Repeaters _expectedViewportShift = default; _pendingViewportShift = default; _unshiftableShift = default; - ResetCacheBuffer(); _effectiveViewportChangedRevoker?.Dispose(); @@ -250,10 +247,6 @@ namespace Avalonia.Controls.Repeaters _horizontalCacheBufferPerSide = Math.Min(_horizontalCacheBufferPerSide, maximumHorizontalCacheBufferPerSide); _verticalCacheBufferPerSide = Math.Min(_verticalCacheBufferPerSide, maximumVerticalCacheBufferPerSide); - - // Since we grow the cache buffer at the end of the arrange pass, - // we need to register work even if we just reached cache potential. - RegisterCacheBuildWork(); } } } @@ -294,14 +287,6 @@ namespace Avalonia.Controls.Repeaters { if (!_managingViewportDisabled) { - // We do not animate bring-into-view operations where the anchor is disconnected because - // it doesn't look good (the blank space is obvious because the layout can't keep track - // of two realized ranges while the animation is going on). - if (_isAnchorOutsideRealizedRange) - { - ////args.AnimationDesired(false); - } - // During the time between a bring into view request and the element coming into view we do not // want the anchor provider to pick some anchor and jump to it. Instead we want to anchor on the // element that is being brought into view. We can do this by making just that element as a potential @@ -389,8 +374,6 @@ namespace Avalonia.Controls.Repeaters private void UpdateViewport(Rect viewport) { - //assert(!m_managingViewportDisabled); - var previousVisibleWindow = _visibleWindow; var currentVisibleWindow = viewport; if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X && @@ -407,18 +390,6 @@ namespace Avalonia.Controls.Repeaters TryInvalidateMeasure(); } - private void ResetCacheBuffer() - { - _horizontalCacheBufferPerSide = 0.0; - _verticalCacheBufferPerSide = 0.0; - - if (!_managingViewportDisabled) - { - // We need to start building the realization buffer again. - RegisterCacheBuildWork(); - } - } - private static void ValidateCacheLength(double cacheLength) { if (cacheLength < 0.0 || double.IsInfinity(cacheLength) || double.IsNaN(cacheLength)) @@ -427,26 +398,6 @@ namespace Avalonia.Controls.Repeaters } } - private void RegisterCacheBuildWork() - { - ////assert(!m_managingViewportDisabled); - if (_owner.Layout != null && - _cacheBuildAction == null) - { - // We capture 'owner' (a strong refernce on ItemsRepeater) to make sure ItemsRepeater is still around - // when the async action completes. By protecting ItemsRepeater, we also ensure that this instance - // of ViewportManager (referenced by 'this' pointer) is valid because the lifetime of ItemsRepeater - // and ViewportManager is the same (see ItemsRepeater::m_viewportManager). - // We can't simply hold a strong reference on ViewportManager because it's not a COM object. - ////auto strongOwner = m_owner->get_strong(); - ////m_cacheBuildAction.set( - //// m_owner->Dispatcher().RunIdleAsync([this, strongOwner](const winrt::IdleDispatchedHandlerArgs&) - ////{ - //// OnCacheBuildActionCompleted(); - ////})); - } - } - private void TryInvalidateMeasure() { // Don't invalidate measure if we have an invalid window. diff --git a/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs index 5c6554be81..dddf00efd0 100644 --- a/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs +++ b/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs @@ -37,7 +37,6 @@ namespace Avalonia.Controls.Repeaters public void MoveOwnershipToLayoutFromElementFactory(int index, string uniqueId) { - //MUX_ASSERT(_owner == ElementOwner.ElementFactory); Owner = ElementOwner.Layout; Index = index; UniqueId = uniqueId; @@ -45,20 +44,16 @@ namespace Avalonia.Controls.Repeaters public void MoveOwnershipToLayoutFromUniqueIdResetPool() { - //MUX_ASSERT(_owner == ElementOwner.UniqueIdResetPool); Owner = ElementOwner.Layout; } public void MoveOwnershipToLayoutFromPinnedPool() { - //MUX_ASSERT(_owner == ElementOwner.PinnedPool); - //MUX_ASSERT(IsPinned()); Owner = ElementOwner.Layout; } public void MoveOwnershipToElementFactory() { - //MUX_ASSERT(_owner != ElementOwner.ElementFactory); Owner = ElementOwner.ElementFactory; _pinCounter = 0; Index = -1; @@ -68,7 +63,6 @@ namespace Avalonia.Controls.Repeaters public void MoveOwnershipToUniqueIdResetPoolFromLayout() { - //MUX_ASSERT(_owner == ElementOwner.Layout); Owner = ElementOwner.UniqueIdResetPool; // Keep the pinCounter the same. If the container survives the reset // it can go on being pinned as if nothing happened. @@ -79,7 +73,6 @@ namespace Avalonia.Controls.Repeaters // During a unique id reset, some elements might get removed. // Their ownership will go from the UniqueIdResetPool to the Animator. // The common path though is for ownership to go from Layout to Animator. - //MUX_ASSERT(_owner == ElementOwner.Layout || _owner == ElementOwner.UniqueIdResetPool); Owner = ElementOwner.Animator; Index = -1; _pinCounter = 0; @@ -87,7 +80,6 @@ namespace Avalonia.Controls.Repeaters public void MoveOwnershipToPinnedPool() { - //MUX_ASSERT(_owner == ElementOwner.Layout); Owner = ElementOwner.PinnedPool; } From e8a118e11c1fc6cfd031c547af4967199c1f8e76 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 13:58:04 +0200 Subject: [PATCH 010/138] Added headers. --- src/Avalonia.Controls/Repeaters/ElementManager.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs | 7 ++++++- .../Repeaters/IFlowLayoutAlgorithmDelegates.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/ItemsRepeater.cs | 7 ++++++- .../Repeaters/ItemsRepeaterElementClearingEventArgs.cs | 7 ++++++- .../Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs | 7 ++++++- .../Repeaters/ItemsRepeaterElementPreparedEventArgs.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/ItemsSourceView.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/Layout.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/LayoutContext.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs | 7 ++++++- .../Repeaters/OrientationBasedMeasures.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/RecyclePool.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/StackLayout.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/StackLayoutState.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/UniformGridLayout.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/ViewManager.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/ViewportManager.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs | 7 ++++++- 24 files changed, 144 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Controls/Repeaters/ElementManager.cs b/src/Avalonia.Controls/Repeaters/ElementManager.cs index 2ce0ad24fa..871e0527cd 100644 --- a/src/Avalonia.Controls/Repeaters/ElementManager.cs +++ b/src/Avalonia.Controls/Repeaters/ElementManager.cs @@ -1,4 +1,9 @@ -using System; +// 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 System.Text; diff --git a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs index f605bf72c8..27eef3f35e 100644 --- a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs @@ -1,4 +1,9 @@ -using System; +// 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.Specialized; namespace Avalonia.Controls.Repeaters diff --git a/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs b/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs index e27f00540c..04d715ea59 100644 --- a/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs +++ b/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs @@ -1,4 +1,9 @@ -using System; +// 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.Text; diff --git a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs index 300fd11d1c..86d452742c 100644 --- a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs +++ b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs @@ -1,4 +1,9 @@ -using System; +// 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.Text; using Avalonia.Controls.Templates; diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs index b0e9842d57..f5c832a9e8 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs @@ -1,4 +1,9 @@ -using System; +// 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; using System.Collections.Specialized; using Avalonia.Controls.Templates; diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs index 20e62bc3b8..a6984a3e72 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs @@ -1,4 +1,9 @@ -using System; +// 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; namespace Avalonia.Controls.Repeaters { diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs index c81c997bd4..a4457a2b06 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs @@ -1,4 +1,9 @@ -using System; +// 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; namespace Avalonia.Controls.Repeaters { diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs index 3180058fae..12abfa155e 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs @@ -1,4 +1,9 @@ -using System; +// 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.Text; diff --git a/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs b/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs index 0d21af0975..f57db0ec07 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs @@ -1,4 +1,9 @@ -using System; +// 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; using System.Collections.Generic; using System.Collections.Specialized; diff --git a/src/Avalonia.Controls/Repeaters/Layout.cs b/src/Avalonia.Controls/Repeaters/Layout.cs index a03e881616..d5f5b215c1 100644 --- a/src/Avalonia.Controls/Repeaters/Layout.cs +++ b/src/Avalonia.Controls/Repeaters/Layout.cs @@ -1,4 +1,9 @@ -using System; +// 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; namespace Avalonia.Controls.Repeaters { diff --git a/src/Avalonia.Controls/Repeaters/LayoutContext.cs b/src/Avalonia.Controls/Repeaters/LayoutContext.cs index ce462f6a16..75ee815fc0 100644 --- a/src/Avalonia.Controls/Repeaters/LayoutContext.cs +++ b/src/Avalonia.Controls/Repeaters/LayoutContext.cs @@ -1,4 +1,9 @@ -using System; +// 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.Text; diff --git a/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs b/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs index fb8d329d7f..95a66834a0 100644 --- a/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs +++ b/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs @@ -1,4 +1,9 @@ -using System; +// 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.Text; diff --git a/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs b/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs index 2f30f4c551..6f133e6286 100644 --- a/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs +++ b/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs @@ -1,4 +1,9 @@ -namespace Avalonia.Controls.Repeaters +// 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. + +namespace Avalonia.Controls.Repeaters { internal class OrientationBasedMeasures { diff --git a/src/Avalonia.Controls/Repeaters/RecyclePool.cs b/src/Avalonia.Controls/Repeaters/RecyclePool.cs index e602a6b674..a1fa05597b 100644 --- a/src/Avalonia.Controls/Repeaters/RecyclePool.cs +++ b/src/Avalonia.Controls/Repeaters/RecyclePool.cs @@ -1,4 +1,9 @@ -using System; +// 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.Linq; using System.Runtime.CompilerServices; diff --git a/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs b/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs index 3f002c3121..c4fdc0021f 100644 --- a/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs +++ b/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs @@ -1,4 +1,9 @@ -using System; +// 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.Text; diff --git a/src/Avalonia.Controls/Repeaters/StackLayout.cs b/src/Avalonia.Controls/Repeaters/StackLayout.cs index 66b0be5b92..2377061d60 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayout.cs +++ b/src/Avalonia.Controls/Repeaters/StackLayout.cs @@ -1,4 +1,9 @@ -using System; +// 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.Specialized; namespace Avalonia.Controls.Repeaters diff --git a/src/Avalonia.Controls/Repeaters/StackLayoutState.cs b/src/Avalonia.Controls/Repeaters/StackLayoutState.cs index cefaf38f3a..9ffc5be586 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayoutState.cs +++ b/src/Avalonia.Controls/Repeaters/StackLayoutState.cs @@ -1,4 +1,9 @@ -using System; +// 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.Linq; diff --git a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs b/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs index 624e477354..633f1a968d 100644 --- a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs +++ b/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs @@ -1,4 +1,9 @@ -using System; +// 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.Specialized; namespace Avalonia.Controls.Repeaters diff --git a/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs b/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs index e62f64192d..0570c1dde2 100644 --- a/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs +++ b/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs @@ -1,4 +1,9 @@ -using System; +// 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 System.Text; diff --git a/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs b/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs index 3ad883c04b..f360c4a9f9 100644 --- a/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs +++ b/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs @@ -1,4 +1,9 @@ -using System; +// 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; using System.Collections.Generic; using System.Text; diff --git a/src/Avalonia.Controls/Repeaters/ViewManager.cs b/src/Avalonia.Controls/Repeaters/ViewManager.cs index 1167705b5b..3578ef0498 100644 --- a/src/Avalonia.Controls/Repeaters/ViewManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewManager.cs @@ -1,4 +1,9 @@ -using System; +// 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.Controls.Templates; diff --git a/src/Avalonia.Controls/Repeaters/ViewportManager.cs b/src/Avalonia.Controls/Repeaters/ViewportManager.cs index 238870644f..643ec4c87d 100644 --- a/src/Avalonia.Controls/Repeaters/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewportManager.cs @@ -1,4 +1,9 @@ -using System; +// 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.Reactive.Linq; using System.Threading.Tasks; diff --git a/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs index dddf00efd0..0e33badef3 100644 --- a/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs +++ b/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs @@ -1,4 +1,9 @@ -using System; +// 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; namespace Avalonia.Controls.Repeaters { diff --git a/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs b/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs index fa31095f9e..02700f716c 100644 --- a/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs +++ b/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs @@ -1,4 +1,9 @@ -using System; +// 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 System.Text; From 1707496b3f6adb5fa1c28f7edc89315b4ec3fdb7 Mon Sep 17 00:00:00 2001 From: Ivan Kochurkin Date: Sat, 29 Jun 2019 16:49:05 +0300 Subject: [PATCH 011/138] Add `Add`, `Remove` buttons and `SelectionMode` combobox to ListBoxPage.xaml --- samples/ControlCatalog/Pages/ListBoxPage.xaml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index 4783c8cfb8..e7c81a28d4 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -9,7 +9,20 @@ Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> - + + + + + + + + + Single + Multiple + Toggle + AlwaysSelected + + From c3f0841142cd253c6b97ae31f9fbf965b472b9e7 Mon Sep 17 00:00:00 2001 From: Ivan Kochurkin Date: Sat, 29 Jun 2019 16:50:14 +0300 Subject: [PATCH 012/138] Implement functionality for added controls in ListBoxPage.xaml.cs --- .../ControlCatalog/Pages/ListBoxPage.xaml.cs | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs index dbe6c74800..e1a615af4b 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; +using System.Reactive; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ReactiveUI; namespace ControlCatalog.Pages { @@ -11,9 +11,8 @@ namespace ControlCatalog.Pages { public ListBoxPage() { - this.InitializeComponent(); - DataContext = Enumerable.Range(1, 10).Select(i => $"Item {i}" ) - .ToArray(); + InitializeComponent(); + DataContext = new PageViewModel(this.Find("listBox")); } private void InitializeComponent() @@ -21,5 +20,46 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } + private class PageViewModel : ReactiveObject + { + private readonly ListBox _listBox; + private int _counter; + private SelectionMode _selectionMode; + + public PageViewModel(ListBox listBox) + { + _listBox = listBox; + + Items = new ObservableCollection(Enumerable.Range(1, 10).Select(i => GenerateItem())); + + AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); + + RemoveItemCommand = ReactiveCommand.Create(() => + { + foreach (string selectedItem in listBox.SelectedItems) + { + Items.Remove(selectedItem); + } + }); + } + + public ObservableCollection Items { get; } + + public ReactiveCommand AddItemCommand { get; } + + public ReactiveCommand RemoveItemCommand { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set + { + _listBox.SelectedItems.Clear(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + + private string GenerateItem() => $"Item {_counter++}"; + } } } From 9e874529c6ef747105b76f1ed66f50b3579efcce Mon Sep 17 00:00:00 2001 From: Ivan Kochurkin Date: Sat, 29 Jun 2019 18:40:51 +0300 Subject: [PATCH 013/138] Add `Add`, `Remove` buttons and `SelectionMode` combobox to TreeViewPage.xaml --- .../ControlCatalog/Pages/TreeViewPage.xaml | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index c03edb8b03..1b01a38c60 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -6,16 +6,29 @@ Displays a hierachical tree of data. - - - - - - - + Margin="0,16,0,0" + HorizontalAlignment="Center" + Spacing="16"> + + + + + + + + + + + + + + + Single + Multiple + Toggle + AlwaysSelected + + From 6f58bb0e392081739453ac2fef67c93552b5fa55 Mon Sep 17 00:00:00 2001 From: Ivan Kochurkin Date: Sat, 29 Jun 2019 18:41:29 +0300 Subject: [PATCH 014/138] Implement functionality for added controls in TreeViewPage.xaml.cs --- .../ControlCatalog/Pages/ListBoxPage.xaml.cs | 2 +- .../ControlCatalog/Pages/TreeViewPage.xaml.cs | 93 +++++++++++++++++-- 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs index e1a615af4b..d6d48cd030 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs @@ -36,7 +36,7 @@ namespace ControlCatalog.Pages RemoveItemCommand = ReactiveCommand.Create(() => { - foreach (string selectedItem in listBox.SelectedItems) + foreach (string selectedItem in _listBox.SelectedItems) { Items.Remove(selectedItem); } diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs index a83f9cf43f..cf6aa50db7 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs @@ -1,8 +1,9 @@ -using System.Collections; -using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; +using System.Reactive; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ReactiveUI; namespace ControlCatalog.Pages { @@ -10,8 +11,8 @@ namespace ControlCatalog.Pages { public TreeViewPage() { - this.InitializeComponent(); - DataContext = new Node().Children; + InitializeComponent(); + DataContext = new PageViewModel(this.Find("treeView")); } private void InitializeComponent() @@ -19,22 +20,96 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } - public class Node + private class PageViewModel : ReactiveObject { - private IList _children; + private readonly TreeView _treeView; + private SelectionMode _selectionMode; + + public PageViewModel(TreeView treeView) + { + _treeView = treeView; + + Node root = new Node(); + Items = root.Children; + + AddItemCommand = ReactiveCommand.Create(() => + { + Node selectedItem = _treeView.SelectedItems.Count > 0 ? (Node)_treeView.SelectedItems[0] : root; + selectedItem.AddNewItem(); + }); + + RemoveItemCommand = ReactiveCommand.Create(() => + { + foreach (Node selectedItem in _treeView.SelectedItems) + { + RecursiveRemove(Items, selectedItem); + } + + _treeView.SelectedItems.Clear(); + + bool RecursiveRemove(ObservableCollection items, Node selectedItem) + { + if (items.Remove(selectedItem)) + { + return true; + } + + foreach (Node item in items) + { + if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem)) + { + return true; + } + } + + return false; + } + }); + } + + public ObservableCollection Items { get; } + + public ReactiveCommand AddItemCommand { get; } + + public ReactiveCommand RemoveItemCommand { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set + { + _treeView.SelectedItems.Clear(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + } + + private class Node + { + private int _counter; + private ObservableCollection _children; + public string Header { get; private set; } - public IList Children + + public bool AreChildrenInitialized => _children != null; + + public ObservableCollection Children { get { if (_children == null) { - _children = Enumerable.Range(1, 10).Select(i => new Node() {Header = $"Item {i}"}) - .ToArray(); + _children = new ObservableCollection(Enumerable.Range(1, 10).Select(i => CreateNewNode())); } return _children; } } + + public void AddNewItem() => Children.Add(CreateNewNode()); + + public override string ToString() => Header; + + private Node CreateNewNode() => new Node {Header = $"Item {_counter++}"}; } } } From df6068604b69270f14c785465a7a703e9da3ed73 Mon Sep 17 00:00:00 2001 From: Ivan Kochurkin Date: Sat, 29 Jun 2019 23:10:13 +0300 Subject: [PATCH 015/138] Fix @Gillibald review notes: get rid of passing control to viewmodel, use binding instead --- samples/ControlCatalog/Pages/ListBoxPage.xaml | 2 +- .../ControlCatalog/Pages/ListBoxPage.xaml.cs | 16 ++++++------- .../ControlCatalog/Pages/TreeViewPage.xaml | 2 +- .../ControlCatalog/Pages/TreeViewPage.xaml.cs | 24 +++++++++---------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index e7c81a28d4..49e9aafc4a 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -10,7 +10,7 @@ HorizontalAlignment="Center" Spacing="16"> - + diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs index d6d48cd030..8a67766c76 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs @@ -12,7 +12,7 @@ namespace ControlCatalog.Pages public ListBoxPage() { InitializeComponent(); - DataContext = new PageViewModel(this.Find("listBox")); + DataContext = new PageViewModel(); } private void InitializeComponent() @@ -22,29 +22,29 @@ namespace ControlCatalog.Pages private class PageViewModel : ReactiveObject { - private readonly ListBox _listBox; private int _counter; private SelectionMode _selectionMode; - public PageViewModel(ListBox listBox) + public PageViewModel() { - _listBox = listBox; - Items = new ObservableCollection(Enumerable.Range(1, 10).Select(i => GenerateItem())); + SelectedItems = new ObservableCollection(); AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); RemoveItemCommand = ReactiveCommand.Create(() => { - foreach (string selectedItem in _listBox.SelectedItems) + while (SelectedItems.Count > 0) { - Items.Remove(selectedItem); + Items.Remove(SelectedItems[0]); } }); } public ObservableCollection Items { get; } + public ObservableCollection SelectedItems { get; } + public ReactiveCommand AddItemCommand { get; } public ReactiveCommand RemoveItemCommand { get; } @@ -54,7 +54,7 @@ namespace ControlCatalog.Pages get => _selectionMode; set { - _listBox.SelectedItems.Clear(); + SelectedItems.Clear(); this.RaiseAndSetIfChanged(ref _selectionMode, value); } } diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index 1b01a38c60..3a81e2ed02 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -10,7 +10,7 @@ HorizontalAlignment="Center" Spacing="16"> - + diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs index cf6aa50db7..1f35f05f1d 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs @@ -12,7 +12,7 @@ namespace ControlCatalog.Pages public TreeViewPage() { InitializeComponent(); - DataContext = new PageViewModel(this.Find("treeView")); + DataContext = new PageViewModel(); } private void InitializeComponent() @@ -22,31 +22,29 @@ namespace ControlCatalog.Pages private class PageViewModel : ReactiveObject { - private readonly TreeView _treeView; private SelectionMode _selectionMode; - public PageViewModel(TreeView treeView) + public PageViewModel() { - _treeView = treeView; - Node root = new Node(); Items = root.Children; + SelectedItems = new ObservableCollection(); AddItemCommand = ReactiveCommand.Create(() => { - Node selectedItem = _treeView.SelectedItems.Count > 0 ? (Node)_treeView.SelectedItems[0] : root; - selectedItem.AddNewItem(); + Node parentItem = SelectedItems.Count > 0 ? SelectedItems[0] : root; + parentItem.AddNewItem(); }); RemoveItemCommand = ReactiveCommand.Create(() => { - foreach (Node selectedItem in _treeView.SelectedItems) + while (SelectedItems.Count > 0) { - RecursiveRemove(Items, selectedItem); + Node lastItem = SelectedItems[0]; + RecursiveRemove(Items, lastItem); + SelectedItems.Remove(lastItem); } - _treeView.SelectedItems.Clear(); - bool RecursiveRemove(ObservableCollection items, Node selectedItem) { if (items.Remove(selectedItem)) @@ -69,6 +67,8 @@ namespace ControlCatalog.Pages public ObservableCollection Items { get; } + public ObservableCollection SelectedItems { get; } + public ReactiveCommand AddItemCommand { get; } public ReactiveCommand RemoveItemCommand { get; } @@ -78,7 +78,7 @@ namespace ControlCatalog.Pages get => _selectionMode; set { - _treeView.SelectedItems.Clear(); + SelectedItems.Clear(); this.RaiseAndSetIfChanged(ref _selectionMode, value); } } From f944dde6c4cee21c7b82269bb32ac35cf70fdc3b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 13:41:58 +0200 Subject: [PATCH 016/138] Added IScrollAnchorProvider. Though nothing implements it yet. Uncomment/implement the parts of `ViewportManager` that required this interface. --- .../IScrollAnchorProvider.cs | 9 ++ .../Repeaters/ViewportManager.cs | 123 ++++++++++++++---- 2 files changed, 107 insertions(+), 25 deletions(-) create mode 100644 src/Avalonia.Controls/IScrollAnchorProvider.cs diff --git a/src/Avalonia.Controls/IScrollAnchorProvider.cs b/src/Avalonia.Controls/IScrollAnchorProvider.cs new file mode 100644 index 0000000000..6b5cb2ee25 --- /dev/null +++ b/src/Avalonia.Controls/IScrollAnchorProvider.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Controls +{ + public interface IScrollAnchorProvider + { + IControl CurrentAnchor { get; } + void RegisterAnchorCandidate(IControl element); + void UnregisterAnchorCandidate(IControl element); + } +} diff --git a/src/Avalonia.Controls/Repeaters/ViewportManager.cs b/src/Avalonia.Controls/Repeaters/ViewportManager.cs index 643ec4c87d..7a34d7f12a 100644 --- a/src/Avalonia.Controls/Repeaters/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewportManager.cs @@ -17,6 +17,8 @@ namespace Avalonia.Controls.Repeaters { private const double CacheBufferPerSideInflationPixelDelta = 40.0; private readonly ItemsRepeater _owner; + private bool _ensuredScroller; + private IScrollAnchorProvider _scroller; private IControl _makeAnchorElement; private bool _isAnchorOutsideRealizedRange; private Task _cacheBuildAction; @@ -50,11 +52,45 @@ namespace Avalonia.Controls.Repeaters _owner = owner; } - // TODO: Implement - public IControl SuggestedAnchor => null; + public IControl SuggestedAnchor + { + get + { + // The element generated during the ItemsRepeater.MakeAnchor call has precedence over the next tick. + var suggestedAnchor = _makeAnchorElement; + var owner = _owner; + + if (suggestedAnchor == null) + { + var anchorElement = _scroller?.CurrentAnchor; + + if (anchorElement != null) + { + // We can't simply return anchorElement because, in case of nested Repeaters, it may not + // be a direct child of ours, or even an indirect child. We need to walk up the tree starting + // from anchorElement to figure out what child of ours (if any) to use as the suggested element. + var child = anchorElement; + var parent = child.VisualParent as IControl; + + while (parent != null) + { + if (parent == owner) + { + suggestedAnchor = child; + break; + } + + child = parent; + parent = parent.VisualParent as IControl; + } + } + } - // TODO: Implement - public bool HasScroller => false; + return suggestedAnchor; + } + } + + public bool HasScroller => _scroller != null; public IControl MadeAnchor => _makeAnchorElement; @@ -169,13 +205,10 @@ namespace Avalonia.Controls.Repeaters // We just finished a measure pass and have a new extent. // Let's make sure the scrollers will run its arrange so that they track the anchor. - ////if (_scroller != null) - ////{ - //// ((IControl)_scroller).InvalidateArrange(); - ////} + ((IControl)_scroller)?.InvalidateArrange(); } - public Point GetOrigin() => throw new NotImplementedException(); + public Point GetOrigin() => _layoutExtent.TopLeft; public void OnLayoutChanged(bool isVirtualizing) { @@ -190,18 +223,7 @@ namespace Avalonia.Controls.Repeaters if (!_managingViewportDisabled) { - // HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater - - // we can get this from TransformedBounds, but this property is updated after layout has - // run, resulting in the UI being updated too late when scrolling quickly. We can - // partially remedey this by triggering also on Bounds changes, but this won't work so - // well for nested ItemsRepeaters. - // - // UWP uses the EffectiveBoundsChanged event (which I think was implemented specially - // for this case): we need to implement that in Avalonia. - _effectiveViewportChangedRevoker = _owner.GetObservable(Visual.TransformedBoundsProperty) - .Merge(_owner.GetObservable(Visual.BoundsProperty).Select(_ => _owner.TransformedBounds)) - .Skip(1) - .Subscribe(OnEffectiveViewportChanged); + _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); } } @@ -222,7 +244,7 @@ namespace Avalonia.Controls.Repeaters // This is because of a bug that causes effective viewport to not // fire if you register during arrange. // Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport - //EnsureScroller(); + EnsureScroller(); } public void OnOwnerArranged() @@ -342,9 +364,10 @@ namespace Avalonia.Controls.Repeaters public void ResetScrollers() { - ////_scroller = null; - ////_effectiveViewportChangedRevoker.Dispose(); - ////m_ensuredScroller = false; + _scroller = null; + _effectiveViewportChangedRevoker.Dispose(); + _effectiveViewportChangedRevoker = null; + _ensuredScroller = false; } private void OnEffectiveViewportChanged(TransformedBounds? bounds) @@ -377,6 +400,40 @@ namespace Avalonia.Controls.Repeaters } } + private void EnsureScroller() + { + if (!_ensuredScroller) + { + ResetScrollers(); + + var parent = _owner.GetVisualParent(); + while (parent != null) + { + if (parent is IScrollAnchorProvider scroller) + { + _scroller = scroller; + break; + } + + parent = parent.VisualParent; + } + + if (_scroller == null) + { + // We usually update the viewport in the post arrange handler. But, since we don't have + // a scroller, let's do it now. + UpdateViewport(Rect.Empty); + } + else if (!_managingViewportDisabled) + { + _effectiveViewportChangedRevoker?.Dispose(); + _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); + } + + _ensuredScroller = true; + } + } + private void UpdateViewport(Rect viewport) { var currentVisibleWindow = viewport; @@ -415,6 +472,22 @@ namespace Avalonia.Controls.Repeaters } } + private IDisposable SubscribeToEffectiveViewportChanged(IControl control) + { + // HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater - + // we can get this from TransformedBounds, but this property is updated after layout has + // run, resulting in the UI being updated too late when scrolling quickly. We can + // partially remedey this by triggering also on Bounds changes, but this won't work so + // well for nested ItemsRepeaters. + // + // UWP uses the EffectiveBoundsChanged event (which I think was implemented specially + // for this case): we need to implement that in Avalonia. + return control.GetObservable(Visual.TransformedBoundsProperty) + .Merge(control.GetObservable(Visual.BoundsProperty).Select(_ => control.TransformedBounds)) + .Skip(1) + .Subscribe(OnEffectiveViewportChanged); + } + private class ScrollerInfo { public ScrollerInfo(ScrollViewer scroller) From 5d813c901190a678a5ef43698726b734f64f11a8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 14:37:53 +0200 Subject: [PATCH 017/138] Scaffold IScrollAnchorProvider on ScrollViewer. It doesn't do anything yet, but `ItemsPresenter` requires `ScrollViewer` to implement this interface. --- .../Repeaters/ViewportManager.cs | 2 +- src/Avalonia.Controls/ScrollViewer.cs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Repeaters/ViewportManager.cs b/src/Avalonia.Controls/Repeaters/ViewportManager.cs index 7a34d7f12a..c32ac3855f 100644 --- a/src/Avalonia.Controls/Repeaters/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewportManager.cs @@ -365,7 +365,7 @@ namespace Avalonia.Controls.Repeaters public void ResetScrollers() { _scroller = null; - _effectiveViewportChangedRevoker.Dispose(); + _effectiveViewportChangedRevoker?.Dispose(); _effectiveViewportChangedRevoker = null; _ensuredScroller = false; } diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 264b1fd2ce..c9b5cbb75b 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -11,7 +11,7 @@ namespace Avalonia.Controls /// /// A control scrolls its content if the content is bigger than the space available. /// - public class ScrollViewer : ContentControl, IScrollable + public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider { /// /// Defines the property. @@ -333,6 +333,9 @@ namespace Avalonia.Controls get { return _viewport.Height; } } + /// + IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement + /// /// Gets the value of the HorizontalScrollBarVisibility attached property. /// @@ -373,6 +376,16 @@ namespace Avalonia.Controls control.SetValue(VerticalScrollBarVisibilityProperty, value); } + void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element) + { + // TODO: Implement + } + + void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element) + { + // TODO: Implement + } + internal static Vector CoerceOffset(Size extent, Size viewport, Vector offset) { var maxX = Math.Max(extent.Width - viewport.Width, 0); From 3a2876f1ef17c943c5586fba3eb15ea788d42be5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 20:32:17 +0200 Subject: [PATCH 018/138] Added docs for ItemsRepeater-related classes. --- .../Repeaters/ItemTemplateWrapper.cs | 3 - .../Repeaters/ItemsRepeater.cs | 108 ++++++++++++++- .../ItemsRepeaterElementClearingEventArgs.cs | 8 ++ ...emsRepeaterElementIndexChangedEventArgs.cs | 12 ++ .../ItemsRepeaterElementPreparedEventArgs.cs | 13 +- .../Repeaters/ItemsSourceView.cs | 49 ++++++- src/Avalonia.Controls/Repeaters/Layout.cs | 79 ++++++++++- .../Repeaters/LayoutContext.cs | 14 +- .../Repeaters/NonVirtualizingLayout.cs | 8 +- .../Repeaters/StackLayout.cs | 27 +++- .../Repeaters/StackLayoutState.cs | 3 + .../Repeaters/UniformGridLayout.cs | 124 ++++++++++++++++++ .../Repeaters/UniformGridLayoutState.cs | 3 + .../Repeaters/VirtualizingLayout.cs | 68 +++++++++- 14 files changed, 494 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs index 86d452742c..00da39cd9d 100644 --- a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs +++ b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs @@ -3,9 +3,6 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -using System; -using System.Collections.Generic; -using System.Text; using Avalonia.Controls.Templates; namespace Avalonia.Controls.Repeaters diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs index f5c832a9e8..2f39d62e08 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs @@ -11,18 +11,42 @@ using Avalonia.Input; namespace Avalonia.Controls.Repeaters { + /// + /// Represents a data-driven collection control that incorporates a flexible layout system, + /// custom views, and virtualization. + /// public class ItemsRepeater : Panel { + /// + /// Defines the property. + /// public static readonly AvaloniaProperty HorizontalCacheLengthProperty = AvaloniaProperty.Register(nameof(HorizontalCacheLength), 2.0); + + /// + /// Defines the property. + /// public static readonly StyledProperty ItemTemplateProperty = ItemsControl.ItemTemplateProperty.AddOwner(); + + /// + /// Defines the property. + /// public static readonly DirectProperty ItemsProperty = ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v); + + /// + /// Defines the property. + /// public static readonly AvaloniaProperty LayoutProperty = AvaloniaProperty.Register(nameof(Layout), new StackLayout()); + + /// + /// Defines the property. + /// public static readonly AvaloniaProperty VerticalCacheLengthProperty = AvaloniaProperty.Register(nameof(VerticalCacheLength), 2.0); + private static readonly AttachedProperty VirtualizationInfoProperty = AvaloniaProperty.RegisterAttached("VirtualizationInfo"); @@ -40,6 +64,9 @@ namespace Avalonia.Controls.Repeaters private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs; + /// + /// Initializes a new instance of the class. + /// public ItemsRepeater() { _viewManager = new ViewManager(this); @@ -53,36 +80,61 @@ namespace Avalonia.Controls.Repeaters ClipToBoundsProperty.OverrideDefaultValue(true); } + /// + /// Gets or sets the layout used to size and position elements in the ItemsRepeater. + /// + /// + /// The layout used to size and position elements. The default is a StackLayout with + /// vertical orientation. + /// public Layout Layout { get => GetValue(LayoutProperty); set => SetValue(LayoutProperty, value); } + /// + /// Gets or sets an object source used to generate the content of the ItemsRepeater. + /// public IEnumerable Items { get => _items; set => SetAndRaise(ItemsProperty, ref _items, value); } + /// + /// Gets or sets the template used to display each item. + /// public IDataTemplate ItemTemplate { get => GetValue(ItemTemplateProperty); set => SetValue(ItemTemplateProperty, value); } + /// + /// Gets or sets a value that indicates the size of the buffer used to realize items when + /// panning or scrolling horizontally. + /// public double HorizontalCacheLength { get => GetValue(HorizontalCacheLengthProperty); set => SetValue(HorizontalCacheLengthProperty, value); } + /// + /// Gets or sets a value that indicates the size of the buffer used to realize items when + /// panning or scrolling vertically. + /// public double VerticalCacheLength { get => GetValue(VerticalCacheLengthProperty); set => SetValue(VerticalCacheLengthProperty, value); } + /// + /// Gets a standardized view of the supported interactions between a given Items object and + /// the ItemsRepeater control and its associated components. + /// public ItemsSourceView ItemsSourceView { get; private set; } internal ItemTemplateWrapper ItemTemplateShim { get; set; } @@ -107,19 +159,69 @@ namespace Avalonia.Controls.Repeaters } } + /// + /// Occurs each time an element is cleared and made available to be re-used. + /// + /// + /// This event is raised immediately each time an element is cleared, such as when it falls + /// outside the range of realized items. Elements are cleared when they become available + /// for re-use. + /// public event EventHandler ElementClearing; + + /// + /// Occurs for each realized when the index for the item it + /// represents has changed. + /// + /// + /// When you use ItemsRepeater to build a more complex control that supports specific + /// interactions on the child elements (such as selection or click), it is useful to be + /// able to keep an up-to-date identifier for the backing data item. + /// + /// This event is raised for each realized IControl where the index for the item it + /// represents has changed. For example, when another item is added or removed in the data + /// source, the index for items that come after in the ordering will be impacted. + /// public event EventHandler ElementIndexChanged; + + /// + /// Occurs each time an element is prepared for use. + /// + /// + /// The prepared element might be newly created or an existing element that is being re- + /// used. + /// public event EventHandler ElementPrepared; + /// + /// Retrieves the index of the item from the data source that corresponds to the specified + /// . + /// + /// + /// The element that corresponds to the item to get the index of. + /// + /// + /// The index of the item from the data source that corresponds to the specified UIElement, + /// or -1 if the element is not supported. + /// public int GetElementIndex(IControl element) => GetElementIndexImpl(element); + /// + /// Retrieves the realized UIElement that corresponds to the item at the specified index in + /// the data source. + /// + /// The index of the item. + /// + /// he UIElement that corresponds to the item at the specified index if the item is + /// realized, or null if the item is not realized. + /// public IControl TryGetElement(int index) => GetElementFromIndexImpl(index); - public void PinElement(IControl element) => _viewManager.UpdatePin(element, true); + internal void PinElement(IControl element) => _viewManager.UpdatePin(element, true); - public void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false); + internal void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false); - public IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index); + internal IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index); internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element) { diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs index a6984a3e72..ff011469e7 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs @@ -7,10 +7,18 @@ using System; namespace Avalonia.Controls.Repeaters { + /// + /// Provides data for the event. + /// public class ItemsRepeaterElementClearingEventArgs : EventArgs { internal ItemsRepeaterElementClearingEventArgs(IControl element) => Element = element; + + /// + /// Gets the element that is being cleared for re-use. + /// public IControl Element { get; private set; } + internal void Update(IControl element) => Element = element; } } diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs index a4457a2b06..c30ba38d23 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs @@ -7,6 +7,9 @@ using System; namespace Avalonia.Controls.Repeaters { + /// + /// Provides data for the event. + /// public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs { internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int newIndex, int oldIndex) @@ -16,10 +19,19 @@ namespace Avalonia.Controls.Repeaters OldIndex = oldIndex; } + /// + /// Get the element for which the index changed. + /// public IControl Element { get; private set; } + /// + /// Gets the index of the element after the change. + /// public int NewIndex { get; private set; } + /// + /// Gets the index of the element before the change. + /// public int OldIndex { get; private set; } internal void Update(IControl element, int newIndex, int oldIndex) diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs index 12abfa155e..9c26c0d136 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs @@ -3,12 +3,11 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -using System; -using System.Collections.Generic; -using System.Text; - namespace Avalonia.Controls.Repeaters { + /// + /// Provides data for the event. + /// public class ItemsRepeaterElementPreparedEventArgs { internal ItemsRepeaterElementPreparedEventArgs(IControl element, int index) @@ -17,8 +16,14 @@ namespace Avalonia.Controls.Repeaters Index = index; } + /// + /// Gets the prepared element. + /// public IControl Element { get; private set; } + /// + /// Gets the index of the item the element was prepared for. + /// public int Index { get; private set; } internal void Update(IControl element, int index) diff --git a/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs b/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs index f57db0ec07..1caffe881a 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs @@ -11,12 +11,26 @@ using System.Linq; namespace Avalonia.Controls.Repeaters { + /// + /// Represents a standardized view of the supported interactions between a given ItemsSource + /// object and an control. + /// + /// + /// Components written to work with ItemsRepeater should consume the + /// via ItemsSourceView since this provides a normalized + /// view of the Items. That way, each component does not need to know if the source is an + /// IEnumerable, an IList, or something else. + /// public class ItemsSourceView : INotifyCollectionChanged, IDisposable { private readonly IList _inner; private INotifyCollectionChanged _notifyCollectionChanged; private int _cachedSize = -1; + /// + /// Initializes a new instance of the ItemsSourceView class for the specified data source. + /// + /// The data source. public ItemsSourceView(IEnumerable source) { Contract.Requires(source != null); @@ -35,6 +49,9 @@ namespace Avalonia.Controls.Repeaters ListenToCollectionChanges(); } + /// + /// Gets the number of items in the collection. + /// public int Count { get @@ -48,11 +65,20 @@ namespace Avalonia.Controls.Repeaters } } + /// + /// Gets a value that indicates whether the items source can provide a unique key for each item. + /// + /// + /// TODO: Not yet implemented in Avalonia. + /// public bool HasKeyIndexMapping => false; - + /// + /// Occurs when the collection has changed to indicate the reason for the change and which items changed. + /// public event NotifyCollectionChangedEventHandler CollectionChanged; + /// public void Dispose() { if (_notifyCollectionChanged != null) @@ -61,13 +87,34 @@ namespace Avalonia.Controls.Repeaters } } + /// + /// Retrieves the item at the specified index. + /// + /// The index. + /// the item. public object GetAt(int index) => _inner[index]; + /// + /// Retrieves the index of the item that has the specified unique identifier (key). + /// + /// The index. + /// The key + /// + /// TODO: Not yet implemented in Avalonia. + /// public string KeyFromIndex(int index) { throw new NotImplementedException(); } + /// + /// Retrieves the unique identifier (key) for the item at the specified index. + /// + /// The key. + /// The index. + /// + /// TODO: Not yet implemented in Avalonia. + /// public int IndexFromKey(string key) { throw new NotImplementedException(); diff --git a/src/Avalonia.Controls/Repeaters/Layout.cs b/src/Avalonia.Controls/Repeaters/Layout.cs index d5f5b215c1..bf7ce95a23 100644 --- a/src/Avalonia.Controls/Repeaters/Layout.cs +++ b/src/Avalonia.Controls/Repeaters/Layout.cs @@ -7,23 +7,100 @@ using System; namespace Avalonia.Controls.Repeaters { + /// + /// Represents the base class for an object that sizes and arranges child elements for a host. + /// public abstract class Layout : AvaloniaObject { - public string LayoutId { get; set; } + internal string LayoutId { get; set; } + /// + /// Occurs when the measurement state (layout) has been invalidated. + /// public event EventHandler MeasureInvalidated; + + /// + /// Occurs when the arrange state (layout) has been invalidated. + /// public event EventHandler ArrangeInvalidated; + /// + /// Initializes any per-container state the layout requires when it is attached to an + /// container. + /// + /// + /// The context object that facilitates communication between the layout and its host + /// container. + /// + /// + /// Container elements that support attached layouts should call this method when a layout + /// instance is first assigned. The container is expected to give the attached layout + /// instance a way to store and retrieve any per-container state by way of the provided + /// context. It is also the responsibility of the container to not reuse the context, or + /// otherwise expose the state from one layout to another. + /// + /// When an attached layout is removed the container should release any reference to the + /// layout state it stored. + /// + /// Override or + /// to provide the behavior for + /// this method in a derived class. + /// public abstract void InitializeForContext(LayoutContext context); + /// + /// Removes any state the layout previously stored on the IControl container. + /// + /// + /// The context object that facilitates communication between the layout and its host + /// container. + /// public abstract void UninitializeForContext(LayoutContext context); + /// + /// Suggests a DesiredSize for a container element. A container element that supports + /// attached layouts should call this method from their own MeasureOverride implementations + /// to form a recursive layout update. The attached layout is expected to call the Measure + /// for each of the container’s IControl children. + /// + /// + /// The context object that facilitates communication between the layout and its host + /// container. + /// + /// + /// The available space that a container can allocate to a child object. A child object can + /// request a larger space than what is available; the provided size might be accommodated + /// if scrolling or other resize behavior is possible in that particular container. + /// + /// public abstract Size Measure(LayoutContext context, Size availableSize); + /// + /// Positions child elements and determines a size for a container UIElement. Container + /// elements that support attached layouts should call this method from their layout + /// override implementations to form a recursive layout update. + /// + /// + /// The context object that facilitates communication between the layout and its host + /// container. + /// + /// + /// The final size that the container computes for the child in layout. + /// + /// The actual size that is used after the element is arranged in layout. public abstract Size Arrange(LayoutContext context, Size finalSize); + /// + /// Invalidates the measurement state (layout) for all IControl containers that reference + /// this layout. + /// protected void InvalidateMeasure() => MeasureInvalidated?.Invoke(this, EventArgs.Empty); + /// + /// Invalidates the arrange state (layout) for all UIElement containers that reference this + /// layout. After the invalidation, the UIElement will have its layout updated, which + /// occurs asynchronously. + /// protected void InvalidateArrange() => ArrangeInvalidated?.Invoke(this, EventArgs.Empty); } } diff --git a/src/Avalonia.Controls/Repeaters/LayoutContext.cs b/src/Avalonia.Controls/Repeaters/LayoutContext.cs index 75ee815fc0..1a4b26c547 100644 --- a/src/Avalonia.Controls/Repeaters/LayoutContext.cs +++ b/src/Avalonia.Controls/Repeaters/LayoutContext.cs @@ -3,16 +3,22 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -using System; -using System.Collections.Generic; -using System.Text; - namespace Avalonia.Controls.Repeaters { + /// + /// Represents the base class for an object that facilitates communication between an attached + /// layout and its host container. + /// public class LayoutContext : AvaloniaObject { + /// + /// Gets or sets an object that represents the state of a layout. + /// public object LayoutState { get; set; } + /// + /// Implements the behavior of in a derived or custom LayoutContext. + /// protected virtual object LayoutStateCore { get; set; } } } diff --git a/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs b/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs index 95a66834a0..bac2e10f88 100644 --- a/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs +++ b/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs @@ -3,12 +3,12 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -using System; -using System.Collections.Generic; -using System.Text; - namespace Avalonia.Controls.Repeaters { + /// + /// Represents the base class for an object that sizes and arranges child elements for a host + /// and and does not support virtualization. + /// public abstract class NonVirtualizingLayout : Layout { } diff --git a/src/Avalonia.Controls/Repeaters/StackLayout.cs b/src/Avalonia.Controls/Repeaters/StackLayout.cs index 2377061d60..8b887b149e 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayout.cs +++ b/src/Avalonia.Controls/Repeaters/StackLayout.cs @@ -8,34 +8,57 @@ using System.Collections.Specialized; namespace Avalonia.Controls.Repeaters { + /// + /// Arranges elements into a single line (with spacing) that can be oriented horizontally or vertically. + /// public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates { + /// + /// Defines the property. + /// public static readonly StyledProperty OrientationProperty = StackPanel.OrientationProperty.AddOwner(); + /// + /// Defines the property. + /// public static readonly StyledProperty SpacingProperty = StackPanel.SpacingProperty.AddOwner(); private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); + /// + /// Initializes a new instance of the StackLayout class. + /// public StackLayout() { LayoutId = "StackLayout"; } + /// + /// Gets or sets the axis along which items are laid out. + /// + /// + /// One of the enumeration values that specifies the axis along which items are laid out. + /// The default is Vertical. + /// public Orientation Orientation { get => GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); } + /// + /// Gets or sets a uniform distance (in pixels) between stacked items. It is applied in the + /// direction of the StackLayout's Orientation. + /// public double Spacing { get => GetValue(SpacingProperty); set => SetValue(SpacingProperty, value); } - public Rect GetExtent( + internal Rect GetExtent( Size availableSize, VirtualizingLayoutContext context, IControl firstRealized, @@ -73,7 +96,7 @@ namespace Avalonia.Controls.Repeaters return extent; } - public void OnElementMeasured( + internal void OnElementMeasured( IControl element, int index, Size availableSize, diff --git a/src/Avalonia.Controls/Repeaters/StackLayoutState.cs b/src/Avalonia.Controls/Repeaters/StackLayoutState.cs index 9ffc5be586..130522c908 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayoutState.cs +++ b/src/Avalonia.Controls/Repeaters/StackLayoutState.cs @@ -9,6 +9,9 @@ using System.Linq; namespace Avalonia.Controls.Repeaters { + /// + /// Represents the state of a StackLayout. + /// public class StackLayoutState { private const int BufferSize = 100; diff --git a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs b/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs index 633f1a968d..4c2327b15b 100644 --- a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs +++ b/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs @@ -8,43 +8,111 @@ using System.Collections.Specialized; namespace Avalonia.Controls.Repeaters { + /// + /// Defines constants that specify how items are aligned on the non-scrolling or non-virtualizing axis. + /// public enum UniformGridLayoutItemsJustification { + /// + /// Items are aligned with the start of the row or column, with extra space at the end. + /// Spacing between items does not change. + /// Start = 0, + + /// + /// Items are aligned in the center of the row or column, with extra space at the start and + /// end. Spacing between items does not change. + /// Center = 1, + + /// + /// Items are aligned with the end of the row or column, with extra space at the start. + /// Spacing between items does not change. + /// End = 2, + + /// + /// Items are aligned so that extra space is added evenly before and after each item. + /// SpaceAround = 3, + + /// + /// Items are aligned so that extra space is added evenly between adjacent items. No space + /// is added at the start or end. + /// SpaceBetween = 4, + SpaceEvenly = 5, }; + /// + /// Defines constants that specify how items are sized to fill the available space. + /// public enum UniformGridLayoutItemsStretch { + /// + /// The item retains its natural size. Use of extra space is determined by the + /// property. + /// None = 0, + + /// + /// The item is sized to fill the available space in the non-scrolling direction. Item size + /// in the scrolling direction is not changed. + /// Fill = 1, + + /// + /// The item is sized to both fill the available space in the non-scrolling direction and + /// maintain its aspect ratio. + /// Uniform = 2, }; + /// + /// Positions elements sequentially from left to right or top to bottom in a wrapping layout. + /// public class UniformGridLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates { + /// + /// Defines the property. + /// public static readonly StyledProperty ItemsJustificationProperty = AvaloniaProperty.Register(nameof(ItemsJustification)); + /// + /// Defines the property. + /// public static readonly StyledProperty ItemsStretchProperty = AvaloniaProperty.Register(nameof(ItemsStretch)); + /// + /// Defines the property. + /// public static readonly StyledProperty MinColumnSpacingProperty = AvaloniaProperty.Register(nameof(MinColumnSpacing)); + /// + /// Defines the property. + /// public static readonly StyledProperty MinItemHeightProperty = AvaloniaProperty.Register(nameof(MinItemHeight)); + /// + /// Defines the property. + /// public static readonly StyledProperty MinItemWidthProperty = AvaloniaProperty.Register(nameof(MinItemWidth)); + /// + /// Defines the property. + /// public static readonly StyledProperty MinRowSpacingProperty = AvaloniaProperty.Register(nameof(MinRowSpacing)); + /// + /// Defines the property. + /// public static readonly StyledProperty OrientationProperty = StackPanel.OrientationProperty.AddOwner(); @@ -56,6 +124,9 @@ namespace Avalonia.Controls.Repeaters private UniformGridLayoutItemsJustification _itemsJustification; private UniformGridLayoutItemsStretch _itemsStretch; + /// + /// Initializes a new instance of the class. + /// public UniformGridLayout() { LayoutId = "UniformGridLayout"; @@ -66,42 +137,95 @@ namespace Avalonia.Controls.Repeaters OrientationProperty.OverrideDefaultValue(Orientation.Horizontal); } + /// + /// Gets or sets a value that indicates how items are aligned on the non-scrolling or non- + /// virtualizing axis. + /// + /// + /// An enumeration value that indicates how items are aligned. The default is Start. + /// public UniformGridLayoutItemsJustification ItemsJustification { get => GetValue(ItemsJustificationProperty); set => SetValue(ItemsJustificationProperty, value); } + /// + /// Gets or sets a value that indicates how items are sized to fill the available space. + /// + /// + /// An enumeration value that indicates how items are sized to fill the available space. + /// The default is None. + /// + /// + /// This property enables adaptive layout behavior where the items are sized to fill the + /// available space along the non-scrolling axis, and optionally maintain their aspect ratio. + /// public UniformGridLayoutItemsStretch ItemsStretch { get => GetValue(ItemsStretchProperty); set => SetValue(ItemsStretchProperty, value); } + /// + /// Gets or sets the minimum space between items on the horizontal axis. + /// + /// + /// The spacing may exceed this minimum value when is set + /// to SpaceEvenly, SpaceAround, or SpaceBetween. + /// public double MinColumnSpacing { get => GetValue(MinColumnSpacingProperty); set => SetValue(MinColumnSpacingProperty, value); } + /// + /// Gets or sets the minimum height of each item. + /// + /// + /// The minimum height (in pixels) of each item. The default is NaN, in which case the + /// height of the first item is used as the minimum. + /// public double MinItemHeight { get => GetValue(MinItemHeightProperty); set => SetValue(MinItemHeightProperty, value); } + /// + /// Gets or sets the minimum width of each item. + /// + /// + /// The minimum width (in pixels) of each item. The default is NaN, in which case the width + /// of the first item is used as the minimum. + /// public double MinItemWidth { get => GetValue(MinItemWidthProperty); set => SetValue(MinItemWidthProperty, value); } + /// + /// Gets or sets the minimum space between items on the vertical axis. + /// + /// + /// The spacing may exceed this minimum value when is set + /// to SpaceEvenly, SpaceAround, or SpaceBetween. + /// public double MinRowSpacing { get => GetValue(MinRowSpacingProperty); set => SetValue(MinRowSpacingProperty, value); } + /// + /// Gets or sets the axis along which items are laid out. + /// + /// + /// One of the enumeration values that specifies the axis along which items are laid out. + /// The default is Vertical. + /// public Orientation Orientation { get => GetValue(OrientationProperty); diff --git a/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs b/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs index 0570c1dde2..5095ff35a6 100644 --- a/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs +++ b/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs @@ -10,6 +10,9 @@ using System.Text; namespace Avalonia.Controls.Repeaters { + /// + /// Represents the state of a . + /// public class UniformGridLayoutState { // We need to measure the element at index 0 to know what size to measure all other items. diff --git a/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs b/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs index 02700f716c..08c72b188e 100644 --- a/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs +++ b/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs @@ -3,47 +3,109 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -using System; -using System.Collections.Generic; using System.Collections.Specialized; -using System.Text; namespace Avalonia.Controls.Repeaters { + /// + /// Represents the base class for an object that sizes and arranges child elements for a host + /// and supports virtualization. + /// public abstract class VirtualizingLayout : Layout { + /// public sealed override void InitializeForContext(LayoutContext context) { InitializeForContextCore((VirtualizingLayoutContext)context); } + /// public sealed override void UninitializeForContext(LayoutContext context) { UninitializeForContextCore((VirtualizingLayoutContext)context); } + /// public sealed override Size Measure(LayoutContext context, Size availableSize) { return MeasureOverride((VirtualizingLayoutContext)context, availableSize); } + /// public sealed override Size Arrange(LayoutContext context, Size finalSize) { return ArrangeOverride((VirtualizingLayoutContext)context, finalSize); } + /// + /// When overridden in a derived class, initializes any per-container state the layout + /// requires when it is attached to an IControl container. + /// + /// + /// The context object that facilitates communication between the layout and its host + /// container. + /// protected virtual void InitializeForContextCore(VirtualizingLayoutContext context) { } + /// + /// When overridden in a derived class, removes any state the layout previously stored on + /// the IControl container. + /// + /// + /// The context object that facilitates communication between the layout and its host + /// container. + /// protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context) { } + /// + /// Provides the behavior for the "Measure" pass of the layout cycle. Classes can override + /// this method to define their own "Measure" pass behavior. + /// + /// + /// The context object that facilitates communication between the layout and its host + /// container. + /// + /// + /// The available size that this object can give to child objects. Infinity can be + /// specified as a value to indicate that the object will size to whatever content is + /// available. + /// + /// + /// The size that this object determines it needs during layout, based on its calculations + /// of the allocated sizes for child objects or based on other considerations such as a + /// fixed container size. + /// protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize); + /// + /// When implemented in a derived class, provides the behavior for the "Arrange" pass of + /// layout. Classes can override this method to define their own "Arrange" pass behavior. + /// + /// + /// The context object that facilitates communication between the layout and its host + /// container. + /// + /// + /// The final area within the container that this object should use to arrange itself and + /// its children. + /// + /// The actual size that is used after the element is arranged in layout. protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize; + /// + /// Notifies the layout when the data collection assigned to the container element (Items) + /// has changed. + /// + /// + /// The context object that facilitates communication between the layout and its host + /// container. + /// + /// The data source. + /// Data about the collection change. protected internal virtual void OnItemsChangedCore( VirtualizingLayoutContext context, object source, From 2210b441a2fd63fafb00ad910839dcf2d8648fdc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 23:23:46 +0200 Subject: [PATCH 019/138] Moved attached layout code to Avalonia.Layout. Also involved moving `Orientation` enum. --- .../Pages/ItemsRepeaterPage.xaml.cs | 1 + .../ViewModels/MainWindowViewModel.cs | 1 + src/Avalonia.Controls.DataGrid/DataGrid.cs | 1 + src/Avalonia.Controls/ContextMenu.cs | 1 + src/Avalonia.Controls/GridSplitter.cs | 1 + src/Avalonia.Controls/Menu.cs | 1 + .../Presenters/ItemVirtualizer.cs | 1 + src/Avalonia.Controls/Primitives/ScrollBar.cs | 1 + src/Avalonia.Controls/Primitives/TabStrip.cs | 1 + src/Avalonia.Controls/Primitives/Track.cs | 1 + src/Avalonia.Controls/ProgressBar.cs | 7 +-- .../Repeaters/ItemsRepeater.cs | 23 +++++----- .../Repeaters/RepeaterLayoutContext.cs | 5 ++- .../Repeaters/ViewManager.cs | 1 + .../Repeaters/ViewportManager.cs | 2 +- src/Avalonia.Controls/Slider.cs | 1 + src/Avalonia.Controls/StackPanel.cs | 4 +- src/Avalonia.Controls/WrapPanel.cs | 1 + .../AttachedLayout.cs} | 12 ++--- .../ElementManager.cs | 45 +++++++++---------- .../FlowLayoutAlgorithm.cs | 14 +++--- .../IFlowLayoutAlgorithmDelegates.cs | 12 ++--- .../LayoutContext.cs | 2 +- .../NonVirtualizingLayout.cs | 4 +- .../Orientation.cs | 2 +- .../OrientationBasedMeasures.cs | 40 ++++++++++------- .../StackLayout.cs | 20 ++++----- .../StackLayoutState.cs | 2 +- .../UniformGridLayout.cs | 20 ++++----- .../UniformGridLayoutState.cs | 8 ++-- .../Utils/ListUtils.cs | 2 +- .../VirtualizingLayout.cs | 27 +++++++++-- .../VirtualizingLayoutContext.cs | 14 +++--- .../Data/DefaultValueConverterTests.cs | 1 + .../Primitives/RangeBaseTests.cs | 3 +- .../Primitives/TrackTests.cs | 1 + .../ScrollViewerTests.cs | 3 +- .../SliderTests.cs | 1 + .../WrapPanelTests.cs | 3 +- .../Converters/NullableConverterTests.cs | 1 + .../VisualExtensions_GetVisualsAt.cs | 1 + 41 files changed, 166 insertions(+), 126 deletions(-) rename src/{Avalonia.Controls/Repeaters/Layout.cs => Avalonia.Layout/AttachedLayout.cs} (94%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/ElementManager.cs (89%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/FlowLayoutAlgorithm.cs (99%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/IFlowLayoutAlgorithmDelegates.cs (89%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/LayoutContext.cs (95%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/NonVirtualizingLayout.cs (80%) rename src/{Avalonia.Controls => Avalonia.Layout}/Orientation.cs (94%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/OrientationBasedMeasures.cs (67%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/StackLayout.cs (95%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/StackLayoutState.cs (98%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/UniformGridLayout.cs (96%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/UniformGridLayoutState.cs (97%) rename src/{Avalonia.Controls => Avalonia.Layout}/Utils/ListUtils.cs (95%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/VirtualizingLayout.cs (80%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/VirtualizingLayoutContext.cs (94%) diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index dccb242a78..f32d01ca38 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -5,6 +5,7 @@ using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Repeaters; +using Avalonia.Layout; using Avalonia.Markup.Xaml; namespace ControlCatalog.Pages diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index 80e0fb2586..93fe09a156 100644 --- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -9,6 +9,7 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using ReactiveUI.Legacy; using ReactiveUI; +using Avalonia.Layout; namespace VirtualizationDemo.ViewModels { diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index bcd12fbfbb..490a724eda 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -24,6 +24,7 @@ using System.Linq; using Avalonia.Input.Platform; using System.ComponentModel.DataAnnotations; using Avalonia.Controls.Utils; +using Avalonia.Layout; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 92293a32d6..cf5fa512e8 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.LogicalTree; namespace Avalonia.Controls diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 304a760216..28b9b3a38f 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.VisualTree; namespace Avalonia.Controls diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index b0fb3f2b3b..e65e674eb0 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Platform; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 46da8fe3f8..ae52e733b7 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -8,6 +8,7 @@ using System.Reactive.Linq; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls.Presenters { diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index e1b3061b54..c6119e89dc 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -7,6 +7,7 @@ using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Interactivity; using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs index 0e15ae4d7b..346d4340fb 100644 --- a/src/Avalonia.Controls/Primitives/TabStrip.cs +++ b/src/Avalonia.Controls/Primitives/TabStrip.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index c96fea6c25..21a7dd68f8 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.Metadata; namespace Avalonia.Controls.Primitives diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index a0f51099cd..29e3a17f74 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -3,6 +3,7 @@ using Avalonia.Controls.Primitives; +using Avalonia.Layout; namespace Avalonia.Controls { @@ -33,8 +34,8 @@ namespace Avalonia.Controls static ProgressBar() { - PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical"); - PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal"); + PseudoClass(OrientationProperty, o => o == Orientation.Vertical, ":vertical"); + PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal"); PseudoClass(IsIndeterminateProperty, ":indeterminate"); ValueProperty.Changed.AddClassHandler(x => x.UpdateIndicatorWhenPropChanged); @@ -120,4 +121,4 @@ namespace Avalonia.Controls UpdateIndicator(Bounds.Size); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs index 2f39d62e08..d6e03d804f 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs @@ -8,6 +8,7 @@ using System.Collections; using System.Collections.Specialized; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls.Repeaters { @@ -38,8 +39,8 @@ namespace Avalonia.Controls.Repeaters /// /// Defines the property. /// - public static readonly AvaloniaProperty LayoutProperty = - AvaloniaProperty.Register(nameof(Layout), new StackLayout()); + public static readonly AvaloniaProperty LayoutProperty = + AvaloniaProperty.Register(nameof(Layout), new StackLayout()); /// /// Defines the property. @@ -58,7 +59,6 @@ namespace Avalonia.Controls.Repeaters private IEnumerable _items; private VirtualizingLayoutContext _layoutContext; private NotifyCollectionChangedEventArgs _processingItemsSourceChange; - private Size _lastAvailableSize; private bool _isLayoutInProgress; private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs; private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; @@ -87,7 +87,7 @@ namespace Avalonia.Controls.Repeaters /// The layout used to size and position elements. The default is a StackLayout with /// vertical orientation. /// - public Layout Layout + public AttachedLayout Layout { get => GetValue(LayoutProperty); set => SetValue(LayoutProperty, value); @@ -300,7 +300,6 @@ namespace Avalonia.Controls.Repeaters } _viewportManager.SetLayoutExtent(extent); - _lastAvailableSize = availableSize; return desiredSize; } finally @@ -396,7 +395,7 @@ namespace Avalonia.Controls.Repeaters } else if (property == LayoutProperty) { - OnLayoutChanged((Layout)args.OldValue, (Layout)args.NewValue); + OnLayoutChanged((AttachedLayout)args.OldValue, (AttachedLayout)args.NewValue); } else if (property == HorizontalCacheLengthProperty) { @@ -479,7 +478,7 @@ namespace Avalonia.Controls.Repeaters throw new InvalidOperationException("Cannot make an Anchor when there is no attached layout."); } - element = GetLayoutContext().GetOrCreateElementAt(index); + element = (IControl)GetLayoutContext().GetOrCreateElementAt(index); element.Measure(Size.Infinity); } @@ -566,7 +565,7 @@ namespace Avalonia.Controls.Repeaters if (Layout is VirtualizingLayout virtualLayout) { var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); - virtualLayout.OnItemsChangedCore(GetLayoutContext(), newValue, args); + virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); } else if (Layout is NonVirtualizingLayout nonVirtualLayout) { @@ -605,14 +604,14 @@ namespace Avalonia.Controls.Repeaters try { - virtualLayout.OnItemsChangedCore(GetLayoutContext(), newValue, args); + virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); } finally { _processingItemsSourceChange = null; } } - else if (Layout is NonVirtualizingLayout nonVirtualLayout) + else if (Layout is NonVirtualizingLayout) { // Walk through all the elements and make sure they are cleared for // non-virtualizing layouts. @@ -631,7 +630,7 @@ namespace Avalonia.Controls.Repeaters InvalidateMeasure(); } - private void OnLayoutChanged(Layout oldValue, Layout newValue) + private void OnLayoutChanged(AttachedLayout oldValue, AttachedLayout newValue) { if (_isLayoutInProgress) { @@ -693,7 +692,7 @@ namespace Avalonia.Controls.Repeaters { if (Layout is VirtualizingLayout virtualLayout) { - virtualLayout.OnItemsChangedCore(GetLayoutContext(), sender, args); + virtualLayout.OnItemsChanged(GetLayoutContext(), sender, args); } else { diff --git a/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs b/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs index c4fdc0021f..acdecde9d0 100644 --- a/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs +++ b/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Text; +using Avalonia.Layout; namespace Avalonia.Controls.Repeaters { @@ -47,7 +48,7 @@ namespace Avalonia.Controls.Repeaters protected override int ItemCountCore() => _owner.ItemsSourceView?.Count ?? 0; - protected override IControl GetOrCreateElementAtCore(int index, ElementRealizationOptions options) + protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options) { return _owner.GetElementImpl( index, @@ -57,7 +58,7 @@ namespace Avalonia.Controls.Repeaters protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index); - protected override void RecycleElementCore(IControl element) => _owner.ClearElementImpl(element); + protected override void RecycleElementCore(ILayoutable element) => _owner.ClearElementImpl((IControl)element); protected override Rect RealizationRectCore() => _owner.RealizationWindow; } diff --git a/src/Avalonia.Controls/Repeaters/ViewManager.cs b/src/Avalonia.Controls/Repeaters/ViewManager.cs index 3578ef0498..441946b323 100644 --- a/src/Avalonia.Controls/Repeaters/ViewManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewManager.cs @@ -9,6 +9,7 @@ using System.Collections.Specialized; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.VisualTree; namespace Avalonia.Controls.Repeaters diff --git a/src/Avalonia.Controls/Repeaters/ViewportManager.cs b/src/Avalonia.Controls/Repeaters/ViewportManager.cs index c32ac3855f..6696e645eb 100644 --- a/src/Avalonia.Controls/Repeaters/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewportManager.cs @@ -234,7 +234,7 @@ namespace Avalonia.Controls.Repeaters ////element.CanBeScrollAnchor(true); } - public void OnElementCleared(IControl element) + public void OnElementCleared(ILayoutable element) { ////element.CanBeScrollAnchor(false); } diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index bc4733296b..9eaa246434 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -5,6 +5,7 @@ using System; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index b391ff061d..d7d49c1870 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -17,13 +17,13 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty SpacingProperty = - AvaloniaProperty.Register(nameof(Spacing)); + StackLayout.SpacingProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); + StackLayout.OrientationProperty.AddOwner(); /// /// Initializes static members of the class. diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 4df1b39400..0dab3e326a 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.Utilities; using static System.Math; diff --git a/src/Avalonia.Controls/Repeaters/Layout.cs b/src/Avalonia.Layout/AttachedLayout.cs similarity index 94% rename from src/Avalonia.Controls/Repeaters/Layout.cs rename to src/Avalonia.Layout/AttachedLayout.cs index bf7ce95a23..5622731a7c 100644 --- a/src/Avalonia.Controls/Repeaters/Layout.cs +++ b/src/Avalonia.Layout/AttachedLayout.cs @@ -5,12 +5,12 @@ using System; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { /// /// Represents the base class for an object that sizes and arranges child elements for a host. /// - public abstract class Layout : AvaloniaObject + public abstract class AttachedLayout : AvaloniaObject { internal string LayoutId { get; set; } @@ -26,7 +26,7 @@ namespace Avalonia.Controls.Repeaters /// /// Initializes any per-container state the layout requires when it is attached to an - /// container. + /// container. /// /// /// The context object that facilitates communication between the layout and its host @@ -49,7 +49,7 @@ namespace Avalonia.Controls.Repeaters public abstract void InitializeForContext(LayoutContext context); /// - /// Removes any state the layout previously stored on the IControl container. + /// Removes any state the layout previously stored on the ILayoutable container. /// /// /// The context object that facilitates communication between the layout and its host @@ -61,7 +61,7 @@ namespace Avalonia.Controls.Repeaters /// Suggests a DesiredSize for a container element. A container element that supports /// attached layouts should call this method from their own MeasureOverride implementations /// to form a recursive layout update. The attached layout is expected to call the Measure - /// for each of the container’s IControl children. + /// for each of the container’s ILayoutable children. /// /// /// The context object that facilitates communication between the layout and its host @@ -91,7 +91,7 @@ namespace Avalonia.Controls.Repeaters public abstract Size Arrange(LayoutContext context, Size finalSize); /// - /// Invalidates the measurement state (layout) for all IControl containers that reference + /// Invalidates the measurement state (layout) for all ILayoutable containers that reference /// this layout. /// protected void InvalidateMeasure() => MeasureInvalidated?.Invoke(this, EventArgs.Empty); diff --git a/src/Avalonia.Controls/Repeaters/ElementManager.cs b/src/Avalonia.Layout/ElementManager.cs similarity index 89% rename from src/Avalonia.Controls/Repeaters/ElementManager.cs rename to src/Avalonia.Layout/ElementManager.cs index 871e0527cd..0a8816480f 100644 --- a/src/Avalonia.Controls/Repeaters/ElementManager.cs +++ b/src/Avalonia.Layout/ElementManager.cs @@ -6,14 +6,13 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Text; -using Avalonia.Controls.Utils; +using Avalonia.Layout.Utils; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { internal class ElementManager { - private readonly List _realizedElements = new List(); + private readonly List _realizedElements = new List(); private readonly List _realizedElementLayoutBounds = new List(); private int _firstRealizedDataIndex; private VirtualizingLayoutContext _context; @@ -34,7 +33,7 @@ namespace Avalonia.Controls.Repeaters public void SetContext(VirtualizingLayoutContext virtualContext) => _context = virtualContext; - public void OnBeginMeasure(Orientation orientation) + public void OnBeginMeasure(ScrollOrientation orientation) { if (_context != null) { @@ -69,9 +68,9 @@ namespace Avalonia.Controls.Repeaters return IsVirtualizingContext ? _realizedElements.Count : _context.ItemCount; } - public IControl GetAt(int realizedIndex) + public ILayoutable GetAt(int realizedIndex) { - IControl element; + ILayoutable element; if (IsVirtualizingContext) { @@ -100,7 +99,7 @@ namespace Avalonia.Controls.Repeaters return element; } - public void Add(IControl element, int dataIndex) + public void Add(ILayoutable element, int dataIndex) { if (_realizedElements.Count == 0) { @@ -111,7 +110,7 @@ namespace Avalonia.Controls.Repeaters _realizedElementLayoutBounds.Add(default); } - public void Insert(int realizedIndex, int dataIndex, IControl element) + public void Insert(int realizedIndex, int dataIndex, ILayoutable element) { if (realizedIndex == 0) { @@ -208,7 +207,7 @@ namespace Avalonia.Controls.Repeaters public bool IsIndexValidInData(int currentIndex) => currentIndex >= 0 && currentIndex < _context.ItemCount; - public IControl GetRealizedElement(int dataIndex) + public ILayoutable GetRealizedElement(int dataIndex) { return IsVirtualizingContext ? GetAt(GetRealizedRangeIndexFromDataIndex(dataIndex)) : @@ -236,7 +235,7 @@ namespace Avalonia.Controls.Repeaters } } - public bool IsWindowConnected(in Rect window, Orientation orientation, bool scrollOrientationSameAsFlow) + public bool IsWindowConnected(in Rect window, ScrollOrientation orientation, bool scrollOrientationSameAsFlow) { bool intersects = false; @@ -246,14 +245,14 @@ namespace Avalonia.Controls.Repeaters var lastElementBounds = GetLayoutBoundsForRealizedIndex(GetRealizedElementCount() - 1); var effectiveOrientation = scrollOrientationSameAsFlow ? - (orientation == Orientation.Vertical ? Orientation.Horizontal : Orientation.Vertical) : + (orientation == ScrollOrientation.Vertical ? ScrollOrientation.Horizontal : ScrollOrientation.Vertical) : orientation; - var windowStart = effectiveOrientation == Orientation.Vertical ? window.Y : window.X; - var windowEnd = effectiveOrientation == Orientation.Vertical ? window.Y + window.Height : window.X + window.Width; - var firstElementStart = effectiveOrientation == Orientation.Vertical ? firstElementBounds.Y : firstElementBounds.X; - var lastElementEnd = effectiveOrientation == Orientation.Vertical ? lastElementBounds.Y + lastElementBounds.Height : lastElementBounds.X + lastElementBounds.Width; + 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 && @@ -298,7 +297,7 @@ namespace Avalonia.Controls.Repeaters } } - public int GetElementDataIndex(IControl suggestedAnchor) + public int GetElementDataIndex(ILayoutable suggestedAnchor) { var it = _realizedElements.IndexOf(suggestedAnchor); return it != -1 ? GetDataIndexFromRealizedRangeIndex(it) : -1; @@ -314,7 +313,7 @@ namespace Avalonia.Controls.Repeaters return IsVirtualizingContext ? dataIndex - _firstRealizedDataIndex : dataIndex; } - private void DiscardElementsOutsideWindow(in Rect window, Orientation orientation) + 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 @@ -369,12 +368,12 @@ namespace Avalonia.Controls.Repeaters } } - private static bool Intersects(in Rect lhs, in Rect rhs, Orientation orientation) + private static bool Intersects(in Rect lhs, in Rect rhs, ScrollOrientation orientation) { - var lhsStart = orientation == Orientation.Vertical ? lhs.Y : lhs.X; - var lhsEnd = orientation == Orientation.Vertical ? lhs.Y + lhs.Height : lhs.X + lhs.Width; - var rhsStart = orientation == Orientation.Vertical ? rhs.Y : rhs.X; - var rhsEnd = orientation == Orientation.Vertical ? rhs.Y + rhs.Height : rhs.X + rhs.Width; + 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; } diff --git a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs similarity index 99% rename from src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs rename to src/Avalonia.Layout/FlowLayoutAlgorithm.cs index 27eef3f35e..0617a96825 100644 --- a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -6,7 +6,7 @@ using System; using System.Collections.Specialized; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { internal class FlowLayoutAlgorithm { @@ -72,7 +72,7 @@ namespace Avalonia.Controls.Repeaters bool isWrapping, double minItemSpacing, double lineSpacing, - Orientation orientation, + ScrollOrientation orientation, string layoutId) { _orientation.ScrollOrientation = orientation; @@ -135,7 +135,7 @@ namespace Avalonia.Controls.Repeaters } public Size MeasureElement( - IControl element, + ILayoutable element, int index, Size availableSize, VirtualizingLayoutContext context) @@ -469,9 +469,9 @@ namespace Avalonia.Controls.Repeaters private Rect EstimateExtent(Size availableSize, string layoutId) { - IControl firstRealizedElement = null; + ILayoutable firstRealizedElement = null; Rect firstBounds = new Rect(); - IControl lastRealizedElement = null; + ILayoutable lastRealizedElement = null; Rect lastBounds = new Rect(); int firstDataIndex = -1; int lastDataIndex = -1; @@ -667,7 +667,7 @@ namespace Avalonia.Controls.Repeaters } } - public IControl GetElementIfRealized(int dataIndex) + public ILayoutable GetElementIfRealized(int dataIndex) { if (_elementManager.IsDataIndexRealized(dataIndex)) { @@ -677,7 +677,7 @@ namespace Avalonia.Controls.Repeaters return null; } - public bool TryAddElement0(IControl element) + public bool TryAddElement0(ILayoutable element) { if (_elementManager.GetRealizedElementCount() == 0) { diff --git a/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs b/src/Avalonia.Layout/IFlowLayoutAlgorithmDelegates.cs similarity index 89% rename from src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs rename to src/Avalonia.Layout/IFlowLayoutAlgorithmDelegates.cs index 04d715ea59..907a3adf0f 100644 --- a/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs +++ b/src/Avalonia.Layout/IFlowLayoutAlgorithmDelegates.cs @@ -3,11 +3,7 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -using System; -using System.Collections.Generic; -using System.Text; - -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { internal struct FlowLayoutAnchorInfo { @@ -25,14 +21,14 @@ namespace Avalonia.Controls.Repeaters Rect Algorithm_GetExtent( Size availableSize, VirtualizingLayoutContext context, - IControl firstRealized, + ILayoutable firstRealized, int firstRealizedItemIndex, Rect firstRealizedLayoutBounds, - IControl lastRealized, + ILayoutable lastRealized, int lastRealizedItemIndex, Rect lastRealizedLayoutBounds); void Algorithm_OnElementMeasured( - IControl element, + ILayoutable element, int index, Size availableSize, Size measureSize, diff --git a/src/Avalonia.Controls/Repeaters/LayoutContext.cs b/src/Avalonia.Layout/LayoutContext.cs similarity index 95% rename from src/Avalonia.Controls/Repeaters/LayoutContext.cs rename to src/Avalonia.Layout/LayoutContext.cs index 1a4b26c547..45a8048ea2 100644 --- a/src/Avalonia.Controls/Repeaters/LayoutContext.cs +++ b/src/Avalonia.Layout/LayoutContext.cs @@ -3,7 +3,7 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { /// /// Represents the base class for an object that facilitates communication between an attached diff --git a/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs similarity index 80% rename from src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs rename to src/Avalonia.Layout/NonVirtualizingLayout.cs index bac2e10f88..681096ab19 100644 --- a/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs +++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs @@ -3,13 +3,13 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { /// /// Represents the base class for an object that sizes and arranges child elements for a host /// and and does not support virtualization. /// - public abstract class NonVirtualizingLayout : Layout + public abstract class NonVirtualizingLayout : AttachedLayout { } } diff --git a/src/Avalonia.Controls/Orientation.cs b/src/Avalonia.Layout/Orientation.cs similarity index 94% rename from src/Avalonia.Controls/Orientation.cs rename to src/Avalonia.Layout/Orientation.cs index fe998c024a..f03b087adc 100644 --- a/src/Avalonia.Controls/Orientation.cs +++ b/src/Avalonia.Layout/Orientation.cs @@ -1,7 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -namespace Avalonia.Controls +namespace Avalonia.Layout { /// /// Defines vertical or horizontal orientation. diff --git a/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs b/src/Avalonia.Layout/OrientationBasedMeasures.cs similarity index 67% rename from src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs rename to src/Avalonia.Layout/OrientationBasedMeasures.cs index 6f133e6286..23a8b0e168 100644 --- a/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs +++ b/src/Avalonia.Layout/OrientationBasedMeasures.cs @@ -3,24 +3,30 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { + internal enum ScrollOrientation + { + Vertical, + Horizontal, + } + internal class OrientationBasedMeasures { - public Orientation ScrollOrientation { get; set; } = Orientation.Vertical; + public ScrollOrientation ScrollOrientation { get; set; } = ScrollOrientation.Vertical; - public double Major(in Size size) => ScrollOrientation == Orientation.Vertical ? size.Height : size.Width; - public double Minor(in Size size) => ScrollOrientation == Orientation.Vertical ? size.Width : size.Height; - public double MajorSize(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Height : rect.Width; - public double MinorSize(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Width : rect.Height; - public double MajorStart(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Y : rect.X; - public double MinorStart(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.X : rect.Y; - public double MajorEnd(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Bottom : rect.Right; - public double MinorEnd(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Right : rect.Bottom; + public double Major(in Size size) => ScrollOrientation == ScrollOrientation.Vertical ? size.Height : size.Width; + public double Minor(in Size size) => ScrollOrientation == ScrollOrientation.Vertical ? size.Width : size.Height; + public double MajorSize(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Height : rect.Width; + public double MinorSize(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Width : rect.Height; + public double MajorStart(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Y : rect.X; + public double MinorStart(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.X : rect.Y; + public double MajorEnd(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Bottom : rect.Right; + public double MinorEnd(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Right : rect.Bottom; public void SetMajorSize(ref Rect rect, double value) { - if (ScrollOrientation == Orientation.Vertical) + if (ScrollOrientation == ScrollOrientation.Vertical) { rect = rect.WithHeight(value); } @@ -32,7 +38,7 @@ namespace Avalonia.Controls.Repeaters public void SetMinorSize(ref Rect rect, double value) { - if (ScrollOrientation == Orientation.Vertical) + if (ScrollOrientation == ScrollOrientation.Vertical) { rect = rect.WithWidth(value); } @@ -44,7 +50,7 @@ namespace Avalonia.Controls.Repeaters public void SetMajorStart(ref Rect rect, double value) { - if (ScrollOrientation == Orientation.Vertical) + if (ScrollOrientation == ScrollOrientation.Vertical) { rect = rect.WithY(value); } @@ -56,7 +62,7 @@ namespace Avalonia.Controls.Repeaters public void SetMinorStart(ref Rect rect, double value) { - if (ScrollOrientation == Orientation.Vertical) + if (ScrollOrientation == ScrollOrientation.Vertical) { rect = rect.WithX(value); } @@ -68,21 +74,21 @@ namespace Avalonia.Controls.Repeaters public Rect MinorMajorRect(double minor, double major, double minorSize, double majorSize) { - return ScrollOrientation == Orientation.Vertical ? + return ScrollOrientation == ScrollOrientation.Vertical ? new Rect(minor, major, minorSize, majorSize) : new Rect(major, minor, majorSize, minorSize); } public Point MinorMajorPoint(double minor, double major) { - return ScrollOrientation == Orientation.Vertical ? + return ScrollOrientation == ScrollOrientation.Vertical ? new Point(minor, major) : new Point(major, minor); } public Size MinorMajorSize(double minor, double major) { - return ScrollOrientation == Orientation.Vertical ? + return ScrollOrientation == ScrollOrientation.Vertical ? new Size(minor, major) : new Size(major, minor); } diff --git a/src/Avalonia.Controls/Repeaters/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs similarity index 95% rename from src/Avalonia.Controls/Repeaters/StackLayout.cs rename to src/Avalonia.Layout/StackLayout.cs index 8b887b149e..3cc094f091 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -6,7 +6,7 @@ using System; using System.Collections.Specialized; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { /// /// Arranges elements into a single line (with spacing) that can be oriented horizontally or vertically. @@ -17,13 +17,13 @@ namespace Avalonia.Controls.Repeaters /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - StackPanel.OrientationProperty.AddOwner(); + AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); /// /// Defines the property. /// public static readonly StyledProperty SpacingProperty = - StackPanel.SpacingProperty.AddOwner(); + AvaloniaProperty.Register(nameof(Spacing)); private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); @@ -61,10 +61,10 @@ namespace Avalonia.Controls.Repeaters internal Rect GetExtent( Size availableSize, VirtualizingLayoutContext context, - IControl firstRealized, + ILayoutable firstRealized, int firstRealizedItemIndex, Rect firstRealizedLayoutBounds, - IControl lastRealized, + ILayoutable lastRealized, int lastRealizedItemIndex, Rect lastRealizedLayoutBounds) { @@ -97,7 +97,7 @@ namespace Avalonia.Controls.Repeaters } internal void OnElementMeasured( - IControl element, + ILayoutable element, int index, Size availableSize, Size measureSize, @@ -164,10 +164,10 @@ namespace Avalonia.Controls.Repeaters Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent( Size availableSize, VirtualizingLayoutContext context, - IControl firstRealized, + ILayoutable firstRealized, int firstRealizedItemIndex, Rect firstRealizedLayoutBounds, - IControl lastRealized, + ILayoutable lastRealized, int lastRealizedItemIndex, Rect lastRealizedLayoutBounds) { @@ -182,7 +182,7 @@ namespace Avalonia.Controls.Repeaters lastRealizedLayoutBounds); } - void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(IControl element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context) + void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(ILayoutable element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context) { OnElementMeasured( element, @@ -295,7 +295,7 @@ namespace Avalonia.Controls.Repeaters { //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation. //Horizontal Orientation means we have a Horizontal ScrollOrientation. - _orientation.ScrollOrientation = (Orientation)e.NewValue; + _orientation.ScrollOrientation = (ScrollOrientation)e.NewValue; } InvalidateLayout(); diff --git a/src/Avalonia.Controls/Repeaters/StackLayoutState.cs b/src/Avalonia.Layout/StackLayoutState.cs similarity index 98% rename from src/Avalonia.Controls/Repeaters/StackLayoutState.cs rename to src/Avalonia.Layout/StackLayoutState.cs index 130522c908..05ad9bca8e 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayoutState.cs +++ b/src/Avalonia.Layout/StackLayoutState.cs @@ -7,7 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { /// /// Represents the state of a StackLayout. diff --git a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs similarity index 96% rename from src/Avalonia.Controls/Repeaters/UniformGridLayout.cs rename to src/Avalonia.Layout/UniformGridLayout.cs index 4c2327b15b..6d157715a3 100644 --- a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -6,7 +6,7 @@ using System; using System.Collections.Specialized; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { /// /// Defines constants that specify how items are aligned on the non-scrolling or non-virtualizing axis. @@ -114,7 +114,7 @@ namespace Avalonia.Controls.Repeaters /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - StackPanel.OrientationProperty.AddOwner(); + StackLayout.OrientationProperty.AddOwner(); private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); private double _minItemWidth = double.NaN; @@ -316,10 +316,10 @@ namespace Avalonia.Controls.Repeaters Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent( Size availableSize, VirtualizingLayoutContext context, - IControl firstRealized, + ILayoutable firstRealized, int firstRealizedItemIndex, Rect firstRealizedLayoutBounds, - IControl lastRealized, + ILayoutable lastRealized, int lastRealizedItemIndex, Rect lastRealizedLayoutBounds) { @@ -359,7 +359,7 @@ namespace Avalonia.Controls.Repeaters return extent; } - void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(IControl element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context) + void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(ILayoutable element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context) { } @@ -440,7 +440,7 @@ namespace Avalonia.Controls.Repeaters //Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation. //i.e. the properties are the inverse of each other. - var scrollOrientation = (orientation == Orientation.Horizontal) ? Orientation.Vertical : Orientation.Horizontal; + var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal; _orientation.ScrollOrientation = scrollOrientation; } else if (args.Property == MinColumnSpacingProperty) @@ -475,7 +475,7 @@ namespace Avalonia.Controls.Repeaters { var minItemSpacing = MinItemSpacing; var gridState = (UniformGridLayoutState)context.LayoutState; - return _orientation.ScrollOrientation == Orientation.Vertical? + return _orientation.ScrollOrientation == ScrollOrientation.Vertical? gridState.EffectiveItemWidth + minItemSpacing : gridState.EffectiveItemHeight + minItemSpacing; } @@ -484,7 +484,7 @@ namespace Avalonia.Controls.Repeaters { var lineSpacing = LineSpacing; var gridState = (UniformGridLayoutState)context.LayoutState; - return _orientation.ScrollOrientation == Orientation.Vertical ? + return _orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemHeight + lineSpacing : gridState.EffectiveItemWidth + lineSpacing; } @@ -503,8 +503,8 @@ namespace Avalonia.Controls.Repeaters Rect bounds = _orientation.MinorMajorRect( indexInRow * GetMinorSizeWithSpacing(context) + _orientation.MinorStart(lastExtent), rowIndex * GetMajorSizeWithSpacing(context) + _orientation.MajorStart(lastExtent), - _orientation.ScrollOrientation == Orientation.Vertical ? gridState.EffectiveItemWidth : gridState.EffectiveItemHeight, - _orientation.ScrollOrientation == Orientation.Vertical ? gridState.EffectiveItemHeight : gridState.EffectiveItemWidth); + _orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemWidth : gridState.EffectiveItemHeight, + _orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemHeight : gridState.EffectiveItemWidth); return bounds; } diff --git a/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs b/src/Avalonia.Layout/UniformGridLayoutState.cs similarity index 97% rename from src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs rename to src/Avalonia.Layout/UniformGridLayoutState.cs index 5095ff35a6..37660d0354 100644 --- a/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs +++ b/src/Avalonia.Layout/UniformGridLayoutState.cs @@ -4,11 +4,9 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; -using System.Collections.Generic; using System.Collections.Specialized; -using System.Text; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { /// /// Represents the state of a . @@ -20,7 +18,7 @@ namespace Avalonia.Controls.Repeaters // If it does not, then we need to do context.GetElement(0) at which point we have requested an element and are on point to clear it. // If we are responsible for clearing element 0 we keep m_cachedFirstElement valid. // If we are not (because FlowLayoutAlgorithm is holding it for us) then we just null out this field and use the one from FlowLayoutAlgorithm. - private IControl _cachedFirstElement; + private ILayoutable _cachedFirstElement; internal FlowLayoutAlgorithm FlowAlgorithm { get; } = new FlowLayoutAlgorithm(); internal double EffectiveItemWidth { get; private set; } @@ -86,7 +84,7 @@ namespace Avalonia.Controls.Repeaters } private void SetSize( - IControl element, + ILayoutable element, double layoutItemWidth, double LayoutItemHeight, Size availableSize, diff --git a/src/Avalonia.Controls/Utils/ListUtils.cs b/src/Avalonia.Layout/Utils/ListUtils.cs similarity index 95% rename from src/Avalonia.Controls/Utils/ListUtils.cs rename to src/Avalonia.Layout/Utils/ListUtils.cs index 8599c53057..eb2480acd3 100644 --- a/src/Avalonia.Controls/Utils/ListUtils.cs +++ b/src/Avalonia.Layout/Utils/ListUtils.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; -namespace Avalonia.Controls.Utils +namespace Avalonia.Layout.Utils { internal static class ListUtils { diff --git a/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs b/src/Avalonia.Layout/VirtualizingLayout.cs similarity index 80% rename from src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs rename to src/Avalonia.Layout/VirtualizingLayout.cs index 08c72b188e..c25277b9ce 100644 --- a/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs +++ b/src/Avalonia.Layout/VirtualizingLayout.cs @@ -5,13 +5,13 @@ using System.Collections.Specialized; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { /// /// Represents the base class for an object that sizes and arranges child elements for a host /// and supports virtualization. /// - public abstract class VirtualizingLayout : Layout + public abstract class VirtualizingLayout : AttachedLayout { /// public sealed override void InitializeForContext(LayoutContext context) @@ -37,9 +37,28 @@ namespace Avalonia.Controls.Repeaters return ArrangeOverride((VirtualizingLayoutContext)context, finalSize); } + /// + /// Notifies the layout when the data collection assigned to the container element (Items) + /// has changed. + /// + /// + /// The context object that facilitates communication between the layout and its host + /// container. + /// + /// The data source. + /// Data about the collection change. + /// + /// Override + /// to provide the behavior for this method in a derived class. + /// + public void OnItemsChanged( + VirtualizingLayoutContext context, + object source, + NotifyCollectionChangedEventArgs args) => OnItemsChangedCore(context, source, args); + /// /// When overridden in a derived class, initializes any per-container state the layout - /// requires when it is attached to an IControl container. + /// requires when it is attached to an ILayoutable container. /// /// /// The context object that facilitates communication between the layout and its host @@ -51,7 +70,7 @@ namespace Avalonia.Controls.Repeaters /// /// When overridden in a derived class, removes any state the layout previously stored on - /// the IControl container. + /// the ILayoutable container. /// /// /// The context object that facilitates communication between the layout and its host diff --git a/src/Avalonia.Controls/Repeaters/VirtualizingLayoutContext.cs b/src/Avalonia.Layout/VirtualizingLayoutContext.cs similarity index 94% rename from src/Avalonia.Controls/Repeaters/VirtualizingLayoutContext.cs rename to src/Avalonia.Layout/VirtualizingLayoutContext.cs index da8d6f0f03..980daec2eb 100644 --- a/src/Avalonia.Controls/Repeaters/VirtualizingLayoutContext.cs +++ b/src/Avalonia.Layout/VirtualizingLayoutContext.cs @@ -5,7 +5,7 @@ using System; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { /// /// Defines constants that specify whether to suppress automatic recycling of the retrieved @@ -114,7 +114,7 @@ namespace Avalonia.Controls.Repeaters /// This method calls /// with options set to None. GetElementAtCore must be implemented in a derived class. /// - public IControl GetOrCreateElementAt(int index) + public ILayoutable GetOrCreateElementAt(int index) => GetOrCreateElementAtCore(index, ElementRealizationOptions.None); /// @@ -138,7 +138,7 @@ namespace Avalonia.Controls.Repeaters /// advanced layouts that choose to explicitly manage the realization and recycling of /// elements as a performance optimization. /// - public IControl GetOrCreateElementAt(int index, ElementRealizationOptions options) + public ILayoutable GetOrCreateElementAt(int index, ElementRealizationOptions options) => GetOrCreateElementAtCore(index, options); /// @@ -146,10 +146,10 @@ namespace Avalonia.Controls.Repeaters /// /// The element to clear. /// - /// This method calls , which must be implemented + /// This method calls , which must be implemented /// in a derived class. /// - public void RecycleElement(IControl element) => RecycleElementCore(element); + public void RecycleElement(ILayoutable element) => RecycleElementCore(element); /// /// When implemented in a derived class, retrieves the number of items in the data. @@ -178,13 +178,13 @@ namespace Avalonia.Controls.Repeaters /// A value of that specifies whether to suppress /// automatic recycling of the retrieved element or force creation of a new element. /// - protected abstract IControl GetOrCreateElementAtCore(int index, ElementRealizationOptions options); + protected abstract ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options); /// /// When implemented in a derived class, clears the specified UIElement and allows it to be /// either re-used or released. /// /// The element to clear. - protected abstract void RecycleElementCore(IControl element); + protected abstract void RecycleElementCore(ILayoutable element); } } diff --git a/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs b/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs index eeb502d730..ecf559951a 100644 --- a/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs @@ -8,6 +8,7 @@ using Xunit; using System.Windows.Input; using System; using Avalonia.Data.Converters; +using Avalonia.Layout; namespace Avalonia.Base.UnitTests.Data.Converters { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs index 42578c61ac..bbeba2fe2e 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Layout; using Avalonia.Markup.Data; using Avalonia.Styling; using Xunit; @@ -199,4 +200,4 @@ namespace Avalonia.Controls.UnitTests.Primitives } } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs index 5396a43f3a..dde0e0000d 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Controls.Primitives; +using Avalonia.Layout; using Avalonia.LogicalTree; using Xunit; diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index d1385176c5..cd27436241 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Layout; using Xunit; namespace Avalonia.Controls.UnitTests @@ -113,4 +114,4 @@ namespace Avalonia.Controls.UnitTests }; } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/SliderTests.cs b/tests/Avalonia.Controls.UnitTests/SliderTests.cs index dc47d9eb89..1c3c052144 100644 --- a/tests/Avalonia.Controls.UnitTests/SliderTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SliderTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using Avalonia.Layout; using Xunit; namespace Avalonia.Controls.UnitTests diff --git a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs index cd35627064..a0511761e4 100644 --- a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Layout; using Xunit; namespace Avalonia.Controls.UnitTests @@ -93,4 +94,4 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(100, 0, 100, 50), target.Children[1].Bounds); } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs index abe6fa84b0..bb44d069b5 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Avalonia.Layout; using Avalonia.UnitTests; using Xunit; diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs index 867d4d7450..139a7925b1 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Avalonia.Controls; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Rendering; using Avalonia.UnitTests; From f6cba5ab698f36ffd6e6c88044415f735c0fcecf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 23:45:39 +0200 Subject: [PATCH 020/138] Implemented NonVirtualizingLayout. --- src/Avalonia.Layout/NonVirtualizingLayout.cs | 82 ++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/src/Avalonia.Layout/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs index 681096ab19..ad4dc302b0 100644 --- a/src/Avalonia.Layout/NonVirtualizingLayout.cs +++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs @@ -11,5 +11,87 @@ namespace Avalonia.Layout /// public abstract class NonVirtualizingLayout : AttachedLayout { + /// + public sealed override void InitializeForContext(LayoutContext context) + { + InitializeForContextCore((VirtualizingLayoutContext)context); + } + + /// + public sealed override void UninitializeForContext(LayoutContext context) + { + UninitializeForContextCore((VirtualizingLayoutContext)context); + } + + /// + public sealed override Size Measure(LayoutContext context, Size availableSize) + { + return MeasureOverride((VirtualizingLayoutContext)context, availableSize); + } + + /// + public sealed override Size Arrange(LayoutContext context, Size finalSize) + { + return ArrangeOverride((VirtualizingLayoutContext)context, finalSize); + } + + /// + /// When overridden in a derived class, initializes any per-container state the layout + /// requires when it is attached to an ILayoutable container. + /// + /// + /// The context object that facilitates communication between the layout and its host + /// container. + /// + protected virtual void InitializeForContextCore(VirtualizingLayoutContext context) + { + } + + /// + /// When overridden in a derived class, removes any state the layout previously stored on + /// the ILayoutable container. + /// + /// + /// The context object that facilitates communication between the layout and its host + /// container. + /// + protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context) + { + } + + /// + /// Provides the behavior for the "Measure" pass of the layout cycle. Classes can override + /// this method to define their own "Measure" pass behavior. + /// + /// + /// The context object that facilitates communication between the layout and its host + /// container. + /// + /// + /// The available size that this object can give to child objects. Infinity can be + /// specified as a value to indicate that the object will size to whatever content is + /// available. + /// + /// + /// The size that this object determines it needs during layout, based on its calculations + /// of the allocated sizes for child objects or based on other considerations such as a + /// fixed container size. + /// + protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize); + + /// + /// When implemented in a derived class, provides the behavior for the "Arrange" pass of + /// layout. Classes can override this method to define their own "Arrange" pass behavior. + /// + /// + /// The context object that facilitates communication between the layout and its host + /// container. + /// + /// + /// The final area within the container that this object should use to arrange itself and + /// its children. + /// + /// The actual size that is used after the element is arranged in layout. + protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize; } } From 9297d46e3adadbaeb22ef436e0196851f1be9fa7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 3 Jul 2019 00:15:08 +0200 Subject: [PATCH 021/138] More docs. --- src/Avalonia.Layout/NonVirtualizingLayout.cs | 6 ++++++ src/Avalonia.Layout/VirtualizingLayout.cs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/Avalonia.Layout/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs index ad4dc302b0..fba91e66c7 100644 --- a/src/Avalonia.Layout/NonVirtualizingLayout.cs +++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs @@ -9,6 +9,12 @@ namespace Avalonia.Layout /// Represents the base class for an object that sizes and arranges child elements for a host /// and and does not support virtualization. /// + /// + /// NonVirtualizingLayout is the base class for layouts that do not support virtualization. You + /// can inherit from it to create your own layout. + /// + /// A non-virtualizing layout can measure and arrange child elements. + /// public abstract class NonVirtualizingLayout : AttachedLayout { /// diff --git a/src/Avalonia.Layout/VirtualizingLayout.cs b/src/Avalonia.Layout/VirtualizingLayout.cs index c25277b9ce..4c601175f3 100644 --- a/src/Avalonia.Layout/VirtualizingLayout.cs +++ b/src/Avalonia.Layout/VirtualizingLayout.cs @@ -11,6 +11,12 @@ namespace Avalonia.Layout /// Represents the base class for an object that sizes and arranges child elements for a host /// and supports virtualization. /// + /// + /// is the base class for layouts that support virtualization. + /// You can use one of the provided derived class, or inherit from it to create your own layout. + /// Provided concrete virtualizing layout classes are and + /// . + /// public abstract class VirtualizingLayout : AttachedLayout { /// From 997fd1aa30ddf7bba3e6b94546c529e2dd554035 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 3 Jul 2019 09:27:29 +0200 Subject: [PATCH 022/138] Moved ItemsRepeater to Avalonia.Controls. Keep it in a subdirectory though. --- samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs | 4 ---- src/Avalonia.Controls/Properties/AssemblyInfo.cs | 1 - .../{Repeaters => Repeater}/ItemTemplateWrapper.cs | 2 +- .../{Repeaters => Repeater}/ItemsRepeater.cs | 2 +- .../ItemsRepeaterElementClearingEventArgs.cs | 2 +- .../ItemsRepeaterElementIndexChangedEventArgs.cs | 2 +- .../ItemsRepeaterElementPreparedEventArgs.cs | 2 +- .../{Repeaters => Repeater}/ItemsSourceView.cs | 2 +- src/Avalonia.Controls/{Repeaters => Repeater}/RecyclePool.cs | 2 +- .../{Repeaters => Repeater}/RepeaterLayoutContext.cs | 2 +- .../{Repeaters => Repeater}/UniqueIdElementPool.cs | 2 +- src/Avalonia.Controls/{Repeaters => Repeater}/ViewManager.cs | 2 +- .../{Repeaters => Repeater}/ViewportManager.cs | 2 +- .../{Repeaters => Repeater}/VirtualizationInfo.cs | 2 +- 14 files changed, 12 insertions(+), 17 deletions(-) rename src/Avalonia.Controls/{Repeaters => Repeater}/ItemTemplateWrapper.cs (97%) rename src/Avalonia.Controls/{Repeaters => Repeater}/ItemsRepeater.cs (99%) rename src/Avalonia.Controls/{Repeaters => Repeater}/ItemsRepeaterElementClearingEventArgs.cs (95%) rename src/Avalonia.Controls/{Repeaters => Repeater}/ItemsRepeaterElementIndexChangedEventArgs.cs (97%) rename src/Avalonia.Controls/{Repeaters => Repeater}/ItemsRepeaterElementPreparedEventArgs.cs (96%) rename src/Avalonia.Controls/{Repeaters => Repeater}/ItemsSourceView.cs (99%) rename src/Avalonia.Controls/{Repeaters => Repeater}/RecyclePool.cs (99%) rename src/Avalonia.Controls/{Repeaters => Repeater}/RepeaterLayoutContext.cs (98%) rename src/Avalonia.Controls/{Repeaters => Repeater}/UniqueIdElementPool.cs (97%) rename src/Avalonia.Controls/{Repeaters => Repeater}/ViewManager.cs (99%) rename src/Avalonia.Controls/{Repeaters => Repeater}/ViewportManager.cs (99%) rename src/Avalonia.Controls/{Repeaters => Repeater}/VirtualizationInfo.cs (99%) diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index f32d01ca38..214de89253 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections; -using System.Collections.Generic; using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Repeaters; using Avalonia.Layout; using Avalonia.Markup.Xaml; diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs index 8fa80e55d2..c04c66a77f 100644 --- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls/Properties/AssemblyInfo.cs @@ -13,7 +13,6 @@ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Embedding")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Presenters")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Repeaters")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Shapes")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Templates")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Notifications")] diff --git a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs similarity index 97% rename from src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs rename to src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs index 00da39cd9d..04d859c742 100644 --- a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs +++ b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs @@ -5,7 +5,7 @@ using Avalonia.Controls.Templates; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { internal class ItemTemplateWrapper { diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs similarity index 99% rename from src/Avalonia.Controls/Repeaters/ItemsRepeater.cs rename to src/Avalonia.Controls/Repeater/ItemsRepeater.cs index d6e03d804f..44783e2c97 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -10,7 +10,7 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { /// /// Represents a data-driven collection control that incorporates a flexible layout system, diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs similarity index 95% rename from src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs rename to src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs index ff011469e7..75d50e52a6 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs @@ -5,7 +5,7 @@ using System; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { /// /// Provides data for the event. diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs similarity index 97% rename from src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs rename to src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs index c30ba38d23..7ca68140b2 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs @@ -5,7 +5,7 @@ using System; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { /// /// Provides data for the event. diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs similarity index 96% rename from src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs rename to src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs index 9c26c0d136..5a30dbcf2a 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs @@ -3,7 +3,7 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { /// /// Provides data for the event. diff --git a/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs similarity index 99% rename from src/Avalonia.Controls/Repeaters/ItemsSourceView.cs rename to src/Avalonia.Controls/Repeater/ItemsSourceView.cs index 1caffe881a..732ba8501c 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs +++ b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs @@ -9,7 +9,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { /// /// Represents a standardized view of the supported interactions between a given ItemsSource diff --git a/src/Avalonia.Controls/Repeaters/RecyclePool.cs b/src/Avalonia.Controls/Repeater/RecyclePool.cs similarity index 99% rename from src/Avalonia.Controls/Repeaters/RecyclePool.cs rename to src/Avalonia.Controls/Repeater/RecyclePool.cs index a1fa05597b..4e5950bdc5 100644 --- a/src/Avalonia.Controls/Repeaters/RecyclePool.cs +++ b/src/Avalonia.Controls/Repeater/RecyclePool.cs @@ -9,7 +9,7 @@ using System.Linq; using System.Runtime.CompilerServices; using Avalonia.Controls.Templates; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { internal class RecyclePool { diff --git a/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs similarity index 98% rename from src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs rename to src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs index acdecde9d0..977d9d794c 100644 --- a/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs +++ b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs @@ -8,7 +8,7 @@ using System.Collections.Generic; using System.Text; using Avalonia.Layout; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { internal class RepeaterLayoutContext : VirtualizingLayoutContext { diff --git a/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs b/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs similarity index 97% rename from src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs rename to src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs index f360c4a9f9..775aa3f113 100644 --- a/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs +++ b/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs @@ -8,7 +8,7 @@ using System.Collections; using System.Collections.Generic; using System.Text; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { internal class UniqueIdElementPool : IEnumerable> { diff --git a/src/Avalonia.Controls/Repeaters/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs similarity index 99% rename from src/Avalonia.Controls/Repeaters/ViewManager.cs rename to src/Avalonia.Controls/Repeater/ViewManager.cs index 441946b323..b90f8dacfe 100644 --- a/src/Avalonia.Controls/Repeaters/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -12,7 +12,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.VisualTree; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { internal sealed class ViewManager { diff --git a/src/Avalonia.Controls/Repeaters/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs similarity index 99% rename from src/Avalonia.Controls/Repeaters/ViewportManager.cs rename to src/Avalonia.Controls/Repeater/ViewportManager.cs index 6696e645eb..6f0e792aa5 100644 --- a/src/Avalonia.Controls/Repeaters/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -11,7 +11,7 @@ using Avalonia.Layout; using Avalonia.Threading; using Avalonia.VisualTree; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { internal class ViewportManager { diff --git a/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs similarity index 99% rename from src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs rename to src/Avalonia.Controls/Repeater/VirtualizationInfo.cs index 0e33badef3..eb30c1b7cf 100644 --- a/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs +++ b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs @@ -5,7 +5,7 @@ using System; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { internal enum ElementOwner { From ac660567f737f990f00e0c757850f6f08e0839b0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 3 Jul 2019 11:20:09 +0200 Subject: [PATCH 023/138] Fix horizontal StackLayout. --- src/Avalonia.Layout/StackLayout.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index 3cc094f091..e89d178b0e 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -293,9 +293,11 @@ namespace Avalonia.Layout { if (e.Property == OrientationProperty) { + var orientation = (Orientation)e.NewValue; + //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation. //Horizontal Orientation means we have a Horizontal ScrollOrientation. - _orientation.ScrollOrientation = (ScrollOrientation)e.NewValue; + _orientation.ScrollOrientation = orientation == Orientation.Horizontal ? ScrollOrientation.Horizontal : ScrollOrientation.Vertical; } InvalidateLayout(); From 66e01468983b073d012d01f047abe4aae34823d5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 3 Jul 2019 13:05:02 +0200 Subject: [PATCH 024/138] Set default cache size to 0. Scrolling on UWP happens asynchronous to the UI thread, so `ItemsRepeater` on that platform generate an additional buffer of prepared elements sufficient to fill an area larger than the viewport. We don't have async scroll rendering on Avalonia so make the default cache size 0. --- src/Avalonia.Controls/Repeater/ViewportManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index 6f0e792aa5..10c11889d0 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -35,8 +35,8 @@ namespace Avalonia.Controls // actually happened. This can happen in cases where no scrollviewer // in the parent chain can scroll in the shift direction. private Point _unshiftableShift; - private double _maximumHorizontalCacheLength = 2.0; - private double _maximumVerticalCacheLength = 2.0; + private double _maximumHorizontalCacheLength = 0.0; + private double _maximumVerticalCacheLength = 0.0; private double _horizontalCacheBufferPerSide; private double _verticalCacheBufferPerSide; private bool _isBringIntoViewInProgress; From cd04e768f0939b1f187ba650800788d592fbb85b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 3 Jul 2019 13:45:21 +0200 Subject: [PATCH 025/138] Avoid reset of containers when anchor element is replaced in ItemsRepeater. * avoid reset of containers when anchor element is replaced * fix element 0 ownership issue Ported from https://github.com/microsoft/microsoft-ui-xaml/commit/e45e83d010502911aa1c7e14ea5bf681ceffb048 --- src/Avalonia.Layout/ElementManager.cs | 34 +++++++++++++++++-- src/Avalonia.Layout/UniformGridLayout.cs | 4 +++ src/Avalonia.Layout/UniformGridLayoutState.cs | 7 ++-- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Layout/ElementManager.cs b/src/Avalonia.Layout/ElementManager.cs index 0a8816480f..1748a3be03 100644 --- a/src/Avalonia.Layout/ElementManager.cs +++ b/src/Avalonia.Layout/ElementManager.cs @@ -276,8 +276,38 @@ namespace Avalonia.Layout case NotifyCollectionChangedAction.Replace: { - OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count); - OnItemsAdded(args.NewStartingIndex, args.NewItems.Count); + 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; diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index 6d157715a3..edc2042922 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -409,6 +409,10 @@ namespace Avalonia.Layout _orientation.ScrollOrientation, LayoutId); + // If after Measure the first item is in the realization rect, then we revoke grid state's ownership, + // and only use the layout when to clear it when it's done. + gridState.EnsureFirstElementOwnership(context); + return new Size(desiredSize.Width, desiredSize.Height); } diff --git a/src/Avalonia.Layout/UniformGridLayoutState.cs b/src/Avalonia.Layout/UniformGridLayoutState.cs index 37660d0354..e30c1a3c4b 100644 --- a/src/Avalonia.Layout/UniformGridLayoutState.cs +++ b/src/Avalonia.Layout/UniformGridLayoutState.cs @@ -134,10 +134,13 @@ namespace Avalonia.Layout } } - internal void EnsureFirstElementOwnership() + internal void EnsureFirstElementOwnership(VirtualizingLayoutContext context) { - if (FlowAlgorithm.GetElementIfRealized(0) != null) + if (_cachedFirstElement != null && FlowAlgorithm.GetElementIfRealized(0) != null) { + // We created the element, but then flowlayout algorithm took ownership, so we can clear it and + // let flowlayout algorithm do its thing. + context.RecycleElement(_cachedFirstElement); _cachedFirstElement = null; } } From 6f0befabdbf76fd97d936494fdf8f6e50cfe947e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 3 Jul 2019 18:35:13 +0200 Subject: [PATCH 026/138] Prevent IndexOutOfRange exception. Prevent the exception described in https://github.com/AvaloniaUI/Avalonia/pull/2603#issuecomment-508011071. This makes our codebase diverge from WinUI's but Im unable to debug into the WinUI code consistently enough to work out why this problem doesn't show up there. --- src/Avalonia.Layout/FlowLayoutAlgorithm.cs | 25 +++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs index 0617a96825..615ce725bd 100644 --- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -281,6 +281,7 @@ namespace Avalonia.Layout var lineOffset = _orientation.MajorStart(anchorBounds); var lineMajorSize = _orientation.MajorSize(anchorBounds); int countInLine = 1; + int count = 0; bool lineNeedsReposition = false; while (_elementManager.IsIndexValidInData(currentIndex) && @@ -290,6 +291,7 @@ namespace Avalonia.Layout _elementManager.EnsureElementRealized(direction == GenerateDirection.Forward, currentIndex, layoutId); var currentElement = _elementManager.GetRealizedElement(currentIndex); var desiredSize = MeasureElement(currentElement, currentIndex, availableSize, _context); + ++count; // Lay it out. var previousElement = _elementManager.GetRealizedElement(previousIndex); @@ -387,17 +389,20 @@ namespace Avalonia.Layout // If we did not reach the top or bottom of the extent, we realized one // extra item before we knew we were outside the realization window. Do not // account for that element in the indicies inside the realization window. - if (direction == GenerateDirection.Forward) + if (count > 0) { - int dataCount = _context.ItemCount; - _lastRealizedDataIndexInsideRealizationWindow = previousIndex == dataCount - 1 ? dataCount - 1 : previousIndex - 1; - _lastRealizedDataIndexInsideRealizationWindow = Math.Max(0, _lastRealizedDataIndexInsideRealizationWindow); - } - else - { - int dataCount = _context.ItemCount; - _firstRealizedDataIndexInsideRealizationWindow = previousIndex == 0 ? 0 : previousIndex + 1; - _firstRealizedDataIndexInsideRealizationWindow = Math.Min(dataCount - 1, _firstRealizedDataIndexInsideRealizationWindow); + if (direction == GenerateDirection.Forward) + { + int dataCount = _context.ItemCount; + _lastRealizedDataIndexInsideRealizationWindow = previousIndex == dataCount - 1 ? dataCount - 1 : previousIndex - 1; + _lastRealizedDataIndexInsideRealizationWindow = Math.Max(0, _lastRealizedDataIndexInsideRealizationWindow); + } + else + { + int dataCount = _context.ItemCount; + _firstRealizedDataIndexInsideRealizationWindow = previousIndex == 0 ? 0 : previousIndex + 1; + _firstRealizedDataIndexInsideRealizationWindow = Math.Min(dataCount - 1, _firstRealizedDataIndexInsideRealizationWindow); + } } _elementManager.DiscardElementsOutsideWindow(direction == GenerateDirection.Forward, currentIndex); From b26ec443749c8186c2d1b3aeed3f760280f56dd7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 4 Jul 2019 12:04:11 +0200 Subject: [PATCH 027/138] Removed non-working code. And replace it with a TODO. --- src/Avalonia.Controls/Repeater/ViewManager.cs | 33 ++----------------- 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index b90f8dacfe..833e708e9e 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; @@ -180,38 +181,10 @@ namespace Avalonia.Controls } } - // Find the next element if one exists, if not use the previous element. + // TODO: Find the next element if one exists, if not use the previous element. // If the container itself is not focusable, find a descendent that is. - IControl focusCandidate = null; - if (nextElement != null) - { - focusCandidate = nextElement as IControl; - if (focusCandidate != null) - { - var firstFocus = KeyboardNavigationHandler.GetNext(nextElement, NavigationDirection.First); - - if (firstFocus != null) - { - focusCandidate = firstFocus as IControl; - } - } - } - - if (focusCandidate == null && previousElement != null) - { - focusCandidate = previousElement as IControl; - if (previousElement != null) - { - var lastFocus = KeyboardNavigationHandler.GetNext(previousElement, NavigationDirection.Last); - - if (lastFocus != null) - { - focusCandidate = lastFocus as IControl; - } - } - } - return focusCandidate; + return nextElement; } public int GetElementIndex(VirtualizationInfo virtInfo) From d52d3417aea2fb281c91ad3d2907f0fa7c2475a7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 6 Jul 2019 21:09:51 +0300 Subject: [PATCH 028/138] Backported eb07c4b - jitter fix --- .../ControlCatalog.NetCore.csproj | 1 + samples/ControlCatalog.NetCore/Program.cs | 7 ++++ src/Avalonia.OpenGL/EglDisplay.cs | 1 + src/Avalonia.OpenGL/EglGlPlatformSurface.cs | 33 +++++++++++++++---- src/Avalonia.OpenGL/EglInterface.cs | 12 +++++++ .../IGlPlatformSurfaceRenderTarget.cs | 7 +++- .../Platform/IRenderTarget.cs | 5 +++ .../Rendering/DeferredRenderer.cs | 5 +++ .../Rendering/ManagedDeferredRendererLock.cs | 14 ++++++++ .../Rendering/UiThreadRenderTimer.cs | 32 ++++++++++++++++++ src/Skia/Avalonia.Skia/GlRenderTarget.cs | 2 ++ src/Windows/Avalonia.Win32/WindowImpl.cs | 28 +++++++++++----- 12 files changed, 132 insertions(+), 15 deletions(-) create mode 100644 src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 589f41c06b..a25cbfc696 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -10,6 +10,7 @@ + diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index d13a5b5ef3..a364d191db 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using Avalonia; using Avalonia.Skia; +using Avalonia.ReactiveUI; namespace ControlCatalog.NetCore { @@ -45,6 +46,12 @@ namespace ControlCatalog.NetCore public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() + .With(new X11PlatformOptions {EnableMultiTouch = true}) + .With(new Win32PlatformOptions + { + EnableMultitouch = true, + AllowEglInitialization = true + }) .UseSkia() .UseReactiveUI() .UseDataGrid(); diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index b14932acfe..ec445a4605 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -97,6 +97,7 @@ namespace Avalonia.OpenGL public GlDisplayType Type { get; } public GlInterface GlInterface { get; } + public EglInterface EglInterface => _egl; public IGlContext CreateContext(IGlContext share) { var shareCtx = (EglContext)share; diff --git a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs index f5dd413b0f..d2e4543af3 100644 --- a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs @@ -26,31 +26,44 @@ namespace Avalonia.OpenGL public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() { var glSurface = _display.CreateWindowSurface(_info.Handle); - return new RenderTarget(_context, glSurface, _info); + return new RenderTarget(_display, _context, glSurface, _info); } - class RenderTarget : IGlPlatformSurfaceRenderTarget + class RenderTarget : IGlPlatformSurfaceRenderTargetWithCorruptionInfo { + private readonly EglDisplay _display; private readonly EglContext _context; private readonly EglSurface _glSurface; private readonly IEglWindowGlPlatformSurfaceInfo _info; + private PixelSize _initialSize; - public RenderTarget(EglContext context, EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) + public RenderTarget(EglDisplay display, EglContext context, + EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) { + _display = display; _context = context; _glSurface = glSurface; _info = info; + _initialSize = info.Size; } public void Dispose() => _glSurface.Dispose(); + public bool IsCorrupted => _initialSize != _info.Size; + public IGlPlatformSurfaceRenderingSession BeginDraw() { var l = _context.Lock(); try { + if (IsCorrupted) + throw new RenderTargetCorruptedException(); _context.MakeCurrent(_glSurface); - return new Session(_context, _glSurface, _info, l); + _display.EglInterface.WaitClient(); + _display.EglInterface.WaitGL(); + _display.EglInterface.WaitNative(); + + return new Session(_display, _context, _glSurface, _info, l); } catch { @@ -61,15 +74,19 @@ namespace Avalonia.OpenGL class Session : IGlPlatformSurfaceRenderingSession { - private readonly IGlContext _context; + private readonly EglContext _context; private readonly EglSurface _glSurface; private readonly IEglWindowGlPlatformSurfaceInfo _info; + private readonly EglDisplay _display; private IDisposable _lock; + - public Session(IGlContext context, EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info, + public Session(EglDisplay display, EglContext context, + EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info, IDisposable @lock) { _context = context; + _display = display; _glSurface = glSurface; _info = info; _lock = @lock; @@ -78,7 +95,11 @@ namespace Avalonia.OpenGL public void Dispose() { _context.Display.GlInterface.Flush(); + _display.EglInterface.WaitGL(); _glSurface.SwapBuffers(); + _display.EglInterface.WaitClient(); + _display.EglInterface.WaitGL(); + _display.EglInterface.WaitNative(); _context.Display.ClearContext(); _lock.Dispose(); } diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/EglInterface.cs index 00fcd97af0..2838e41461 100644 --- a/src/Avalonia.OpenGL/EglInterface.cs +++ b/src/Avalonia.OpenGL/EglInterface.cs @@ -91,6 +91,18 @@ namespace Avalonia.OpenGL [GlEntryPoint("eglGetConfigAttrib")] public EglGetConfigAttrib GetConfigAttrib { get; } + public delegate bool EglWaitGL(); + [GlEntryPoint("eglWaitGL")] + public EglWaitGL WaitGL { get; } + + public delegate bool EglWaitClient(); + [GlEntryPoint("eglWaitClient")] + public EglWaitGL WaitClient { get; } + + public delegate bool EglWaitNative(); + [GlEntryPoint("eglWaitNative")] + public EglWaitGL WaitNative { get; } + // ReSharper restore UnassignedGetOnlyAutoProperty } } diff --git a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs b/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs index 53da93315c..d198d46e5c 100644 --- a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs +++ b/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs @@ -6,4 +6,9 @@ namespace Avalonia.OpenGL { IGlPlatformSurfaceRenderingSession BeginDraw(); } -} \ No newline at end of file + + public interface IGlPlatformSurfaceRenderTargetWithCorruptionInfo : IGlPlatformSurfaceRenderTarget + { + bool IsCorrupted { get; } + } +} diff --git a/src/Avalonia.Visuals/Platform/IRenderTarget.cs b/src/Avalonia.Visuals/Platform/IRenderTarget.cs index 522de64ec7..516bea782e 100644 --- a/src/Avalonia.Visuals/Platform/IRenderTarget.cs +++ b/src/Avalonia.Visuals/Platform/IRenderTarget.cs @@ -23,4 +23,9 @@ namespace Avalonia.Platform /// IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer); } + + public interface IRenderTargetWithCorruptionInfo : IRenderTarget + { + bool IsCorrupted { get; } + } } diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 5293e1b978..6f2ffe916d 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -248,6 +248,11 @@ namespace Avalonia.Rendering { if (context != null) return context; + if ((RenderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true) + { + RenderTarget.Dispose(); + RenderTarget = null; + } if (RenderTarget == null) RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); return context = RenderTarget.CreateDrawingContext(this); diff --git a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs index 75d8f036d6..2d4a39e026 100644 --- a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs +++ b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs @@ -7,11 +7,25 @@ namespace Avalonia.Rendering public class ManagedDeferredRendererLock : IDeferredRendererLock { private readonly object _lock = new object(); + + /// + /// Tries to lock the target surface or window + /// + /// IDisposable if succeeded to obtain the lock public IDisposable TryLock() { if (Monitor.TryEnter(_lock)) return Disposable.Create(() => Monitor.Exit(_lock)); return null; } + + /// + /// Enters a waiting lock, only use from platform code, not from the renderer + /// + public IDisposable Lock() + { + Monitor.Enter(_lock); + return Disposable.Create(() => Monitor.Exit(_lock)); + } } } diff --git a/src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs b/src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs new file mode 100644 index 0000000000..dd6cf7ad15 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs @@ -0,0 +1,32 @@ +using System; +using System.Diagnostics; +using System.Reactive.Disposables; +using Avalonia.Threading; + +namespace Avalonia.Rendering +{ + /// + /// Render timer that ticks on UI thread. Useful for debugging or bootstrapping on new platforms + /// + + public class UiThreadRenderTimer : DefaultRenderTimer + { + public UiThreadRenderTimer(int framesPerSecond) : base(framesPerSecond) + { + } + + protected override IDisposable StartCore(Action tick) + { + bool cancelled = false; + var st = Stopwatch.StartNew(); + DispatcherTimer.Run(() => + { + if (cancelled) + return false; + tick(st.Elapsed); + return !cancelled; + }, TimeSpan.FromSeconds(1.0 / FramesPerSecond), DispatcherPriority.Render); + return Disposable.Create(() => cancelled = true); + } + } +} diff --git a/src/Skia/Avalonia.Skia/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/GlRenderTarget.cs index cd8c334b53..e9e1727dfe 100644 --- a/src/Skia/Avalonia.Skia/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/GlRenderTarget.cs @@ -21,6 +21,8 @@ namespace Avalonia.Skia public void Dispose() => _surface.Dispose(); + public bool IsCorrupted => (_surface as IGlPlatformSurfaceRenderTargetWithCorruptionInfo)?.IsCorrupted == true; + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) { var session = _surface.BeginDraw(); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 081a713e95..11d6c467de 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -31,6 +31,7 @@ namespace Avalonia.Win32 private string _className; private IntPtr _hwnd; private IInputRoot _owner; + private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock(); private bool _trackingMouse; private bool _decorated = true; private bool _resizable = true; @@ -148,7 +149,9 @@ namespace Avalonia.Win32 if (customRendererFactory != null) return customRendererFactory.Create(root, loop); - return Win32Platform.UseDeferredRendering ? (IRenderer)new DeferredRenderer(root, loop) : new ImmediateRenderer(root); + return Win32Platform.UseDeferredRendering ? + (IRenderer)new DeferredRenderer(root, loop, rendererLock: _rendererLock) : + new ImmediateRenderer(root); } public void Resize(Size value) @@ -627,18 +630,26 @@ namespace Avalonia.Win32 break; case UnmanagedMethods.WindowsMessage.WM_PAINT: - UnmanagedMethods.PAINTSTRUCT ps; - if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero) + using (_rendererLock.Lock()) { - var f = Scaling; - var r = ps.rcPaint; - Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, (r.bottom - r.top) / f)); - UnmanagedMethods.EndPaint(_hwnd, ref ps); + UnmanagedMethods.PAINTSTRUCT ps; + if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero) + { + var f = Scaling; + var r = ps.rcPaint; + Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, + (r.bottom - r.top) / f)); + UnmanagedMethods.EndPaint(_hwnd, ref ps); + } } return IntPtr.Zero; case UnmanagedMethods.WindowsMessage.WM_SIZE: + using (_rendererLock.Lock()) + { + // Do nothing here, just block until the pending frame render is completed on the render thread + } var size = (UnmanagedMethods.SizeCommand)wParam; if (Resized != null && @@ -704,7 +715,8 @@ namespace Avalonia.Win32 } } - return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + using (_rendererLock.Lock()) + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); } static InputModifiers GetMouseModifiers(IntPtr wParam) From ce84902ec65f1793f7a811d1eefd941a3579fa9c Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 4 Jun 2019 13:17:23 +0300 Subject: [PATCH 029/138] Use Direct3D9 as ANGLE platform type by default --- src/Avalonia.OpenGL/AngleOptions.cs | 18 ++++++++ src/Avalonia.OpenGL/EglDisplay.cs | 70 ++++++++++++++++++++--------- src/Avalonia.OpenGL/EglInterface.cs | 13 ++++++ 3 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 src/Avalonia.OpenGL/AngleOptions.cs diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs new file mode 100644 index 0000000000..4b9c04f4e6 --- /dev/null +++ b/src/Avalonia.OpenGL/AngleOptions.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Avalonia.OpenGL +{ + public class AngleOptions + { + public enum PlatformApi + { + DirectX9, + DirectX11 + } + + public List AllowedPlatformApis = new List + { + PlatformApi.DirectX9 + }; + } +} diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index ec445a4605..b2b5a1a646 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using Avalonia.Platform.Interop; using static Avalonia.OpenGL.EglConsts; @@ -13,21 +14,42 @@ namespace Avalonia.OpenGL private readonly int[] _contextAttributes; public IntPtr Handle => _display; + private AngleOptions.PlatformApi? _angleApi; public EglDisplay(EglInterface egl) { _egl = egl; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _egl.GetPlatformDisplayEXT != null) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - foreach (var dapi in new[] {EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE, EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE}) + if (_egl.GetPlatformDisplayEXT == null) + throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); + + var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis + ?? new List {AngleOptions.PlatformApi.DirectX9}; + + foreach (var platformApi in allowedApis) { + int dapi; + if (platformApi == AngleOptions.PlatformApi.DirectX9) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; + else if (platformApi == AngleOptions.PlatformApi.DirectX11) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; + else + continue; + _display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE }); - if(_display != IntPtr.Zero) + if (_display != IntPtr.Zero) + { + _angleApi = platformApi; break; + } } + + if (_display == IntPtr.Zero) + throw new OpenGlException("Unable to create ANGLE display"); } if (_display == IntPtr.Zero) @@ -64,29 +86,35 @@ namespace Avalonia.OpenGL if (!_egl.BindApi(cfg.Api)) continue; - var attribs = new[] + foreach(var stencilSize in new[]{8, 1, 0}) + foreach (var depthSize in new []{8, 1, 0}) { - EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, - EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit, - EGL_RED_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, - EGL_ALPHA_SIZE, 8, - EGL_STENCIL_SIZE, 8, - EGL_DEPTH_SIZE, 8, - EGL_NONE - }; - if (!_egl.ChooseConfig(_display, attribs, out _config, 1, out int numConfigs)) - continue; - if (numConfigs == 0) - continue; - _contextAttributes = cfg.Attributes; - Type = cfg.Type; + var attribs = new[] + { + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + + EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit, + + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_STENCIL_SIZE, stencilSize, + EGL_DEPTH_SIZE, depthSize, + EGL_NONE + }; + if (!_egl.ChooseConfig(_display, attribs, out _config, 1, out int numConfigs)) + continue; + if (numConfigs == 0) + continue; + _contextAttributes = cfg.Attributes; + Type = cfg.Type; + } } if (_contextAttributes == null) throw new OpenGlException("No suitable EGL config was found"); - + GlInterface = GlInterface.FromNativeUtf8GetProcAddress(b => _egl.GetProcAddress(b)); } diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/EglInterface.cs index 2838e41461..0e4b012dbe 100644 --- a/src/Avalonia.OpenGL/EglInterface.cs +++ b/src/Avalonia.OpenGL/EglInterface.cs @@ -102,6 +102,19 @@ namespace Avalonia.OpenGL public delegate bool EglWaitNative(); [GlEntryPoint("eglWaitNative")] public EglWaitGL WaitNative { get; } + + public delegate IntPtr EglQueryString(IntPtr display, int i); + + [GlEntryPoint("eglQueryString")] + public EglQueryString QueryStringNative { get; } + + public string QueryString(IntPtr display, int i) + { + var rv = QueryStringNative(display, i); + if (rv == IntPtr.Zero) + return null; + return Marshal.PtrToStringAnsi(rv); + } // ReSharper restore UnassignedGetOnlyAutoProperty } From 9b606ab513f4c8eb941c5d108c7ad76a96079f8d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 28 Jun 2019 15:21:05 +0300 Subject: [PATCH 030/138] [X11] Fixed NumLock --- src/Avalonia.X11/X11KeyTransform.cs | 10 +++------- src/Avalonia.X11/X11Window.cs | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.X11/X11KeyTransform.cs b/src/Avalonia.X11/X11KeyTransform.cs index 26495111d1..c68cb04733 100644 --- a/src/Avalonia.X11/X11KeyTransform.cs +++ b/src/Avalonia.X11/X11KeyTransform.cs @@ -221,12 +221,8 @@ namespace Avalonia.X11 //{ X11Key.?, Key.DeadCharProcessed } }; - public static Key ConvertKey(IntPtr key) - { - var ikey = key.ToInt32(); - Key result; - return KeyDic.TryGetValue((X11Key)ikey, out result) ? result : Key.None; - } -} + public static Key ConvertKey(X11Key key) + => KeyDic.TryGetValue(key, out var result) ? result : Key.None; + } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index c5e77fe352..878ed12b66 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -419,10 +419,21 @@ namespace Avalonia.X11 return; var buffer = stackalloc byte[40]; - var latinKeysym = XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, 0); + var index = ev.KeyEvent.state.HasFlag(XModifierMask.ShiftMask); + + // We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway + var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32(); + + // Manually switch the Shift index for the keypad, + // there should be a proper way to do this + if (ev.KeyEvent.state.HasFlag(XModifierMask.Mod2Mask) + && key > X11Key.Num_Lock && key <= X11Key.KP_9) + key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32(); + + ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, - X11KeyTransform.ConvertKey(latinKeysym), TranslateModifiers(ev.KeyEvent.state)), ref ev); + X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev); if (ev.type == XEventName.KeyPress) { From f65fcb6fa413199e02258f086eb098d532364e6d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 6 Jul 2019 21:25:45 +0300 Subject: [PATCH 031/138] [X11] Blacklist llvmpipe --- src/Avalonia.OpenGL/GlInterface.cs | 18 ++++++++++++++---- src/Avalonia.X11/Glx/GlxDisplay.cs | 13 +++++++++++++ src/Avalonia.X11/X11Platform.cs | 8 ++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs index 718afc4a94..f556949cfa 100644 --- a/src/Avalonia.OpenGL/GlInterface.cs +++ b/src/Avalonia.OpenGL/GlInterface.cs @@ -9,12 +9,14 @@ namespace Avalonia.OpenGL public class GlInterface : GlInterfaceBase { public string Version { get; } + public string Vendor { get; } + public string Renderer { get; } public GlInterface(Func getProcAddress) : base(getProcAddress) { - var versionPtr = GetString(GlConsts.GL_VERSION); - if (versionPtr != IntPtr.Zero) - Version = Marshal.PtrToStringAnsi(versionPtr); + Version = GetString(GlConsts.GL_VERSION); + Renderer = GetString(GlConsts.GL_RENDERER); + Vendor = GetString(GlConsts.GL_VENDOR); } public GlInterface(Func n) : this(ConvertNative(n)) @@ -54,7 +56,15 @@ namespace Avalonia.OpenGL public delegate IntPtr GlGetString(int v); [GlEntryPoint("glGetString")] - public GlGetString GetString { get; } + public GlGetString GetStringNative { get; } + + public string GetString(int v) + { + var ptr = GetStringNative(v); + if (ptr != IntPtr.Zero) + return Marshal.PtrToStringAnsi(ptr); + return null; + } public delegate void GlGetIntegerv(int name, out int rv); [GlEntryPoint("glGetIntegerv")] diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index 5602b33280..04f2a7137c 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -90,6 +90,19 @@ namespace Avalonia.X11.Glx GlInterface = new GlInterface(GlxInterface.GlxGetProcAddress); if (GlInterface.Version == null) throw new OpenGlException("GL version string is null, aborting"); + if (GlInterface.Renderer == null) + throw new OpenGlException("GL renderer string is null, aborting"); + + if (Environment.GetEnvironmentVariable("AVALONIA_GLX_IGNORE_RENDERER_BLACKLIST") != "1") + { + var blacklist = AvaloniaLocator.Current.GetService() + ?.GlxRendererBlacklist; + if (blacklist != null) + foreach(var item in blacklist) + if (GlInterface.Renderer.Contains(item)) + throw new OpenGlException($"Renderer '{GlInterface.Renderer}' is blacklisted by '{item}'"); + } + } public void ClearContext() => Glx.MakeContextCurrent(_x11.Display, diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index ce03113169..47d5276fbc 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -95,6 +95,14 @@ namespace Avalonia { public bool UseEGL { get; set; } public bool UseGpu { get; set; } = true; + + public List GlxRendererBlacklist { get; set; } = new List + { + // llvmpipe is a software GL rasterizer. If it's returned by glGetString, + // that usually means that something in the system is horribly misconfigured + // and sometimes attempts to use GLX might cause a segfault + "llvmpipe" + }; public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication"; } public static class AvaloniaX11PlatformExtensions From f3e504ca152922ea1492e28a0be12949c1729316 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 6 Jul 2019 21:33:03 +0300 Subject: [PATCH 032/138] Bump version for 0.8.1 --- build/SharedVersion.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/SharedVersion.props b/build/SharedVersion.props index 7ea1dd0c65..16a4b828f4 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -2,7 +2,7 @@ xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> Avalonia - 0.8.0 + 0.8.1 Copyright 2018 © The AvaloniaUI Project https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md https://github.com/AvaloniaUI/Avalonia/ From bc2cc9928c71d7be9fa8e143002a3c5ea164eff0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 6 Jul 2019 21:34:58 +0300 Subject: [PATCH 033/138] Compilation --- samples/ControlCatalog.NetCore/Program.cs | 4 ---- src/Avalonia.Native/Avalonia.Native.csproj | 2 -- src/Avalonia.OpenGL/EglInterface.cs | 1 + 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index a364d191db..c58d071ac8 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -3,8 +3,6 @@ using System.Diagnostics; using System.Linq; using System.Threading; using Avalonia; -using Avalonia.Skia; -using Avalonia.ReactiveUI; namespace ControlCatalog.NetCore { @@ -46,10 +44,8 @@ namespace ControlCatalog.NetCore public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() - .With(new X11PlatformOptions {EnableMultiTouch = true}) .With(new Win32PlatformOptions { - EnableMultitouch = true, AllowEglInitialization = true }) .UseSkia() diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index c8ee73ad5d..9d6a82a2fb 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -7,8 +7,6 @@ /usr/bin/castxml /usr/local/bin/castxml true - - $(MSBuildThisFileDirectory)/Generated diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/EglInterface.cs index 0e4b012dbe..fd1374f3fe 100644 --- a/src/Avalonia.OpenGL/EglInterface.cs +++ b/src/Avalonia.OpenGL/EglInterface.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using Avalonia.Platform; using Avalonia.Platform.Interop; From 80a3d0471a52f0fe1de4ddc92d8f2c159bc432c2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 6 Jul 2019 22:00:08 +0300 Subject: [PATCH 034/138] Azure pipelines update --- azure-pipelines.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 39333f37ba..981b17b0de 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -32,7 +32,7 @@ jobs: - job: macOS pool: - vmImage: 'xcode9-macos10.13' + vmImage: 'macOS-10.14' steps: - task: DotNetCoreInstaller@0 inputs: @@ -49,7 +49,7 @@ jobs: inputs: actions: 'build' scheme: '' - sdk: 'macosx10.13' + sdk: 'macosx10.14' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' xcodeVersion: 'default' # Options: 8, 9, default, specifyPath @@ -134,3 +134,4 @@ jobs: pathToPublish: '$(Build.SourcesDirectory)/artifacts/zip' artifactName: 'Samples' condition: succeeded() + From 94e2cbbd43a2bb246b0a68c726bd68d8d627cba7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 6 Jul 2019 22:02:46 +0300 Subject: [PATCH 035/138] Updated Build.cs to version from master --- nukebuild/Build.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index bb31034299..84092d52eb 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -122,6 +122,14 @@ partial class Build : NukeBuild foreach(var fw in frameworks) { + if (fw.StartsWith("net4") + && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1") + { + Information($"Skipping {fw} tests on Linux - https://github.com/mono/mono/issues/13969"); + continue; + } + Information("Running for " + fw); DotNetTest(c => { From ec45084e6a662c488047bff17a51abf5dddd9a64 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 15 Jul 2019 20:54:40 +0200 Subject: [PATCH 036/138] Make Pen mutable. And add `IPen` interface and `ImmutablePen` class. --- src/Avalonia.Visuals/Media/BrushExtensions.cs | 14 +-- src/Avalonia.Visuals/Media/DrawingContext.cs | 8 +- src/Avalonia.Visuals/Media/GeometryDrawing.cs | 2 +- src/Avalonia.Visuals/Media/IMutablePen.cs | 17 +++ src/Avalonia.Visuals/Media/IPen.cs | 39 ++++++ .../Media/Immutable/ImmutablePen.cs | 85 +++++++++++++ src/Avalonia.Visuals/Media/Pen.cs | 117 ++++++++++++++++-- .../Platform/IDrawingContextImpl.cs | 6 +- .../Platform/IGeometryImpl.cs | 4 +- .../SceneGraph/BrushDrawOperation.cs | 2 +- .../SceneGraph/DeferredDrawingContextImpl.cs | 6 +- .../Rendering/SceneGraph/DrawOperation.cs | 2 +- .../Rendering/SceneGraph/GeometryNode.cs | 9 +- .../Rendering/SceneGraph/LineNode.cs | 7 +- .../Rendering/SceneGraph/RectangleNode.cs | 9 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 8 +- src/Skia/Avalonia.Skia/GeometryImpl.cs | 4 +- .../Media/DrawingContextImpl.cs | 6 +- .../Avalonia.Direct2D1/Media/GeometryImpl.cs | 4 +- .../Avalonia.Direct2D1/PrimitiveExtensions.cs | 4 +- .../MockStreamGeometryImpl.cs | 4 +- .../VisualTree/MockRenderInterface.cs | 4 +- 22 files changed, 294 insertions(+), 67 deletions(-) create mode 100644 src/Avalonia.Visuals/Media/IMutablePen.cs create mode 100644 src/Avalonia.Visuals/Media/IPen.cs create mode 100644 src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs diff --git a/src/Avalonia.Visuals/Media/BrushExtensions.cs b/src/Avalonia.Visuals/Media/BrushExtensions.cs index 522953eb04..081029e205 100644 --- a/src/Avalonia.Visuals/Media/BrushExtensions.cs +++ b/src/Avalonia.Visuals/Media/BrushExtensions.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Media.Immutable; namespace Avalonia.Media { @@ -30,20 +31,11 @@ namespace Avalonia.Media /// A copy of the pen with an immutable brush, or if the pen's brush /// is already immutable or null. /// - public static Pen ToImmutable(this Pen pen) + public static ImmutablePen ToImmutable(this IPen pen) { Contract.Requires(pen != null); - var brush = pen.Brush?.ToImmutable(); - return ReferenceEquals(pen.Brush, brush) ? - pen : - new Pen( - brush, - thickness: pen.Thickness, - dashStyle: pen.DashStyle, - lineCap: pen.LineCap, - lineJoin: pen.LineJoin, - miterLimit: pen.MiterLimit); + return pen as ImmutablePen ?? ((IMutablePen)pen).ToImmutable(); } } } diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index d3af71ffcb..4c9bf9ebd4 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -94,7 +94,7 @@ namespace Avalonia.Media /// The stroke pen. /// The first point of the line. /// The second point of the line. - public void DrawLine(Pen pen, Point p1, Point p2) + public void DrawLine(IPen pen, Point p1, Point p2) { if (PenIsVisible(pen)) { @@ -108,7 +108,7 @@ namespace Avalonia.Media /// The fill brush. /// The stroke pen. /// The geometry. - public void DrawGeometry(IBrush brush, Pen pen, Geometry geometry) + public void DrawGeometry(IBrush brush, IPen pen, Geometry geometry) { Contract.Requires(geometry != null); @@ -124,7 +124,7 @@ namespace Avalonia.Media /// The pen. /// The rectangle bounds. /// The corner radius. - public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f) + public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f) { if (PenIsVisible(pen)) { @@ -328,7 +328,7 @@ namespace Avalonia.Media PlatformImpl.Dispose(); } - private static bool PenIsVisible(Pen pen) + private static bool PenIsVisible(IPen pen) { return pen?.Brush != null && pen.Thickness > 0; } diff --git a/src/Avalonia.Visuals/Media/GeometryDrawing.cs b/src/Avalonia.Visuals/Media/GeometryDrawing.cs index ac0cc1c17d..3dad10fb8f 100644 --- a/src/Avalonia.Visuals/Media/GeometryDrawing.cs +++ b/src/Avalonia.Visuals/Media/GeometryDrawing.cs @@ -23,7 +23,7 @@ public static readonly StyledProperty PenProperty = AvaloniaProperty.Register(nameof(Pen)); - public Pen Pen + public IPen Pen { get => GetValue(PenProperty); set => SetValue(PenProperty, value); diff --git a/src/Avalonia.Visuals/Media/IMutablePen.cs b/src/Avalonia.Visuals/Media/IMutablePen.cs new file mode 100644 index 0000000000..8b02929dec --- /dev/null +++ b/src/Avalonia.Visuals/Media/IMutablePen.cs @@ -0,0 +1,17 @@ +using System; +using Avalonia.Media.Immutable; + +namespace Avalonia.Media +{ + /// + /// Represents a mutable pen which can return an immutable clone of itself. + /// + public interface IMutablePen : IPen, IAffectsRender + { + /// + /// Creates an immutable clone of the pen. + /// + /// The immutable clone. + ImmutablePen ToImmutable(); + } +} diff --git a/src/Avalonia.Visuals/Media/IPen.cs b/src/Avalonia.Visuals/Media/IPen.cs new file mode 100644 index 0000000000..589595bb5c --- /dev/null +++ b/src/Avalonia.Visuals/Media/IPen.cs @@ -0,0 +1,39 @@ +namespace Avalonia.Media +{ + /// + /// Describes how a stroke is drawn. + /// + public interface IPen + { + /// + /// Gets the brush used to draw the stroke. + /// + IBrush Brush { get; } + + /// + /// Gets the style of dashed lines drawn with a object. + /// + DashStyle DashStyle { get; } + + /// + /// Gets the type of shape to use on both ends of a line. + /// + PenLineCap LineCap { get; } + + /// + /// Gets a value describing how to join consecutive line or curve segments in a + /// contained in a object. + /// + PenLineJoin LineJoin { get; } + + /// + /// Gets the limit of the thickness of the join on a mitered corner. + /// + double MiterLimit { get; } + + /// + /// Gets the stroke thickness. + /// + double Thickness { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs new file mode 100644 index 0000000000..bcfec5dccf --- /dev/null +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs @@ -0,0 +1,85 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace Avalonia.Media.Immutable +{ + /// + /// Describes how a stroke is drawn. + /// + public class ImmutablePen : IPen + { + /// + /// Initializes a new instance of the class. + /// + /// The stroke color. + /// The stroke thickness. + /// The dash style. + /// Specifies the type of graphic shape to use on both ends of a line. + /// The line join. + /// The miter limit. + public ImmutablePen( + uint color, + double thickness = 1.0, + DashStyle dashStyle = null, + PenLineCap lineCap = PenLineCap.Flat, + PenLineJoin lineJoin = PenLineJoin.Miter, + double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The brush used to draw. + /// The stroke thickness. + /// The dash style. + /// The line cap. + /// The line join. + /// The miter limit. + public ImmutablePen( + IBrush brush, + double thickness = 1.0, + DashStyle dashStyle = null, + PenLineCap lineCap = PenLineCap.Flat, + PenLineJoin lineJoin = PenLineJoin.Miter, + double miterLimit = 10.0) + { + Brush = brush; + Thickness = thickness; + LineCap = lineCap; + LineJoin = lineJoin; + MiterLimit = miterLimit; + DashStyle = dashStyle; + } + + /// + /// Gets the brush used to draw the stroke. + /// + public IBrush Brush { get; } + + /// + /// Gets the stroke thickness. + /// + public double Thickness { get; } + + /// + /// Specifies the style of dashed lines drawn with a object. + /// + public DashStyle DashStyle { get; } + + /// + /// Specifies the type of graphic shape to use on both ends of a line. + /// + public PenLineCap LineCap { get; } + + /// + /// Specifies how to join consecutive line or curve segments in a (subpath) contained in a object. + /// + public PenLineJoin LineJoin { get; } + + /// + /// The limit on the ratio of the miter length to half this pen's Thickness. + /// + public double MiterLimit { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index ee427c913b..2bfd4f472e 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -1,13 +1,59 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using Avalonia.Media.Immutable; + namespace Avalonia.Media { /// /// Describes how a stroke is drawn. /// - public class Pen + public class Pen : AvaloniaObject, IMutablePen { + /// + /// Defines the property. + /// + public static readonly StyledProperty BrushProperty = + AvaloniaProperty.Register(nameof(Brush)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ThicknessProperty = + AvaloniaProperty.Register(nameof(Thickness), 1.0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty DashStyleProperty = + AvaloniaProperty.Register(nameof(DashStyle)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty LineCapProperty = + AvaloniaProperty.Register(nameof(LineCap)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty LineJoinProperty = + AvaloniaProperty.Register(nameof(LineJoin)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MiterLimitProperty = + AvaloniaProperty.Register(nameof(MiterLimit), 10.0); + + /// + /// Initializes a new instance of the class. + /// + public Pen() + { + } + /// /// Initializes a new instance of the class. /// @@ -53,33 +99,78 @@ namespace Avalonia.Media } /// - /// Gets the brush used to draw the stroke. + /// Gets or sets the brush used to draw the stroke. + /// + public IBrush Brush + { + get => GetValue(BrushProperty); + set => SetValue(BrushProperty, value); + } + + /// + /// Gets or sets the stroke thickness. + /// + public double Thickness + { + get => GetValue(ThicknessProperty); + set => SetValue(ThicknessProperty, value); + } + + /// + /// Gets or sets the style of dashed lines drawn with a object. /// - public IBrush Brush { get; } + public DashStyle DashStyle + { + get => GetValue(DashStyleProperty); + set => SetValue(DashStyleProperty, value); + } /// - /// Gets the stroke thickness. + /// Gets or sets the type of shape to use on both ends of a line. /// - public double Thickness { get; } + public PenLineCap LineCap + { + get => GetValue(LineCapProperty); + set => SetValue(LineCapProperty, value); + } /// - /// Specifies the style of dashed lines drawn with a object. + /// Gets or sets the join style for the ends of two consecutive lines drawn with this + /// . /// - public DashStyle DashStyle { get; } + public PenLineJoin LineJoin + { + get => GetValue(LineJoinProperty); + set => SetValue(LineJoinProperty, value); + } /// - /// Specifies the type of graphic shape to use on both ends of a line. + /// Gets or sets the limit of the thickness of the join on a mitered corner. /// - public PenLineCap LineCap { get; } + public double MiterLimit + { + get => GetValue(MiterLimitProperty); + set => SetValue(MiterLimitProperty, value); + } /// - /// Specifies how to join consecutive line or curve segments in a (subpath) contained in a object. + /// Raised when the pen changes. /// - public PenLineJoin LineJoin { get; } + public event EventHandler Invalidated; /// - /// The limit on the ratio of the miter length to half this pen's Thickness. + /// Creates an immutable clone of the brush. /// - public double MiterLimit { get; } + /// The immutable clone. + public ImmutablePen ToImmutable() + { + return new ImmutablePen( + Brush, + Thickness, + DashStyle, + LineCap, + LineJoin, + MiterLimit); + } } } diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index e5be04ebf9..f74c551fe0 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -50,7 +50,7 @@ namespace Avalonia.Platform /// The stroke pen. /// The first point of the line. /// The second point of the line. - void DrawLine(Pen pen, Point p1, Point p2); + void DrawLine(IPen pen, Point p1, Point p2); /// /// Draws a geometry. @@ -58,7 +58,7 @@ namespace Avalonia.Platform /// The fill brush. /// The stroke pen. /// The geometry. - void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry); + void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry); /// /// Draws the outline of a rectangle. @@ -66,7 +66,7 @@ namespace Avalonia.Platform /// The pen. /// The rectangle bounds. /// The corner radius. - void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f); + void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f); /// /// Draws text. diff --git a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs index 4e8e6521bd..b762859d1d 100644 --- a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs +++ b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs @@ -20,7 +20,7 @@ namespace Avalonia.Platform /// /// The pen to use. May be null. /// The bounding rectangle. - Rect GetRenderBounds(Pen pen); + Rect GetRenderBounds(IPen pen); /// /// Indicates whether the geometry's fill contains the specified point. @@ -42,7 +42,7 @@ namespace Avalonia.Platform /// The stroke to use. /// The point. /// true if the geometry contains the point; otherwise, false. - bool StrokeContains(Pen pen, Point point); + bool StrokeContains(IPen pen, Point point); /// /// Makes a clone of the geometry with the specified transform. diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs index 4c09dc2ddd..b2c0581388 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs @@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph /// internal abstract class BrushDrawOperation : DrawOperation { - public BrushDrawOperation(Rect bounds, Matrix transform, Pen pen) + public BrushDrawOperation(Rect bounds, Matrix transform, IPen pen) : base(bounds, transform, pen) { } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 0b33851911..3af56f5215 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -100,7 +100,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { var next = NextDrawAs(); @@ -137,7 +137,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawLine(Pen pen, Point p1, Point p2) + public void DrawLine(IPen pen, Point p1, Point p2) { var next = NextDrawAs(); @@ -152,7 +152,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) + public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0) { var next = NextDrawAs(); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs index 1a5a6fad3f..d9dfd8bd55 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs @@ -9,7 +9,7 @@ namespace Avalonia.Rendering.SceneGraph /// internal abstract class DrawOperation : IDrawOperation { - public DrawOperation(Rect bounds, Matrix transform, Pen pen) + public DrawOperation(Rect bounds, Matrix transform, IPen pen) { bounds = bounds.Inflate((pen?.Thickness ?? 0) / 2).TransformToAABB(transform); Bounds = new Rect( diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs index 2d01b117d9..0940533070 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.VisualTree; @@ -24,7 +25,7 @@ namespace Avalonia.Rendering.SceneGraph public GeometryNode( Matrix transform, IBrush brush, - Pen pen, + IPen pen, IGeometryImpl geometry, IDictionary childScenes = null) : base(geometry.GetRenderBounds(pen), transform, null) @@ -49,7 +50,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the stroke pen. /// - public Pen Pen { get; } + public ImmutablePen Pen { get; } /// /// Gets the geometry to draw. @@ -71,11 +72,11 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(Matrix transform, IBrush brush, Pen pen, IGeometryImpl geometry) + public bool Equals(Matrix transform, IBrush brush, IPen pen, IGeometryImpl geometry) { return transform == Transform && Equals(brush, Brush) && - pen == Pen && + Equals(pen, Pen) && Equals(geometry, Geometry); } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 11c763fcc9..0781a233f7 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.VisualTree; @@ -23,7 +24,7 @@ namespace Avalonia.Rendering.SceneGraph /// Child scenes for drawing visual brushes. public LineNode( Matrix transform, - Pen pen, + IPen pen, Point p1, Point p2, IDictionary childScenes = null) @@ -44,7 +45,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the stroke pen. /// - public Pen Pen { get; } + public ImmutablePen Pen { get; } /// /// Gets the start point of the line. @@ -71,7 +72,7 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(Matrix transform, Pen pen, Point p1, Point p2) + public bool Equals(Matrix transform, IPen pen, Point p1, Point p2) { return transform == Transform && pen == Pen && p1 == P1 && p2 == P2; } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index c622dc8a43..a10364e9ba 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.VisualTree; @@ -25,7 +26,7 @@ namespace Avalonia.Rendering.SceneGraph public RectangleNode( Matrix transform, IBrush brush, - Pen pen, + IPen pen, Rect rect, float cornerRadius, IDictionary childScenes = null) @@ -52,7 +53,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the stroke pen. /// - public Pen Pen { get; } + public ImmutablePen Pen { get; } /// /// Gets the rectangle to draw. @@ -80,11 +81,11 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(Matrix transform, IBrush brush, Pen pen, Rect rect, float cornerRadius) + public bool Equals(Matrix transform, IBrush brush, IPen pen, Rect rect, float cornerRadius) { return transform == Transform && Equals(brush, Brush) && - pen == Pen && + Equals(pen, Pen) && rect == Rect && cornerRadius == CornerRadius; } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 262d87d8b6..47e651ce91 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -154,7 +154,7 @@ namespace Avalonia.Skia } /// - public void DrawLine(Pen pen, Point p1, Point p2) + public void DrawLine(IPen pen, Point p1, Point p2) { using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) { @@ -163,7 +163,7 @@ namespace Avalonia.Skia } /// - public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; @@ -184,7 +184,7 @@ namespace Avalonia.Skia } /// - public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) + public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0) { using (var paint = CreatePaint(pen, rect.Size)) { @@ -561,7 +561,7 @@ namespace Avalonia.Skia /// Source pen. /// Target size. /// - private PaintWrapper CreatePaint(Pen pen, Size targetSize) + private PaintWrapper CreatePaint(IPen pen, Size targetSize) { // In Skia 0 thickness means - use hairline rendering // and for us it means - there is nothing rendered. diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 5940de418e..23980fb913 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -26,7 +26,7 @@ namespace Avalonia.Skia } /// - public bool StrokeContains(Pen pen, Point point) + public bool StrokeContains(IPen pen, Point point) { // Skia requires to compute stroke path to check for point containment. // Due to that we are caching using stroke width. @@ -89,7 +89,7 @@ namespace Avalonia.Skia } /// - public Rect GetRenderBounds(Pen pen) + public Rect GetRenderBounds(IPen pen) { var strokeWidth = (float)(pen?.Thickness ?? 0); diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index e90d444c44..39d801eb2f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -174,7 +174,7 @@ namespace Avalonia.Direct2D1.Media /// The stroke pen. /// The first point of the line. /// The second point of the line. - public void DrawLine(Pen pen, Point p1, Point p2) + public void DrawLine(IPen pen, Point p1, Point p2) { if (pen != null) { @@ -202,7 +202,7 @@ namespace Avalonia.Direct2D1.Media /// The fill brush. /// The stroke pen. /// The geometry. - public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { if (brush != null) { @@ -236,7 +236,7 @@ namespace Avalonia.Direct2D1.Media /// The pen. /// The rectangle bounds. /// The corner radius. - public void DrawRectangle(Pen pen, Rect rect, float cornerRadius) + public void DrawRectangle(IPen pen, Rect rect, float cornerRadius) { using (var brush = CreateBrush(pen.Brush, rect.Size)) using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs index 7c8ddaca3f..51ca2520ad 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs @@ -22,7 +22,7 @@ namespace Avalonia.Direct2D1.Media public Geometry Geometry { get; } /// - public Rect GetRenderBounds(Avalonia.Media.Pen pen) + public Rect GetRenderBounds(Avalonia.Media.IPen pen) { return Geometry.GetWidenedBounds((float)(pen?.Thickness ?? 0)).ToAvalonia(); } @@ -46,7 +46,7 @@ namespace Avalonia.Direct2D1.Media } /// - public bool StrokeContains(Avalonia.Media.Pen pen, Point point) + public bool StrokeContains(Avalonia.Media.IPen pen, Point point) { return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)(pen?.Thickness ?? 0)); } diff --git a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs index 6b0d30f250..065895859d 100644 --- a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs +++ b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs @@ -109,7 +109,7 @@ namespace Avalonia.Direct2D1 /// The pen to convert. /// The render target. /// The Direct2D brush. - public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, SharpDX.Direct2D1.RenderTarget renderTarget) + public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.IPen pen, SharpDX.Direct2D1.RenderTarget renderTarget) { return pen.ToDirect2DStrokeStyle(renderTarget.Factory); } @@ -120,7 +120,7 @@ namespace Avalonia.Direct2D1 /// The pen to convert. /// The factory associated with this resource. /// The Direct2D brush. - public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, Factory factory) + public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.IPen pen, Factory factory) { var d2dLineCap = pen.LineCap.ToDirect2D(); diff --git a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs index 63da9ed3f0..4fa3fbf523 100644 --- a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs +++ b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs @@ -47,12 +47,12 @@ namespace Avalonia.UnitTests return _context.FillContains(point); } - public bool StrokeContains(Pen pen, Point point) + public bool StrokeContains(IPen pen, Point point) { return false; } - public Rect GetRenderBounds(Pen pen) => Bounds; + public Rect GetRenderBounds(IPen pen) => Bounds; public IGeometryImpl Intersect(IGeometryImpl geometry) { diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 03470670d2..d31210bc71 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -96,7 +96,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree return _impl.FillContains(point); } - public Rect GetRenderBounds(Pen pen) + public Rect GetRenderBounds(IPen pen) { throw new NotImplementedException(); } @@ -111,7 +111,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree return _impl; } - public bool StrokeContains(Pen pen, Point point) + public bool StrokeContains(IPen pen, Point point) { throw new NotImplementedException(); } From cad119ebfbf16d5d792e63d0991cb153000baad7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 16 Jul 2019 11:35:36 +0200 Subject: [PATCH 037/138] Invalidate Pen when properties change. --- src/Avalonia.Visuals/Media/Pen.cs | 61 +++++++++++++++++++ .../Media/PenTests.cs | 33 ++++++++++ 2 files changed, 94 insertions(+) create mode 100644 tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index 2bfd4f472e..64c4e4ce44 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Media.Immutable; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -98,6 +99,17 @@ namespace Avalonia.Media DashStyle = dashStyle; } + static Pen() + { + AffectsRender( + BrushProperty, + ThicknessProperty, + DashStyleProperty, + LineCapProperty, + LineJoinProperty, + MiterLimitProperty); + } + /// /// Gets or sets the brush used to draw the stroke. /// @@ -172,5 +184,54 @@ namespace Avalonia.Media LineJoin, MiterLimit); } + + /// + /// Marks a property as affecting the pen's visual representation. + /// + /// The properties. + /// + /// After a call to this method in a pen's static constructor, any change to the + /// property will cause the event to be raised on the pen. + /// + protected static void AffectsRender(params AvaloniaProperty[] properties) + where T : Pen + { + void Invalidate(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is T sender) + { + if (e.OldValue is IAffectsRender oldValue) + { + WeakEventHandlerManager.Unsubscribe( + oldValue, + nameof(oldValue.Invalidated), + sender.AffectsRenderInvalidated); + } + + if (e.NewValue is IAffectsRender newValue) + { + WeakEventHandlerManager.Subscribe( + newValue, + nameof(newValue.Invalidated), + sender.AffectsRenderInvalidated); + } + + sender.RaiseInvalidated(EventArgs.Empty); + } + } + + foreach (var property in properties) + { + property.Changed.Subscribe(Invalidate); + } + } + + /// + /// Raises the event. + /// + /// The event args. + protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); + + private void AffectsRenderInvalidated(object sender, EventArgs e) => RaiseInvalidated(EventArgs.Empty); } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs new file mode 100644 index 0000000000..a3746f80b5 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs @@ -0,0 +1,33 @@ +using Avalonia.Media; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Media +{ + public class PenTests + { + [Fact] + public void Changing_Thickness_Raises_Invalidated() + { + var target = new Pen(); + var raised = false; + + target.Invalidated += (s, e) => raised = true; + target.Thickness = 18; + + Assert.True(raised); + } + + [Fact] + public void Changing_Brush_Color_Raises_Invalidated() + { + var brush = new SolidColorBrush(Colors.Red); + var target = new Pen { Brush = brush }; + var raised = false; + + target.Invalidated += (s, e) => raised = true; + brush.Color = Colors.Green; + + Assert.True(raised); + } + } +} From 21093cac31f6391f848cb8881c15aa4beb13f4ec Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 16 Jul 2019 11:46:21 +0200 Subject: [PATCH 038/138] Make sure immutable pen has immutable brush. --- src/Avalonia.Visuals/Media/Pen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index 64c4e4ce44..d609335d68 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -177,7 +177,7 @@ namespace Avalonia.Media public ImmutablePen ToImmutable() { return new ImmutablePen( - Brush, + Brush?.ToImmutable(), Thickness, DashStyle, LineCap, From afa594cd4893aa5411da56bb29a3d27b5c7ff48c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 16 Jul 2019 12:21:04 +0200 Subject: [PATCH 039/138] Implemented equality for Pens. --- .../Media/Immutable/ImmutablePen.cs | 13 +++++- src/Avalonia.Visuals/Media/Pen.cs | 43 ++++++++++++++++++- .../Rendering/SceneGraph/LineNode.cs | 2 +- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs index bcfec5dccf..326083dec1 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs @@ -1,12 +1,14 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; + namespace Avalonia.Media.Immutable { /// /// Describes how a stroke is drawn. /// - public class ImmutablePen : IPen + public class ImmutablePen : IPen, IEquatable { /// /// Initializes a new instance of the class. @@ -81,5 +83,14 @@ namespace Avalonia.Media.Immutable /// The limit on the ratio of the miter length to half this pen's Thickness. /// public double MiterLimit { get; } + + /// + public override bool Equals(object obj) => Pen.PenEquals(this, obj as IPen); + + /// + public bool Equals(IPen other) => Pen.PenEquals(this, other); + + /// + public override int GetHashCode() => Pen.GetHashCode(this); } } diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index d609335d68..b89ca15a0e 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using Avalonia.Media.Immutable; using Avalonia.Utilities; @@ -10,7 +11,7 @@ namespace Avalonia.Media /// /// Describes how a stroke is drawn. /// - public class Pen : AvaloniaObject, IMutablePen + public class Pen : AvaloniaObject, IMutablePen, IEquatable { /// /// Defines the property. @@ -170,6 +171,15 @@ namespace Avalonia.Media /// public event EventHandler Invalidated; + /// + public override bool Equals(object obj) => PenEquals(this, obj as IPen); + + /// + public bool Equals(IPen other) => PenEquals(this, other); + + /// + public override int GetHashCode() => GetHashCode(this); + /// /// Creates an immutable clone of the brush. /// @@ -232,6 +242,37 @@ namespace Avalonia.Media /// The event args. protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); + internal static int GetHashCode(IPen pen) + { + var hashCode = 1181807663; + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(pen.Brush); + hashCode = hashCode * -1521134295 + pen.Thickness.GetHashCode(); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(pen.DashStyle); + hashCode = hashCode * -1521134295 + pen.LineCap.GetHashCode(); + hashCode = hashCode * -1521134295 + pen.LineJoin.GetHashCode(); + hashCode = hashCode * -1521134295 + pen.MiterLimit.GetHashCode(); + return hashCode; + } + + internal static bool PenEquals(IPen a, IPen b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + else if (a is null && !(b is null) || (b is null && !(a is null))) + { + return false; + } + + return EqualityComparer.Default.Equals(a.Brush, b.Brush) && + a.Thickness == b.Thickness && + EqualityComparer.Default.Equals(a.DashStyle, b.DashStyle) && + a.LineCap == b.LineCap && + a.LineJoin == b.LineJoin && + a.MiterLimit == b.MiterLimit; + } + private void AffectsRenderInvalidated(object sender, EventArgs e) => RaiseInvalidated(EventArgs.Empty); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 0781a233f7..ff97ced349 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -74,7 +74,7 @@ namespace Avalonia.Rendering.SceneGraph /// public bool Equals(Matrix transform, IPen pen, Point p1, Point p2) { - return transform == Transform && pen == Pen && p1 == P1 && p2 == P2; + return transform == Transform && Equals(pen, Pen) && p1 == P1 && p2 == P2; } public override void Render(IDrawingContextImpl context) From 3c1dfcfcf5e9eb4e05b596ad6d9f3203e1632911 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 16 Jul 2019 12:40:52 +0200 Subject: [PATCH 040/138] Added equality test. And fix bug in `DashStyle`. --- src/Avalonia.Visuals/Media/DashStyle.cs | 2 +- .../Media/PenTests.cs | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/DashStyle.cs b/src/Avalonia.Visuals/Media/DashStyle.cs index c7e1db57b2..9f03a0bd85 100644 --- a/src/Avalonia.Visuals/Media/DashStyle.cs +++ b/src/Avalonia.Visuals/Media/DashStyle.cs @@ -10,7 +10,7 @@ namespace Avalonia.Media { get { - if (dashDotDot == null) + if (dash == null) { dash = new DashStyle(new double[] { 2, 2 }, 1); } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs index a3746f80b5..c4fe0155f9 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs @@ -1,4 +1,5 @@ using Avalonia.Media; +using Avalonia.Media.Immutable; using Xunit; namespace Avalonia.Visuals.UnitTests.Media @@ -29,5 +30,27 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.True(raised); } + + [Fact] + public void Equality_Is_Implemented_Between_Mutable_And_Immutable_Pens() + { + var brush = new SolidColorBrush(Colors.Red); + var target1 = new Pen( + brush: brush, + thickness: 2, + dashStyle: DashStyle.Dash, + lineCap: PenLineCap.Round, + lineJoin: PenLineJoin.Round, + miterLimit: 21); + var target2 = new ImmutablePen( + brush: brush, + thickness: 2, + dashStyle: DashStyle.Dash, + lineCap: PenLineCap.Round, + lineJoin: PenLineJoin.Round, + miterLimit: 21); + + Assert.True(Equals(target1, target2)); + } } } From 2af5a03c2db8e5295b073bef311eb939cc536fb8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 16 Jul 2019 22:35:34 +0200 Subject: [PATCH 041/138] Make DashStyle mutable. --- src/Avalonia.Visuals/Media/BrushExtensions.cs | 21 ++- src/Avalonia.Visuals/Media/DashStyle.cs | 128 ++++++++++++------ src/Avalonia.Visuals/Media/IDashStyle.cs | 20 +++ src/Avalonia.Visuals/Media/IPen.cs | 2 +- .../Media/Immutable/ImmutableDashStyle.cs | 30 ++++ .../Media/Immutable/ImmutablePen.cs | 6 +- src/Avalonia.Visuals/Media/Pen.cs | 16 +-- .../Media/PenTests.cs | 15 +- 8 files changed, 179 insertions(+), 59 deletions(-) create mode 100644 src/Avalonia.Visuals/Media/IDashStyle.cs create mode 100644 src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs diff --git a/src/Avalonia.Visuals/Media/BrushExtensions.cs b/src/Avalonia.Visuals/Media/BrushExtensions.cs index 081029e205..265337476a 100644 --- a/src/Avalonia.Visuals/Media/BrushExtensions.cs +++ b/src/Avalonia.Visuals/Media/BrushExtensions.cs @@ -24,12 +24,27 @@ namespace Avalonia.Media } /// - /// Converts a pen to a pen with an immutable brush + /// Converts a dash style to an immutable dash style. + /// + /// The dash style. + /// + /// The result of calling if the style is mutable, + /// otherwise . + /// + public static ImmutableDashStyle ToImmutable(this IDashStyle style) + { + Contract.Requires(style != null); + + return style as ImmutableDashStyle ?? ((DashStyle)style).ToImmutable(); + } + + /// + /// Converts a pen to an immutable pen. /// /// The pen. /// - /// A copy of the pen with an immutable brush, or if the pen's brush - /// is already immutable or null. + /// The result of calling if the brush is mutable, + /// otherwise . /// public static ImmutablePen ToImmutable(this IPen pen) { diff --git a/src/Avalonia.Visuals/Media/DashStyle.cs b/src/Avalonia.Visuals/Media/DashStyle.cs index 9f03a0bd85..7784c73736 100644 --- a/src/Avalonia.Visuals/Media/DashStyle.cs +++ b/src/Avalonia.Visuals/Media/DashStyle.cs @@ -1,72 +1,114 @@ namespace Avalonia.Media { + using System; using System.Collections.Generic; + using System.Linq; using Avalonia.Animation; + using Avalonia.Media.Immutable; - public class DashStyle : Animatable + /// + /// Represents the sequence of dashes and gaps that will be applied by a . + /// + public class DashStyle : Animatable, IDashStyle, IAffectsRender { - private static DashStyle dash; - public static DashStyle Dash - { - get - { - if (dash == null) - { - dash = new DashStyle(new double[] { 2, 2 }, 1); - } - - return dash; - } - } + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty> DashesProperty = + AvaloniaProperty.Register>(nameof(Dashes)); + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty OffsetProperty = + AvaloniaProperty.Register(nameof(Offset)); + private static ImmutableDashStyle s_dash; + private static ImmutableDashStyle s_dot; + private static ImmutableDashStyle s_dashDot; + private static ImmutableDashStyle s_dashDotDot; - private static DashStyle dot; - public static DashStyle Dot + /// + /// Initializes a new instance of the class. + /// + public DashStyle() + : this(null, 0) { - get { return dot ?? (dot = new DashStyle(new double[] {0, 2}, 0)); } } - private static DashStyle dashDot; - public static DashStyle DashDot + /// + /// Initializes a new instance of the class. + /// + /// The dashes collection. + /// The dash sequence offset. + public DashStyle(IEnumerable dashes, double offset) { - get - { - if (dashDot == null) - { - dashDot = new DashStyle(new double[] { 2, 2, 0, 2 }, 1); - } - - return dashDot; - } + Dashes = (IReadOnlyList)dashes?.ToList() ?? Array.Empty(); + Offset = offset; } - private static DashStyle dashDotDot; - public static DashStyle DashDotDot + static DashStyle() { - get + void RaiseInvalidated(AvaloniaPropertyChangedEventArgs e) { - if (dashDotDot == null) - { - dashDotDot = new DashStyle(new double[] { 2, 2, 0, 2, 0, 2 }, 1); - } - - return dashDotDot; + ((DashStyle)e.Sender).Invalidated?.Invoke(e.Sender, EventArgs.Empty); } + + DashesProperty.Changed.Subscribe(RaiseInvalidated); + OffsetProperty.Changed.Subscribe(RaiseInvalidated); } + /// + /// Represents a dashed . + /// + public static IDashStyle Dash => + s_dash ?? (s_dash = new ImmutableDashStyle(new double[] { 2, 2 }, 1)); + + /// + /// Represents a dotted . + /// + public static IDashStyle Dot => + s_dot ?? (s_dot = new ImmutableDashStyle(new double[] { 0, 2 }, 0)); + + /// + /// Represents a dashed dotted . + /// + public static IDashStyle DashDot => + s_dashDot ?? (s_dashDot = new ImmutableDashStyle(new double[] { 2, 2, 0, 2 }, 1)); + + /// + /// Represents a dashed double dotted . + /// + public static IDashStyle DashDotDot => + s_dashDotDot ?? (s_dashDotDot = new ImmutableDashStyle(new double[] { 2, 2, 0, 2, 0, 2 }, 1)); - public DashStyle(IReadOnlyList dashes = null, double offset = 0.0) + /// + /// Gets or sets the length of alternating dashes and gaps. + /// + public IReadOnlyList Dashes { - this.Dashes = dashes; - this.Offset = offset; + get => GetValue(DashesProperty); + set => SetValue(DashesProperty, value); } /// - /// Gets and sets the length of alternating dashes and gaps. + /// Gets or sets how far in the dash sequence the stroke will start. /// - public IReadOnlyList Dashes { get; } + public double Offset + { + get => GetValue(OffsetProperty); + set => SetValue(OffsetProperty, value); + } - public double Offset { get; } + /// + /// Raised when the dash style changes. + /// + public event EventHandler Invalidated; + + /// + /// Returns an immutable clone of the . + /// + /// + public ImmutableDashStyle ToImmutable() => new ImmutableDashStyle(Dashes, Offset); } } diff --git a/src/Avalonia.Visuals/Media/IDashStyle.cs b/src/Avalonia.Visuals/Media/IDashStyle.cs new file mode 100644 index 0000000000..7835c7a1e9 --- /dev/null +++ b/src/Avalonia.Visuals/Media/IDashStyle.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Avalonia.Media +{ + /// + /// Represents the sequence of dashes and gaps that will be applied by a . + /// + public interface IDashStyle + { + /// + /// Gets or sets the length of alternating dashes and gaps. + /// + IReadOnlyList Dashes { get; } + + /// + /// Gets or sets how far in the dash sequence the stroke will start. + /// + double Offset { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/IPen.cs b/src/Avalonia.Visuals/Media/IPen.cs index 589595bb5c..0cdac312cc 100644 --- a/src/Avalonia.Visuals/Media/IPen.cs +++ b/src/Avalonia.Visuals/Media/IPen.cs @@ -13,7 +13,7 @@ /// /// Gets the style of dashed lines drawn with a object. /// - DashStyle DashStyle { get; } + IDashStyle DashStyle { get; } /// /// Gets the type of shape to use on both ends of a line. diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs new file mode 100644 index 0000000000..a40682babd --- /dev/null +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Media.Immutable +{ + /// + /// Represents the sequence of dashes and gaps that will be applied by an + /// . + /// + public class ImmutableDashStyle : IDashStyle + { + /// + /// Initializes a new instance of the class. + /// + /// The dashes collection. + /// The dash sequence offset. + public ImmutableDashStyle(IEnumerable dashes, double offset) + { + Dashes = (IReadOnlyList)dashes?.ToList() ?? Array.Empty(); + Offset = offset; + } + + /// + public IReadOnlyList Dashes { get; } + + /// + public double Offset { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs index 326083dec1..ddc050aa5c 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs @@ -22,7 +22,7 @@ namespace Avalonia.Media.Immutable public ImmutablePen( uint color, double thickness = 1.0, - DashStyle dashStyle = null, + ImmutableDashStyle dashStyle = null, PenLineCap lineCap = PenLineCap.Flat, PenLineJoin lineJoin = PenLineJoin.Miter, double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit) @@ -41,7 +41,7 @@ namespace Avalonia.Media.Immutable public ImmutablePen( IBrush brush, double thickness = 1.0, - DashStyle dashStyle = null, + ImmutableDashStyle dashStyle = null, PenLineCap lineCap = PenLineCap.Flat, PenLineJoin lineJoin = PenLineJoin.Miter, double miterLimit = 10.0) @@ -67,7 +67,7 @@ namespace Avalonia.Media.Immutable /// /// Specifies the style of dashed lines drawn with a object. /// - public DashStyle DashStyle { get; } + public IDashStyle DashStyle { get; } /// /// Specifies the type of graphic shape to use on both ends of a line. diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index b89ca15a0e..c0963adc08 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -28,8 +28,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty DashStyleProperty = - AvaloniaProperty.Register(nameof(DashStyle)); + public static readonly StyledProperty DashStyleProperty = + AvaloniaProperty.Register(nameof(DashStyle)); /// /// Defines the property. @@ -68,7 +68,7 @@ namespace Avalonia.Media public Pen( uint color, double thickness = 1.0, - DashStyle dashStyle = null, + IDashStyle dashStyle = null, PenLineCap lineCap = PenLineCap.Flat, PenLineJoin lineJoin = PenLineJoin.Miter, double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit) @@ -87,7 +87,7 @@ namespace Avalonia.Media public Pen( IBrush brush, double thickness = 1.0, - DashStyle dashStyle = null, + IDashStyle dashStyle = null, PenLineCap lineCap = PenLineCap.Flat, PenLineJoin lineJoin = PenLineJoin.Miter, double miterLimit = 10.0) @@ -132,7 +132,7 @@ namespace Avalonia.Media /// /// Gets or sets the style of dashed lines drawn with a object. /// - public DashStyle DashStyle + public IDashStyle DashStyle { get => GetValue(DashStyleProperty); set => SetValue(DashStyleProperty, value); @@ -189,7 +189,7 @@ namespace Avalonia.Media return new ImmutablePen( Brush?.ToImmutable(), Thickness, - DashStyle, + DashStyle?.ToImmutable(), LineCap, LineJoin, MiterLimit); @@ -247,7 +247,7 @@ namespace Avalonia.Media var hashCode = 1181807663; hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(pen.Brush); hashCode = hashCode * -1521134295 + pen.Thickness.GetHashCode(); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(pen.DashStyle); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(pen.DashStyle); hashCode = hashCode * -1521134295 + pen.LineCap.GetHashCode(); hashCode = hashCode * -1521134295 + pen.LineJoin.GetHashCode(); hashCode = hashCode * -1521134295 + pen.MiterLimit.GetHashCode(); @@ -267,7 +267,7 @@ namespace Avalonia.Media return EqualityComparer.Default.Equals(a.Brush, b.Brush) && a.Thickness == b.Thickness && - EqualityComparer.Default.Equals(a.DashStyle, b.DashStyle) && + EqualityComparer.Default.Equals(a.DashStyle, b.DashStyle) && a.LineCap == b.LineCap && a.LineJoin == b.LineJoin && a.MiterLimit == b.MiterLimit; diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs index c4fe0155f9..70b4281083 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs @@ -31,6 +31,19 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.True(raised); } + [Fact] + public void Changing_DashStyle_Dashes_Raises_Invalidated() + { + var dashes = new DashStyle(); + var target = new Pen { DashStyle = dashes }; + var raised = false; + + target.Invalidated += (s, e) => raised = true; + dashes.Dashes = new[] { 0.1, 0.2 }; + + Assert.True(raised); + } + [Fact] public void Equality_Is_Implemented_Between_Mutable_And_Immutable_Pens() { @@ -45,7 +58,7 @@ namespace Avalonia.Visuals.UnitTests.Media var target2 = new ImmutablePen( brush: brush, thickness: 2, - dashStyle: DashStyle.Dash, + dashStyle: (ImmutableDashStyle)DashStyle.Dash, lineCap: PenLineCap.Round, lineJoin: PenLineJoin.Round, miterLimit: 21); From 2523e7a2901aba996224d978ab729fd028fa4153 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 16 Jul 2019 22:37:30 +0200 Subject: [PATCH 042/138] Remove IMutablePen. It's not really needed. Mutable pens will always be `Pen`s. --- src/Avalonia.Visuals/Media/BrushExtensions.cs | 2 +- src/Avalonia.Visuals/Media/IMutablePen.cs | 17 ----------------- src/Avalonia.Visuals/Media/Pen.cs | 2 +- 3 files changed, 2 insertions(+), 19 deletions(-) delete mode 100644 src/Avalonia.Visuals/Media/IMutablePen.cs diff --git a/src/Avalonia.Visuals/Media/BrushExtensions.cs b/src/Avalonia.Visuals/Media/BrushExtensions.cs index 265337476a..87e698e705 100644 --- a/src/Avalonia.Visuals/Media/BrushExtensions.cs +++ b/src/Avalonia.Visuals/Media/BrushExtensions.cs @@ -50,7 +50,7 @@ namespace Avalonia.Media { Contract.Requires(pen != null); - return pen as ImmutablePen ?? ((IMutablePen)pen).ToImmutable(); + return pen as ImmutablePen ?? ((Pen)pen).ToImmutable(); } } } diff --git a/src/Avalonia.Visuals/Media/IMutablePen.cs b/src/Avalonia.Visuals/Media/IMutablePen.cs deleted file mode 100644 index 8b02929dec..0000000000 --- a/src/Avalonia.Visuals/Media/IMutablePen.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using Avalonia.Media.Immutable; - -namespace Avalonia.Media -{ - /// - /// Represents a mutable pen which can return an immutable clone of itself. - /// - public interface IMutablePen : IPen, IAffectsRender - { - /// - /// Creates an immutable clone of the pen. - /// - /// The immutable clone. - ImmutablePen ToImmutable(); - } -} diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index c0963adc08..fdf006e054 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -11,7 +11,7 @@ namespace Avalonia.Media /// /// Describes how a stroke is drawn. /// - public class Pen : AvaloniaObject, IMutablePen, IEquatable + public class Pen : AvaloniaObject, IPen, IEquatable { /// /// Defines the property. From e235efe3881a29d5b0fe4d82f1aae59bce61a988 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 16 Jul 2019 23:03:48 +0200 Subject: [PATCH 043/138] Implement DashStyle equality. --- src/Avalonia.Visuals/Media/DashStyle.cs | 56 ++++++++++++++++++- .../Media/Immutable/ImmutableDashStyle.cs | 12 +++- .../Media/PenTests.cs | 22 ++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Media/DashStyle.cs b/src/Avalonia.Visuals/Media/DashStyle.cs index 7784c73736..bd81cb1d03 100644 --- a/src/Avalonia.Visuals/Media/DashStyle.cs +++ b/src/Avalonia.Visuals/Media/DashStyle.cs @@ -9,7 +9,7 @@ namespace Avalonia.Media /// /// Represents the sequence of dashes and gaps that will be applied by a . /// - public class DashStyle : Animatable, IDashStyle, IAffectsRender + public class DashStyle : Animatable, IDashStyle, IAffectsRender, IEquatable { /// /// Defines the property. @@ -105,10 +105,64 @@ namespace Avalonia.Media /// public event EventHandler Invalidated; + /// + public override bool Equals(object obj) => DashEquals(this, obj as IDashStyle); + + /// + public bool Equals(IDashStyle other) => DashEquals(this, other); + + /// + public override int GetHashCode() => GetHashCode(this); + /// /// Returns an immutable clone of the . /// /// public ImmutableDashStyle ToImmutable() => new ImmutableDashStyle(Dashes, Offset); + + internal static bool DashEquals(IDashStyle a, IDashStyle b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + else if ((a is null && !(b is null)) || (b is null && !(a is null))) + { + return false; + } + + if (a.Offset != b.Offset) + { + return false; + } + + if (ReferenceEquals(a.Dashes, b.Dashes)) + { + return true; + } + + if ((a.Dashes is null && !(b.Dashes is null)) || (b.Dashes is null && !(a.Dashes is null))) + { + return false; + } + + return a.Dashes.SequenceEqual(b.Dashes); + } + + internal static int GetHashCode(IDashStyle style) + { + var hashCode = 717868523; + hashCode = hashCode * -1521134295 + style.Offset.GetHashCode(); + + if (style.Dashes != null) + { + foreach (var i in style.Dashes) + { + hashCode = hashCode * -1521134295 + i.GetHashCode(); + } + } + + return hashCode; + } } } diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs index a40682babd..65e27bf9b5 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs @@ -8,7 +8,7 @@ namespace Avalonia.Media.Immutable /// Represents the sequence of dashes and gaps that will be applied by an /// . /// - public class ImmutableDashStyle : IDashStyle + public class ImmutableDashStyle : IDashStyle, IEquatable { /// /// Initializes a new instance of the class. @@ -26,5 +26,15 @@ namespace Avalonia.Media.Immutable /// public double Offset { get; } + + /// + public override bool Equals(object obj) => DashStyle.DashEquals(this, obj as IDashStyle); + + /// + public bool Equals(IDashStyle other) => DashStyle.DashEquals(this, other); + + /// + public override int GetHashCode() => DashStyle.GetHashCode(this); + } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs index 70b4281083..d5601c7497 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs @@ -65,5 +65,27 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.True(Equals(target1, target2)); } + + [Fact] + public void Equality_Is_Implemented_Between_Mutable_And_Immutable_DashStyles() + { + var brush = new SolidColorBrush(Colors.Red); + var target1 = new Pen( + brush: brush, + thickness: 2, + dashStyle: new DashStyle(new[] { 0.1, 0.2 }, 5), + lineCap: PenLineCap.Round, + lineJoin: PenLineJoin.Round, + miterLimit: 21); + var target2 = new ImmutablePen( + brush: brush, + thickness: 2, + dashStyle: new ImmutableDashStyle(new[] { 0.1, 0.2 }, 5), + lineCap: PenLineCap.Round, + lineJoin: PenLineJoin.Round, + miterLimit: 21); + + Assert.True(Equals(target1, target2)); + } } } From c5aca416918032404429c613d7e2e7aeb68e76bb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 17 Jul 2019 10:21:26 +0200 Subject: [PATCH 044/138] Use custom SequenceEqual. LINQ version is slower and this is potentially a hot path. --- src/Avalonia.Visuals/Media/DashStyle.cs | 35 +++++++++++++++++-------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Visuals/Media/DashStyle.cs b/src/Avalonia.Visuals/Media/DashStyle.cs index bd81cb1d03..4ae2285dfd 100644 --- a/src/Avalonia.Visuals/Media/DashStyle.cs +++ b/src/Avalonia.Visuals/Media/DashStyle.cs @@ -136,17 +136,7 @@ namespace Avalonia.Media return false; } - if (ReferenceEquals(a.Dashes, b.Dashes)) - { - return true; - } - - if ((a.Dashes is null && !(b.Dashes is null)) || (b.Dashes is null && !(a.Dashes is null))) - { - return false; - } - - return a.Dashes.SequenceEqual(b.Dashes); + return SequenceEqual(a.Dashes, b.Dashes); } internal static int GetHashCode(IDashStyle style) @@ -164,5 +154,28 @@ namespace Avalonia.Media return hashCode; } + + private static bool SequenceEqual(IReadOnlyList left, IReadOnlyList right) + { + if (left == right) + { + return true; + } + + if (left == null || right == null || left.Count != right.Count) + { + return false; + } + + for (var c = 0; c < left.Count; c++) + { + if (left[c] != right[c]) + { + return false; + } + } + + return true; + } } } From 52c3b6de59b5a56dc8d657a52a9da4ac91ad1d6d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 18 Jul 2019 14:34:54 +0200 Subject: [PATCH 045/138] Use ValueTuple to calculate hash. --- src/Avalonia.Visuals/Media/Pen.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index fdf006e054..b7219bbc6a 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -244,14 +244,8 @@ namespace Avalonia.Media internal static int GetHashCode(IPen pen) { - var hashCode = 1181807663; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(pen.Brush); - hashCode = hashCode * -1521134295 + pen.Thickness.GetHashCode(); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(pen.DashStyle); - hashCode = hashCode * -1521134295 + pen.LineCap.GetHashCode(); - hashCode = hashCode * -1521134295 + pen.LineJoin.GetHashCode(); - hashCode = hashCode * -1521134295 + pen.MiterLimit.GetHashCode(); - return hashCode; + return (pen.Brush, pen.Thickness, pen.DashStyle, pen.LineCap, pen.LineJoin, pen.MiterLimit) + .GetHashCode(); } internal static bool PenEquals(IPen a, IPen b) From bc1ddc31ab3e9ad21440f91bca870ced98794285 Mon Sep 17 00:00:00 2001 From: JaggerJo Date: Fri, 19 Jul 2019 17:44:00 +0200 Subject: [PATCH 046/138] remove duplicate setter for "HeadersVisibility". --- src/Avalonia.Controls.DataGrid/Themes/Default.xaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index 2889a5c77c..eaa267ba66 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml @@ -195,7 +195,6 @@ - @@ -230,4 +229,4 @@ - \ No newline at end of file + From 689781f24e481083318c65bb3110ae465e093b14 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 20 Jul 2019 16:59:13 +0200 Subject: [PATCH 047/138] Make ViewModelBase and ViewLocator internal. --- samples/ControlCatalog/ViewModels/MainWindowViewModel.cs | 3 +-- src/Avalonia.Diagnostics/ViewLocator.cs | 2 +- src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs | 7 +++++-- .../Views/{TreePage.xaml.cs => TreePageView.xaml.cs} | 0 4 files changed, 7 insertions(+), 5 deletions(-) rename src/Avalonia.Diagnostics/Views/{TreePage.xaml.cs => TreePageView.xaml.cs} (100%) diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 28cb84dad0..adf0345a70 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -1,11 +1,10 @@ using System.Reactive; using Avalonia.Controls.Notifications; -using Avalonia.Diagnostics.ViewModels; using ReactiveUI; namespace ControlCatalog.ViewModels { - class MainWindowViewModel : ViewModelBase + class MainWindowViewModel : ReactiveObject { private IManagedNotificationManager _notificationManager; diff --git a/src/Avalonia.Diagnostics/ViewLocator.cs b/src/Avalonia.Diagnostics/ViewLocator.cs index cda511909a..a66703301d 100644 --- a/src/Avalonia.Diagnostics/ViewLocator.cs +++ b/src/Avalonia.Diagnostics/ViewLocator.cs @@ -7,7 +7,7 @@ using Avalonia.Controls.Templates; namespace Avalonia.Diagnostics { - public class ViewLocator : IDataTemplate + internal class ViewLocator : IDataTemplate { public bool SupportsRecycling => false; diff --git a/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs b/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs index 00660754c0..a6ff4dd853 100644 --- a/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs +++ b/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs @@ -1,11 +1,14 @@ -using System.Collections.Generic; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; using JetBrains.Annotations; namespace Avalonia.Diagnostics.ViewModels { - public class ViewModelBase : INotifyPropertyChanged + internal class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; diff --git a/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs b/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs similarity index 100% rename from src/Avalonia.Diagnostics/Views/TreePage.xaml.cs rename to src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs From 4f83c4e8c79bae43648d0508f6fd4ba924d28626 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 20 Jul 2019 17:45:32 +0200 Subject: [PATCH 048/138] Reformat Avalonia.Diagnostics. --- .editorconfig | 3 +- src/Avalonia.Diagnostics/DevTools.xaml | 36 +++---- src/Avalonia.Diagnostics/DevTools.xaml.cs | 33 +++--- .../Models/EventChainLink.cs | 7 +- .../ViewModels/ControlDetailsViewModel.cs | 12 +-- .../ViewModels/DevToolsViewModel.cs | 1 - .../ViewModels/EventOwnerTreeNode.cs | 15 ++- .../ViewModels/EventTreeNode.cs | 9 +- .../ViewModels/EventTreeNodeBase.cs | 17 +-- .../ViewModels/FiredEvent.cs | 12 +-- .../ViewModels/LogicalTreeNode.cs | 3 +- .../ViewModels/PropertyDetails.cs | 16 +-- .../ViewModels/TreeNode.cs | 15 ++- .../ViewModels/TreePageViewModel.cs | 22 ++-- .../ViewModels/VisualTreeNode.cs | 5 +- .../Views/ControlDetailsView.cs | 17 ++- .../Views/EventsView.xaml | 100 +++++++++--------- .../Views/EventsView.xaml.cs | 8 +- .../Views/GridRepeater.cs | 3 +- .../Views/PropertyChangedExtensions.cs | 10 +- src/Avalonia.Diagnostics/Views/SimpleGrid.cs | 8 +- .../Views/TreePageView.xaml | 12 +-- .../Views/TreePageView.xaml.cs | 4 +- 23 files changed, 173 insertions(+), 195 deletions(-) diff --git a/.editorconfig b/.editorconfig index 5f08d1e940..f6bce9cb76 100644 --- a/.editorconfig +++ b/.editorconfig @@ -131,13 +131,14 @@ csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false +space_within_single_line_array_initializer_braces = true # Wrapping preferences csharp_wrap_before_ternary_opsigns = false # Xaml files [*.xaml] -indent_size = 4 +indent_size = 2 # Xml project files [*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] diff --git a/src/Avalonia.Diagnostics/DevTools.xaml b/src/Avalonia.Diagnostics/DevTools.xaml index a538516c1a..1df0f3a097 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml +++ b/src/Avalonia.Diagnostics/DevTools.xaml @@ -1,24 +1,24 @@ - + - - - - - - - + + + + + + + - - Hold Ctrl+Shift over a control to inspect. - - Focused: - - - Pointer Over: - - - + + Hold Ctrl+Shift over a control to inspect. + + Focused: + + + Pointer Over: + + + diff --git a/src/Avalonia.Diagnostics/DevTools.xaml.cs b/src/Avalonia.Diagnostics/DevTools.xaml.cs index ccb6151ada..cc3c545d84 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml.cs +++ b/src/Avalonia.Diagnostics/DevTools.xaml.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -18,22 +17,22 @@ using Avalonia.VisualTree; namespace Avalonia { - public static class DevToolsExtensions - { - public static void AttachDevTools(this TopLevel control) - { - Avalonia.Diagnostics.DevTools.Attach(control); - } - } + public static class DevToolsExtensions + { + public static void AttachDevTools(this TopLevel control) + { + Diagnostics.DevTools.Attach(control); + } + } } namespace Avalonia.Diagnostics { - public class DevTools : UserControl + public class DevTools : UserControl { - private static Dictionary s_open = new Dictionary(); - private static HashSet s_visualTreeRoots = new HashSet(); - private IDisposable _keySubscription; + private static readonly Dictionary s_open = new Dictionary(); + private static readonly HashSet s_visualTreeRoots = new HashSet(); + private readonly IDisposable _keySubscription; public DevTools(IControl root) { @@ -49,7 +48,6 @@ namespace Avalonia.Diagnostics // HACK: needed for XAMLIL, will fix that later public DevTools() { - } public IControl Root { get; } @@ -67,9 +65,8 @@ namespace Avalonia.Diagnostics if (e.Key == Key.F12) { var control = (TopLevel)sender; - var devToolsWindow = default(Window); - if (s_open.TryGetValue(control, out devToolsWindow)) + if (s_open.TryGetValue(control, out var devToolsWindow)) { devToolsWindow.Activate(); } @@ -82,10 +79,7 @@ namespace Avalonia.Diagnostics Width = 1024, Height = 512, Content = devTools, - DataTemplates = - { - new ViewLocator(), - }, + DataTemplates = { new ViewLocator() }, Title = "Avalonia DevTools" }; @@ -118,7 +112,6 @@ namespace Avalonia.Diagnostics if ((e.Modifiers) == modifiers) { - var point = (Root.VisualRoot as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default(Point); var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible)) .FirstOrDefault(); diff --git a/src/Avalonia.Diagnostics/Models/EventChainLink.cs b/src/Avalonia.Diagnostics/Models/EventChainLink.cs index aab50a13dd..464187a048 100644 --- a/src/Avalonia.Diagnostics/Models/EventChainLink.cs +++ b/src/Avalonia.Diagnostics/Models/EventChainLink.cs @@ -12,9 +12,9 @@ namespace Avalonia.Diagnostics.Models { Contract.Requires(handler != null); - this.Handler = handler; - this.Handled = handled; - this.Route = route; + Handler = handler; + Handled = handled; + Route = route; } public object Handler { get; } @@ -27,6 +27,7 @@ namespace Avalonia.Diagnostics.Models { return named.Name + " (" + Handler.GetType().Name + ")"; } + return Handler.GetType().Name; } } diff --git a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs index d723890196..4b832f7ce6 100644 --- a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -20,16 +20,6 @@ namespace Avalonia.Diagnostics.ViewModels } } - public IEnumerable Classes - { - get; - private set; - } - - public IEnumerable Properties - { - get; - private set; - } + public IEnumerable Properties { get; } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs index bc80ab0550..9f524a21eb 100644 --- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Avalonia.Controls; diff --git a/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs index 0674918400..7e38749a6f 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs @@ -13,22 +13,18 @@ namespace Avalonia.Diagnostics.ViewModels { internal class EventOwnerTreeNode : EventTreeNodeBase { - private static readonly RoutedEvent[] s_defaultEvents = new RoutedEvent[] + private static readonly RoutedEvent[] s_defaultEvents = { - Button.ClickEvent, - InputElement.KeyDownEvent, - InputElement.KeyUpEvent, - InputElement.TextInputEvent, - InputElement.PointerReleasedEvent, - InputElement.PointerPressedEvent, + Button.ClickEvent, InputElement.KeyDownEvent, InputElement.KeyUpEvent, InputElement.TextInputEvent, + InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent }; public EventOwnerTreeNode(Type type, IEnumerable events, EventsViewModel vm) : base(null, type.Name) { - this.Children = new AvaloniaList(events.OrderBy(e => e.Name) + Children = new AvaloniaList(events.OrderBy(e => e.Name) .Select(e => new EventTreeNode(this, e, vm) { IsEnabled = s_defaultEvents.Contains(e) })); - this.IsExpanded = true; + IsExpanded = true; } public override bool? IsEnabled @@ -39,6 +35,7 @@ namespace Avalonia.Diagnostics.ViewModels if (base.IsEnabled != value) { base.IsEnabled = value; + if (_updateChildren && value != null) { foreach (var child in Children) diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs index 7ece790310..36f1904253 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; - using Avalonia.Diagnostics.Models; using Avalonia.Interactivity; using Avalonia.Threading; @@ -12,8 +11,8 @@ namespace Avalonia.Diagnostics.ViewModels { internal class EventTreeNode : EventTreeNodeBase { - private RoutedEvent _event; - private EventsViewModel _parentViewModel; + private readonly RoutedEvent _event; + private readonly EventsViewModel _parentViewModel; private bool _isRegistered; private FiredEvent _currentEvent; @@ -23,8 +22,8 @@ namespace Avalonia.Diagnostics.ViewModels Contract.Requires(@event != null); Contract.Requires(vm != null); - this._event = @event; - this._parentViewModel = vm; + _event = @event; + _parentViewModel = vm; } public override bool? IsEnabled diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs b/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs index 146a8cea8e..4be4d8f74e 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs +++ b/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs @@ -12,10 +12,10 @@ namespace Avalonia.Diagnostics.ViewModels private bool _isExpanded; private bool? _isEnabled = false; - public EventTreeNodeBase(EventTreeNodeBase parent, string text) + protected EventTreeNodeBase(EventTreeNodeBase parent, string text) { - this.Parent = parent; - this.Text = text; + Parent = parent; + Text = text; } public IAvaloniaReadOnlyList Children @@ -26,14 +26,14 @@ namespace Avalonia.Diagnostics.ViewModels public bool IsExpanded { - get { return _isExpanded; } - set { RaiseAndSetIfChanged(ref _isExpanded, value); } + get => _isExpanded; + set => RaiseAndSetIfChanged(ref _isExpanded, value); } public virtual bool? IsEnabled { - get { return _isEnabled; } - set { RaiseAndSetIfChanged(ref _isEnabled, value); } + get => _isEnabled; + set => RaiseAndSetIfChanged(ref _isEnabled, value); } public EventTreeNodeBase Parent @@ -44,7 +44,6 @@ namespace Avalonia.Diagnostics.ViewModels public string Text { get; - private set; } internal void UpdateChecked() @@ -55,7 +54,9 @@ namespace Avalonia.Diagnostics.ViewModels { if (Children == null) return false; + bool? value = false; + for (int i = 0; i < Children.Count; i++) { if (i == 0) diff --git a/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs index 049280c390..daf8ebd0f6 100644 --- a/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs +++ b/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs @@ -3,7 +3,6 @@ using System; using System.Collections.ObjectModel; - using Avalonia.Diagnostics.Models; using Avalonia.Interactivity; @@ -11,7 +10,7 @@ namespace Avalonia.Diagnostics.ViewModels { internal class FiredEvent : ViewModelBase { - private RoutedEventArgs _eventArgs; + private readonly RoutedEventArgs _eventArgs; private EventChainLink _handledBy; public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator) @@ -19,8 +18,8 @@ namespace Avalonia.Diagnostics.ViewModels Contract.Requires(eventArgs != null); Contract.Requires(originator != null); - this._eventArgs = eventArgs; - this.Originator = originator; + _eventArgs = eventArgs; + Originator = originator; AddToChain(originator); } @@ -42,8 +41,9 @@ namespace Avalonia.Diagnostics.ViewModels if (IsHandled) { return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine + - $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}"; + $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}"; } + return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}"; } } @@ -52,7 +52,7 @@ namespace Avalonia.Diagnostics.ViewModels public EventChainLink HandledBy { - get { return _handledBy; } + get => _handledBy; set { if (_handledBy != value) diff --git a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs index 638cf6c88f..0b9bd85b4f 100644 --- a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs @@ -17,8 +17,7 @@ namespace Avalonia.Diagnostics.ViewModels public static LogicalTreeNode[] Create(object control) { - var logical = control as ILogical; - return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null; + return control is ILogical logical ? new[] { new LogicalTreeNode(logical, null) } : null; } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs index 2609b74ce0..523be406c8 100644 --- a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs +++ b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs @@ -26,7 +26,9 @@ namespace Avalonia.Diagnostics.ViewModels Value = diagnostic.Value ?? "(null)"; Priority = (diagnostic.Priority != BindingPriority.Unset) ? diagnostic.Priority.ToString() : - diagnostic.Property.Inherits ? "Inherited" : "Unset"; + diagnostic.Property.Inherits ? + "Inherited" : + "Unset"; Diagnostic = diagnostic.Diagnostic; }); } @@ -37,20 +39,20 @@ namespace Avalonia.Diagnostics.ViewModels public string Priority { - get { return _priority; } - private set { RaiseAndSetIfChanged(ref _priority, value); } + get => _priority; + private set => RaiseAndSetIfChanged(ref _priority, value); } public string Diagnostic { - get { return _diagnostic; } - private set { RaiseAndSetIfChanged(ref _diagnostic, value); } + get => _diagnostic; + private set => RaiseAndSetIfChanged(ref _diagnostic, value); } public object Value { - get { return _value; } - private set { RaiseAndSetIfChanged(ref _value, value); } + get => _value; + private set => RaiseAndSetIfChanged(ref _value, value); } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs index 7c403e1b04..902eb81bd9 100644 --- a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs @@ -27,9 +27,9 @@ namespace Avalonia.Diagnostics.ViewModels var classesChanged = Observable.FromEventPattern< NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>( - x => styleable.Classes.CollectionChanged += x, - x => styleable.Classes.CollectionChanged -= x) - .TakeUntil(((IStyleable)styleable).StyleDetach); + x => styleable.Classes.CollectionChanged += x, + x => styleable.Classes.CollectionChanged -= x) + .TakeUntil(styleable.StyleDetach); classesChanged.Select(_ => Unit.Default) .StartWith(Unit.Default) @@ -55,8 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels public string Classes { - get { return _classes; } - private set { RaiseAndSetIfChanged(ref _classes, value); } + get => _classes; + private set => RaiseAndSetIfChanged(ref _classes, value); } public IVisual Visual @@ -66,8 +66,8 @@ namespace Avalonia.Diagnostics.ViewModels public bool IsExpanded { - get { return _isExpanded; } - set { RaiseAndSetIfChanged(ref _isExpanded, value); } + get => _isExpanded; + set => RaiseAndSetIfChanged(ref _isExpanded, value); } public TreeNode Parent @@ -78,7 +78,6 @@ namespace Avalonia.Diagnostics.ViewModels public string Type { get; - private set; } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs index 6b294c98bd..b2b1aaa723 100644 --- a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs @@ -23,7 +23,7 @@ namespace Avalonia.Diagnostics.ViewModels public TreeNode SelectedNode { - get { return _selected; } + get => _selected; set { if (RaiseAndSetIfChanged(ref _selected, value)) @@ -35,8 +35,8 @@ namespace Avalonia.Diagnostics.ViewModels public ControlDetailsViewModel Details { - get { return _details; } - private set { RaiseAndSetIfChanged(ref _details, value); } + get => _details; + private set => RaiseAndSetIfChanged(ref _details, value); } public TreeNode FindNode(IControl control) @@ -66,7 +66,7 @@ namespace Avalonia.Diagnostics.ViewModels { control = control.GetVisualParent(); } - } + } if (node != null) { @@ -90,16 +90,14 @@ namespace Avalonia.Diagnostics.ViewModels { return node; } - else + + foreach (var child in node.Children) { - foreach (var child in node.Children) - { - var result = FindNode(child, control); + var result = FindNode(child, control); - if (result != null) - { - return result; - } + if (result != null) + { + return result; } } diff --git a/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs index 8c070261d9..47ef91507a 100644 --- a/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs @@ -29,12 +29,11 @@ namespace Avalonia.Diagnostics.ViewModels } } - public bool IsInTemplate { get; private set; } + public bool IsInTemplate { get; } public static VisualTreeNode[] Create(object control) { - var visual = control as IVisual; - return visual != null ? new[] { new VisualTreeNode(visual, null) } : null; + return control is IVisual visual ? new[] { new VisualTreeNode(visual, null) } : null; } } } diff --git a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs index 868bc774bb..fb867ab55e 100644 --- a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs +++ b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs @@ -14,6 +14,7 @@ namespace Avalonia.Diagnostics.Views { private static readonly StyledProperty ViewModelProperty = AvaloniaProperty.Register(nameof(ViewModel)); + private SimpleGrid _grid; public ControlDetailsView() @@ -25,7 +26,7 @@ namespace Avalonia.Diagnostics.Views public ControlDetailsViewModel ViewModel { - get { return GetValue(ViewModelProperty); } + get => GetValue(ViewModelProperty); private set { SetValue(ViewModelProperty, value); @@ -37,13 +38,7 @@ namespace Avalonia.Diagnostics.Views { Func> pt = PropertyTemplate; - Content = new ScrollViewer - { - Content = _grid = new SimpleGrid - { - [GridRepeater.TemplateProperty] = pt, - } - }; + Content = new ScrollViewer { Content = _grid = new SimpleGrid { [GridRepeater.TemplateProperty] = pt } }; } private IEnumerable PropertyTemplate(object i) @@ -57,7 +52,7 @@ namespace Avalonia.Diagnostics.Views Margin = margin, Text = property.Name, TextWrapping = TextWrapping.NoWrap, - [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding(), + [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding() }; yield return new TextBlock @@ -66,14 +61,14 @@ namespace Avalonia.Diagnostics.Views TextWrapping = TextWrapping.NoWrap, [!TextBlock.TextProperty] = property.GetObservable(nameof(property.Value)) .Select(v => v?.ToString()) - .ToBinding(), + .ToBinding() }; yield return new TextBlock { Margin = margin, TextWrapping = TextWrapping.NoWrap, - [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding(), + [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding() }; } } diff --git a/src/Avalonia.Diagnostics/Views/EventsView.xaml b/src/Avalonia.Diagnostics/Views/EventsView.xaml index 8d4d37f7b3..406dd433a2 100644 --- a/src/Avalonia.Diagnostics/Views/EventsView.xaml +++ b/src/Avalonia.Diagnostics/Views/EventsView.xaml @@ -2,53 +2,57 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" x:Class="Avalonia.Diagnostics.Views.EventsView"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -