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.
1141 lines
43 KiB
1141 lines
43 KiB
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Avalonia.Animation;
|
|
using Avalonia.Collections;
|
|
using Avalonia.Controls.Presenters;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Controls.Templates;
|
|
using Avalonia.Input;
|
|
using Avalonia.Input.GestureRecognizers;
|
|
using Avalonia.Layout;
|
|
using Avalonia.Media;
|
|
using Avalonia.Threading;
|
|
using Avalonia.UnitTests;
|
|
using Avalonia.VisualTree;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
#nullable enable
|
|
|
|
namespace Avalonia.Controls.UnitTests
|
|
{
|
|
public class VirtualizingCarouselPanelTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void Initial_Item_Is_Displayed()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar" };
|
|
var (target, _) = CreateTarget(items);
|
|
|
|
Assert.Single(target.Children);
|
|
var container = Assert.IsType<ContentPresenter>(target.Children[0]);
|
|
Assert.Equal("foo", container.Content);
|
|
}
|
|
|
|
[Fact]
|
|
public void Initial_SelectedIndex_Is_Displayed()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar" };
|
|
var (target, _) = CreateTarget(items, selectedIndex: 1);
|
|
|
|
Assert.Single(target.Children);
|
|
var container = Assert.IsType<ContentPresenter>(target.Children[0]);
|
|
Assert.Equal("bar", container.Content);
|
|
}
|
|
|
|
[Fact]
|
|
public void Refreshing_Swipe_Wiring_Reuses_A_Single_Recognizer()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar" };
|
|
var (target, carousel) = CreateTarget(items);
|
|
|
|
carousel.IsSwipeEnabled = true;
|
|
carousel.PageTransition = new PageSlide(TimeSpan.FromMilliseconds(1));
|
|
carousel.IsSwipeEnabled = false;
|
|
carousel.IsSwipeEnabled = true;
|
|
carousel.PageTransition = null;
|
|
carousel.PageTransition = new PageSlide(TimeSpan.FromMilliseconds(1));
|
|
|
|
var recognizers = target.GestureRecognizers.OfType<SwipeGestureRecognizer>().ToArray();
|
|
var recognizer = Assert.Single(recognizers);
|
|
Assert.True(recognizer.IsEnabled);
|
|
}
|
|
|
|
[Fact]
|
|
public void Displays_Next_Item()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar" };
|
|
var (target, carousel) = CreateTarget(items);
|
|
|
|
carousel.SelectedIndex = 1;
|
|
Layout(target);
|
|
|
|
Assert.Single(target.Children);
|
|
var container = Assert.IsType<ContentPresenter>(target.Children[0]);
|
|
Assert.Equal("bar", container.Content);
|
|
}
|
|
|
|
[Fact]
|
|
public void Handles_Inserted_Item()
|
|
{
|
|
using var app = Start();
|
|
var items = new ObservableCollection<string> { "foo", "bar" };
|
|
var (target, carousel) = CreateTarget(items);
|
|
var container = Assert.IsType<ContentPresenter>(target.Children[0]);
|
|
|
|
items.Insert(0, "baz");
|
|
Layout(target);
|
|
|
|
Assert.Single(target.Children);
|
|
Assert.Same(container, target.Children[0]);
|
|
Assert.Equal("foo", container.Content);
|
|
}
|
|
|
|
[Fact]
|
|
public void Handles_Removed_Item()
|
|
{
|
|
using var app = Start();
|
|
var items = new ObservableCollection<string> { "foo", "bar" };
|
|
var (target, carousel) = CreateTarget(items);
|
|
var container = Assert.IsType<ContentPresenter>(target.Children[0]);
|
|
|
|
items.RemoveAt(0);
|
|
Layout(target);
|
|
|
|
Assert.Single(target.Children);
|
|
Assert.Same(container, target.Children[0]);
|
|
Assert.Equal("bar", container.Content);
|
|
}
|
|
|
|
[Fact]
|
|
public void Handles_Replaced_Item()
|
|
{
|
|
using var app = Start();
|
|
var items = new ObservableCollection<string> { "foo", "bar" };
|
|
var (target, carousel) = CreateTarget(items);
|
|
var container = Assert.IsType<ContentPresenter>(target.Children[0]);
|
|
|
|
items[0] = "baz";
|
|
Layout(target);
|
|
|
|
Assert.Single(target.Children);
|
|
Assert.Same(container, target.Children[0]);
|
|
Assert.Equal("baz", container.Content);
|
|
}
|
|
|
|
[Fact]
|
|
public void Handles_Moved_Item()
|
|
{
|
|
using var app = Start();
|
|
var items = new ObservableCollection<string> { "foo", "bar" };
|
|
var (target, carousel) = CreateTarget(items);
|
|
var container = Assert.IsType<ContentPresenter>(target.Children[0]);
|
|
|
|
items.Move(0, 1);
|
|
Layout(target);
|
|
|
|
Assert.Single(target.Children);
|
|
Assert.Same(container, target.Children[0]);
|
|
Assert.Equal("bar", container.Content);
|
|
}
|
|
|
|
[Fact]
|
|
public void Handles_Moved_Item_Range()
|
|
{
|
|
using var app = Start();
|
|
AvaloniaList<string> items = ["foo", "bar", "baz", "qux", "quux"];
|
|
var (target, carousel) = CreateTarget(items);
|
|
var container = Assert.IsType<ContentPresenter>(target.Children[0]);
|
|
|
|
carousel.SelectedIndex = 3;
|
|
Layout(target);
|
|
items.MoveRange(0, 2, 4);
|
|
Layout(target);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.Single(target.Children);
|
|
Assert.Same(container, target.Children[0]);
|
|
Assert.Equal("qux", container.Content);
|
|
Assert.Equal(1, carousel.SelectedIndex);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void ViewportFraction_Centers_Selected_Item_And_Peeks_Neighbors()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar", "baz" };
|
|
var (target, _) = CreateTarget(items, viewportFraction: 0.8, clientSize: new Size(400, 300));
|
|
|
|
var realized = target.GetRealizedContainers()!
|
|
.OfType<ContentPresenter>()
|
|
.ToDictionary(x => (string)x.Content!);
|
|
|
|
Assert.Equal(2, realized.Count);
|
|
Assert.Equal(40d, realized["foo"].Bounds.X, 6);
|
|
Assert.Equal(320d, realized["foo"].Bounds.Width, 6);
|
|
Assert.Equal(360d, realized["bar"].Bounds.X, 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void ViewportFraction_OneThird_Shows_Three_Full_Items()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar", "baz", "qux" };
|
|
var (target, carousel) = CreateTarget(items, viewportFraction: 1d / 3d, clientSize: new Size(300, 120));
|
|
|
|
carousel.SelectedIndex = 1;
|
|
Layout(target);
|
|
|
|
var realized = target.GetRealizedContainers()!
|
|
.OfType<ContentPresenter>()
|
|
.ToDictionary(x => (string)x.Content!);
|
|
|
|
Assert.Equal(3, realized.Count);
|
|
Assert.Equal(0d, realized["foo"].Bounds.X, 6);
|
|
Assert.Equal(100d, realized["bar"].Bounds.X, 6);
|
|
Assert.Equal(200d, realized["baz"].Bounds.X, 6);
|
|
Assert.Equal(100d, realized["bar"].Bounds.Width, 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void Changing_SelectedIndex_Repositions_Fractional_Viewport()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar", "baz" };
|
|
var (target, carousel) = CreateTarget(items, viewportFraction: 0.8, clientSize: new Size(400, 300));
|
|
|
|
carousel.SelectedIndex = 1;
|
|
Layout(target);
|
|
|
|
var realized = target.GetRealizedContainers()!
|
|
.OfType<ContentPresenter>()
|
|
.ToDictionary(x => (string)x.Content!);
|
|
|
|
Assert.Equal(40d, realized["bar"].Bounds.X, 6);
|
|
Assert.Equal(-280d, realized["foo"].Bounds.X, 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void Changing_ViewportFraction_Does_Not_Change_Selected_Item()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar", "baz" };
|
|
var (target, carousel) = CreateTarget(items, viewportFraction: 0.72, clientSize: new Size(400, 300));
|
|
|
|
carousel.WrapSelection = true;
|
|
carousel.SelectedIndex = 2;
|
|
Layout(target);
|
|
|
|
carousel.ViewportFraction = 1d;
|
|
Layout(target);
|
|
|
|
var visible = target.Children
|
|
.OfType<ContentPresenter>()
|
|
.Where(x => x.IsVisible)
|
|
.ToList();
|
|
|
|
Assert.Single(visible);
|
|
Assert.Equal("baz", visible[0].Content);
|
|
Assert.Equal(2, carousel.SelectedIndex);
|
|
}
|
|
|
|
public class Transitions : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void Initial_Item_Does_Not_Start_Transition()
|
|
{
|
|
using var app = Start();
|
|
var items = new Control[] { new Button(), new Canvas() };
|
|
var transition = new Mock<IPageTransition>();
|
|
var (target, _) = CreateTarget(items, transition.Object);
|
|
|
|
transition.Verify(x => x.Start(
|
|
It.IsAny<Visual>(),
|
|
It.IsAny<Visual>(),
|
|
It.IsAny<bool>(),
|
|
It.IsAny<CancellationToken>()),
|
|
Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public void Changing_SelectedIndex_Starts_Transition()
|
|
{
|
|
using var app = Start();
|
|
var items = new Control[] { new Button(), new Canvas() };
|
|
var transition = new Mock<IPageTransition>();
|
|
var (target, carousel) = CreateTarget(items, transition.Object);
|
|
|
|
carousel.SelectedIndex = 1;
|
|
Layout(target);
|
|
|
|
transition.Verify(x => x.Start(
|
|
items[0],
|
|
items[1],
|
|
true,
|
|
It.IsAny<CancellationToken>()),
|
|
Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public void Changing_SelectedIndex_From_First_To_Last_Transitions_Forward()
|
|
{
|
|
using var app = Start();
|
|
Dispatcher.UIThread.Invoke(() => // This sets up a proper sync context
|
|
{
|
|
var items = new Control[] { new Button(), new Canvas(), new Label() };
|
|
var transition = new Mock<IPageTransition>();
|
|
var (target, carousel) = CreateTarget(items, transition.Object);
|
|
|
|
carousel.SelectedIndex = 2;
|
|
Layout(target);
|
|
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
transition.Verify(x => x.Start(
|
|
items[0],
|
|
items[2],
|
|
true,
|
|
It.IsAny<CancellationToken>()),
|
|
Times.Once);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Changing_SelectedIndex_From_Last_To_First_Transitions_Backward()
|
|
{
|
|
using var app = Start();
|
|
Dispatcher.UIThread.Invoke(() => // This sets up a proper sync context
|
|
{
|
|
var items = new Control[] { new Button(), new Canvas(), new Label() };
|
|
var transition = new Mock<IPageTransition>();
|
|
var (target, carousel) = CreateTarget(items, transition.Object);
|
|
|
|
carousel.SelectedIndex = 2;
|
|
Layout(target);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
carousel.SelectedIndex = 0;
|
|
Layout(target);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
transition.Verify(x => x.Start(
|
|
items[2],
|
|
items[0],
|
|
false,
|
|
It.IsAny<CancellationToken>()),
|
|
Times.Once);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void TransitionFrom_Control_Is_Recycled_When_Transition_Completes()
|
|
{
|
|
using var app = Start();
|
|
using var sync = UnitTestSynchronizationContext.Begin();
|
|
var items = new Control[] { new Button(), new Canvas() };
|
|
var transition = new Mock<IPageTransition>();
|
|
var (target, carousel) = CreateTarget(items, transition.Object);
|
|
var transitionTask = new TaskCompletionSource();
|
|
|
|
transition.Setup(x => x.Start(
|
|
items[0],
|
|
items[1],
|
|
true,
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(() => transitionTask.Task);
|
|
|
|
carousel.SelectedIndex = 1;
|
|
Layout(target);
|
|
|
|
Assert.Equal(items, target.Children);
|
|
Assert.All(items, x => Assert.True(x.IsVisible));
|
|
|
|
transitionTask.SetResult();
|
|
sync.ExecutePostedCallbacks();
|
|
|
|
Assert.Equal(items, target.Children);
|
|
Assert.False(items[0].IsVisible);
|
|
Assert.True(items[1].IsVisible);
|
|
}
|
|
|
|
[Fact]
|
|
public void Existing_Transition_Is_Canceled_If_Interrupted()
|
|
{
|
|
using var app = Start();
|
|
using var sync = UnitTestSynchronizationContext.Begin();
|
|
var items = new Control[] { new Button(), new Canvas() };
|
|
var transition = new Mock<IPageTransition>();
|
|
var (target, carousel) = CreateTarget(items, transition.Object);
|
|
var transitionTask = new TaskCompletionSource();
|
|
CancellationToken? cancelationToken = null;
|
|
|
|
transition.Setup(x => x.Start(
|
|
items[0],
|
|
items[1],
|
|
true,
|
|
It.IsAny<CancellationToken>()))
|
|
.Callback<Visual, Visual, bool, CancellationToken>((_, _, _, c) => cancelationToken = c)
|
|
.Returns(() => transitionTask.Task);
|
|
|
|
carousel.SelectedIndex = 1;
|
|
Layout(target);
|
|
|
|
Assert.NotNull(cancelationToken);
|
|
Assert.False(cancelationToken!.Value.IsCancellationRequested);
|
|
|
|
carousel.SelectedIndex = 0;
|
|
Layout(target);
|
|
|
|
Assert.True(cancelationToken!.Value.IsCancellationRequested);
|
|
}
|
|
|
|
[Fact]
|
|
public void Completed_Transition_Is_Flushed_Before_Starting_Next_Transition()
|
|
{
|
|
using var app = Start();
|
|
using var sync = UnitTestSynchronizationContext.Begin();
|
|
var items = new Control[] { new Button(), new Canvas(), new Label() };
|
|
var transition = new Mock<IPageTransition>();
|
|
|
|
transition.Setup(x => x.Start(
|
|
It.IsAny<Visual>(),
|
|
It.IsAny<Visual>(),
|
|
It.IsAny<bool>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(Task.CompletedTask);
|
|
|
|
var (target, carousel) = CreateTarget(items, transition.Object);
|
|
|
|
carousel.SelectedIndex = 1;
|
|
Layout(target);
|
|
|
|
carousel.SelectedIndex = 2;
|
|
Layout(target);
|
|
|
|
transition.Verify(x => x.Start(
|
|
items[0],
|
|
items[1],
|
|
true,
|
|
It.IsAny<CancellationToken>()),
|
|
Times.Once);
|
|
transition.Verify(x => x.Start(
|
|
items[1],
|
|
items[2],
|
|
true,
|
|
It.IsAny<CancellationToken>()),
|
|
Times.Once);
|
|
|
|
sync.ExecutePostedCallbacks();
|
|
}
|
|
|
|
[Fact]
|
|
public void Interrupted_Transition_Resets_Current_Page_Before_Starting_Next_Transition()
|
|
{
|
|
using var app = Start();
|
|
var items = new Control[] { new Button(), new Canvas(), new Label() };
|
|
var transition = new DirtyStateTransition();
|
|
var (target, carousel) = CreateTarget(items, transition);
|
|
|
|
carousel.SelectedIndex = 1;
|
|
Layout(target);
|
|
|
|
carousel.SelectedIndex = 2;
|
|
Layout(target);
|
|
|
|
Assert.Equal(2, transition.Starts.Count);
|
|
Assert.Equal(1d, transition.Starts[1].FromOpacity);
|
|
Assert.Null(transition.Starts[1].FromTransform);
|
|
}
|
|
}
|
|
|
|
private static IDisposable Start() => UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
|
|
|
|
private static (VirtualizingCarouselPanel, Carousel) CreateTarget(
|
|
IEnumerable items,
|
|
IPageTransition? transition = null,
|
|
int? selectedIndex = null,
|
|
double viewportFraction = 1d,
|
|
Size? clientSize = null)
|
|
{
|
|
var size = clientSize ?? new Size(400, 300);
|
|
var carousel = new Carousel
|
|
{
|
|
ItemsSource = items,
|
|
Template = CarouselTemplate(),
|
|
PageTransition = transition,
|
|
ViewportFraction = viewportFraction,
|
|
Width = size.Width,
|
|
Height = size.Height,
|
|
};
|
|
|
|
if (selectedIndex.HasValue)
|
|
carousel.SelectedIndex = selectedIndex.Value;
|
|
|
|
var root = new TestRoot(carousel)
|
|
{
|
|
ClientSize = size,
|
|
};
|
|
root.LayoutManager.ExecuteInitialLayoutPass();
|
|
return ((VirtualizingCarouselPanel)carousel.Presenter!.Panel!, carousel);
|
|
}
|
|
|
|
private static IControlTemplate CarouselTemplate()
|
|
{
|
|
return new FuncControlTemplate((c, ns) =>
|
|
new ScrollViewer
|
|
{
|
|
Name = "PART_ScrollViewer",
|
|
Template = ScrollViewerTemplate(),
|
|
HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden,
|
|
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
|
|
Content = new ItemsPresenter
|
|
{
|
|
Name = "PART_ItemsPresenter",
|
|
[~ItemsPresenter.ItemsPanelProperty] = c[~ItemsControl.ItemsPanelProperty],
|
|
}.RegisterInNameScope(ns)
|
|
}.RegisterInNameScope(ns));
|
|
}
|
|
|
|
private static FuncControlTemplate ScrollViewerTemplate()
|
|
{
|
|
return new FuncControlTemplate<ScrollViewer>((parent, scope) =>
|
|
new Panel
|
|
{
|
|
Children =
|
|
{
|
|
new ScrollContentPresenter
|
|
{
|
|
Name = "PART_ContentPresenter",
|
|
}.RegisterInNameScope(scope),
|
|
}
|
|
});
|
|
}
|
|
|
|
private static void Layout(Control c) => c.GetLayoutManager()?.ExecuteLayoutPass();
|
|
|
|
private sealed class DirtyStateTransition : IPageTransition
|
|
{
|
|
public List<(double FromOpacity, ITransform? FromTransform)> Starts { get; } = new();
|
|
|
|
public Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
|
|
{
|
|
Starts.Add((from?.Opacity ?? 1d, from?.RenderTransform));
|
|
|
|
if (to is not null)
|
|
{
|
|
to.Opacity = 0.25;
|
|
to.RenderTransform = new TranslateTransform { X = 50 };
|
|
}
|
|
|
|
return Task.Delay(Timeout.Infinite, cancellationToken);
|
|
}
|
|
}
|
|
|
|
public class WrapSelectionTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void Next_Wraps_To_First_Item_When_WrapSelection_Enabled()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar", "baz" };
|
|
var (target, carousel) = CreateTarget(items);
|
|
|
|
carousel.WrapSelection = true;
|
|
carousel.SelectedIndex = 2; // Last item
|
|
Layout(target);
|
|
|
|
carousel.Next();
|
|
Layout(target);
|
|
|
|
Assert.Equal(0, carousel.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void Next_Does_Not_Wrap_When_WrapSelection_Disabled()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar", "baz" };
|
|
var (target, carousel) = CreateTarget(items);
|
|
|
|
carousel.WrapSelection = false;
|
|
carousel.SelectedIndex = 2; // Last item
|
|
Layout(target);
|
|
|
|
carousel.Next();
|
|
Layout(target);
|
|
|
|
Assert.Equal(2, carousel.SelectedIndex); // Should stay at last item
|
|
}
|
|
|
|
[Fact]
|
|
public void Previous_Wraps_To_Last_Item_When_WrapSelection_Enabled()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar", "baz" };
|
|
var (target, carousel) = CreateTarget(items);
|
|
|
|
carousel.WrapSelection = true;
|
|
carousel.SelectedIndex = 0; // First item
|
|
Layout(target);
|
|
|
|
carousel.Previous();
|
|
Layout(target);
|
|
|
|
Assert.Equal(2, carousel.SelectedIndex); // Should wrap to last item
|
|
}
|
|
|
|
[Fact]
|
|
public void Previous_Does_Not_Wrap_When_WrapSelection_Disabled()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar", "baz" };
|
|
var (target, carousel) = CreateTarget(items);
|
|
|
|
carousel.WrapSelection = false;
|
|
carousel.SelectedIndex = 0; // First item
|
|
Layout(target);
|
|
|
|
carousel.Previous();
|
|
Layout(target);
|
|
|
|
Assert.Equal(0, carousel.SelectedIndex); // Should stay at first item
|
|
}
|
|
|
|
[Fact]
|
|
public void WrapSelection_Works_With_Two_Items()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar" };
|
|
var (target, carousel) = CreateTarget(items);
|
|
|
|
carousel.WrapSelection = true;
|
|
carousel.SelectedIndex = 1;
|
|
Layout(target);
|
|
|
|
carousel.Next();
|
|
Layout(target);
|
|
|
|
Assert.Equal(0, carousel.SelectedIndex);
|
|
|
|
carousel.Previous();
|
|
Layout(target);
|
|
|
|
Assert.Equal(1, carousel.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void WrapSelection_Does_Not_Apply_To_Single_Item()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo" };
|
|
var (target, carousel) = CreateTarget(items);
|
|
|
|
carousel.WrapSelection = true;
|
|
carousel.SelectedIndex = 0;
|
|
Layout(target);
|
|
|
|
carousel.Next();
|
|
Layout(target);
|
|
|
|
Assert.Equal(0, carousel.SelectedIndex);
|
|
|
|
carousel.Previous();
|
|
Layout(target);
|
|
|
|
Assert.Equal(0, carousel.SelectedIndex);
|
|
}
|
|
}
|
|
|
|
public class Gestures : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void Swiping_Forward_Realizes_Next_Item()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar" };
|
|
var (panel, carousel) = CreateTarget(items);
|
|
carousel.IsSwipeEnabled = true;
|
|
|
|
var e = new SwipeGestureEventArgs(1, new Vector(10, 0), default);
|
|
panel.RaiseEvent(e);
|
|
|
|
Assert.True(carousel.IsSwiping);
|
|
Assert.Equal(2, panel.Children.Count);
|
|
var target = panel.Children[1] as Control;
|
|
Assert.NotNull(target);
|
|
Assert.True(target.IsVisible);
|
|
Assert.Equal("bar", ((target as ContentPresenter)?.Content));
|
|
}
|
|
|
|
[Fact]
|
|
public void Swiping_Backward_At_Start_RubberBands_When_WrapSelection_False()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar" };
|
|
var (panel, carousel) = CreateTarget(items);
|
|
carousel.IsSwipeEnabled = true;
|
|
carousel.WrapSelection = false;
|
|
|
|
var e = new SwipeGestureEventArgs(1, new Vector(-10, 0), default);
|
|
panel.RaiseEvent(e);
|
|
|
|
Assert.True(carousel.IsSwiping);
|
|
Assert.Single(panel.Children);
|
|
}
|
|
|
|
[Fact]
|
|
public void Swiping_Backward_At_Start_Wraps_When_WrapSelection_True()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar", "baz" };
|
|
var (panel, carousel) = CreateTarget(items);
|
|
carousel.IsSwipeEnabled = true;
|
|
carousel.WrapSelection = true;
|
|
|
|
var e = new SwipeGestureEventArgs(1, new Vector(-10, 0), default);
|
|
panel.RaiseEvent(e);
|
|
|
|
Assert.True(carousel.IsSwiping);
|
|
Assert.Equal(2, panel.Children.Count);
|
|
var target = panel.Children[1] as Control;
|
|
Assert.Equal("baz", ((target as ContentPresenter)?.Content));
|
|
}
|
|
|
|
[Fact]
|
|
public void ViewportFraction_Swiping_Backward_At_Start_Wraps_When_WrapSelection_True()
|
|
{
|
|
var clock = new MockGlobalClock();
|
|
|
|
using var app = UnitTestApplication.Start(
|
|
TestServices.MockPlatformRenderInterface.With(globalClock: clock));
|
|
using var sync = UnitTestSynchronizationContext.Begin();
|
|
|
|
var items = new[] { "foo", "bar", "baz" };
|
|
var (panel, carousel) = CreateTarget(items, viewportFraction: 0.8);
|
|
carousel.IsSwipeEnabled = true;
|
|
carousel.WrapSelection = true;
|
|
Layout(panel);
|
|
|
|
panel.RaiseEvent(new SwipeGestureEventArgs(1, new Vector(-120, 0), default));
|
|
|
|
Assert.True(carousel.IsSwiping);
|
|
Assert.Contains(panel.Children.OfType<ContentPresenter>(), x => Equals(x.Content, "baz"));
|
|
|
|
panel.RaiseEvent(new SwipeGestureEndedEventArgs(1, default));
|
|
|
|
clock.Pulse(TimeSpan.Zero);
|
|
clock.Pulse(TimeSpan.FromSeconds(1));
|
|
sync.ExecutePostedCallbacks();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
Assert.Equal(2, carousel.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void Swiping_Forward_At_End_RubberBands_When_WrapSelection_False()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar" };
|
|
var (panel, carousel) = CreateTarget(items);
|
|
carousel.IsSwipeEnabled = true;
|
|
carousel.WrapSelection = false;
|
|
carousel.SelectedIndex = 1;
|
|
|
|
Layout(panel);
|
|
Layout(panel);
|
|
|
|
Assert.Equal(2, ((IReadOnlyList<string>?)carousel.ItemsSource)?.Count);
|
|
Assert.Equal(1, carousel.SelectedIndex);
|
|
Assert.False(carousel.WrapSelection, "WrapSelection should be false");
|
|
|
|
var container = Assert.IsType<ContentPresenter>(panel.Children[0]);
|
|
Assert.Equal("bar", container.Content);
|
|
|
|
var e = new SwipeGestureEventArgs(1, new Vector(10, 0), default);
|
|
panel.RaiseEvent(e);
|
|
|
|
Assert.True(carousel.IsSwiping);
|
|
Assert.Single(panel.Children);
|
|
}
|
|
|
|
[Fact]
|
|
public void Swiping_Locks_To_Dominant_Axis()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar" };
|
|
var (panel, carousel) = CreateTarget(items, new CrossFade(TimeSpan.FromSeconds(1)));
|
|
carousel.IsSwipeEnabled = true;
|
|
|
|
var e = new SwipeGestureEventArgs(1, new Vector(10, 2), default);
|
|
panel.RaiseEvent(e);
|
|
|
|
Assert.True(carousel.IsSwiping);
|
|
}
|
|
|
|
[Fact]
|
|
public void Swipe_Completion_Does_Not_Update_With_Same_From_And_To()
|
|
{
|
|
var clock = new MockGlobalClock();
|
|
|
|
using var app = UnitTestApplication.Start(
|
|
TestServices.MockPlatformRenderInterface.With(globalClock: clock));
|
|
using var sync = UnitTestSynchronizationContext.Begin();
|
|
|
|
var items = new[] { "foo", "bar" };
|
|
var transition = new TrackingInteractiveTransition();
|
|
var (panel, carousel) = CreateTarget(items, transition);
|
|
carousel.IsSwipeEnabled = true;
|
|
|
|
panel.RaiseEvent(new SwipeGestureEventArgs(1, new Vector(1000, 0), default));
|
|
panel.RaiseEvent(new SwipeGestureEndedEventArgs(1, new Vector(1000, 0)));
|
|
|
|
clock.Pulse(TimeSpan.Zero);
|
|
clock.Pulse(TimeSpan.FromSeconds(1));
|
|
sync.ExecutePostedCallbacks();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
Assert.True(transition.UpdateCallCount > 0);
|
|
Assert.False(transition.SawAliasedUpdate);
|
|
Assert.Equal(1d, transition.LastProgress);
|
|
Assert.Equal(1, carousel.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void Swipe_Completion_Keeps_Target_Final_Interactive_Visual_State()
|
|
{
|
|
var clock = new MockGlobalClock();
|
|
|
|
using var app = UnitTestApplication.Start(
|
|
TestServices.MockPlatformRenderInterface.With(globalClock: clock));
|
|
using var sync = UnitTestSynchronizationContext.Begin();
|
|
|
|
var items = new[] { "foo", "bar" };
|
|
var transition = new TransformTrackingInteractiveTransition();
|
|
var (panel, carousel) = CreateTarget(items, transition);
|
|
carousel.IsSwipeEnabled = true;
|
|
|
|
panel.RaiseEvent(new SwipeGestureEventArgs(1, new Vector(1000, 0), default));
|
|
panel.RaiseEvent(new SwipeGestureEndedEventArgs(1, new Vector(1000, 0)));
|
|
|
|
clock.Pulse(TimeSpan.Zero);
|
|
clock.Pulse(TimeSpan.FromSeconds(1));
|
|
sync.ExecutePostedCallbacks();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
Assert.Equal(1, carousel.SelectedIndex);
|
|
var realized = Assert.Single(panel.Children.OfType<ContentPresenter>(), x => Equals(x.Content, "bar"));
|
|
Assert.NotNull(transition.LastTargetTransform);
|
|
Assert.Same(transition.LastTargetTransform, realized.RenderTransform);
|
|
}
|
|
|
|
[Fact]
|
|
public void Swipe_Completion_Hides_Outgoing_Page_Before_Resetting_Visual_State()
|
|
{
|
|
var clock = new MockGlobalClock();
|
|
|
|
using var app = UnitTestApplication.Start(
|
|
TestServices.MockPlatformRenderInterface.With(globalClock: clock));
|
|
using var sync = UnitTestSynchronizationContext.Begin();
|
|
|
|
var items = new[] { "foo", "bar" };
|
|
var transition = new OutgoingTransformTrackingInteractiveTransition();
|
|
var (panel, carousel) = CreateTarget(items, transition);
|
|
carousel.IsSwipeEnabled = true;
|
|
|
|
var outgoing = Assert.Single(panel.Children.OfType<ContentPresenter>(), x => Equals(x.Content, "foo"));
|
|
bool? hiddenWhenReset = null;
|
|
outgoing.PropertyChanged += (_, args) =>
|
|
{
|
|
if (args.Property == Visual.RenderTransformProperty &&
|
|
args.GetNewValue<ITransform?>() is null)
|
|
{
|
|
hiddenWhenReset = !outgoing.IsVisible;
|
|
}
|
|
};
|
|
|
|
panel.RaiseEvent(new SwipeGestureEventArgs(1, new Vector(1000, 0), default));
|
|
panel.RaiseEvent(new SwipeGestureEndedEventArgs(1, new Vector(1000, 0)));
|
|
|
|
clock.Pulse(TimeSpan.Zero);
|
|
clock.Pulse(TimeSpan.FromSeconds(1));
|
|
sync.ExecutePostedCallbacks();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
Assert.True(hiddenWhenReset);
|
|
}
|
|
|
|
[Fact]
|
|
public void RubberBand_Swipe_Release_Animates_Back_Through_Intermediate_Progress()
|
|
{
|
|
var clock = new MockGlobalClock();
|
|
|
|
using var app = UnitTestApplication.Start(
|
|
TestServices.MockPlatformRenderInterface.With(globalClock: clock));
|
|
using var sync = UnitTestSynchronizationContext.Begin();
|
|
|
|
var items = new[] { "foo", "bar" };
|
|
var transition = new ProgressTrackingInteractiveTransition();
|
|
var (panel, carousel) = CreateTarget(items, transition);
|
|
carousel.IsSwipeEnabled = true;
|
|
carousel.WrapSelection = false;
|
|
|
|
panel.RaiseEvent(new SwipeGestureEventArgs(1, new Vector(-100, 0), default));
|
|
|
|
var releaseStartProgress = transition.Progresses[^1];
|
|
var updatesBeforeRelease = transition.Progresses.Count;
|
|
|
|
panel.RaiseEvent(new SwipeGestureEndedEventArgs(1, default));
|
|
|
|
clock.Pulse(TimeSpan.Zero);
|
|
clock.Pulse(TimeSpan.FromSeconds(0.1));
|
|
sync.ExecutePostedCallbacks();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
var postReleaseProgresses = transition.Progresses.Skip(updatesBeforeRelease).ToArray();
|
|
|
|
Assert.Contains(postReleaseProgresses, p => p > 0 && p < releaseStartProgress);
|
|
|
|
clock.Pulse(TimeSpan.FromSeconds(1));
|
|
sync.ExecutePostedCallbacks();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
Assert.Equal(0d, transition.Progresses[^1]);
|
|
Assert.Equal(0, carousel.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void ViewportFraction_SelectedIndex_Change_Drives_Progress_Updates()
|
|
{
|
|
var clock = new MockGlobalClock();
|
|
|
|
using var app = UnitTestApplication.Start(
|
|
TestServices.MockPlatformRenderInterface.With(globalClock: clock));
|
|
using var sync = UnitTestSynchronizationContext.Begin();
|
|
|
|
var items = new[] { "foo", "bar", "baz" };
|
|
var transition = new ProgressTrackingInteractiveTransition();
|
|
var (panel, carousel) = CreateTarget(items, transition, viewportFraction: 0.8);
|
|
|
|
carousel.SelectedIndex = 1;
|
|
|
|
clock.Pulse(TimeSpan.Zero);
|
|
clock.Pulse(TimeSpan.FromSeconds(0.1));
|
|
clock.Pulse(TimeSpan.FromSeconds(1));
|
|
sync.ExecutePostedCallbacks();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
Assert.NotEmpty(transition.Progresses);
|
|
Assert.Contains(transition.Progresses, p => p > 0 && p < 1);
|
|
Assert.Equal(1d, transition.Progresses[^1]);
|
|
Assert.Equal(1, carousel.SelectedIndex);
|
|
}
|
|
|
|
private sealed class TrackingInteractiveTransition : IProgressPageTransition
|
|
{
|
|
public int UpdateCallCount { get; private set; }
|
|
public bool SawAliasedUpdate { get; private set; }
|
|
public double LastProgress { get; private set; }
|
|
|
|
public Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
|
|
=> Task.CompletedTask;
|
|
|
|
public void Update(
|
|
double progress,
|
|
Visual? from,
|
|
Visual? to,
|
|
bool forward,
|
|
double pageLength,
|
|
IReadOnlyList<PageTransitionItem> visibleItems)
|
|
{
|
|
UpdateCallCount++;
|
|
LastProgress = progress;
|
|
|
|
if (from is not null && ReferenceEquals(from, to))
|
|
SawAliasedUpdate = true;
|
|
}
|
|
|
|
public void Reset(Visual visual)
|
|
{
|
|
visual.RenderTransform = null;
|
|
visual.Opacity = 1;
|
|
visual.ZIndex = 0;
|
|
visual.Clip = null;
|
|
}
|
|
}
|
|
|
|
private sealed class ProgressTrackingInteractiveTransition : IProgressPageTransition
|
|
{
|
|
public List<double> Progresses { get; } = new();
|
|
|
|
public Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
|
|
=> Task.CompletedTask;
|
|
|
|
public void Update(
|
|
double progress,
|
|
Visual? from,
|
|
Visual? to,
|
|
bool forward,
|
|
double pageLength,
|
|
IReadOnlyList<PageTransitionItem> visibleItems)
|
|
{
|
|
Progresses.Add(progress);
|
|
}
|
|
|
|
public void Reset(Visual visual)
|
|
{
|
|
visual.RenderTransform = null;
|
|
visual.Opacity = 1;
|
|
visual.ZIndex = 0;
|
|
visual.Clip = null;
|
|
}
|
|
}
|
|
|
|
private sealed class TransformTrackingInteractiveTransition : IProgressPageTransition
|
|
{
|
|
public TransformGroup? LastTargetTransform { get; private set; }
|
|
|
|
public Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
|
|
=> Task.CompletedTask;
|
|
|
|
public void Update(
|
|
double progress,
|
|
Visual? from,
|
|
Visual? to,
|
|
bool forward,
|
|
double pageLength,
|
|
IReadOnlyList<PageTransitionItem> visibleItems)
|
|
{
|
|
if (to is not Control target)
|
|
return;
|
|
|
|
if (target.RenderTransform is not TransformGroup group)
|
|
{
|
|
group = new TransformGroup
|
|
{
|
|
Children =
|
|
{
|
|
new ScaleTransform(),
|
|
new TranslateTransform()
|
|
}
|
|
};
|
|
target.RenderTransform = group;
|
|
}
|
|
|
|
var scale = Assert.IsType<ScaleTransform>(group.Children[0]);
|
|
var translate = Assert.IsType<TranslateTransform>(group.Children[1]);
|
|
scale.ScaleX = scale.ScaleY = 0.9 + (0.1 * progress);
|
|
translate.X = 100 * (1 - progress);
|
|
LastTargetTransform = group;
|
|
}
|
|
|
|
public void Reset(Visual visual)
|
|
{
|
|
visual.RenderTransform = null;
|
|
}
|
|
}
|
|
|
|
private sealed class OutgoingTransformTrackingInteractiveTransition : IProgressPageTransition
|
|
{
|
|
public Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
|
|
=> Task.CompletedTask;
|
|
|
|
public void Update(
|
|
double progress,
|
|
Visual? from,
|
|
Visual? to,
|
|
bool forward,
|
|
double pageLength,
|
|
IReadOnlyList<PageTransitionItem> visibleItems)
|
|
{
|
|
if (from is Control source)
|
|
source.RenderTransform = new TranslateTransform(100 * progress, 0);
|
|
|
|
if (to is Control target)
|
|
target.RenderTransform = new TranslateTransform(100 * (1 - progress), 0);
|
|
}
|
|
|
|
public void Reset(Visual visual)
|
|
{
|
|
visual.RenderTransform = null;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Vertical_Swipe_Forward_Realizes_Next_Item()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar" };
|
|
var transition = new PageSlide(TimeSpan.FromSeconds(1), PageSlide.SlideAxis.Vertical);
|
|
var (panel, carousel) = CreateTarget(items, transition);
|
|
carousel.IsSwipeEnabled = true;
|
|
|
|
var e = new SwipeGestureEventArgs(1, new Vector(0, 10), default);
|
|
panel.RaiseEvent(e);
|
|
|
|
Assert.True(carousel.IsSwiping);
|
|
Assert.Equal(2, panel.Children.Count);
|
|
var target = panel.Children[1] as ContentPresenter;
|
|
Assert.NotNull(target);
|
|
Assert.Equal("bar", target.Content);
|
|
}
|
|
|
|
[Fact]
|
|
public void New_Swipe_Interrupts_Active_Completion_Animation()
|
|
{
|
|
var clock = new MockGlobalClock();
|
|
|
|
using var app = UnitTestApplication.Start(
|
|
TestServices.MockPlatformRenderInterface.With(globalClock: clock));
|
|
using var sync = UnitTestSynchronizationContext.Begin();
|
|
|
|
var items = new[] { "foo", "bar", "baz" };
|
|
var transition = new TrackingInteractiveTransition();
|
|
var (panel, carousel) = CreateTarget(items, transition);
|
|
carousel.IsSwipeEnabled = true;
|
|
|
|
panel.RaiseEvent(new SwipeGestureEventArgs(1, new Vector(1000, 0), default));
|
|
panel.RaiseEvent(new SwipeGestureEndedEventArgs(1, new Vector(1000, 0)));
|
|
|
|
clock.Pulse(TimeSpan.Zero);
|
|
clock.Pulse(TimeSpan.FromMilliseconds(50));
|
|
sync.ExecutePostedCallbacks();
|
|
|
|
Assert.Equal(0, carousel.SelectedIndex);
|
|
|
|
panel.RaiseEvent(new SwipeGestureEventArgs(2, new Vector(10, 0), default));
|
|
|
|
Assert.True(carousel.IsSwiping);
|
|
Assert.Equal(1, carousel.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void Swipe_With_NonInteractive_Transition_Does_Not_Crash()
|
|
{
|
|
using var app = Start();
|
|
var items = new[] { "foo", "bar" };
|
|
var transition = new Mock<IPageTransition>();
|
|
transition.Setup(x => x.Start(It.IsAny<Visual>(), It.IsAny<Visual>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
|
|
.Returns(Task.CompletedTask);
|
|
var (panel, carousel) = CreateTarget(items, transition.Object);
|
|
carousel.IsSwipeEnabled = true;
|
|
|
|
var e = new SwipeGestureEventArgs(1, new Vector(10, 0), default);
|
|
panel.RaiseEvent(e);
|
|
|
|
Assert.True(carousel.IsSwiping);
|
|
Assert.Equal(2, panel.Children.Count);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|