Overhauled layout system.
From lessons learnt from porting moonlight's Grid tests. Instead of making a child invalidate its parents directly, send a message from the child to the parent when its DesiredSize changes. LayoutManager also needed rewriting to allow this.
This commit is contained in:
@@ -112,5 +112,11 @@ namespace Perspex.Layout
|
||||
/// Invalidates the arrangement of the control and queues a new layout pass.
|
||||
/// </summary>
|
||||
void InvalidateArrange();
|
||||
|
||||
/// <summary>
|
||||
/// Called when a child control's desired size changes.
|
||||
/// </summary>
|
||||
/// <param name="control">The child control.</param>
|
||||
void ChildDesiredSizeChanged(ILayoutable control);
|
||||
}
|
||||
}
|
||||
|
||||
+126
-248
@@ -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,52 +73,6 @@ namespace Perspex.Layout
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a layout pass.
|
||||
/// </summary>
|
||||
public void ExecuteLayoutPass()
|
||||
{
|
||||
if (_running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (Disposable.Create(() => _running = false))
|
||||
{
|
||||
_running = true;
|
||||
LayoutQueued = false;
|
||||
|
||||
_log.Information(
|
||||
"Started layout pass. To measure: {Measure} To arrange: {Arrange}",
|
||||
_toMeasure.Count,
|
||||
_toArrange.Count);
|
||||
|
||||
var stopwatch = new System.Diagnostics.Stopwatch();
|
||||
stopwatch.Start();
|
||||
|
||||
for (int i = 0; i < MaxTries; ++i)
|
||||
{
|
||||
if (_measureNeeded)
|
||||
{
|
||||
ExecuteMeasure();
|
||||
_measureNeeded = false;
|
||||
}
|
||||
|
||||
ExecuteArrange();
|
||||
|
||||
if (_toMeasure.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
_log.Information("Layout pass finised in {Time}", stopwatch.Elapsed);
|
||||
|
||||
_layoutCompleted.OnNext(Unit.Default);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the layout manager that a control requires a measure.
|
||||
/// </summary>
|
||||
@@ -167,18 +80,12 @@ namespace Perspex.Layout
|
||||
/// <param name="distance">The control's distance from the layout root.</param>
|
||||
public void InvalidateMeasure(ILayoutable control, int distance)
|
||||
{
|
||||
var item = new Item(control, distance);
|
||||
_toMeasure.Add(item);
|
||||
_toArrange.Add(item);
|
||||
Contract.Requires<ArgumentNullException>(control != null);
|
||||
Dispatcher.UIThread.VerifyAccess();
|
||||
|
||||
_measureNeeded = true;
|
||||
|
||||
if (!LayoutQueued)
|
||||
{
|
||||
IVisual visual = control as IVisual;
|
||||
_layoutNeeded.OnNext(Unit.Default);
|
||||
LayoutQueued = true;
|
||||
}
|
||||
_toMeasure.Enqueue(control);
|
||||
_toArrange.Enqueue(control);
|
||||
FireLayoutNeeded();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -188,154 +95,125 @@ namespace Perspex.Layout
|
||||
/// <param name="distance">The control's distance from the layout root.</param>
|
||||
public void InvalidateArrange(ILayoutable control, int distance)
|
||||
{
|
||||
_toArrange.Add(new Item(control, distance));
|
||||
Contract.Requires<ArgumentNullException>(control != null);
|
||||
Dispatcher.UIThread.VerifyAccess();
|
||||
|
||||
_toArrange.Enqueue(control);
|
||||
FireLayoutNeeded();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a layout pass.
|
||||
/// </summary>
|
||||
public void ExecuteLayoutPass()
|
||||
{
|
||||
const int MaxPasses = 3;
|
||||
|
||||
Dispatcher.UIThread.VerifyAccess();
|
||||
|
||||
if (Root == null)
|
||||
{
|
||||
throw new InvalidOperationException("Root must be set before executing layout pass.");
|
||||
}
|
||||
|
||||
if (!_running)
|
||||
{
|
||||
_running = true;
|
||||
|
||||
_log.Information(
|
||||
"Started layout pass. To measure: {Measure} To arrange: {Arrange}",
|
||||
_toMeasure.Count,
|
||||
_toArrange.Count);
|
||||
|
||||
var stopwatch = new System.Diagnostics.Stopwatch();
|
||||
stopwatch.Start();
|
||||
|
||||
try
|
||||
{
|
||||
if (_first)
|
||||
{
|
||||
Measure(Root);
|
||||
Arrange(Root);
|
||||
_first = false;
|
||||
}
|
||||
|
||||
for (var pass = 0; pass < MaxPasses; ++pass)
|
||||
{
|
||||
ExecuteMeasurePass();
|
||||
ExecuteArrangePass();
|
||||
|
||||
if (_toMeasure.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_running = false;
|
||||
LayoutQueued = false;
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
_log.Information("Layout pass finised in {Time}", stopwatch.Elapsed);
|
||||
|
||||
_layoutCompleted.OnNext(Unit.Default);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteMeasurePass()
|
||||
{
|
||||
while (_toMeasure.Count > 0)
|
||||
{
|
||||
var next = _toMeasure.Dequeue();
|
||||
Measure(next);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteArrangePass()
|
||||
{
|
||||
while (_toArrange.Count > 0 && _toMeasure.Count == 0)
|
||||
{
|
||||
var next = _toArrange.Dequeue();
|
||||
Arrange(next);
|
||||
}
|
||||
}
|
||||
|
||||
private void Measure(ILayoutable control)
|
||||
{
|
||||
var root = control as ILayoutRoot;
|
||||
|
||||
if (root != null)
|
||||
{
|
||||
root.Measure(Size.Infinity);
|
||||
}
|
||||
else if (control.PreviousMeasure.HasValue)
|
||||
{
|
||||
control.Measure(control.PreviousMeasure.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void Arrange(ILayoutable control)
|
||||
{
|
||||
var root = control as ILayoutRoot;
|
||||
|
||||
if (root != null)
|
||||
{
|
||||
root.Arrange(new Rect(root.DesiredSize));
|
||||
}
|
||||
else if (control.PreviousArrange.HasValue)
|
||||
{
|
||||
control.Arrange(control.PreviousArrange.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void FireLayoutNeeded()
|
||||
{
|
||||
if (!LayoutQueued)
|
||||
{
|
||||
IVisual visual = control as IVisual;
|
||||
_layoutNeeded.OnNext(Unit.Default);
|
||||
LayoutQueued = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the measure part of the layout pass.
|
||||
/// </summary>
|
||||
private void ExecuteMeasure()
|
||||
{
|
||||
for (int i = 0; i < MaxTries; ++i)
|
||||
{
|
||||
var measure = _toMeasure;
|
||||
|
||||
_toMeasure = new List<Item>();
|
||||
measure.Sort();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the arrange part of the layout pass.
|
||||
/// </summary>
|
||||
private void ExecuteArrange()
|
||||
{
|
||||
for (int i = 0; i < MaxTries; ++i)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An item to be layed-out.
|
||||
/// </summary>
|
||||
private class Item : IComparable<Item>
|
||||
{
|
||||
/// <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)
|
||||
{
|
||||
Control = control;
|
||||
Distance = distance;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
return Distance - other.Distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,11 +132,10 @@ namespace Perspex.Layout
|
||||
public static readonly StyledProperty<bool> UseLayoutRoundingProperty =
|
||||
PerspexProperty.Register<Layoutable, bool>(nameof(UseLayoutRounding), defaultValue: true, inherits: true);
|
||||
|
||||
private Size? _previousMeasure;
|
||||
|
||||
private Rect? _previousArrange;
|
||||
|
||||
private readonly ILogger _layoutLog;
|
||||
private bool _measuring;
|
||||
private Size? _previousMeasure;
|
||||
private Rect? _previousArrange;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes static members of the <see cref="Layoutable"/> 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<ILayoutable>()?.ChildDesiredSizeChanged(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,24 +388,13 @@ namespace Perspex.Layout
|
||||
/// </summary>
|
||||
public void InvalidateMeasure()
|
||||
{
|
||||
var parent = this.GetVisualParent<ILayoutable>();
|
||||
|
||||
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
|
||||
/// </summary>
|
||||
public void InvalidateArrange()
|
||||
{
|
||||
var root = GetLayoutRoot();
|
||||
|
||||
if (IsArrangeValid)
|
||||
{
|
||||
_layoutLog.Verbose("Arrange measure");
|
||||
}
|
||||
|
||||
IsArrangeValid = false;
|
||||
_previousArrange = null;
|
||||
root?.Item1.LayoutManager?.InvalidateArrange(this, root.Item2);
|
||||
IsArrangeValid = false;
|
||||
|
||||
var root = GetLayoutRoot();
|
||||
root?.Item1.LayoutManager?.InvalidateArrange(this, root.Item2);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void ILayoutable.ChildDesiredSizeChanged(ILayoutable control)
|
||||
{
|
||||
if (!_measuring)
|
||||
{
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -624,16 +636,6 @@ namespace Perspex.Layout
|
||||
control?.InvalidateArrange();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether a control's size can be changed by a layout pass.
|
||||
/// </summary>
|
||||
/// <param name="control">The control.</param>
|
||||
/// <returns>True if the control's size can change; otherwise false.</returns>
|
||||
private static bool IsResizable(ILayoutable control)
|
||||
{
|
||||
return double.IsNaN(control.Width) || double.IsNaN(control.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether any of a <see cref="Rect"/>'s properties incude nagative values,
|
||||
/// a NaN or Infinity.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -86,8 +86,10 @@
|
||||
</Choose>
|
||||
<ItemGroup>
|
||||
<Compile Include="FullLayoutTests.cs" />
|
||||
<Compile Include="LayoutManagerTests.cs" />
|
||||
<Compile Include="MeasureTests.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="TestLayoutRoot.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Perspex.Animation\Perspex.Animation.csproj">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,13 +40,13 @@ namespace Perspex.SceneGraph.UnitTests.VisualTree
|
||||
var results = new List<TransformedBounds>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user