// ----------------------------------------------------------------------- // // Copyright 2014 MIT Licence. See licence.md for more information. // // ----------------------------------------------------------------------- namespace Perspex.Layout { using System; using System.Reactive; using System.Reactive.Subjects; using NGenerics.DataStructures.General; using Perspex.VisualTree; using Serilog; using Serilog.Core.Enrichers; using System.Reactive.Disposables; /// /// 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 Subject layoutNeeded; /// /// Called when a layout is completed. /// private Subject layoutCompleted; /// /// Whether a measure is needed on the next layout pass. /// private bool measureNeeded = true; /// /// The controls that need to be measured, sorted by distance to layout root. /// private Heap toMeasure = new Heap(HeapType.Minimum); /// /// The controls that need to be arranged, sorted by distance to layout root. /// private Heap toArrange = new Heap(HeapType.Minimum); /// /// Prevents re-entrancy. /// private bool running; /// /// The logger to use. /// private ILogger log; /// /// Initializes a new instance of the class. /// public LayoutManager() { this.log = Log.ForContext(new[] { new PropertyEnricher("Area", "Layout"), new PropertyEnricher("SourceContext", this.GetType()), new PropertyEnricher("Id", this.GetHashCode()), }); this.layoutNeeded = new Subject(); this.layoutCompleted = new Subject(); } /// /// Gets or sets the root element that the manager is attached to. /// /// /// This must be set before the layout manager can be used. /// public ILayoutRoot Root { get; set; } /// /// Gets an observable that is fired when a layout pass is needed. /// public IObservable LayoutNeeded => this.layoutNeeded; /// /// Gets an observable that is fired when a layout pass is completed. /// public IObservable LayoutCompleted => this.layoutCompleted; /// /// Gets a value indicating whether a layout is queued. /// /// /// Returns true when has been fired, but /// has not yet been called. /// public bool LayoutQueued { get; private set; } /// /// Executes a layout pass. /// public void ExecuteLayoutPass() { if (this.running) { return; } using (Disposable.Create(() => this.running = false)) { this.running = true; this.LayoutQueued = false; this.log.Information( "Started layout pass. To measure: {Measure} To arrange: {Arrange}", this.toMeasure.Count, this.toArrange.Count); var stopwatch = new System.Diagnostics.Stopwatch(); stopwatch.Start(); for (int i = 0; i < MaxTries; ++i) { if (this.measureNeeded) { this.ExecuteMeasure(); this.measureNeeded = false; } this.ExecuteArrange(); if (this.toMeasure.Count == 0) { break; } } stopwatch.Stop(); this.log.Information("Layout pass finised in {Time}", stopwatch.Elapsed); this.layoutCompleted.OnNext(Unit.Default); } } /// /// 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) { var item = new Item(control, distance); this.toMeasure.Add(item); this.toArrange.Add(item); this.measureNeeded = true; if (!this.LayoutQueued) { IVisual visual = control as IVisual; this.layoutNeeded.OnNext(Unit.Default); this.LayoutQueued = true; } } /// /// 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) { this.toArrange.Add(new Item(control, distance)); if (!this.LayoutQueued) { IVisual visual = control as IVisual; this.layoutNeeded.OnNext(Unit.Default); this.LayoutQueued = true; } } /// /// Executes the measure part of the layout pass. /// private void ExecuteMeasure() { for (int i = 0; i < MaxTries; ++i) { var measure = this.toMeasure; this.toMeasure = new Heap(HeapType.Minimum); if (!this.Root.IsMeasureValid) { var size = new Size( double.IsNaN(this.Root.Width) ? double.PositiveInfinity : this.Root.Width, double.IsNaN(this.Root.Height) ? double.PositiveInfinity : this.Root.Height); this.Root.Measure(size); } foreach (var item in measure) { if (!item.Control.IsMeasureValid) { if (item.Control != this.Root) { var parent = item.Control.GetVisualParent(); while (parent.PreviousMeasure == null) { parent = parent.GetVisualParent(); } if (parent.GetVisualRoot() == this.Root) { parent.Measure(parent.PreviousMeasure.Value, true); } } } } if (this.toMeasure.Count == 0) { break; } } } /// /// Executes the arrange part of the layout pass. /// private void ExecuteArrange() { for (int i = 0; i < MaxTries; ++i) { var arrange = this.toArrange; this.toArrange = new Heap(HeapType.Minimum); if (!this.Root.IsArrangeValid && this.Root.IsMeasureValid) { this.Root.Arrange(new Rect(this.Root.DesiredSize)); } if (this.toMeasure.Count > 0) { return; } foreach (var item in arrange) { if (!item.Control.IsArrangeValid) { if (item.Control != this.Root) { var control = item.Control; while (control.PreviousArrange == null) { control = control.GetVisualParent(); } if (control.GetVisualRoot() == this.Root) { control.Arrange(control.PreviousArrange.Value, true); } if (this.toMeasure.Count > 0) { return; } } } } if (this.toArrange.Count == 0) { break; } } } private class Item : IComparable { public Item(ILayoutable control, int distance) { this.Control = control; this.Distance = distance; } public ILayoutable Control { get; private set; } public int Distance { get; private set; } public int CompareTo(Item other) { return this.Distance - other.Distance; } } } }