csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
265 lines
9.0 KiB
265 lines
9.0 KiB
using System;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Layout;
|
|
using Avalonia.UnitTests;
|
|
using Xunit;
|
|
|
|
namespace Avalonia.Base.UnitTests.Layout;
|
|
|
|
public class LayoutManagerTests_BringIntoView : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void Request_Is_Executed_At_End_Of_Layout_Pass()
|
|
{
|
|
var control = new LayoutTestControl();
|
|
var root = new LayoutTestRoot { Child = control };
|
|
root.LayoutManager.ExecuteInitialLayoutPass();
|
|
|
|
var request = new TestRequest(control);
|
|
GetLayoutManager(root).EnqueueBringIntoView(request);
|
|
|
|
Assert.Equal(0, request.ExecuteAttempts);
|
|
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
|
|
Assert.Equal(1, request.ExecuteAttempts);
|
|
Assert.Equal(1, request.Executions);
|
|
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
|
|
// Should not have been executed twice.
|
|
Assert.Equal(1, request.ExecuteAttempts);
|
|
Assert.Equal(1, request.Executions);
|
|
}
|
|
|
|
[Fact]
|
|
public void Request_Is_Executed_Before_LayoutUpdated_Is_Raised()
|
|
{
|
|
var control = new LayoutTestControl();
|
|
var root = new LayoutTestRoot { Child = control };
|
|
root.LayoutManager.ExecuteInitialLayoutPass();
|
|
|
|
var layoutUpdatedRaised = false;
|
|
var executedBeforeLayoutUpdated = false;
|
|
root.LayoutManager.LayoutUpdated += (_, _) => layoutUpdatedRaised = true;
|
|
|
|
var request = new TestRequest(control)
|
|
{
|
|
OnExecute = () => executedBeforeLayoutUpdated = !layoutUpdatedRaised,
|
|
};
|
|
|
|
GetLayoutManager(root).EnqueueBringIntoView(request);
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
|
|
Assert.Equal(1, request.Executions);
|
|
Assert.True(executedBeforeLayoutUpdated);
|
|
}
|
|
|
|
[Fact]
|
|
public void Request_Is_Retained_Until_It_Can_Execute()
|
|
{
|
|
var control = new LayoutTestControl();
|
|
var root = new LayoutTestRoot { Child = control };
|
|
root.LayoutManager.ExecuteInitialLayoutPass();
|
|
|
|
var canExecute = false;
|
|
var request = new TestRequest(control) { CanExecute = () => canExecute };
|
|
GetLayoutManager(root).EnqueueBringIntoView(request);
|
|
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
|
|
Assert.Equal(1, request.ExecuteAttempts);
|
|
Assert.Equal(0, request.Executions);
|
|
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
|
|
Assert.Equal(2, request.ExecuteAttempts);
|
|
Assert.Equal(0, request.Executions);
|
|
|
|
canExecute = true;
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
|
|
Assert.Equal(3, request.ExecuteAttempts);
|
|
Assert.Equal(1, request.Executions);
|
|
}
|
|
|
|
[Fact]
|
|
public void Requests_Are_Coalesced_By_Target()
|
|
{
|
|
var control = new LayoutTestControl();
|
|
var root = new LayoutTestRoot { Child = control };
|
|
root.LayoutManager.ExecuteInitialLayoutPass();
|
|
|
|
var first = new TestRequest(control);
|
|
var second = new TestRequest(control);
|
|
var layoutManager = GetLayoutManager(root);
|
|
|
|
layoutManager.EnqueueBringIntoView(first);
|
|
layoutManager.EnqueueBringIntoView(second);
|
|
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
|
|
Assert.Equal(0, first.ExecuteAttempts);
|
|
Assert.Equal(1, second.Executions);
|
|
}
|
|
|
|
[Fact]
|
|
public void Request_Is_Dropped_When_Target_Is_Detached()
|
|
{
|
|
var control = new LayoutTestControl();
|
|
var root = new LayoutTestRoot { Child = control };
|
|
root.LayoutManager.ExecuteInitialLayoutPass();
|
|
|
|
var request = new TestRequest(control);
|
|
GetLayoutManager(root).EnqueueBringIntoView(request);
|
|
|
|
root.Child = null;
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
|
|
Assert.Equal(0, request.ExecuteAttempts);
|
|
|
|
root.Child = control;
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
|
|
Assert.Equal(0, request.ExecuteAttempts);
|
|
}
|
|
|
|
[Fact]
|
|
public void Layout_Invalidated_By_Request_Converges_Within_Same_Pass()
|
|
{
|
|
var control = new LayoutTestControl();
|
|
var root = new LayoutTestRoot { Child = control };
|
|
root.LayoutManager.ExecuteInitialLayoutPass();
|
|
|
|
control.Measured = control.Arranged = false;
|
|
|
|
var request = new TestRequest(control) { OnExecute = control.InvalidateMeasure };
|
|
GetLayoutManager(root).EnqueueBringIntoView(request);
|
|
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
|
|
// The layout invalidated by the request (e.g. a scroll offset change) has been
|
|
// re-run before ExecuteLayoutPass returned, so the frame is rendered fully scrolled.
|
|
Assert.Equal(1, request.Executions);
|
|
Assert.True(control.Measured);
|
|
Assert.True(control.Arranged);
|
|
}
|
|
|
|
[Fact]
|
|
public void LayoutUpdated_Is_Not_Raised_Until_All_Requests_Have_Been_Processed()
|
|
{
|
|
var first = new LayoutTestControl();
|
|
var second = new LayoutTestControl();
|
|
var root = new LayoutTestRoot { Child = new StackPanel { Children = { first, second } } };
|
|
root.LayoutManager.ExecuteInitialLayoutPass();
|
|
|
|
var layoutManager = GetLayoutManager(root);
|
|
var layoutUpdatedRaised = false;
|
|
var secondExecutedBeforeLayoutUpdated = false;
|
|
root.LayoutManager.LayoutUpdated += (_, _) => layoutUpdatedRaised = true;
|
|
|
|
// The second request can only execute once the layout invalidated by the first one has
|
|
// been re-run, so it is executed by a following pass of the processing loop.
|
|
first.Measured = false;
|
|
|
|
var secondRequest = new TestRequest(second)
|
|
{
|
|
CanExecute = () => first.Measured,
|
|
OnExecute = () => secondExecutedBeforeLayoutUpdated = !layoutUpdatedRaised,
|
|
};
|
|
|
|
var firstRequest = new TestRequest(first) { OnExecute = first.InvalidateMeasure };
|
|
|
|
layoutManager.EnqueueBringIntoView(firstRequest);
|
|
layoutManager.EnqueueBringIntoView(secondRequest);
|
|
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
|
|
Assert.Equal(1, secondRequest.Executions);
|
|
Assert.True(secondExecutedBeforeLayoutUpdated);
|
|
}
|
|
|
|
[Fact]
|
|
public void LayoutUpdated_Is_Raised_Once_When_Request_Invalidates_Layout()
|
|
{
|
|
var control = new LayoutTestControl();
|
|
var root = new LayoutTestRoot { Child = control };
|
|
root.LayoutManager.ExecuteInitialLayoutPass();
|
|
|
|
var layoutUpdated = 0;
|
|
root.LayoutManager.LayoutUpdated += (_, _) => ++layoutUpdated;
|
|
|
|
var request = new TestRequest(control) { OnExecute = control.InvalidateMeasure };
|
|
GetLayoutManager(root).EnqueueBringIntoView(request);
|
|
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
|
|
Assert.Equal(1, request.Executions);
|
|
Assert.Equal(1, layoutUpdated);
|
|
}
|
|
|
|
[Fact]
|
|
public void Request_Enqueued_While_Processing_Is_Attempted_By_The_Same_Pass()
|
|
{
|
|
var first = new LayoutTestControl();
|
|
var second = new LayoutTestControl();
|
|
var root = new LayoutTestRoot { Child = new StackPanel { Children = { first, second } } };
|
|
root.LayoutManager.ExecuteInitialLayoutPass();
|
|
|
|
var layoutManager = GetLayoutManager(root);
|
|
var canExecuteSecond = false;
|
|
var secondRequest = new TestRequest(second) { CanExecute = () => canExecuteSecond };
|
|
|
|
// Executing a request can enqueue another one (as ControlExtensions.BringIntoViewCore does):
|
|
// the new request must be attempted by the same pass, and retained if it can't execute yet.
|
|
var firstRequest = new TestRequest(first)
|
|
{
|
|
OnExecute = () =>
|
|
{
|
|
layoutManager.EnqueueBringIntoView(secondRequest);
|
|
first.InvalidateMeasure();
|
|
}
|
|
};
|
|
|
|
layoutManager.EnqueueBringIntoView(firstRequest);
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
|
|
Assert.Equal(1, firstRequest.Executions);
|
|
|
|
// Attempted first during the same pass as the first request, then retried once after a new layout pass.
|
|
Assert.Equal(2, secondRequest.ExecuteAttempts);
|
|
Assert.Equal(0, secondRequest.Executions);
|
|
|
|
// The second request couldn't execute despite having been through an extra layout pass.
|
|
// We can't retry forever (nothing has changed). The request will be retried again on the next "natural" pass.
|
|
canExecuteSecond = true;
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
|
|
Assert.Equal(3, secondRequest.ExecuteAttempts);
|
|
Assert.Equal(1, secondRequest.Executions);
|
|
}
|
|
|
|
private static LayoutManager GetLayoutManager(TestRoot root)
|
|
=> Assert.IsType<LayoutManager>(root.LayoutManager, exactMatch: false);
|
|
|
|
private sealed class TestRequest(Layoutable target) : BringIntoViewRequest(target)
|
|
{
|
|
public int ExecuteAttempts { get; private set; }
|
|
public int Executions { get; private set; }
|
|
public Func<bool>? CanExecute { get; init; }
|
|
public Action? OnExecute { get; init; }
|
|
|
|
public override bool TryExecute()
|
|
{
|
|
++ExecuteAttempts;
|
|
|
|
var canExecute = CanExecute?.Invoke() ?? true;
|
|
if (!canExecute)
|
|
return false;
|
|
|
|
++Executions;
|
|
OnExecute?.Invoke();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|