|
|
@@ -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
|
|
|
/// <summary>
|
|
|
/// Manages measuring and arranging of controls.
|
|
|
/// </summary>
|
|
|
- /// <remarks>
|
|
|
- /// 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 <see cref="LayoutNeeded"/>
|
|
|
- /// observable will fire and the root element should respond by calling
|
|
|
- /// <see cref="ExecuteLayoutPass"/> at the earliest opportunity to carry out the layout.
|
|
|
- /// </remarks>
|
|
|
public class LayoutManager : ILayoutManager
|
|
|
{
|
|
|
- /// <summary>
|
|
|
- /// The maximum number of times a measure/arrange loop can be retried.
|
|
|
- /// </summary>
|
|
|
- private const int MaxTries = 3;
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Called when a layout is needed.
|
|
|
- /// </summary>
|
|
|
- private readonly Subject<Unit> _layoutNeeded;
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Called when a layout is completed.
|
|
|
- /// </summary>
|
|
|
- private readonly Subject<Unit> _layoutCompleted;
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Whether a measure is needed on the next layout pass.
|
|
|
- /// </summary>
|
|
|
- private bool _measureNeeded = true;
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// The controls that need to be measured.
|
|
|
- /// </summary>
|
|
|
- private List<Item> _toMeasure = new List<Item>();
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// The controls that need to be arranged.
|
|
|
- /// </summary>
|
|
|
- private List<Item> _toArrange = new List<Item>();
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Prevents re-entrancy.
|
|
|
- /// </summary>
|
|
|
- private bool _running;
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// The logger to use.
|
|
|
- /// </summary>
|
|
|
+ private readonly Queue<ILayoutable> _toMeasure = new Queue<ILayoutable>();
|
|
|
+ private readonly Queue<ILayoutable> _toArrange = new Queue<ILayoutable>();
|
|
|
+ private readonly Subject<Unit> _layoutNeeded = new Subject<Unit>();
|
|
|
+ private readonly Subject<Unit> _layoutCompleted = new Subject<Unit>();
|
|
|
private readonly ILogger _log;
|
|
|
+ private bool _first = true;
|
|
|
+ private bool _running;
|
|
|
|
|
|
/// <summary>
|
|
|
/// Initializes a new instance of the <see cref="LayoutManager"/> class.
|
|
|
@@ -74,9 +36,6 @@ namespace Perspex.Layout
|
|
|
new PropertyEnricher("SourceContext", GetType()),
|
|
|
new PropertyEnricher("Id", GetHashCode()),
|
|
|
});
|
|
|
-
|
|
|
- _layoutNeeded = new Subject<Unit>();
|
|
|
- _layoutCompleted = new Subject<Unit>();
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
@@ -114,20 +73,52 @@ namespace Perspex.Layout
|
|
|
private set;
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// Notifies the layout manager that a control requires a measure.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="control">The control.</param>
|
|
|
+ /// <param name="distance">The control's distance from the layout root.</param>
|
|
|
+ public void InvalidateMeasure(ILayoutable control, int distance)
|
|
|
+ {
|
|
|
+ Contract.Requires<ArgumentNullException>(control != null);
|
|
|
+ Dispatcher.UIThread.VerifyAccess();
|
|
|
+
|
|
|
+ _toMeasure.Enqueue(control);
|
|
|
+ _toArrange.Enqueue(control);
|
|
|
+ FireLayoutNeeded();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Notifies the layout manager that a control requires an arrange.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="control">The control.</param>
|
|
|
+ /// <param name="distance">The control's distance from the layout root.</param>
|
|
|
+ public void InvalidateArrange(ILayoutable control, int distance)
|
|
|
+ {
|
|
|
+ Contract.Requires<ArgumentNullException>(control != null);
|
|
|
+ Dispatcher.UIThread.VerifyAccess();
|
|
|
+
|
|
|
+ _toArrange.Enqueue(control);
|
|
|
+ FireLayoutNeeded();
|
|
|
+ }
|
|
|
+
|
|
|
/// <summary>
|
|
|
/// Executes a layout pass.
|
|
|
/// </summary>
|
|
|
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
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Notifies the layout manager that a control requires a measure.
|
|
|
- /// </summary>
|
|
|
- /// <param name="control">The control.</param>
|
|
|
- /// <param name="distance">The control's distance from the layout root.</param>
|
|
|
- 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);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Notifies the layout manager that a control requires an arrange.
|
|
|
- /// </summary>
|
|
|
- /// <param name="control">The control.</param>
|
|
|
- /// <param name="distance">The control's distance from the layout root.</param>
|
|
|
- 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);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Executes the measure part of the layout pass.
|
|
|
- /// </summary>
|
|
|
- private void ExecuteMeasure()
|
|
|
+ private void Measure(ILayoutable control)
|
|
|
{
|
|
|
- for (int i = 0; i < MaxTries; ++i)
|
|
|
- {
|
|
|
- var measure = _toMeasure;
|
|
|
-
|
|
|
- _toMeasure = new List<Item>();
|
|
|
- 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<ILayoutable>();
|
|
|
-
|
|
|
- while (parent != null && parent.PreviousMeasure == null)
|
|
|
- {
|
|
|
- parent = parent.GetVisualParent<ILayoutable>();
|
|
|
- }
|
|
|
-
|
|
|
- if (parent != null && parent.GetVisualRoot() == Root)
|
|
|
- {
|
|
|
- parent.Measure(parent.PreviousMeasure.Value, true);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (_toMeasure.Count == 0)
|
|
|
- {
|
|
|
- break;
|
|
|
- }
|
|
|
+ if (root != null)
|
|
|
+ {
|
|
|
+ root.Measure(Size.Infinity);
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Executes the arrange part of the layout pass.
|
|
|
- /// </summary>
|
|
|
- private void ExecuteArrange()
|
|
|
- {
|
|
|
- for (int i = 0; i < MaxTries; ++i)
|
|
|
+ else if (control.PreviousMeasure.HasValue)
|
|
|
{
|
|
|
- var arrange = _toArrange;
|
|
|
-
|
|
|
- _toArrange = new List<Item>();
|
|
|
- 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<ILayoutable>();
|
|
|
- }
|
|
|
-
|
|
|
- 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);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// An item to be layed-out.
|
|
|
- /// </summary>
|
|
|
- private class Item : IComparable<Item>
|
|
|
+ private void Arrange(ILayoutable control)
|
|
|
{
|
|
|
- /// <summary>
|
|
|
- /// Initializes a new instance of the <see cref="Item"/> class.
|
|
|
- /// </summary>
|
|
|
- /// <param name="control">The control.</param>
|
|
|
- /// <param name="distance">The control's distance from the layout root.</param>
|
|
|
- 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Gets the control.
|
|
|
- /// </summary>
|
|
|
- public ILayoutable Control { get; }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Gets the control's distance from the layout root.
|
|
|
- /// </summary>
|
|
|
- public int Distance { get; }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Compares the distance of two items.
|
|
|
- /// </summary>
|
|
|
- /// <param name="other">The other item/</param>
|
|
|
- /// <returns>The comparison.</returns>
|
|
|
- public int CompareTo(Item other)
|
|
|
+ private void FireLayoutNeeded()
|
|
|
+ {
|
|
|
+ if (!LayoutQueued)
|
|
|
{
|
|
|
- return Distance - other.Distance;
|
|
|
+ _layoutNeeded.OnNext(Unit.Default);
|
|
|
+ LayoutQueued = true;
|
|
|
}
|
|
|
}
|
|
|
}
|