Browse Source

Merge branch 'master' into fixes/1099-inherited-propertychanged-order

Steven Kirk 6 years ago
parent
commit
272298ba04

+ 2 - 2
src/Avalonia.Animation/Cue.cs

@@ -30,7 +30,7 @@ namespace Avalonia.Animation
         /// <summary>
         /// Parses a string to a <see cref="Cue"/> object.
         /// </summary>
-        public static object Parse(string value, CultureInfo culture)
+        public static Cue Parse(string value, CultureInfo culture)
         {
             string v = value;
 
@@ -70,7 +70,7 @@ namespace Avalonia.Animation
         }
     }
 
-    public class CueTypeConverter : TypeConverter 
+    public class CueTypeConverter : TypeConverter
     {
         public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
         {

+ 10 - 5
src/Avalonia.Layout/LayoutManager.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 Avalonia.Logging;
 using Avalonia.Threading;
 
@@ -13,8 +12,8 @@ namespace Avalonia.Layout
     /// </summary>
     public class LayoutManager : ILayoutManager
     {
-        private readonly Queue<ILayoutable> _toMeasure = new Queue<ILayoutable>();
-        private readonly Queue<ILayoutable> _toArrange = new Queue<ILayoutable>();
+        private readonly LayoutQueue<ILayoutable> _toMeasure = new LayoutQueue<ILayoutable>(v => !v.IsMeasureValid);
+        private readonly LayoutQueue<ILayoutable> _toArrange = new LayoutQueue<ILayoutable>(v => !v.IsArrangeValid);
         private bool _queued;
         private bool _running;
 
@@ -80,6 +79,9 @@ namespace Avalonia.Layout
                 var stopwatch = new System.Diagnostics.Stopwatch();
                 stopwatch.Start();
 
+                _toMeasure.BeginLoop(MaxPasses);
+                _toArrange.BeginLoop(MaxPasses);
+
                 try
                 {
                     for (var pass = 0; pass < MaxPasses; ++pass)
@@ -98,6 +100,9 @@ namespace Avalonia.Layout
                     _running = false;
                 }
 
+                _toMeasure.EndLoop();
+                _toArrange.EndLoop();
+
                 stopwatch.Stop();
                 Logger.Information(LogArea.Layout, this, "Layout pass finished in {Time}", stopwatch.Elapsed);
             }
@@ -112,7 +117,7 @@ namespace Avalonia.Layout
             Arrange(root);
 
             // Running the initial layout pass may have caused some control to be invalidated
-            // so run a full layout pass now (this usually due to scrollbars; its not known 
+            // so run a full layout pass now (this usually due to scrollbars; its not known
             // whether they will need to be shown until the layout pass has run and if the
             // first guess was incorrect the layout will need to be updated).
             ExecuteLayoutPass();
@@ -133,7 +138,7 @@ namespace Avalonia.Layout
 
         private void ExecuteArrangePass()
         {
-            while (_toArrange.Count > 0 && _toMeasure.Count == 0)
+            while (_toArrange.Count > 0)
             {
                 var control = _toArrange.Dequeue();
 

+ 79 - 0
src/Avalonia.Layout/LayoutQueue.cs

@@ -0,0 +1,79 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Avalonia.Layout
+{
+    internal class LayoutQueue<T> : IReadOnlyCollection<T>
+    {
+        private struct Info
+        {
+            public bool Active;
+            public int Count;
+        }
+
+        public LayoutQueue(Func<T, bool> shouldEnqueue)
+        {
+            _shouldEnqueue = shouldEnqueue;
+        }
+
+        private Func<T, bool> _shouldEnqueue;
+        private Queue<T> _inner = new Queue<T>();
+        private Dictionary<T, Info> _loopQueueInfo = new Dictionary<T, Info>();
+        private int _maxEnqueueCountPerLoop = 1;
+
+        public int Count => _inner.Count;
+
+        public IEnumerator<T> GetEnumerator() => (_inner as IEnumerable<T>).GetEnumerator();
+
+        IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator();
+
+        public T Dequeue()
+        {
+            var result = _inner.Dequeue();
+
+            if (_loopQueueInfo.TryGetValue(result, out var info))
+            {
+                info.Active = false;
+                _loopQueueInfo[result] = info;
+            }
+
+            return result;
+        }
+
+        public void Enqueue(T item)
+        {
+            _loopQueueInfo.TryGetValue(item, out var info);
+
+            if (!info.Active && info.Count < _maxEnqueueCountPerLoop)
+            {
+                _inner.Enqueue(item);
+                _loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 };
+            }
+        }
+
+        public void BeginLoop(int maxEnqueueCountPerLoop)
+        {
+            _maxEnqueueCountPerLoop = maxEnqueueCountPerLoop;
+        }
+
+        public void EndLoop()
+        {
+            var notfinalized = _loopQueueInfo.Where(v => v.Value.Count == _maxEnqueueCountPerLoop).ToArray();
+
+            _loopQueueInfo.Clear();
+
+            //prevent layout cycle but add to next layout the non arranged/measured items that might have caused cycle
+            //one more time as a final attempt
+            foreach (var item in notfinalized)
+            {
+                if (_shouldEnqueue(item.Key))
+                {
+                    _loopQueueInfo[item.Key] = new Info() { Active = true, Count = item.Value.Count + 1 };
+                    _inner.Enqueue(item.Key);
+                }
+            }
+        }
+    }
+}

+ 4 - 0
src/Avalonia.Layout/Properties/AssemblyInfo.cs

@@ -1,6 +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.
 
+using System.Runtime.CompilerServices;
 using Avalonia.Metadata;
 
+[assembly: InternalsVisibleTo("Avalonia.Layout.UnitTests")]
+
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Layout")]
+

+ 26 - 11
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@@ -322,36 +322,51 @@ namespace Avalonia.Rendering.SceneGraph
             }
         }
 
+        /// <summary>
+        /// Ensures that this node draw operations have been created and are mutable (in case we are using cloned operations).
+        /// </summary>
         private void EnsureDrawOperationsCreated()
         {
             if (_drawOperations == null)
             {
                 _drawOperations = new List<IRef<IDrawOperation>>();
-                _drawOperationsRefCounter = RefCountable.Create(Disposable.Create(DisposeDrawOperations));
+                _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations));
                 _drawOperationsCloned = false;
             }
             else if (_drawOperationsCloned)
             {
                 _drawOperations = new List<IRef<IDrawOperation>>(_drawOperations.Select(op => op.Clone()));
                 _drawOperationsRefCounter.Dispose();
-                _drawOperationsRefCounter = RefCountable.Create(Disposable.Create(DisposeDrawOperations));
+                _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations));
                 _drawOperationsCloned = false;
             }
         }
 
-        public bool Disposed { get; }
-        
-        public void Dispose()
+        /// <summary>
+        /// Creates disposable that will dispose all items in passed draw operations after being disposed.
+        /// It is crucial that we don't capture current <see cref="VisualNode"/> instance
+        /// as draw operations can be cloned and may persist across subsequent scenes.
+        /// </summary>
+        /// <param name="drawOperations">Draw operations that need to be disposed.</param>
+        /// <returns>Disposable for given draw operations.</returns>
+        private static IDisposable CreateDisposeDrawOperations(List<IRef<IDrawOperation>> drawOperations)
         {
-            _drawOperationsRefCounter?.Dispose();
+            return Disposable.Create(() =>
+            {
+                foreach (var operation in drawOperations)
+                {
+                    operation.Dispose();
+                }
+            });
         }
 
-        private void DisposeDrawOperations()
+        public bool Disposed { get; private set; }
+
+        public void Dispose()
         {
-            foreach (var operation in DrawOperations)
-            {
-                operation.Dispose();
-            }
+            _drawOperationsRefCounter?.Dispose();
+
+            Disposed = true;
         }
     }
 }

+ 68 - 0
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@@ -267,6 +267,74 @@ namespace Avalonia.Controls.UnitTests
             Assert.True(true);
         }
 
+        [Fact]
+        public void LayoutManager_Should_Measure_Arrange_All()
+        {
+            var virtualizationMode = ItemVirtualizationMode.Simple;
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var items = new AvaloniaList<string>(Enumerable.Range(1, 7).Select(v => v.ToString()));
+
+                var wnd = new Window() { SizeToContent = SizeToContent.WidthAndHeight };
+
+                wnd.IsVisible = true;
+
+                var target = new ListBox();
+
+                wnd.Content = target;
+
+                var lm = wnd.LayoutManager;
+
+                target.Height = 110;
+                target.Width = 50;
+                target.DataContext = items;
+                target.VirtualizationMode = virtualizationMode;
+
+                target.ItemTemplate = new FuncDataTemplate<object>(c =>
+                {
+                    var tb = new TextBlock() { Height = 10, Width = 30 };
+                    tb.Bind(TextBlock.TextProperty, new Data.Binding());
+                    return tb;
+                }, true);
+
+                lm.ExecuteInitialLayoutPass(wnd);
+
+                target.Items = items;
+
+                lm.ExecuteLayoutPass();
+
+                items.Insert(3, "3+");
+                lm.ExecuteLayoutPass();
+
+                items.Insert(4, "4+");
+                lm.ExecuteLayoutPass();
+
+                //RESET
+                items.Clear();
+                foreach (var i in Enumerable.Range(1, 7))
+                {
+                    items.Add(i.ToString());
+                }
+
+                //working bit better with this line no outof memory or remaining to arrange/measure ???
+                //lm.ExecuteLayoutPass();
+
+                items.Insert(2, "2+");
+
+                lm.ExecuteLayoutPass();
+                //after few more layout cycles layoutmanager shouldn't hold any more visual for measure/arrange
+                lm.ExecuteLayoutPass();
+                lm.ExecuteLayoutPass();
+
+                var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic;
+                var toMeasure = lm.GetType().GetField("_toMeasure", flags).GetValue(lm) as System.Collections.Generic.IEnumerable<Layout.ILayoutable>;
+                var toArrange = lm.GetType().GetField("_toArrange", flags).GetValue(lm) as System.Collections.Generic.IEnumerable<Layout.ILayoutable>;
+
+                Assert.Equal(0, toMeasure.Count());
+                Assert.Equal(0, toArrange.Count());
+            }
+        }
+
         private FuncControlTemplate ListBoxTemplate()
         {
             return new FuncControlTemplate<ListBox>(parent =>

+ 110 - 9
tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs

@@ -1,11 +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.
 
+using System.Collections.Generic;
+using System.Linq;
 using Avalonia.Controls;
-using Avalonia.UnitTests;
-using System;
 using Xunit;
-using System.Collections.Generic;
 
 namespace Avalonia.Layout.UnitTests
 {
@@ -74,7 +73,6 @@ namespace Avalonia.Layout.UnitTests
                 }
             };
 
-
             var order = new List<ILayoutable>();
             Size MeasureOverride(ILayoutable control, Size size)
             {
@@ -110,7 +108,6 @@ namespace Avalonia.Layout.UnitTests
                 }
             };
 
-
             var order = new List<ILayoutable>();
             Size MeasureOverride(ILayoutable control, Size size)
             {
@@ -196,9 +193,9 @@ namespace Avalonia.Layout.UnitTests
                 Width = 100,
                 Height = 100,
             };
- 
+
             var arrangeSize = default(Size);
- 
+
             root.DoArrangeOverride = (_, s) =>
             {
                 arrangeSize = s;
@@ -207,7 +204,7 @@ namespace Avalonia.Layout.UnitTests
 
             root.LayoutManager.ExecuteInitialLayoutPass(root);
             Assert.Equal(new Size(100, 100), arrangeSize);
- 
+
             root.Width = 120;
 
             root.LayoutManager.ExecuteLayoutPass();
@@ -238,7 +235,111 @@ namespace Avalonia.Layout.UnitTests
             border.Height = 100;
 
             root.LayoutManager.ExecuteLayoutPass();
-            Assert.Equal(new Size(100, 100), panel.DesiredSize);             
+            Assert.Equal(new Size(100, 100), panel.DesiredSize);
+        }
+
+        [Fact]
+        public void LayoutManager_Should_Prevent_Infinite_Loop_On_Measure()
+        {
+            var control = new LayoutTestControl();
+            var root = new LayoutTestRoot { Child = control };
+
+            root.LayoutManager.ExecuteInitialLayoutPass(root);
+            control.Measured = false;
+
+            int cnt = 0;
+            int maxcnt = 100;
+            control.DoMeasureOverride = (l, s) =>
+            {
+                //emulate a problem in the logic of a control that triggers
+                //invalidate measure during measure
+                //it can lead to an infinite loop in layoutmanager
+                if (++cnt < maxcnt)
+                {
+                    control.InvalidateMeasure();
+                }
+
+                return new Size(100, 100);
+            };
+
+            control.InvalidateMeasure();
+
+            root.LayoutManager.ExecuteLayoutPass();
+
+            Assert.True(cnt < 100);
+        }
+
+        [Fact]
+        public void LayoutManager_Should_Prevent_Infinite_Loop_On_Arrange()
+        {
+            var control = new LayoutTestControl();
+            var root = new LayoutTestRoot { Child = control };
+
+            root.LayoutManager.ExecuteInitialLayoutPass(root);
+            control.Arranged = false;
+
+            int cnt = 0;
+            int maxcnt = 100;
+            control.DoArrangeOverride = (l, s) =>
+            {
+                //emulate a problem in the logic of a control that triggers
+                //invalidate measure during arrange
+                //it can lead to infinity loop in layoutmanager
+                if (++cnt < maxcnt)
+                {
+                    control.InvalidateArrange();
+                }
+
+                return new Size(100, 100);
+            };
+
+            control.InvalidateArrange();
+
+            root.LayoutManager.ExecuteLayoutPass();
+
+            Assert.True(cnt < 100);
+        }
+
+        [Fact]
+        public void LayoutManager_Should_Properly_Arrange_Visuals_Even_When_There_Are_Issues_With_Previous_Arranged()
+        {
+            var nonArrageableTargets = Enumerable.Range(1, 10).Select(_ => new LayoutTestControl()).ToArray();
+            var targets = Enumerable.Range(1, 10).Select(_ => new LayoutTestControl()).ToArray();
+
+            StackPanel panel;
+
+            var root = new LayoutTestRoot
+            {
+                Child = panel = new StackPanel()
+            };
+
+            panel.Children.AddRange(nonArrageableTargets);
+            panel.Children.AddRange(targets);
+
+            root.LayoutManager.ExecuteInitialLayoutPass(root);
+
+            foreach (var c in panel.Children.OfType<LayoutTestControl>())
+            {
+                c.Measured = c.Arranged = false;
+                c.InvalidateMeasure();
+            }
+
+            foreach (var c in nonArrageableTargets)
+            {
+                c.DoArrangeOverride = (l, s) =>
+                {
+                    //emulate a problem in the logic of a control that triggers
+                    //invalidate measure during arrange
+                    c.InvalidateMeasure();
+                    return new Size(100, 100);
+                };
+            }
+
+            root.LayoutManager.ExecuteLayoutPass();
+
+            //altough nonArrageableTargets has rubbish logic and can't be measured/arranged properly
+            //layoutmanager should process properly other visuals
+            Assert.All(targets, c => Assert.True(c.Arranged));
         }
     }
 }

+ 196 - 0
tests/Avalonia.Layout.UnitTests/LayoutQueueTests.cs

@@ -0,0 +1,196 @@
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace Avalonia.Layout.UnitTests
+{
+    public class LayoutQueueTests
+    {
+        [Fact]
+        public void Should_Enqueue()
+        {
+            var target = new LayoutQueue<string>(_ => true);
+            var refQueue = new Queue<string>();
+            var items = new[] { "1", "2", "3" };
+
+            foreach (var item in items)
+            {
+                target.Enqueue(item);
+                refQueue.Enqueue(item);
+            }
+
+            Assert.Equal(refQueue, target);
+        }
+
+        [Fact]
+        public void Should_Dequeue()
+        {
+            var target = new LayoutQueue<string>(_ => true);
+            var refQueue = new Queue<string>();
+            var items = new[] { "1", "2", "3" };
+
+            foreach (var item in items)
+            {
+                target.Enqueue(item);
+                refQueue.Enqueue(item);
+            }
+
+            while (refQueue.Count > 0)
+            {
+                Assert.Equal(refQueue.Dequeue(), target.Dequeue());
+            }
+        }
+
+        [Fact]
+        public void Should_Enqueue_UniqueElements()
+        {
+            var target = new LayoutQueue<string>(_ => true);
+
+            var items = new[] { "1", "2", "3", "1" };
+
+            foreach (var item in items)
+            {
+                target.Enqueue(item);
+            }
+
+            Assert.Equal(3, target.Count);
+            Assert.Equal(items.Take(3), target);
+        }
+
+        [Fact]
+        public void Shouldnt_Enqueue_More_Than_Limit_In_Loop()
+        {
+            var target = new LayoutQueue<string>(_ => true);
+
+            //1
+            target.Enqueue("Foo");
+
+            Assert.Equal(1, target.Count);
+
+            target.BeginLoop(3);
+
+            target.Dequeue();
+
+            //2
+            target.Enqueue("Foo");
+            target.Dequeue();
+
+            //3
+            target.Enqueue("Foo");
+
+            Assert.Equal(1, target.Count);
+
+            target.Dequeue();
+
+            //4 more than limit shouldn't be added
+            target.Enqueue("Foo");
+
+            Assert.Equal(0, target.Count);
+        }
+
+        [Fact]
+        public void Shouldnt_Count_Unique_Enqueue_For_Limit_In_Loop()
+        {
+            var target = new LayoutQueue<string>(_ => true);
+
+            //1
+            target.Enqueue("Foo");
+
+            Assert.Equal(1, target.Count);
+
+            target.BeginLoop(3);
+
+            target.Dequeue();
+
+            //2
+            target.Enqueue("Foo");
+            target.Enqueue("Foo");
+            target.Dequeue();
+
+            //3
+            target.Enqueue("Foo");
+            target.Enqueue("Foo");
+
+            Assert.Equal(1, target.Count);
+
+            target.Dequeue();
+
+            //4 more than limit shouldn't be added
+            target.Enqueue("Foo");
+
+            Assert.Equal(0, target.Count);
+        }
+
+        [Fact]
+        public void Should_Enqueue_When_Condition_True_After_Loop_When_Limit_Met()
+        {
+            var target = new LayoutQueue<string>(_ => true);
+
+            //1
+            target.Enqueue("Foo");
+
+            Assert.Equal(1, target.Count);
+
+            target.BeginLoop(3);
+
+            target.Dequeue();
+
+            //2
+            target.Enqueue("Foo");
+            target.Dequeue();
+
+            //3
+            target.Enqueue("Foo");
+
+            Assert.Equal(1, target.Count);
+
+            target.Dequeue();
+
+            //4 more than limit shouldn't be added to queue
+            target.Enqueue("Foo");
+
+            Assert.Equal(0, target.Count);
+
+            target.EndLoop();
+
+            //after loop should be added once
+            Assert.Equal(1, target.Count);
+            Assert.Equal("Foo", target.First());
+        }
+
+        [Fact]
+        public void Shouldnt_Enqueue_When_Condition_False_After_Loop_When_Limit_Met()
+        {
+            var target = new LayoutQueue<string>(_ => false);
+
+            //1
+            target.Enqueue("Foo");
+
+            Assert.Equal(1, target.Count);
+
+            target.BeginLoop(3);
+
+            target.Dequeue();
+
+            //2
+            target.Enqueue("Foo");
+            target.Dequeue();
+
+            //3
+            target.Enqueue("Foo");
+
+            Assert.Equal(1, target.Count);
+
+            target.Dequeue();
+
+            //4 more than limit shouldn't be added
+            target.Enqueue("Foo");
+
+            Assert.Equal(0, target.Count);
+
+            target.EndLoop();
+
+            Assert.Equal(0, target.Count);
+        }
+    }
+}