Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix up the Layout Manager #1021

Merged
merged 9 commits into from Jun 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
97 changes: 64 additions & 33 deletions src/Avalonia.Layout/LayoutManager.cs
Expand Up @@ -14,8 +14,8 @@ namespace Avalonia.Layout
/// </summary>
public class LayoutManager : ILayoutManager
{
private readonly HashSet<ILayoutable> _toMeasure = new HashSet<ILayoutable>();
private readonly HashSet<ILayoutable> _toArrange = new HashSet<ILayoutable>();
private readonly Queue<ILayoutable> _toMeasure = new Queue<ILayoutable>();
private readonly Queue<ILayoutable> _toArrange = new Queue<ILayoutable>();
private bool _queued;
private bool _running;

Expand All @@ -30,8 +30,18 @@ public void InvalidateMeasure(ILayoutable control)
Contract.Requires<ArgumentNullException>(control != null);
Dispatcher.UIThread.VerifyAccess();

_toMeasure.Add(control);
_toArrange.Add(control);
if (!control.IsAttachedToVisualTree)
{
#if DEBUG
throw new AvaloniaInternalException(
"LayoutManager.InvalidateMeasure called on a control that is detached from the visual tree.");
#else
return;
#endif
}

_toMeasure.Enqueue(control);
_toArrange.Enqueue(control);
QueueLayoutPass();
}

Expand All @@ -41,7 +51,17 @@ public void InvalidateArrange(ILayoutable control)
Contract.Requires<ArgumentNullException>(control != null);
Dispatcher.UIThread.VerifyAccess();

_toArrange.Add(control);
if (!control.IsAttachedToVisualTree)
{
#if DEBUG
throw new AvaloniaInternalException(
"LayoutManager.InvalidateArrange called on a control that is detached from the visual tree.");
#else
return;
#endif
}

_toArrange.Enqueue(control);
QueueLayoutPass();
}

Expand Down Expand Up @@ -108,62 +128,73 @@ private void ExecuteMeasurePass()
{
while (_toMeasure.Count > 0)
{
var next = _toMeasure.First();
Measure(next);
var control = _toMeasure.Dequeue();

if (!control.IsMeasureValid && control.IsAttachedToVisualTree)
{
Measure(control);
}
}
}

private void ExecuteArrangePass()
{
while (_toArrange.Count > 0 && _toMeasure.Count == 0)
{
var next = _toArrange.First();
Arrange(next);
var control = _toArrange.Dequeue();

if (!control.IsArrangeValid && control.IsAttachedToVisualTree)
{
Arrange(control);
}
}
}

private void Measure(ILayoutable control)
{
var root = control as ILayoutRoot;
var parent = control.VisualParent as ILayoutable;

if (root != null)
{
root.Measure(root.MaxClientSize);
}
else if (parent != null)
// Controls closest to the visual root need to be arranged first. We don't try to store
// ordered invalidation lists, instead we traverse the tree upwards, measuring the
// controls closest to the root first. This has been shown by benchmarks to be the
// fastest and most memory-efficent algorithm.
if (control.VisualParent is ILayoutable parent)
{
Measure(parent);
}

if (!control.IsMeasureValid)
// If the control being measured has IsMeasureValid == true here then its measure was
// handed by an ancestor and can be ignored. The measure may have also caused the
// control to be removed.
if (!control.IsMeasureValid && control.IsAttachedToVisualTree)
{
control.Measure(control.PreviousMeasure.Value);
if (control is ILayoutRoot root)
{
root.Measure(Size.Infinity);
}
else
{
control.Measure(control.PreviousMeasure.Value);
}
}

_toMeasure.Remove(control);
}

private void Arrange(ILayoutable control)
{
var root = control as ILayoutRoot;
var parent = control.VisualParent as ILayoutable;

if (root != null)
{
root.Arrange(new Rect(root.DesiredSize));
}
else if (parent != null)
if (control.VisualParent is ILayoutable parent)
{
Arrange(parent);
}

if (control.PreviousArrange.HasValue)
if (!control.IsArrangeValid && control.IsAttachedToVisualTree)
{
control.Arrange(control.PreviousArrange.Value);
if (control is ILayoutRoot root)
{
root.Arrange(new Rect(control.DesiredSize));
}
else
{
control.Arrange(control.PreviousArrange.Value);
}
}

_toArrange.Remove(control);
}

private void QueueLayoutPass()
Expand Down
16 changes: 12 additions & 4 deletions src/Avalonia.Layout/Layoutable.cs
Expand Up @@ -378,8 +378,12 @@ public void InvalidateMeasure()

IsMeasureValid = false;
IsArrangeValid = false;
LayoutManager.Instance?.InvalidateMeasure(this);
InvalidateVisual();

if (((ILayoutable)this).IsAttachedToVisualTree)
{
LayoutManager.Instance?.InvalidateMeasure(this);
InvalidateVisual();
}
}
}

Expand All @@ -393,8 +397,12 @@ public void InvalidateArrange()
Logger.Verbose(LogArea.Layout, this, "Invalidated arrange");

IsArrangeValid = false;
LayoutManager.Instance?.InvalidateArrange(this);
InvalidateVisual();

if (((ILayoutable)this).IsAttachedToVisualTree)
{
LayoutManager.Instance?.InvalidateArrange(this);
InvalidateVisual();
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
Expand Up @@ -49,6 +49,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Layout\Measure.cs" />
<Compile Include="Styling\ApplyStyling.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
Expand Down Expand Up @@ -100,7 +101,7 @@
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.9.2" />
<PackageReference Include="BenchmarkDotNet" Version="0.10.8" />
</ItemGroup>
<Import Project="$(MSBuildThisFileDirectory)..\..\src\Shared\nuget.workaround.targets" />
</Project>
65 changes: 65 additions & 0 deletions tests/Avalonia.Benchmarks/Layout/Measure.cs
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;

namespace Avalonia.Benchmarks.Layout
{
[MemoryDiagnoser]
public class Measure : IDisposable
{
private IDisposable _app;
private TestRoot root;
private List<Control> controls = new List<Control>();

public Measure()
{
_app = UnitTestApplication.Start(TestServices.RealLayoutManager);

var panel = new StackPanel();
root = new TestRoot { Child = panel };
controls.Add(panel);
CreateChildren(panel, 3, 5);
LayoutManager.Instance.ExecuteInitialLayoutPass(root);
}

public void Dispose()
{
_app.Dispose();
}

[Benchmark]
public void Remeasure_Half()
{
var random = new Random(1);

foreach (var control in controls)
{
if (random.Next(2) == 0)
{
control.InvalidateMeasure();
}
}

LayoutManager.Instance.ExecuteLayoutPass();
}

private void CreateChildren(IPanel parent, int childCount, int iterations)
{
for (var i = 0; i < childCount; ++i)
{
var control = new StackPanel();
parent.Children.Add(control);

if (iterations > 0)
{
CreateChildren(control, childCount, iterations - 1);
}

controls.Add(control);
}
}
}
}
1 change: 1 addition & 0 deletions tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs
Expand Up @@ -11,6 +11,7 @@

namespace Avalonia.Benchmarks.Styling
{
[MemoryDiagnoser]
public class ApplyStyling : IDisposable
{
private IDisposable _app;
Expand Down