From f0c89a614e1c79bb0e68026f20f931c2fd314091 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 9 Dec 2022 09:49:57 +0100 Subject: [PATCH] Reimplemented Carousel. Only `VirtualizingCarouselPanel` currently implemented. --- .../ControlCatalog/Pages/CarouselPage.xaml | 4 +- src/Avalonia.Controls/Carousel.cs | 50 +- .../Presenters/CarouselPresenter.cs | 276 ------- .../VirtualizingCarouselPanel.cs | 351 +++++++++ .../Controls/Carousel.xaml | 20 +- .../Controls/Carousel.xaml | 23 +- .../CarouselTests.cs | 652 ++++++++-------- .../Presenters/CarouselPresenterTests.cs | 732 ------------------ .../VirtualizingCarouselPanelTests.cs | 271 +++++++ .../UnitTestSynchronizationContext.cs | 5 +- 10 files changed, 993 insertions(+), 1391 deletions(-) delete mode 100644 src/Avalonia.Controls/Presenters/CarouselPresenter.cs create mode 100644 src/Avalonia.Controls/VirtualizingCarouselPanel.cs delete mode 100644 tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs create mode 100644 tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs rename tests/{Avalonia.Base.UnitTests/Data => Avalonia.UnitTests}/UnitTestSynchronizationContext.cs (91%) diff --git a/samples/ControlCatalog/Pages/CarouselPage.xaml b/samples/ControlCatalog/Pages/CarouselPage.xaml index 1c2d768966..352fa32e30 100644 --- a/samples/ControlCatalog/Pages/CarouselPage.xaml +++ b/samples/ControlCatalog/Pages/CarouselPage.xaml @@ -12,7 +12,7 @@ - + @@ -35,7 +35,7 @@ Orientation - + Horizontal Vertical diff --git a/src/Avalonia.Controls/Carousel.cs b/src/Avalonia.Controls/Carousel.cs index 572ec5a3d5..a9ffb2119a 100644 --- a/src/Avalonia.Controls/Carousel.cs +++ b/src/Avalonia.Controls/Carousel.cs @@ -2,7 +2,6 @@ using Avalonia.Animation; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; -using Avalonia.Input; namespace Avalonia.Controls { @@ -11,12 +10,6 @@ namespace Avalonia.Controls /// public class Carousel : SelectingItemsControl { - /// - /// Defines the property. - /// - public static readonly StyledProperty IsVirtualizedProperty = - AvaloniaProperty.Register(nameof(IsVirtualized), true); - /// /// Defines the property. /// @@ -28,7 +21,9 @@ namespace Avalonia.Controls /// . /// private static readonly ITemplate PanelTemplate = - new FuncTemplate(() => new Panel()); + new FuncTemplate(() => new VirtualizingCarouselPanel()); + + private IScrollable? _scroller; /// /// Initializes static members of the class. @@ -38,18 +33,6 @@ namespace Avalonia.Controls SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); ItemsPanelProperty.OverrideDefaultValue(PanelTemplate); } - - /// - /// Gets or sets a value indicating whether the items in the carousel are virtualized. - /// - /// - /// When the carousel is virtualized, only the active page is held in memory. - /// - public bool IsVirtualized - { - get { return GetValue(IsVirtualizedProperty); } - set { SetValue(IsVirtualizedProperty, value); } - } /// /// Gets or sets the transition to use when moving between pages. @@ -81,5 +64,32 @@ namespace Avalonia.Controls --SelectedIndex; } } + + protected override Size ArrangeOverride(Size finalSize) + { + var result = base.ArrangeOverride(finalSize); + + if (_scroller is not null) + _scroller.Offset = new(SelectedIndex, 0); + + return result; + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _scroller = e.NameScope.Find("PART_ScrollViewer"); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == SelectedIndexProperty && _scroller is not null) + { + var value = change.GetNewValue(); + _scroller.Offset = new(value, 0); + } + } } } diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs deleted file mode 100644 index d5b89a1bdc..0000000000 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System.Collections.Specialized; -using System.Linq; -using System.Reactive.Linq; -using System.Threading.Tasks; -using Avalonia.Animation; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Utils; -using Avalonia.Data; - -namespace Avalonia.Controls.Presenters -{ - /// - /// Displays pages inside an . - /// - public class CarouselPresenter : ItemsPresenter - { - /// - /// Defines the property. - /// - public static readonly StyledProperty IsVirtualizedProperty = - Carousel.IsVirtualizedProperty.AddOwner(); - - /// - /// Defines the property. - /// - public static readonly DirectProperty SelectedIndexProperty = - SelectingItemsControl.SelectedIndexProperty.AddOwner( - o => o.SelectedIndex, - (o, v) => o.SelectedIndex = v); - - /// - /// Defines the property. - /// - public static readonly StyledProperty PageTransitionProperty = - Carousel.PageTransitionProperty.AddOwner(); - - private int _selectedIndex = -1; - private Task? _currentTransition; - private int _queuedTransitionIndex = -1; - - /// - /// Initializes static members of the class. - /// - static CarouselPresenter() - { - ////IsVirtualizedProperty.Changed.AddClassHandler((x, e) => x.IsVirtualizedChanged(e)); - ////SelectedIndexProperty.Changed.AddClassHandler((x, e) => x.SelectedIndexChanged(e)); - } - - /// - /// Gets or sets a value indicating whether the items in the carousel are virtualized. - /// - /// - /// When the carousel is virtualized, only the active page is held in memory. - /// - public bool IsVirtualized - { - get { return GetValue(IsVirtualizedProperty); } - set { SetValue(IsVirtualizedProperty, value); } - } - - /// - /// Gets or sets the index of the selected page. - /// - public int SelectedIndex - { - get - { - return _selectedIndex; - } - - set - { - ////var old = SelectedIndex; - ////var effective = (value >= 0 && value < Items?.Cast().Count()) ? value : -1; - - ////if (old != effective) - ////{ - //// _selectedIndex = effective; - //// RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue); - ////} - } - } - - /// - /// Gets or sets a transition to use when switching pages. - /// - public IPageTransition? PageTransition - { - get { return GetValue(PageTransitionProperty); } - set { SetValue(PageTransitionProperty, value); } - } - - /// - ////protected override void ItemsChanged(NotifyCollectionChangedEventArgs e) - ////{ - //// if (!IsVirtualized) - //// { - //// base.ItemsChanged(e); - - //// if (Items == null || SelectedIndex >= Items.Count()) - //// { - //// SelectedIndex = Items.Count() - 1; - //// } - - //// foreach (var c in ItemContainerGenerator.Containers) - //// { - //// c.ContainerControl.IsVisible = c.Index == SelectedIndex; - //// } - //// } - //// else if (SelectedIndex != -1 && Panel != null) - //// { - //// switch (e.Action) - //// { - //// case NotifyCollectionChangedAction.Add: - //// if (e.NewStartingIndex > SelectedIndex) - //// { - //// return; - //// } - //// break; - //// case NotifyCollectionChangedAction.Remove: - //// if (e.OldStartingIndex > SelectedIndex) - //// { - //// return; - //// } - //// break; - //// case NotifyCollectionChangedAction.Replace: - //// if (e.OldStartingIndex > SelectedIndex || - //// e.OldStartingIndex + e.OldItems!.Count - 1 < SelectedIndex) - //// { - //// return; - //// } - //// break; - //// case NotifyCollectionChangedAction.Move: - //// if (e.OldStartingIndex > SelectedIndex && - //// e.NewStartingIndex > SelectedIndex) - //// { - //// return; - //// } - //// break; - //// } - - //// if (Items == null || SelectedIndex >= Items.Count()) - //// { - //// SelectedIndex = Items.Count() - 1; - //// } - - //// Panel.Children.Clear(); - //// ItemContainerGenerator.Clear(); - - //// if (SelectedIndex != -1) - //// { - //// GetOrCreateContainer(SelectedIndex); - //// } - //// } - ////} - - ////protected override void PanelCreated(Panel panel) - ////{ - //// ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - ////} - - /////// - /////// Moves to the selected page, animating if a is set. - /////// - /////// The index of the old page. - /////// The index of the new page. - /////// A task tracking the animation. - ////private async Task MoveToPage(int fromIndex, int toIndex) - ////{ - //// if (fromIndex != toIndex) - //// { - //// var generator = ItemContainerGenerator; - //// Control? from = null; - //// Control? to = null; - - //// if (fromIndex != -1) - //// { - //// from = generator.ContainerFromIndex(fromIndex); - //// } - - //// if (toIndex != -1) - //// { - //// to = GetOrCreateContainer(toIndex); - //// } - - //// if (PageTransition != null && (from != null || to != null)) - //// { - //// await PageTransition.Start((Visual?)from, (Visual?)to, fromIndex < toIndex, default); - //// } - //// else if (to != null) - //// { - //// to.IsVisible = true; - //// } - - //// if (from != null) - //// { - //// if (IsVirtualized) - //// { - //// Panel!.Children.Remove(from); - //// generator.Dematerialize(fromIndex, 1); - //// } - //// else - //// { - //// from.IsVisible = false; - //// } - //// } - //// } - ////} - - ////private Control? GetOrCreateContainer(int index) - ////{ - //// var container = ItemContainerGenerator.ContainerFromIndex(index); - - //// if (container == null && IsVirtualized) - //// { - //// var item = Items!.Cast().ElementAt(index); - //// var materialized = ItemContainerGenerator.Materialize(index, item); - //// Panel!.Children.Add(materialized.ContainerControl); - //// container = materialized.ContainerControl; - //// } - - //// return container; - ////} - - /////// - /////// Called when the property changes. - /////// - /////// The event args. - ////private void IsVirtualizedChanged(AvaloniaPropertyChangedEventArgs e) - ////{ - //// if (Panel != null) - //// { - //// ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - //// } - ////} - - /////// - /////// Called when the property changes. - /////// - /////// The event args. - ////private async void SelectedIndexChanged(AvaloniaPropertyChangedEventArgs e) - ////{ - //// if (Panel != null) - //// { - //// if (_currentTransition == null) - //// { - //// int fromIndex = (int)e.OldValue!; - //// int toIndex = (int)e.NewValue!; - - //// for (;;) - //// { - //// _currentTransition = MoveToPage(fromIndex, toIndex); - //// await _currentTransition; - - //// if (_queuedTransitionIndex != -1) - //// { - //// fromIndex = toIndex; - //// toIndex = _queuedTransitionIndex; - //// _queuedTransitionIndex = -1; - //// } - //// else - //// { - //// _currentTransition = null; - //// break; - //// } - //// } - //// } - //// else - //// { - //// _queuedTransitionIndex = (int)e.NewValue!; - //// } - //// } - ////} - } -} diff --git a/src/Avalonia.Controls/VirtualizingCarouselPanel.cs b/src/Avalonia.Controls/VirtualizingCarouselPanel.cs new file mode 100644 index 0000000000..7ea175fc18 --- /dev/null +++ b/src/Avalonia.Controls/VirtualizingCarouselPanel.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Animation; +using Avalonia.Controls.Primitives; +using Avalonia.Input; + +namespace Avalonia.Controls +{ + /// + /// A panel used by to display the current item. + /// + public class VirtualizingCarouselPanel : VirtualizingPanel, ILogicalScrollable + { + private static readonly AttachedProperty ItemIsOwnContainerProperty = + AvaloniaProperty.RegisterAttached("ItemIsOwnContainer"); + + private Size _extent; + private Vector _offset; + private Size _viewport; + private Stack? _recyclePool; + private Control? _realized; + private int _realizedIndex = -1; + private Control? _transitionFrom; + private int _transitionFromIndex = -1; + private CancellationTokenSource? _transition; + private EventHandler? _scrollInvalidated; + + bool ILogicalScrollable.CanHorizontallyScroll { get; set; } + bool ILogicalScrollable.CanVerticallyScroll { get; set; } + bool ILogicalScrollable.IsLogicalScrollEnabled => true; + Size ILogicalScrollable.ScrollSize => new(1, 1); + Size ILogicalScrollable.PageScrollSize => new(1, 1); + Size IScrollable.Extent => Extent; + Size IScrollable.Viewport => Viewport; + + Vector IScrollable.Offset + { + get => _offset; + set + { + if ((int)_offset.X != value.X) + InvalidateMeasure(); + _offset = value; + } + } + + private Size Extent + { + get => _extent; + set + { + if (_extent != value) + { + _extent = value; + _scrollInvalidated?.Invoke(this, EventArgs.Empty); + } + } + } + + private Size Viewport + { + get => _viewport; + set + { + if (_viewport != value) + { + _viewport = value; + _scrollInvalidated?.Invoke(this, EventArgs.Empty); + } + } + } + + event EventHandler? ILogicalScrollable.ScrollInvalidated + { + add => _scrollInvalidated += value; + remove => _scrollInvalidated -= value; + } + + bool ILogicalScrollable.BringIntoView(Control target, Rect targetRect) + { + throw new NotImplementedException(); + } + + Control? ILogicalScrollable.GetControlInDirection(NavigationDirection direction, Control? from) + { + throw new NotImplementedException(); + } + + void ILogicalScrollable.RaiseScrollInvalidated(EventArgs e) => _scrollInvalidated?.Invoke(this, e); + + protected override Size MeasureOverride(Size availableSize) + { + var items = ItemsControl?.Items as IList ?? Array.Empty(); + var index = (int)_offset.X; + + if (index != _realizedIndex) + { + if (_realized is not null) + { + var cancelTransition = _transition is not null; + + // Cancel any already running transition, and recycle the element we're transitioning from. + if (cancelTransition) + { + _transition!.Cancel(); + _transition = null; + if (_transitionFrom is not null) + RecycleElement(_transitionFrom); + _transitionFrom = null; + _transitionFromIndex = -1; + } + + if (cancelTransition || GetTransition() is null) + { + // If don't have a transition or we've just canceled a transition then recycle the element + // we're moving from. + RecycleElement(_realized); + } + else + { + // We have a transition to do: record the current element as the element we're transitioning + // from and we'll start the transition in the arrange pass. + _transitionFrom = _realized; + _transitionFromIndex = _realizedIndex; + } + + _realized = null; + _realizedIndex = -1; + } + + // Get or create an element for the new item. + if (index >= 0 && index < items.Count) + { + _realized = GetOrCreateElement(items, index); + _realizedIndex = index; + } + } + + if (_realized is null) + { + Extent = Viewport = new(0, 0); + _transitionFrom = null; + _transitionFromIndex = -1; + return default; + } + + _realized.Measure(availableSize); + Extent = new(items.Count, 1); + Viewport = new(1, 1); + + return _realized.DesiredSize; + } + + protected override Size ArrangeOverride(Size finalSize) + { + var result = base.ArrangeOverride(finalSize); + + if (_transition is null && + _transitionFrom is not null && + _realized is { } to && + GetTransition() is { } transition) + { + _transition = new CancellationTokenSource(); + transition.Start(_transitionFrom, to, _realizedIndex > _transitionFromIndex, _transition.Token) + .ContinueWith(TransitionFinished, TaskScheduler.FromCurrentSynchronizationContext()); + } + + return result; + } + + protected override IInputElement? GetControl(NavigationDirection direction, IInputElement? from, bool wrap) => null; + + protected internal override Control? ContainerFromIndex(int index) + { + return index == _realizedIndex ? _realized : null; + } + + protected internal override IEnumerable? GetRealizedContainers() + { + return _realized is not null ? new[] { _realized } : null; + } + + protected internal override int IndexFromContainer(Control container) + { + return container == _realized ? _realizedIndex : -1; + } + + protected internal override Control? ScrollIntoView(int index) + { + return null; + } + + protected override void OnItemsChanged(IList items, NotifyCollectionChangedEventArgs e) + { + base.OnItemsChanged(items, e); + + void Add(int index, int count) + { + if (index <= _realizedIndex) + _realizedIndex += count; + } + + void Remove(int index, int count) + { + var end = index + (count - 1); + + if (_realized is not null && index <= _realizedIndex && end >= _realizedIndex) + { + RecycleElement(_realized); + _realized = null; + _realizedIndex = -1; + } + else if (index < _realizedIndex) + { + _realizedIndex -= count; + } + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + Add(e.NewStartingIndex, e.NewItems!.Count); + break; + case NotifyCollectionChangedAction.Remove: + Remove(e.OldStartingIndex, e.OldItems!.Count); + break; + case NotifyCollectionChangedAction.Replace: + case NotifyCollectionChangedAction.Move: + Remove(e.OldStartingIndex, e.OldItems!.Count); + Add(e.NewStartingIndex, e.NewItems!.Count); + break; + case NotifyCollectionChangedAction.Reset: + if (_realized is not null) + { + RecycleElement(_realized); + _realized = null; + _realizedIndex = -1; + } + break; + } + + InvalidateMeasure(); + } + + private Control GetOrCreateElement(IList items, int index) + { + return GetRealizedElement(index) ?? + GetItemIsOwnContainer(items, index) ?? + GetRecycledElement(items, index) ?? + CreateElement(items, index); + } + + private Control? GetRealizedElement(int index) + { + return _realizedIndex == index ? _realized : null; + } + + private Control? GetItemIsOwnContainer(IList items, int index) + { + Debug.Assert(ItemsControl is not null); + + if (items[index] is Control controlItem) + { + var generator = ItemsControl!.ItemContainerGenerator; + + if (controlItem.IsSet(ItemIsOwnContainerProperty)) + { + controlItem.IsVisible = true; + return controlItem; + } + else if (generator.IsItemItsOwnContainer(controlItem)) + { + AddInternalChild(controlItem); + generator.PrepareItemContainer(controlItem, controlItem, index); + controlItem.SetValue(ItemIsOwnContainerProperty, true); + return controlItem; + } + } + + return null; + } + + private Control? GetRecycledElement(IList items, int index) + { + Debug.Assert(ItemsControl is not null); + + var generator = ItemsControl!.ItemContainerGenerator; + var item = items[index]; + + if (_recyclePool?.Count > 0) + { + var recycled = _recyclePool.Pop(); + recycled.IsVisible = true; + generator.PrepareItemContainer(recycled, item, index); + return recycled; + } + + return null; + } + + private Control CreateElement(IList items, int index) + { + Debug.Assert(ItemsControl is not null); + + var generator = ItemsControl!.ItemContainerGenerator; + var item = items[index]; + var container = generator.CreateContainer(); + + AddInternalChild(container); + generator.PrepareItemContainer(container, item, index); + + return container; + } + + private void RecycleElement(Control element) + { + Debug.Assert(ItemsControl is not null); + + if (element.IsSet(ItemIsOwnContainerProperty)) + { + element.IsVisible = false; + } + else + { + ItemsControl!.ItemContainerGenerator.ClearItemContainer(element); + _recyclePool ??= new(); + _recyclePool.Push(element); + element.IsVisible = false; + } + } + + private IPageTransition? GetTransition() => (ItemsControl as Carousel)?.PageTransition; + + private void TransitionFinished(Task task) + { + if (task.IsCanceled) + return; + + if (_transitionFrom is not null) + RecycleElement(_transitionFrom); + _transition = null; + _transitionFrom = null; + _transitionFromIndex = -1; + } + } +} diff --git a/src/Avalonia.Themes.Fluent/Controls/Carousel.xaml b/src/Avalonia.Themes.Fluent/Controls/Carousel.xaml index 1dab50052d..91e331693e 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Carousel.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Carousel.xaml @@ -3,16 +3,16 @@ - - - + + + diff --git a/src/Avalonia.Themes.Simple/Controls/Carousel.xaml b/src/Avalonia.Themes.Simple/Controls/Carousel.xaml index 66a4a1866a..91e331693e 100644 --- a/src/Avalonia.Themes.Simple/Controls/Carousel.xaml +++ b/src/Avalonia.Themes.Simple/Controls/Carousel.xaml @@ -1,19 +1,18 @@ - + - - - + + + diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index ddf471e513..2b043e0e48 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -1,341 +1,321 @@ -////using System.Collections.ObjectModel; -////using System.Linq; -////using System.Reactive.Subjects; -////using Avalonia.Controls.Presenters; -////using Avalonia.Controls.Templates; -////using Avalonia.Data; -////using Avalonia.LogicalTree; -////using Avalonia.UnitTests; -////using Avalonia.VisualTree; -////using Xunit; - -////namespace Avalonia.Controls.UnitTests -////{ -//// public class CarouselTests -//// { -//// [Fact] -//// public void First_Item_Should_Be_Selected_By_Default() -//// { -//// var target = new Carousel -//// { -//// Template = new FuncControlTemplate(CreateTemplate), -//// Items = new[] -//// { -//// "Foo", -//// "Bar" -//// } -//// }; - -//// target.ApplyTemplate(); - -//// Assert.Equal(0, target.SelectedIndex); -//// Assert.Equal("Foo", target.SelectedItem); -//// } - -//// [Fact] -//// public void LogicalChild_Should_Be_Selected_Item() -//// { -//// var target = new Carousel -//// { -//// Template = new FuncControlTemplate(CreateTemplate), -//// Items = new[] -//// { -//// "Foo", -//// "Bar" -//// } -//// }; - -//// target.ApplyTemplate(); -//// ((Control)target.Presenter).ApplyTemplate(); - -//// Assert.Single(target.GetLogicalChildren()); - -//// var child = GetContainerTextBlock(target.GetLogicalChildren().Single()); - -//// Assert.Equal("Foo", child.Text); -//// } - -//// [Fact] -//// public void Should_Remove_NonCurrent_Page_When_IsVirtualized_True() -//// { -//// var target = new Carousel -//// { -//// Template = new FuncControlTemplate(CreateTemplate), -//// Items = new[] { "foo", "bar" }, -//// IsVirtualized = true, -//// SelectedIndex = 0, -//// }; - -//// target.ApplyTemplate(); -//// ((Control)target.Presenter).ApplyTemplate(); - -//// Assert.Single(target.ItemContainerGenerator.Containers); -//// target.SelectedIndex = 1; -//// Assert.Single(target.ItemContainerGenerator.Containers); -//// } - -//// [Fact] -//// public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes() -//// { -//// var items = new ObservableCollection -//// { -//// "Foo", -//// "Bar", -//// "FooBar" -//// }; - -//// var target = new Carousel -//// { -//// Template = new FuncControlTemplate(CreateTemplate), -//// Items = items, -//// IsVirtualized = false -//// }; - -//// target.ApplyTemplate(); -//// ((Control)target.Presenter).ApplyTemplate(); - -//// Assert.Equal(3, target.GetLogicalChildren().Count()); - -//// var child = GetContainerTextBlock(target.GetLogicalChildren().First()); - -//// Assert.Equal("Foo", child.Text); - -//// var newItems = items.ToList(); -//// newItems.RemoveAt(0); - -//// target.Items = newItems; - -//// child = GetContainerTextBlock(target.GetLogicalChildren().First()); - -//// Assert.Equal("Bar", child.Text); -//// } - -//// [Fact] -//// public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes_And_Virtualized() -//// { -//// var items = new ObservableCollection -//// { -//// "Foo", -//// "Bar", -//// "FooBar" -//// }; - -//// var target = new Carousel -//// { -//// Template = new FuncControlTemplate(CreateTemplate), -//// Items = items, -//// IsVirtualized = true, -//// }; - -//// target.ApplyTemplate(); -//// ((Control)target.Presenter).ApplyTemplate(); - -//// Assert.Single(target.GetLogicalChildren()); - -//// var child = GetContainerTextBlock(target.GetLogicalChildren().Single()); - -//// Assert.Equal("Foo", child.Text); - -//// var newItems = items.ToList(); -//// newItems.RemoveAt(0); - -//// target.Items = newItems; - -//// child = GetContainerTextBlock(target.GetLogicalChildren().Single()); - -//// Assert.Equal("Bar", child.Text); -//// } - -//// [Fact] -//// public void Selected_Item_Changes_To_First_Item_When_Item_Added() -//// { -//// var items = new ObservableCollection(); -//// var target = new Carousel -//// { -//// Template = new FuncControlTemplate(CreateTemplate), -//// Items = items, -//// IsVirtualized = false -//// }; - -//// target.ApplyTemplate(); -//// ((Control)target.Presenter).ApplyTemplate(); +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Subjects; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Layout; +using Avalonia.LogicalTree; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class CarouselTests + { + [Fact] + public void First_Item_Should_Be_Selected_By_Default() + { + using var app = Start(); + var target = new Carousel + { + Template = CarouselTemplate(), + Items = new[] + { + "Foo", + "Bar" + } + }; + + Prepare(target); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("Foo", target.SelectedItem); + } + + [Fact] + public void LogicalChild_Should_Be_Selected_Item() + { + using var app = Start(); + var target = new Carousel + { + Template = CarouselTemplate(), + Items = new[] + { + "Foo", + "Bar" + } + }; + + Prepare(target); + + Assert.Single(target.GetRealizedContainers()); + + var child = GetContainerTextBlock(target.GetRealizedContainers().Single()); + + Assert.Equal("Foo", child.Text); + } + + [Fact] + public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes() + { + using var app = Start(); + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; + + var target = new Carousel + { + Template = CarouselTemplate(), + Items = items, + }; + + Prepare(target); + + Assert.Single(target.GetRealizedContainers()); + + var child = GetContainerTextBlock(target.GetRealizedContainers().Single()); + + Assert.Equal("Foo", child.Text); + + var newItems = items.ToList(); + newItems.RemoveAt(0); + Layout(target); + + target.Items = newItems; + Layout(target); + + child = GetContainerTextBlock(target.GetRealizedContainers().Single()); + + Assert.Equal("Bar", child.Text); + } + + [Fact] + public void Selected_Item_Changes_To_First_Item_When_Item_Added() + { + using var app = Start(); + var items = new ObservableCollection(); + var target = new Carousel + { + Template = CarouselTemplate(), + Items = items, + }; + + Prepare(target); + + Assert.Equal(-1, target.SelectedIndex); + Assert.Empty(target.GetRealizedContainers()); + + items.Add("Foo"); + Layout(target); + + Assert.Equal(0, target.SelectedIndex); + Assert.Single(target.GetRealizedContainers()); + } + + [Fact] + public void Selected_Index_Changes_To_None_When_Items_Assigned_Null() + { + using var app = Start(); + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; + + var target = new Carousel + { + Template = CarouselTemplate(), + Items = items, + }; + + Prepare(target); + + Assert.Equal(1, target.GetRealizedContainers().Count()); + + var child = GetContainerTextBlock(target.GetRealizedContainers().First()); + + Assert.Equal("Foo", child.Text); + + target.Items = null; + Layout(target); + + var numChildren = target.GetRealizedContainers().Count(); -//// Assert.Equal(-1, target.SelectedIndex); -//// Assert.Empty(target.GetLogicalChildren()); + Assert.Equal(-1, target.SelectedIndex); + } -//// items.Add("Foo"); + [Fact] + public void Selected_Index_Is_Maintained_Carousel_Created_With_Non_Zero_SelectedIndex() + { + using var app = Start(); + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; + + var target = new Carousel + { + Template = CarouselTemplate(), + Items = items, + SelectedIndex = 2 + }; -//// Assert.Equal(0, target.SelectedIndex); -//// Assert.Single(target.GetLogicalChildren()); -//// } + Prepare(target); + + Assert.Equal("FooBar", target.SelectedItem); -//// [Fact] -//// public void Selected_Index_Changes_To_None_When_Items_Assigned_Null() -//// { -//// var items = new ObservableCollection -//// { -//// "Foo", -//// "Bar", -//// "FooBar" -//// }; + var child = GetContainerTextBlock(target.GetRealizedContainers().LastOrDefault()); -//// var target = new Carousel -//// { -//// Template = new FuncControlTemplate(CreateTemplate), -//// Items = items, -//// IsVirtualized = false -//// }; - -//// target.ApplyTemplate(); -//// ((Control)target.Presenter).ApplyTemplate(); - -//// Assert.Equal(3, target.GetLogicalChildren().Count()); - -//// var child = GetContainerTextBlock(target.GetLogicalChildren().First()); - -//// Assert.Equal("Foo", child.Text); - -//// target.Items = null; - -//// var numChildren = target.GetLogicalChildren().Count(); - -//// Assert.Equal(0, numChildren); -//// Assert.Equal(-1, target.SelectedIndex); -//// } - -//// [Fact] -//// public void Selected_Index_Is_Maintained_Carousel_Created_With_Non_Zero_SelectedIndex() -//// { -//// var items = new ObservableCollection -//// { -//// "Foo", -//// "Bar", -//// "FooBar" -//// }; - -//// var target = new Carousel -//// { -//// Template = new FuncControlTemplate(CreateTemplate), -//// Items = items, -//// IsVirtualized = false, -//// SelectedIndex = 2 -//// }; - -//// target.ApplyTemplate(); -//// ((Control)target.Presenter).ApplyTemplate(); - -//// Assert.Equal("FooBar", target.SelectedItem); - -//// var child = GetContainerTextBlock(target.GetVisualDescendants().LastOrDefault()); - -//// Assert.IsType(child); -//// Assert.Equal("FooBar", ((TextBlock)child).Text); -//// } - -//// [Fact] -//// public void Selected_Item_Changes_To_Next_First_Item_When_Item_Removed_From_Beggining_Of_List() -//// { -//// var items = new ObservableCollection -//// { -//// "Foo", -//// "Bar", -//// "FooBar" -//// }; - -//// var target = new Carousel -//// { -//// Template = new FuncControlTemplate(CreateTemplate), -//// Items = items, -//// IsVirtualized = false -//// }; - -//// target.ApplyTemplate(); -//// ((Control)target.Presenter).ApplyTemplate(); - -//// Assert.Equal(3, target.GetLogicalChildren().Count()); - -//// var child = GetContainerTextBlock(target.GetLogicalChildren().First()); - -//// Assert.Equal("Foo", child.Text); - -//// items.RemoveAt(0); - -//// child = GetContainerTextBlock(target.GetLogicalChildren().First()); - -//// Assert.IsType(child); -//// Assert.Equal("Bar", ((TextBlock)child).Text); -//// } - -//// [Fact] -//// public void Selected_Item_Changes_To_First_Item_If_SelectedItem_Is_Removed_From_Middle() -//// { -//// var items = new ObservableCollection -//// { -//// "Foo", -//// "Bar", -//// "FooBar" -//// }; - -//// var target = new Carousel -//// { -//// Template = new FuncControlTemplate(CreateTemplate), -//// Items = items, -//// IsVirtualized = false -//// }; - -//// target.ApplyTemplate(); -//// ((Control)target.Presenter).ApplyTemplate(); - -//// target.SelectedIndex = 1; - -//// items.RemoveAt(1); - -//// Assert.Equal(0, target.SelectedIndex); -//// Assert.Equal("Foo", target.SelectedItem); -//// } - -//// private Control CreateTemplate(Carousel control, INameScope scope) -//// { -//// return new CarouselPresenter -//// { -//// Name = "PART_ItemsPresenter", -//// [~CarouselPresenter.IsVirtualizedProperty] = control[~Carousel.IsVirtualizedProperty], -//// [~CarouselPresenter.ItemsPanelProperty] = control[~Carousel.ItemsPanelProperty], -//// [~CarouselPresenter.SelectedIndexProperty] = control[~Carousel.SelectedIndexProperty], -//// [~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty], -//// }.RegisterInNameScope(scope); -//// } - -//// private static TextBlock GetContainerTextBlock(object control) -//// { -//// var contentPresenter = Assert.IsType(control); -//// contentPresenter.UpdateChild(); -//// return Assert.IsType(contentPresenter.Child); -//// } - -//// [Fact] -//// public void SelectedItem_Validation() -//// { -//// using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) -//// { -//// var target = new Carousel -//// { -//// Template = new FuncControlTemplate(CreateTemplate), -//// IsVirtualized = false -//// }; - -//// target.ApplyTemplate(); -//// ((Control)target.Presenter).ApplyTemplate(); - -//// var exception = new System.InvalidCastException("failed validation"); -//// var textObservable = -//// new BehaviorSubject(new BindingNotification(exception, -//// BindingErrorType.DataValidationError)); -//// target.Bind(ComboBox.SelectedItemProperty, textObservable); - -//// Assert.True(DataValidationErrors.GetHasErrors(target)); -//// Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); -//// } -//// } -//// } -////} + Assert.Equal("FooBar", child.Text); + } + + [Fact] + public void Selected_Item_Changes_To_Next_First_Item_When_Item_Removed_From_Beggining_Of_List() + { + using var app = Start(); + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; + + var target = new Carousel + { + Template = CarouselTemplate(), + Items = items, + }; + + Prepare(target); + + var child = GetContainerTextBlock(target.GetRealizedContainers().First()); + + Assert.Equal("Foo", child.Text); + + items.RemoveAt(0); + Layout(target); + + child = GetContainerTextBlock(target.GetRealizedContainers().First()); + + Assert.IsType(child); + Assert.Equal("Bar", child.Text); + } + + [Fact] + public void Selected_Item_Changes_To_First_Item_If_SelectedItem_Is_Removed_From_Middle() + { + using var app = Start(); + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; + + var target = new Carousel + { + Template = CarouselTemplate(), + Items = items, + }; + + Prepare(target); + + target.SelectedIndex = 1; + + items.RemoveAt(1); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("Foo", target.SelectedItem); + } + + [Fact] + public void SelectedItem_Validation() + { + using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) + { + var target = new Carousel + { + Template = CarouselTemplate(), + }; + + Prepare(target); + + var exception = new System.InvalidCastException("failed validation"); + var textObservable = + new BehaviorSubject(new BindingNotification(exception, + BindingErrorType.DataValidationError)); + target.Bind(ComboBox.SelectedItemProperty, textObservable); + + Assert.True(DataValidationErrors.GetHasErrors(target)); + Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); + } + } + + private static IDisposable Start() => UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + private static void Prepare(Carousel target) + { + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + } + + private static void Layout(Carousel target) + { + ((ILayoutRoot)target.GetVisualRoot()).LayoutManager.ExecuteLayoutPass(); + } + + 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((parent, scope) => + new Panel + { + Children = + { + new ScrollContentPresenter + { + Name = "PART_ContentPresenter", + [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(), + [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty], + [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty], + [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty], + [~ScrollContentPresenter.CanHorizontallyScrollProperty] = parent[~ScrollViewer.CanHorizontallyScrollProperty], + [~ScrollContentPresenter.CanVerticallyScrollProperty] = parent[~ScrollViewer.CanVerticallyScrollProperty], + }.RegisterInNameScope(scope), + } + }); + } + + private static TextBlock GetContainerTextBlock(object control) + { + var contentPresenter = Assert.IsType(control); + return Assert.IsType(contentPresenter.Child); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs deleted file mode 100644 index f5094accb5..0000000000 --- a/tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs +++ /dev/null @@ -1,732 +0,0 @@ -////using System.Linq; -////using Moq; -////using Avalonia.Controls.Generators; -////using Avalonia.Controls.Presenters; -////using Avalonia.Controls.Templates; -////using Xunit; -////using System.Collections.ObjectModel; -////using System.Collections; - -////namespace Avalonia.Controls.UnitTests.Presenters -////{ -//// public class CarouselPresenterTests -//// { -//// [Fact] -//// public void Should_Register_With_Host_When_TemplatedParent_Set() -//// { -//// var host = new Carousel(); -//// var target = new CarouselPresenter(); - -//// Assert.Null(host.Presenter); - -//// target.SetValue(Control.TemplatedParentProperty, host); - -//// Assert.Same(target, host.Presenter); -//// } - -//// [Fact] -//// public void ApplyTemplate_Should_Create_Panel() -//// { -//// var target = new CarouselPresenter -//// { -//// ItemsPanel = new FuncTemplate(() => new Panel()), -//// }; - -//// target.ApplyTemplate(); - -//// Assert.IsType(target.Panel); -//// } - -//// [Fact] -//// public void ItemContainerGenerator_Should_Be_Picked_Up_From_TemplatedControl() -//// { -//// var parent = new TestItemsControl(); -//// var target = new CarouselPresenter -//// { -//// [StyledElement.TemplatedParentProperty] = parent, -//// }; - -//// Assert.IsType>(target.ItemContainerGenerator); -//// } - -//// public class Virtualized -//// { -//// [Fact] -//// public void Should_Initially_Materialize_Selected_Container() -//// { -//// var target = new CarouselPresenter -//// { -//// Items = new[] { "foo", "bar" }, -//// SelectedIndex = 0, -//// IsVirtualized = true, -//// }; - -//// target.ApplyTemplate(); - -//// AssertSingle(target); -//// } - -//// [Fact] -//// public void Should_Initially_Materialize_Nothing_If_No_Selected_Container() -//// { -//// var target = new CarouselPresenter -//// { -//// Items = new[] { "foo", "bar" }, -//// IsVirtualized = true, -//// }; - -//// target.ApplyTemplate(); - -//// Assert.Empty(target.Panel.Children); -//// Assert.Empty(target.ItemContainerGenerator.Containers); -//// } - -//// [Fact] -//// public void Switching_To_Virtualized_Should_Reset_Containers() -//// { -//// var target = new CarouselPresenter -//// { -//// Items = new[] { "foo", "bar" }, -//// SelectedIndex = 0, -//// IsVirtualized = false, -//// }; - -//// target.ApplyTemplate(); -//// target.IsVirtualized = true; - -//// AssertSingle(target); -//// } - -//// [Fact] -//// public void Changing_SelectedIndex_Should_Show_Page() -//// { -//// var target = new CarouselPresenter -//// { -//// Items = new[] { "foo", "bar" }, -//// SelectedIndex = 0, -//// IsVirtualized = true, -//// }; - -//// target.ApplyTemplate(); -//// AssertSingle(target); - -//// target.SelectedIndex = 1; -//// AssertSingle(target); -//// } - -//// [Fact] -//// public void Should_Remove_NonCurrent_Page() -//// { -//// var target = new CarouselPresenter -//// { -//// Items = new[] { "foo", "bar" }, -//// IsVirtualized = true, -//// SelectedIndex = 0, -//// }; - -//// target.ApplyTemplate(); -//// AssertSingle(target); - -//// target.SelectedIndex = 1; -//// AssertSingle(target); - -//// } - -//// [Fact] -//// public void Should_Handle_Inserting_Item_At_SelectedItem() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 1, -//// IsVirtualized = true, -//// }; - -//// target.ApplyTemplate(); - -//// items.Insert(1, "item1a"); -//// AssertSingle(target); -//// } - -//// [Fact] -//// public void Should_Handle_Inserting_Item_Before_SelectedItem() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 2, -//// IsVirtualized = true, -//// }; - -//// target.ApplyTemplate(); - -//// items.Insert(1, "item1a"); -//// AssertSingle(target); -//// } - -//// [Fact] -//// public void Should_Do_Nothing_When_Inserting_Item_After_SelectedItem() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 1, -//// IsVirtualized = true, -//// }; - -//// target.ApplyTemplate(); -//// var child = AssertSingle(target); -//// items.Insert(2, "after"); -//// Assert.Same(child, AssertSingle(target)); -//// } - -//// [Fact] -//// public void Should_Handle_Removing_Item_At_SelectedItem() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 1, -//// IsVirtualized = true, -//// }; - -//// target.ApplyTemplate(); - -//// items.RemoveAt(1); -//// AssertSingle(target); -//// } - -//// [Fact] -//// public void Should_Handle_Removing_Item_Before_SelectedItem() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 1, -//// IsVirtualized = true, -//// }; - -//// target.ApplyTemplate(); - -//// items.RemoveAt(0); -//// AssertSingle(target); -//// } - -//// [Fact] -//// public void Should_Do_Nothing_When_Removing_Item_After_SelectedItem() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 1, -//// IsVirtualized = true, -//// }; - -//// target.ApplyTemplate(); -//// var child = AssertSingle(target); -//// items.RemoveAt(2); -//// Assert.Same(child, AssertSingle(target)); -//// } - -//// [Fact] -//// public void Should_Handle_Removing_SelectedItem_When_Its_Last() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 2, -//// IsVirtualized = true, -//// }; - -//// target.ApplyTemplate(); - -//// items.RemoveAt(2); -//// Assert.Equal(1, target.SelectedIndex); -//// AssertSingle(target); -//// } - -//// [Fact] -//// public void Should_Handle_Removing_Last_Item() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 0, -//// IsVirtualized = true, -//// }; - -//// target.ApplyTemplate(); - -//// items.RemoveAt(0); -//// Assert.Empty(target.Panel.Children); -//// Assert.Empty(target.ItemContainerGenerator.Containers); -//// } - -//// [Fact] -//// public void Should_Handle_Replacing_SelectedItem() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 1, -//// IsVirtualized = true, -//// }; - -//// target.ApplyTemplate(); - -//// items[1] = "replaced"; -//// AssertSingle(target); -//// } - -//// [Fact] -//// public void Should_Do_Nothing_When_Replacing_Non_SelectedItem() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 1, -//// IsVirtualized = true, -//// }; - -//// target.ApplyTemplate(); -//// var child = AssertSingle(target); -//// items[0] = "replaced"; -//// Assert.Same(child, AssertSingle(target)); -//// } - -//// [Fact] -//// public void Should_Handle_Moving_SelectedItem() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 1, -//// IsVirtualized = true, -//// }; - -//// target.ApplyTemplate(); - -//// items.Move(1, 0); -//// AssertSingle(target); -//// } - -//// private static Control AssertSingle(CarouselPresenter target) -//// { -//// var items = (IList)target.Items; -//// var index = target.SelectedIndex; -//// var content = items[index]; -//// var child = Assert.Single(target.Panel.Children); -//// var presenter = Assert.IsType(child); -//// var container = Assert.Single(target.ItemContainerGenerator.Containers); -//// var visible = Assert.Single(target.Panel.Children.Where(x => x.IsVisible)); - -//// Assert.Same(child, container.ContainerControl); -//// Assert.Same(child, visible); -//// Assert.Equal(content, presenter.Content); -//// Assert.Equal(content, container.Item); -//// Assert.Equal(index, container.Index); - -//// return child; -//// } -//// } - -//// public class NonVirtualized -//// { -//// [Fact] -//// public void Should_Initially_Materialize_All_Containers() -//// { -//// var target = new CarouselPresenter -//// { -//// Items = new[] { "foo", "bar" }, -//// IsVirtualized = false, -//// }; - -//// target.ApplyTemplate(); -//// AssertAll(target); -//// } - -//// [Fact] -//// public void Should_Initially_Show_Selected_Item() -//// { -//// var target = new CarouselPresenter -//// { -//// Items = new[] { "foo", "bar" }, -//// SelectedIndex = 1, -//// IsVirtualized = false, -//// }; - -//// target.ApplyTemplate(); -//// AssertAll(target); -//// } - -//// [Fact] -//// public void Switching_To_Non_Virtualized_Should_Reset_Containers() -//// { -//// var target = new CarouselPresenter -//// { -//// Items = new[] { "foo", "bar" }, -//// SelectedIndex = 0, -//// IsVirtualized = true, -//// }; - -//// target.ApplyTemplate(); -//// target.IsVirtualized = false; - -//// AssertAll(target); -//// } - -//// [Fact] -//// public void Changing_SelectedIndex_Should_Show_Page() -//// { -//// var target = new CarouselPresenter -//// { -//// Items = new[] { "foo", "bar" }, -//// SelectedIndex = 0, -//// IsVirtualized = false, -//// }; - -//// target.ApplyTemplate(); -//// AssertAll(target); - -//// target.SelectedIndex = 1; -//// AssertAll(target); -//// } - -//// [Fact] -//// public void Should_Handle_Inserting_Item_At_SelectedItem() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 1, -//// IsVirtualized = false, -//// }; - -//// target.ApplyTemplate(); - -//// items.Insert(1, "item1a"); -//// AssertAll(target); -//// } - -//// [Fact] -//// public void Should_Handle_Inserting_Item_Before_SelectedItem() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 2, -//// IsVirtualized = false, -//// }; - -//// target.ApplyTemplate(); - -//// items.Insert(1, "item1a"); -//// AssertAll(target); -//// } - -//// [Fact] -//// public void Should_Do_Handle_Inserting_Item_After_SelectedItem() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 1, -//// IsVirtualized = false, -//// }; - -//// target.ApplyTemplate(); -//// items.Insert(2, "after"); -//// AssertAll(target); -//// } - -//// [Fact] -//// public void Should_Handle_Removing_Item_At_SelectedItem() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 1, -//// IsVirtualized = false, -//// }; - -//// target.ApplyTemplate(); - -//// items.RemoveAt(1); -//// AssertAll(target); -//// } - -//// [Fact] -//// public void Should_Handle_Removing_Item_Before_SelectedItem() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 1, -//// IsVirtualized = false, -//// }; - -//// target.ApplyTemplate(); - -//// items.RemoveAt(0); -//// AssertAll(target); -//// } - -//// [Fact] -//// public void Should_Handle_Removing_Item_After_SelectedItem() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 1, -//// IsVirtualized = false, -//// }; - -//// target.ApplyTemplate(); -//// items.RemoveAt(2); -//// AssertAll(target); -//// } - -//// [Fact] -//// public void Should_Handle_Removing_SelectedItem_When_Its_Last() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 2, -//// IsVirtualized = false, -//// }; - -//// target.ApplyTemplate(); - -//// items.RemoveAt(2); -//// Assert.Equal(1, target.SelectedIndex); -//// AssertAll(target); -//// } - -//// [Fact] -//// public void Should_Handle_Removing_Last_Item() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 0, -//// IsVirtualized = false, -//// }; - -//// target.ApplyTemplate(); - -//// items.RemoveAt(0); -//// Assert.Empty(target.Panel.Children); -//// Assert.Empty(target.ItemContainerGenerator.Containers); -//// } - -//// [Fact] -//// public void Should_Handle_Replacing_SelectedItem() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 1, -//// IsVirtualized = false, -//// }; - -//// target.ApplyTemplate(); - -//// items[1] = "replaced"; -//// AssertAll(target); -//// } - -//// [Fact] -//// public void Should_Handle_Moving_SelectedItem() -//// { -//// var items = new ObservableCollection -//// { -//// "item0", -//// "item1", -//// "item2", -//// }; - -//// var target = new CarouselPresenter -//// { -//// Items = items, -//// SelectedIndex = 1, -//// IsVirtualized = false, -//// }; - -//// target.ApplyTemplate(); - -//// items.Move(1, 0); -//// AssertAll(target); -//// } - -//// private static void AssertAll(CarouselPresenter target) -//// { -//// var items = (IList)target.Items; - -//// Assert.Equal(items?.Count ?? 0, target.Panel.Children.Count); -//// Assert.Equal(items?.Count ?? 0, target.ItemContainerGenerator.Containers.Count()); - -//// for (var i = 0; i < items?.Count; ++i) -//// { -//// var content = items[i]; -//// var child = target.Panel.Children[i]; -//// var presenter = Assert.IsType(child); -//// var container = target.ItemContainerGenerator.ContainerFromIndex(i); - -//// Assert.Same(child, container); -//// Assert.Equal(i == target.SelectedIndex, child.IsVisible); -//// Assert.Equal(content, presenter.Content); -//// Assert.Equal(i, target.ItemContainerGenerator.IndexFromContainer(container)); -//// } -//// } -//// } - -//// private class TestItem : ContentControl -//// { -//// } - -//// private class TestItemsControl : ItemsControl -//// { -//// protected override IItemContainerGenerator CreateItemContainerGenerator() -//// { -//// return new ItemContainerGenerator(this, TestItem.ContentProperty, null); -//// } -//// } -//// } -////} diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs new file mode 100644 index 0000000000..ea6b9367cf --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections; +using System.Collections.ObjectModel; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Animation; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Layout; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Moq; +using Xunit; + +#nullable enable + +namespace Avalonia.Controls.UnitTests +{ + public class VirtualizingCarouselPanelTests + { + [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(target.Children[0]); + Assert.Equal("foo", container.Content); + } + + [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(target.Children[0]); + Assert.Equal("bar", container.Content); + } + + [Fact] + public void Handles_Inserted_Item() + { + using var app = Start(); + var items = new ObservableCollection { "foo", "bar" }; + var (target, carousel) = CreateTarget(items); + var container = Assert.IsType(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 { "foo", "bar" }; + var (target, carousel) = CreateTarget(items); + var container = Assert.IsType(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 { "foo", "bar" }; + var (target, carousel) = CreateTarget(items); + var container = Assert.IsType(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 { "foo", "bar" }; + var (target, carousel) = CreateTarget(items); + var container = Assert.IsType(target.Children[0]); + + items.Move(0, 1); + Layout(target); + + Assert.Single(target.Children); + Assert.Same(container, target.Children[0]); + Assert.Equal("bar", container.Content); + } + + public class Transitions + { + [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(); + var (target, _) = CreateTarget(items, transition.Object); + + transition.Verify(x => x.Start( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + 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(); + var (target, carousel) = CreateTarget(items, transition.Object); + + carousel.SelectedIndex = 1; + Layout(target); + + transition.Verify(x => x.Start( + items[0], + items[1], + true, + It.IsAny()), + 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(); + var (target, carousel) = CreateTarget(items, transition.Object); + var transitionTask = new TaskCompletionSource(); + + transition.Setup(x => x.Start( + items[0], + items[1], + true, + It.IsAny())) + .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(); + 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())) + .Callback((_, _, _, 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); + } + } + + private static IDisposable Start() => UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + private static (VirtualizingCarouselPanel, Carousel) CreateTarget( + IEnumerable items, + IPageTransition? transition = null) + { + var carousel = new Carousel + { + Items = items, + Template = CarouselTemplate(), + PageTransition = transition, + }; + + var root = new TestRoot(carousel); + 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((parent, scope) => + new Panel + { + Children = + { + new ScrollContentPresenter + { + Name = "PART_ContentPresenter", + [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(), + [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty], + [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty], + [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty], + [~ScrollContentPresenter.CanHorizontallyScrollProperty] = parent[~ScrollViewer.CanHorizontallyScrollProperty], + [~ScrollContentPresenter.CanVerticallyScrollProperty] = parent[~ScrollViewer.CanVerticallyScrollProperty], + }.RegisterInNameScope(scope), + } + }); + } + + private static void Layout(Control c) => ((ILayoutRoot)c.GetVisualRoot()!).LayoutManager.ExecuteLayoutPass(); + } +} diff --git a/tests/Avalonia.Base.UnitTests/Data/UnitTestSynchronizationContext.cs b/tests/Avalonia.UnitTests/UnitTestSynchronizationContext.cs similarity index 91% rename from tests/Avalonia.Base.UnitTests/Data/UnitTestSynchronizationContext.cs rename to tests/Avalonia.UnitTests/UnitTestSynchronizationContext.cs index 71eed38a4c..23cea7ad4f 100644 --- a/tests/Avalonia.Base.UnitTests/Data/UnitTestSynchronizationContext.cs +++ b/tests/Avalonia.UnitTests/UnitTestSynchronizationContext.cs @@ -1,11 +1,10 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; using System.Threading; -namespace Avalonia.Base.UnitTests.Data +namespace Avalonia.UnitTests { - internal sealed class UnitTestSynchronizationContext : SynchronizationContext + public sealed class UnitTestSynchronizationContext : SynchronizationContext { readonly List> _postedCallbacks = new List>();