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);
}
}
}