diff --git a/src/Perspex.Layout/ILayoutable.cs b/src/Perspex.Layout/ILayoutable.cs index a72488ee5c..1a686e758f 100644 --- a/src/Perspex.Layout/ILayoutable.cs +++ b/src/Perspex.Layout/ILayoutable.cs @@ -112,5 +112,11 @@ namespace Perspex.Layout /// Invalidates the arrangement of the control and queues a new layout pass. /// void InvalidateArrange(); + + /// + /// Called when a child control's desired size changes. + /// + /// The child control. + void ChildDesiredSizeChanged(ILayoutable control); } } diff --git a/src/Perspex.Layout/LayoutManager.cs b/src/Perspex.Layout/LayoutManager.cs index 326549ccae..7d27314cb9 100644 --- a/src/Perspex.Layout/LayoutManager.cs +++ b/src/Perspex.Layout/LayoutManager.cs @@ -3,10 +3,10 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reactive; -using System.Reactive.Disposables; using System.Reactive.Subjects; -using Perspex.VisualTree; +using Perspex.Threading; using Serilog; using Serilog.Core.Enrichers; @@ -15,53 +15,15 @@ namespace Perspex.Layout /// /// Manages measuring and arranging of controls. /// - /// - /// Each layout root element such as a window has its own LayoutManager that is responsible - /// for laying out its child controls. When a layout is required the - /// observable will fire and the root element should respond by calling - /// at the earliest opportunity to carry out the layout. - /// public class LayoutManager : ILayoutManager { - /// - /// The maximum number of times a measure/arrange loop can be retried. - /// - private const int MaxTries = 3; - - /// - /// Called when a layout is needed. - /// - private readonly Subject _layoutNeeded; - - /// - /// Called when a layout is completed. - /// - private readonly Subject _layoutCompleted; - - /// - /// Whether a measure is needed on the next layout pass. - /// - private bool _measureNeeded = true; - - /// - /// The controls that need to be measured. - /// - private List _toMeasure = new List(); - - /// - /// The controls that need to be arranged. - /// - private List _toArrange = new List(); - - /// - /// Prevents re-entrancy. - /// - private bool _running; - - /// - /// The logger to use. - /// + private readonly Queue _toMeasure = new Queue(); + private readonly Queue _toArrange = new Queue(); + private readonly Subject _layoutNeeded = new Subject(); + private readonly Subject _layoutCompleted = new Subject(); private readonly ILogger _log; + private bool _first = true; + private bool _running; /// /// Initializes a new instance of the class. @@ -74,9 +36,6 @@ namespace Perspex.Layout new PropertyEnricher("SourceContext", GetType()), new PropertyEnricher("Id", GetHashCode()), }); - - _layoutNeeded = new Subject(); - _layoutCompleted = new Subject(); } /// @@ -114,20 +73,52 @@ namespace Perspex.Layout private set; } + /// + /// Notifies the layout manager that a control requires a measure. + /// + /// The control. + /// The control's distance from the layout root. + public void InvalidateMeasure(ILayoutable control, int distance) + { + Contract.Requires(control != null); + Dispatcher.UIThread.VerifyAccess(); + + _toMeasure.Enqueue(control); + _toArrange.Enqueue(control); + FireLayoutNeeded(); + } + + /// + /// Notifies the layout manager that a control requires an arrange. + /// + /// The control. + /// The control's distance from the layout root. + public void InvalidateArrange(ILayoutable control, int distance) + { + Contract.Requires(control != null); + Dispatcher.UIThread.VerifyAccess(); + + _toArrange.Enqueue(control); + FireLayoutNeeded(); + } + /// /// Executes a layout pass. /// public void ExecuteLayoutPass() { - if (_running) + const int MaxPasses = 3; + + Dispatcher.UIThread.VerifyAccess(); + + if (Root == null) { - return; + throw new InvalidOperationException("Root must be set before executing layout pass."); } - using (Disposable.Create(() => _running = false)) + if (!_running) { _running = true; - LayoutQueued = false; _log.Information( "Started layout pass. To measure: {Measure} To arrange: {Arrange}", @@ -137,21 +128,31 @@ namespace Perspex.Layout var stopwatch = new System.Diagnostics.Stopwatch(); stopwatch.Start(); - for (int i = 0; i < MaxTries; ++i) + try { - if (_measureNeeded) + if (_first) { - ExecuteMeasure(); - _measureNeeded = false; + Measure(Root); + Arrange(Root); + _first = false; } - ExecuteArrange(); - - if (_toMeasure.Count == 0) + for (var pass = 0; pass < MaxPasses; ++pass) { - break; + ExecuteMeasurePass(); + ExecuteArrangePass(); + + if (_toMeasure.Count == 0) + { + break; + } } } + finally + { + _running = false; + LayoutQueued = false; + } stopwatch.Stop(); _log.Information("Layout pass finised in {Time}", stopwatch.Elapsed); @@ -160,181 +161,58 @@ namespace Perspex.Layout } } - /// - /// Notifies the layout manager that a control requires a measure. - /// - /// The control. - /// The control's distance from the layout root. - public void InvalidateMeasure(ILayoutable control, int distance) + private void ExecuteMeasurePass() { - var item = new Item(control, distance); - _toMeasure.Add(item); - _toArrange.Add(item); - - _measureNeeded = true; - - if (!LayoutQueued) + while (_toMeasure.Count > 0) { - IVisual visual = control as IVisual; - _layoutNeeded.OnNext(Unit.Default); - LayoutQueued = true; + var next = _toMeasure.Dequeue(); + Measure(next); } } - /// - /// Notifies the layout manager that a control requires an arrange. - /// - /// The control. - /// The control's distance from the layout root. - public void InvalidateArrange(ILayoutable control, int distance) + private void ExecuteArrangePass() { - _toArrange.Add(new Item(control, distance)); - - if (!LayoutQueued) + while (_toArrange.Count > 0 && _toMeasure.Count == 0) { - IVisual visual = control as IVisual; - _layoutNeeded.OnNext(Unit.Default); - LayoutQueued = true; + var next = _toArrange.Dequeue(); + Arrange(next); } } - /// - /// Executes the measure part of the layout pass. - /// - private void ExecuteMeasure() + private void Measure(ILayoutable control) { - for (int i = 0; i < MaxTries; ++i) - { - var measure = _toMeasure; - - _toMeasure = new List(); - measure.Sort(); + var root = control as ILayoutRoot; - if (!Root.IsMeasureValid) - { - var size = new Size( - double.IsNaN(Root.Width) ? double.PositiveInfinity : Root.Width, - double.IsNaN(Root.Height) ? double.PositiveInfinity : Root.Height); - Root.Measure(size); - } - - foreach (var item in measure) - { - if (!item.Control.IsMeasureValid) - { - if (item.Control != Root) - { - var parent = item.Control.GetVisualParent(); - - while (parent != null && parent.PreviousMeasure == null) - { - parent = parent.GetVisualParent(); - } - - if (parent != null && parent.GetVisualRoot() == Root) - { - parent.Measure(parent.PreviousMeasure.Value, true); - } - } - } - } - - if (_toMeasure.Count == 0) - { - break; - } + if (root != null) + { + root.Measure(Size.Infinity); } - } - - /// - /// Executes the arrange part of the layout pass. - /// - private void ExecuteArrange() - { - for (int i = 0; i < MaxTries; ++i) + else if (control.PreviousMeasure.HasValue) { - var arrange = _toArrange; - - _toArrange = new List(); - arrange.Sort(); - - if (!Root.IsArrangeValid && Root.IsMeasureValid) - { - Root.Arrange(new Rect(Root.DesiredSize)); - } - - if (_toMeasure.Count > 0) - { - return; - } - - foreach (var item in arrange) - { - if (!item.Control.IsArrangeValid) - { - if (item.Control != Root) - { - var control = item.Control; - - while (control != null && control.PreviousArrange == null) - { - control = control.GetVisualParent(); - } - - if (control != null && control.GetVisualRoot() == Root) - { - control.Arrange(control.PreviousArrange.Value, true); - } - - if (_toMeasure.Count > 0) - { - return; - } - } - } - } - - if (_toArrange.Count == 0) - { - break; - } + control.Measure(control.PreviousMeasure.Value); } } - /// - /// An item to be layed-out. - /// - private class Item : IComparable + private void Arrange(ILayoutable control) { - /// - /// Initializes a new instance of the class. - /// - /// The control. - /// The control's distance from the layout root. - public Item(ILayoutable control, int distance) + var root = control as ILayoutRoot; + + if (root != null) { - Control = control; - Distance = distance; + root.Arrange(new Rect(root.DesiredSize)); } + else if (control.PreviousArrange.HasValue) + { + control.Arrange(control.PreviousArrange.Value); + } + } - /// - /// Gets the control. - /// - public ILayoutable Control { get; } - - /// - /// Gets the control's distance from the layout root. - /// - public int Distance { get; } - - /// - /// Compares the distance of two items. - /// - /// The other item/ - /// The comparison. - public int CompareTo(Item other) + private void FireLayoutNeeded() + { + if (!LayoutQueued) { - return Distance - other.Distance; + _layoutNeeded.OnNext(Unit.Default); + LayoutQueued = true; } } } diff --git a/src/Perspex.Layout/Layoutable.cs b/src/Perspex.Layout/Layoutable.cs index d9f6f96521..850a5bb3bd 100644 --- a/src/Perspex.Layout/Layoutable.cs +++ b/src/Perspex.Layout/Layoutable.cs @@ -132,12 +132,11 @@ namespace Perspex.Layout public static readonly StyledProperty UseLayoutRoundingProperty = PerspexProperty.Register(nameof(UseLayoutRounding), defaultValue: true, inherits: true); + private readonly ILogger _layoutLog; + private bool _measuring; private Size? _previousMeasure; - private Rect? _previousArrange; - private readonly ILogger _layoutLog; - /// /// Initializes static members of the class. /// @@ -320,9 +319,20 @@ namespace Perspex.Layout if (force || !IsMeasureValid || _previousMeasure != availableSize) { + var previousDesiredSize = DesiredSize; + var desiredSize = default(Size); + IsMeasureValid = true; - var desiredSize = MeasureCore(availableSize).Constrain(availableSize); + try + { + _measuring = true; + desiredSize = MeasureCore(availableSize).Constrain(availableSize); + } + finally + { + _measuring = false; + } if (IsInvalidSize(desiredSize)) { @@ -333,6 +343,11 @@ namespace Perspex.Layout _previousMeasure = availableSize; _layoutLog.Verbose("Measure requested {DesiredSize}", DesiredSize); + + if (DesiredSize != previousDesiredSize) + { + this.GetVisualParent()?.ChildDesiredSizeChanged(this); + } } } @@ -373,24 +388,13 @@ namespace Perspex.Layout /// public void InvalidateMeasure() { - var parent = this.GetVisualParent(); - if (IsMeasureValid) { _layoutLog.Verbose("Invalidated measure"); - } - IsMeasureValid = false; - IsArrangeValid = false; - _previousMeasure = null; - _previousArrange = null; + IsMeasureValid = false; + IsArrangeValid = false; - if (parent != null && IsResizable(parent)) - { - parent.InvalidateMeasure(); - } - else - { var root = GetLayoutRoot(); root?.Item1.LayoutManager?.InvalidateMeasure(this, root.Item2); } @@ -401,16 +405,24 @@ namespace Perspex.Layout /// public void InvalidateArrange() { - var root = GetLayoutRoot(); - if (IsArrangeValid) { _layoutLog.Verbose("Arrange measure"); + + IsArrangeValid = false; + + var root = GetLayoutRoot(); + root?.Item1.LayoutManager?.InvalidateArrange(this, root.Item2); } + } - IsArrangeValid = false; - _previousArrange = null; - root?.Item1.LayoutManager?.InvalidateArrange(this, root.Item2); + /// + void ILayoutable.ChildDesiredSizeChanged(ILayoutable control) + { + if (!_measuring) + { + InvalidateMeasure(); + } } /// @@ -624,16 +636,6 @@ namespace Perspex.Layout control?.InvalidateArrange(); } - /// - /// Tests whether a control's size can be changed by a layout pass. - /// - /// The control. - /// True if the control's size can change; otherwise false. - private static bool IsResizable(ILayoutable control) - { - return double.IsNaN(control.Width) || double.IsNaN(control.Height); - } - /// /// Tests whether any of a 's properties incude nagative values, /// a NaN or Infinity. diff --git a/tests/Perspex.Layout.UnitTests/LayoutManagerTests.cs b/tests/Perspex.Layout.UnitTests/LayoutManagerTests.cs new file mode 100644 index 0000000000..2eb770a10c --- /dev/null +++ b/tests/Perspex.Layout.UnitTests/LayoutManagerTests.cs @@ -0,0 +1,38 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Perspex.Controls; +using Xunit; + +namespace Perspex.Layout.UnitTests +{ + public class LayoutManagerTests + { + [Fact] + public void Invalidating_Child_Should_Remeasure_Parent() + { + Border border; + StackPanel panel; + + var root = new TestLayoutRoot + { + Child = panel = new StackPanel + { + Children = new Controls.Controls + { + (border = new Border()) + } + } + }; + + root.LayoutManager.ExecuteLayoutPass(); + Assert.Equal(new Size(0, 0), root.DesiredSize); + + border.Width = 100; + border.Height = 100; + + root.LayoutManager.ExecuteLayoutPass(); + Assert.Equal(new Size(100, 100), panel.DesiredSize); + } + } +} diff --git a/tests/Perspex.Layout.UnitTests/MeasureTests.cs b/tests/Perspex.Layout.UnitTests/MeasureTests.cs index 345ecfe6d6..bbc69e90a2 100644 --- a/tests/Perspex.Layout.UnitTests/MeasureTests.cs +++ b/tests/Perspex.Layout.UnitTests/MeasureTests.cs @@ -8,6 +8,27 @@ namespace Perspex.Layout.UnitTests { public class MeasureTests { + [Fact] + public void Invalidating_Child_Should_Not_Invalidate_Parent() + { + var panel = new StackPanel(); + var child = new Border(); + panel.Children.Add(child); + + panel.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + + Assert.Equal(new Size(0, 0), panel.DesiredSize); + + child.Width = 100; + child.Height = 100; + + Assert.True(panel.IsMeasureValid); + Assert.False(child.IsMeasureValid); + + panel.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + Assert.Equal(new Size(0, 0), panel.DesiredSize); + } + [Fact] public void Negative_Margin_Larger_Than_Constraint_Should_Request_Width_0() { diff --git a/tests/Perspex.Layout.UnitTests/Perspex.Layout.UnitTests.csproj b/tests/Perspex.Layout.UnitTests/Perspex.Layout.UnitTests.csproj index c0139c54d2..f5a046630e 100644 --- a/tests/Perspex.Layout.UnitTests/Perspex.Layout.UnitTests.csproj +++ b/tests/Perspex.Layout.UnitTests/Perspex.Layout.UnitTests.csproj @@ -86,8 +86,10 @@ + + diff --git a/tests/Perspex.Layout.UnitTests/TestLayoutRoot.cs b/tests/Perspex.Layout.UnitTests/TestLayoutRoot.cs new file mode 100644 index 0000000000..148ae688dc --- /dev/null +++ b/tests/Perspex.Layout.UnitTests/TestLayoutRoot.cs @@ -0,0 +1,27 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Perspex.Controls; + +namespace Perspex.Layout.UnitTests +{ + internal class TestLayoutRoot : Decorator, ILayoutRoot + { + public TestLayoutRoot() + { + ClientSize = new Size(500, 500); + LayoutManager = new LayoutManager { Root = this }; + } + + public Size ClientSize + { + get; + set; + } + + public ILayoutManager LayoutManager + { + get; + } + } +} diff --git a/tests/Perspex.SceneGraph.UnitTests/VisualTree/BoundsTrackerTests.cs b/tests/Perspex.SceneGraph.UnitTests/VisualTree/BoundsTrackerTests.cs index 6f3c2d7a32..849fffc609 100644 --- a/tests/Perspex.SceneGraph.UnitTests/VisualTree/BoundsTrackerTests.cs +++ b/tests/Perspex.SceneGraph.UnitTests/VisualTree/BoundsTrackerTests.cs @@ -40,13 +40,13 @@ namespace Perspex.SceneGraph.UnitTests.VisualTree var results = new List(); track.Subscribe(results.Add); - Assert.Equal(new Rect(42, 42, 15, 15), results.Last().Bounds); + Assert.Equal(new Rect(42, 42, 15, 15), results[0].Bounds); tree.Padding = new Thickness(15); tree.Measure(Size.Infinity); tree.Arrange(new Rect(0, 0, 100, 100), true); - Assert.Equal(new Rect(42, 42, 15, 15), results.Last().Bounds); + Assert.Equal(new Rect(47, 47, 15, 15), results[1].Bounds); } } }