Browse Source

Reimplemented Carousel.

Only `VirtualizingCarouselPanel` currently implemented.
Steven Kirk 2 years ago
parent
commit
f0c89a614e

+ 2 - 2
samples/ControlCatalog/Pages/CarouselPage.xaml

@@ -12,7 +12,7 @@
       </Button>
       <Carousel Name="carousel" Grid.Column="1">
         <Carousel.PageTransition>
-          <PageSlide Duration="0.25" Orientation="Vertical" />
+          <PageSlide Duration="0.25" Orientation="Horizontal" />
         </Carousel.PageTransition>
         <Image Source="/Assets/delicate-arch-896885_640.jpg"/>
         <Image Source="/Assets/hirsch-899118_640.jpg"/>
@@ -35,7 +35,7 @@
 
     <StackPanel Orientation="Horizontal" Spacing="4">
       <TextBlock VerticalAlignment="Center">Orientation</TextBlock>
-      <ComboBox Name="orientation" SelectedIndex="1" VerticalAlignment="Center">
+      <ComboBox Name="orientation" SelectedIndex="0" VerticalAlignment="Center">
         <ComboBoxItem>Horizontal</ComboBoxItem>
         <ComboBoxItem>Vertical</ComboBoxItem>
       </ComboBox>

+ 30 - 20
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
     /// </summary>
     public class Carousel : SelectingItemsControl
     {
-        /// <summary>
-        /// Defines the <see cref="IsVirtualized"/> property.
-        /// </summary>
-        public static readonly StyledProperty<bool> IsVirtualizedProperty =
-            AvaloniaProperty.Register<Carousel, bool>(nameof(IsVirtualized), true);
-
         /// <summary>
         /// Defines the <see cref="PageTransition"/> property.
         /// </summary>
@@ -28,7 +21,9 @@ namespace Avalonia.Controls
         /// <see cref="Carousel"/>.
         /// </summary>
         private static readonly ITemplate<Panel> PanelTemplate =
-            new FuncTemplate<Panel>(() => new Panel());
+            new FuncTemplate<Panel>(() => new VirtualizingCarouselPanel());
+
+        private IScrollable? _scroller;
 
         /// <summary>
         /// Initializes static members of the <see cref="Carousel"/> class.
@@ -38,18 +33,6 @@ namespace Avalonia.Controls
             SelectionModeProperty.OverrideDefaultValue<Carousel>(SelectionMode.AlwaysSelected);
             ItemsPanelProperty.OverrideDefaultValue<Carousel>(PanelTemplate);
         }
-        
-        /// <summary>
-        /// Gets or sets a value indicating whether the items in the carousel are virtualized.
-        /// </summary>
-        /// <remarks>
-        /// When the carousel is virtualized, only the active page is held in memory.
-        /// </remarks>
-        public bool IsVirtualized
-        {
-            get { return GetValue(IsVirtualizedProperty); }
-            set { SetValue(IsVirtualizedProperty, value); }
-        }
 
         /// <summary>
         /// 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<IScrollable>("PART_ScrollViewer");
+        }
+
+        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+        {
+            base.OnPropertyChanged(change);
+
+            if (change.Property == SelectedIndexProperty && _scroller is not null)
+            {
+                var value = change.GetNewValue<int>();
+                _scroller.Offset = new(value, 0);
+            }
+        }
     }
 }

+ 0 - 276
src/Avalonia.Controls/Presenters/CarouselPresenter.cs

@@ -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
-{
-    /// <summary>
-    /// Displays pages inside an <see cref="ItemsControl"/>.
-    /// </summary>
-    public class CarouselPresenter : ItemsPresenter
-    {
-        /// <summary>
-        /// Defines the <see cref="IsVirtualized"/> property.
-        /// </summary>
-        public static readonly StyledProperty<bool> IsVirtualizedProperty =
-            Carousel.IsVirtualizedProperty.AddOwner<CarouselPresenter>();
-
-        /// <summary>
-        /// Defines the <see cref="SelectedIndex"/> property.
-        /// </summary>
-        public static readonly DirectProperty<CarouselPresenter, int> SelectedIndexProperty =
-            SelectingItemsControl.SelectedIndexProperty.AddOwner<CarouselPresenter>(
-                o => o.SelectedIndex,
-                (o, v) => o.SelectedIndex = v);
-
-        /// <summary>
-        /// Defines the <see cref="PageTransition"/> property.
-        /// </summary>
-        public static readonly StyledProperty<IPageTransition?> PageTransitionProperty =
-            Carousel.PageTransitionProperty.AddOwner<CarouselPresenter>();
-
-        private int _selectedIndex = -1;
-        private Task? _currentTransition;
-        private int _queuedTransitionIndex = -1;
-
-        /// <summary>
-        /// Initializes static members of the <see cref="CarouselPresenter"/> class.
-        /// </summary>
-        static CarouselPresenter()
-        {
-            ////IsVirtualizedProperty.Changed.AddClassHandler<CarouselPresenter>((x, e) => x.IsVirtualizedChanged(e));
-            ////SelectedIndexProperty.Changed.AddClassHandler<CarouselPresenter>((x, e) => x.SelectedIndexChanged(e));
-        }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether the items in the carousel are virtualized.
-        /// </summary>
-        /// <remarks>
-        /// When the carousel is virtualized, only the active page is held in memory.
-        /// </remarks>
-        public bool IsVirtualized
-        {
-            get { return GetValue(IsVirtualizedProperty); }
-            set { SetValue(IsVirtualizedProperty, value); }
-        }
-
-        /// <summary>
-        /// Gets or sets the index of the selected page.
-        /// </summary>
-        public int SelectedIndex
-        {
-            get
-            {
-                return _selectedIndex;
-            }
-
-            set
-            {
-                ////var old = SelectedIndex;
-                ////var effective = (value >= 0 && value < Items?.Cast<object>().Count()) ? value : -1;
-
-                ////if (old != effective)
-                ////{
-                ////    _selectedIndex = effective;
-                ////    RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue);
-                ////}
-            }
-        }
-
-        /// <summary>
-        /// Gets or sets a transition to use when switching pages.
-        /// </summary>
-        public IPageTransition? PageTransition
-        {
-            get { return GetValue(PageTransitionProperty); }
-            set { SetValue(PageTransitionProperty, value); }
-        }
-
-        /// <inheritdoc/>
-        ////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));
-        ////}
-
-        /////// <summary>
-        /////// Moves to the selected page, animating if a <see cref="PageTransition"/> is set.
-        /////// </summary>
-        /////// <param name="fromIndex">The index of the old page.</param>
-        /////// <param name="toIndex">The index of the new page.</param>
-        /////// <returns>A task tracking the animation.</returns>
-        ////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<object>().ElementAt(index);
-        ////        var materialized = ItemContainerGenerator.Materialize(index, item);
-        ////        Panel!.Children.Add(materialized.ContainerControl);
-        ////        container = materialized.ContainerControl;
-        ////    }
-
-        ////    return container;
-        ////}
-
-        /////// <summary>
-        /////// Called when the <see cref="IsVirtualized"/> property changes.
-        /////// </summary>
-        /////// <param name="e">The event args.</param>
-        ////private void IsVirtualizedChanged(AvaloniaPropertyChangedEventArgs e)
-        ////{
-        ////    if (Panel != null)
-        ////    {
-        ////        ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
-        ////    }
-        ////}
-
-        /////// <summary>
-        /////// Called when the <see cref="SelectedIndex"/> property changes.
-        /////// </summary>
-        /////// <param name="e">The event args.</param>
-        ////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!;
-        ////        }
-        ////    }
-        ////}
-    }
-}

+ 351 - 0
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
+{
+    /// <summary>
+    /// A panel used by <see cref="Carousel"/> to display the current item.
+    /// </summary>
+    public class VirtualizingCarouselPanel : VirtualizingPanel, ILogicalScrollable
+    {
+        private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
+            AvaloniaProperty.RegisterAttached<VirtualizingCarouselPanel, Control, bool>("ItemIsOwnContainer");
+
+        private Size _extent;
+        private Vector _offset;
+        private Size _viewport;
+        private Stack<Control>? _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<object?>();
+            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<Control>? 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;
+        }
+    }
+}

+ 10 - 10
src/Avalonia.Themes.Fluent/Controls/Carousel.xaml

@@ -3,16 +3,16 @@
   <ControlTheme x:Key="{x:Type Carousel}" TargetType="Carousel">
     <Setter Property="Template">
       <ControlTemplate>
-        <Border Background="{TemplateBinding Background}"
-                BorderBrush="{TemplateBinding BorderBrush}"
-                BorderThickness="{TemplateBinding BorderThickness}"
-                CornerRadius="{TemplateBinding CornerRadius}">
-          <CarouselPresenter IsVirtualized="{TemplateBinding IsVirtualized}"
-                             ItemsPanel="{TemplateBinding ItemsPanel}"
-                             Margin="{TemplateBinding Padding}"
-                             SelectedIndex="{TemplateBinding SelectedIndex}"
-                             PageTransition="{TemplateBinding PageTransition}"/>
-        </Border>
+        <ScrollViewer Name="PART_ScrollViewer"
+                      Background="{TemplateBinding Background}"
+                      BorderBrush="{TemplateBinding BorderBrush}"
+                      BorderThickness="{TemplateBinding BorderThickness}"
+                      HorizontalScrollBarVisibility="Hidden"
+                      VerticalScrollBarVisibility="Hidden">
+          <ItemsPresenter Name="PART_ItemsPresenter"
+                          ItemsPanel="{TemplateBinding ItemsPanel}"
+                          Margin="{TemplateBinding Padding}"/>
+        </ScrollViewer>
       </ControlTemplate>
     </Setter>
   </ControlTheme>

+ 11 - 12
src/Avalonia.Themes.Simple/Controls/Carousel.xaml

@@ -1,19 +1,18 @@
 <ResourceDictionary xmlns="https://github.com/avaloniaui"
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-  <ControlTheme x:Key="{x:Type Carousel}"
-                TargetType="Carousel">
+  <ControlTheme x:Key="{x:Type Carousel}" TargetType="Carousel">
     <Setter Property="Template">
       <ControlTemplate>
-        <Border Background="{TemplateBinding Background}"
-                BorderBrush="{TemplateBinding BorderBrush}"
-                BorderThickness="{TemplateBinding BorderThickness}"
-                CornerRadius="{TemplateBinding CornerRadius}">
-          <CarouselPresenter Margin="{TemplateBinding Padding}"
-                             IsVirtualized="{TemplateBinding IsVirtualized}"
-                             ItemsPanel="{TemplateBinding ItemsPanel}"
-                             PageTransition="{TemplateBinding PageTransition}"
-                             SelectedIndex="{TemplateBinding SelectedIndex}" />
-        </Border>
+        <ScrollViewer Name="PART_ScrollViewer"
+                      Background="{TemplateBinding Background}"
+                      BorderBrush="{TemplateBinding BorderBrush}"
+                      BorderThickness="{TemplateBinding BorderThickness}"
+                      HorizontalScrollBarVisibility="Hidden"
+                      VerticalScrollBarVisibility="Hidden">
+          <ItemsPresenter Name="PART_ItemsPresenter"
+                          ItemsPanel="{TemplateBinding ItemsPanel}"
+                          Margin="{TemplateBinding Padding}"/>
+        </ScrollViewer>
       </ControlTemplate>
     </Setter>
   </ControlTheme>

+ 316 - 336
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<Carousel>(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<Carousel>(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<Carousel>(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<string>
-////            {
-////                "Foo",
-////                "Bar",
-////                "FooBar"
-////            };
-
-////            var target = new Carousel
-////            {
-////                Template = new FuncControlTemplate<Carousel>(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<string>
-////            {
-////                "Foo",
-////                "Bar",
-////                "FooBar"
-////            };
-
-////            var target = new Carousel
-////            {
-////                Template = new FuncControlTemplate<Carousel>(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<string>();
-////            var target = new Carousel
-////            {
-////                Template = new FuncControlTemplate<Carousel>(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<string>
+            {
+                "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<string>();
+            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<string>
+            {
+                "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<string>
+            {
+                "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<string>
-////            {
-////                "Foo",
-////                "Bar",
-////                "FooBar"
-////            };
+            var child = GetContainerTextBlock(target.GetRealizedContainers().LastOrDefault());
 
-////            var target = new Carousel
-////            {
-////                Template = new FuncControlTemplate<Carousel>(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<string>
-////            {
-////                "Foo",
-////                "Bar",
-////                "FooBar"
-////            };
-
-////            var target = new Carousel
-////            {
-////                Template = new FuncControlTemplate<Carousel>(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<TextBlock>(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<string>
-////            {
-////                "Foo",
-////                "Bar",
-////                "FooBar"
-////            };
-
-////            var target = new Carousel
-////            {
-////                Template = new FuncControlTemplate<Carousel>(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<TextBlock>(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<string>
-////            {
-////                "Foo",
-////                "Bar",
-////                "FooBar"
-////            };
-
-////            var target = new Carousel
-////            {
-////                Template = new FuncControlTemplate<Carousel>(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<ContentPresenter>(control);
-////            contentPresenter.UpdateChild();
-////            return Assert.IsType<TextBlock>(contentPresenter.Child);
-////        }
-
-////        [Fact]
-////        public void SelectedItem_Validation()
-////        {
-////            using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
-////            {
-////                var target = new Carousel
-////                {
-////                    Template = new FuncControlTemplate<Carousel>(CreateTemplate),
-////                    IsVirtualized = false
-////                };
-
-////                target.ApplyTemplate();
-////                ((Control)target.Presenter).ApplyTemplate();
-
-////                var exception = new System.InvalidCastException("failed validation");
-////                var textObservable =
-////                    new BehaviorSubject<BindingNotification>(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<string>
+            {
+                "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<TextBlock>(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<string>
+            {
+                "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<BindingNotification>(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<ScrollViewer>((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<ContentPresenter>(control);
+            return Assert.IsType<TextBlock>(contentPresenter.Child);
+        }
+    }
+}

+ 0 - 732
tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs

@@ -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<Panel>(() => new Panel()),
-////            };
-
-////            target.ApplyTemplate();
-
-////            Assert.IsType<Panel>(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<ItemContainerGenerator<TestItem>>(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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<ContentPresenter>(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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<string>
-////                {
-////                    "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<ContentPresenter>(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<TestItem>(this, TestItem.ContentProperty, null);
-////            }
-////        }
-////    }
-////}

+ 271 - 0
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<ContentPresenter>(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<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);
+        }
+
+        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<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 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);
+            }
+        }
+
+        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<ScrollViewer>((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();
+    }
+}

+ 2 - 3
tests/Avalonia.Base.UnitTests/Data/UnitTestSynchronizationContext.cs → 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<Tuple<SendOrPostCallback, object>> _postedCallbacks =
             new List<Tuple<SendOrPostCallback, object>>();