diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index b6249fe17f..2a0de7a114 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -54,8 +54,10 @@
ScrollViewer.VerticalScrollBarVisibility="Disabled">
-
-
+
+
diff --git a/samples/ControlCatalog/Pages/CarouselDemoPage.xaml b/samples/ControlCatalog/Pages/CarouselDemoPage.xaml
new file mode 100644
index 0000000000..df4317fcad
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselDemoPage.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/CarouselDemoPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselDemoPage.xaml.cs
new file mode 100644
index 0000000000..64753b9fc4
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselDemoPage.xaml.cs
@@ -0,0 +1,53 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+
+namespace ControlCatalog.Pages
+{
+ public partial class CarouselDemoPage : UserControl
+ {
+ private static readonly (string Group, string Title, string Description, Func Factory)[] Demos =
+ {
+ // Overview
+ ("Overview", "Getting Started",
+ "Basic Carousel with image items and previous/next navigation buttons.",
+ () => new CarouselGettingStartedPage()),
+
+ // Features
+ ("Features", "Transitions",
+ "Configure page transitions: PageSlide, CrossFade, 3D Rotation, or None.",
+ () => new CarouselTransitionsPage()),
+ ("Features", "Customization",
+ "Adjust orientation and transition type to tailor the carousel layout.",
+ () => new CarouselCustomizationPage()),
+ ("Features", "Gestures & Keyboard",
+ "Navigate items via swipe gesture and arrow keys. Toggle each input mode on and off.",
+ () => new CarouselGesturesPage()),
+ ("Features", "Vertical Orientation",
+ "Carousel with Orientation set to Vertical, navigated with Up/Down keys, swipe, or buttons.",
+ () => new CarouselVerticalPage()),
+ ("Features", "Multi-Item Peek",
+ "Adjust ViewportFraction to show multiple items simultaneously with adjacent cards peeking.",
+ () => new CarouselMultiItemPage()),
+ ("Features", "Data Binding",
+ "Bind Carousel to an ObservableCollection and add, remove, or shuffle items at runtime.",
+ () => new CarouselDataBindingPage()),
+
+ // Showcases
+ ("Showcases", "Curated Gallery",
+ "Editorial art gallery app with DrawerPage navigation, hero Carousel with PipsPager dots, and a horizontal peek carousel for collection highlights.",
+ () => new CarouselGalleryAppPage()),
+ };
+
+ public CarouselDemoPage()
+ {
+ InitializeComponent();
+ Loaded += OnLoaded;
+ }
+
+ private async void OnLoaded(object? sender, RoutedEventArgs e)
+ {
+ await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/CarouselPage/CarouselCustomizationPage.xaml b/samples/ControlCatalog/Pages/CarouselPage/CarouselCustomizationPage.xaml
new file mode 100644
index 0000000000..add442e7a1
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselPage/CarouselCustomizationPage.xaml
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Horizontal
+ Vertical
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/CarouselPage/CarouselCustomizationPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage/CarouselCustomizationPage.xaml.cs
new file mode 100644
index 0000000000..c3b919d65d
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselPage/CarouselCustomizationPage.xaml.cs
@@ -0,0 +1,48 @@
+using System;
+using Avalonia.Animation;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Interactivity;
+
+namespace ControlCatalog.Pages
+{
+ public partial class CarouselCustomizationPage : UserControl
+ {
+ public CarouselCustomizationPage()
+ {
+ InitializeComponent();
+ PreviousButton.Click += (_, _) => DemoCarousel.Previous();
+ NextButton.Click += (_, _) => DemoCarousel.Next();
+ OrientationCombo.SelectionChanged += (_, _) => ApplyOrientation();
+ ViewportSlider.ValueChanged += OnViewportFractionChanged;
+ }
+
+ private void ApplyOrientation()
+ {
+ var horizontal = OrientationCombo.SelectedIndex == 0;
+ var axis = horizontal ? PageSlide.SlideAxis.Horizontal : PageSlide.SlideAxis.Vertical;
+ DemoCarousel.PageTransition = new PageSlide(TimeSpan.FromSeconds(0.25), axis);
+ StatusText.Text = $"Orientation: {(horizontal ? "Horizontal" : "Vertical")}";
+ }
+
+ private void OnViewportFractionChanged(object? sender, RangeBaseValueChangedEventArgs e)
+ {
+ var value = Math.Round(e.NewValue, 2);
+ DemoCarousel.ViewportFraction = value;
+ ViewportLabel.Text = value.ToString("0.00");
+ ViewportHint.Text = value >= 1d ?
+ "1.00 shows a single full page." :
+ $"{1d / value:0.##} pages fit in view. Try 0.80 for peeking.";
+ }
+
+ private void OnWrapSelectionChanged(object? sender, RoutedEventArgs e)
+ {
+ DemoCarousel.WrapSelection = WrapSelectionCheck.IsChecked == true;
+ }
+
+ private void OnSwipeEnabledChanged(object? sender, RoutedEventArgs e)
+ {
+ DemoCarousel.IsSwipeEnabled = SwipeEnabledCheck.IsChecked == true;
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/CarouselPage/CarouselDataBindingPage.xaml b/samples/ControlCatalog/Pages/CarouselPage/CarouselDataBindingPage.xaml
new file mode 100644
index 0000000000..fcb7e96f52
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselPage/CarouselDataBindingPage.xaml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/CarouselPage/CarouselDataBindingPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage/CarouselDataBindingPage.xaml.cs
new file mode 100644
index 0000000000..5a7b6e46da
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselPage/CarouselDataBindingPage.xaml.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+
+namespace ControlCatalog.Pages
+{
+ public class CarouselCardItem
+ {
+ public string Number { get; set; } = "";
+ public string Title { get; set; } = "";
+ public IBrush Background { get; set; } = Brushes.Gray;
+ public IBrush Accent { get; set; } = Brushes.White;
+ }
+
+ public partial class CarouselDataBindingPage : UserControl
+ {
+ private static readonly (string Title, string Color, string Accent)[] Palette =
+ {
+ ("Neon Pulse", "#3525CD", "#C3C0FF"), ("Ephemeral Blue", "#0891B2", "#BAF0FA"),
+ ("Forest Forms", "#059669", "#A7F3D0"), ("Golden Hour", "#D97706", "#FDE68A"),
+ ("Crimson Wave", "#BE185D", "#FBCFE8"), ("Stone Age", "#57534E", "#D6D3D1"),
+ };
+
+ private readonly ObservableCollection _items = new();
+ private int _addCounter;
+
+ public CarouselDataBindingPage()
+ {
+ InitializeComponent();
+ DemoCarousel.ItemsSource = _items;
+ DemoCarousel.SelectionChanged += OnSelectionChanged;
+
+ for (var i = 0; i < 4; i++)
+ AppendItem();
+
+ PreviousButton.Click += (_, _) => DemoCarousel.Previous();
+ NextButton.Click += (_, _) => DemoCarousel.Next();
+ AddButton.Click += OnAddItem;
+ RemoveButton.Click += OnRemoveCurrent;
+ ShuffleButton.Click += OnShuffle;
+ UpdateStatus();
+ }
+
+ private void AppendItem()
+ {
+ var (title, color, accent) = Palette[_addCounter % Palette.Length];
+ _items.Add(new CarouselCardItem
+ {
+ Number = $"{_items.Count + 1:D2}",
+ Title = title,
+ Background = new SolidColorBrush(Color.Parse(color)),
+ Accent = new SolidColorBrush(Color.Parse(accent)),
+ });
+ _addCounter++;
+ }
+
+ private void OnAddItem(object? sender, RoutedEventArgs e)
+ {
+ AppendItem();
+ UpdateStatus();
+ }
+
+ private void OnRemoveCurrent(object? sender, RoutedEventArgs e)
+ {
+ if (_items.Count == 0)
+ return;
+ var idx = Math.Clamp(DemoCarousel.SelectedIndex, 0, _items.Count - 1);
+ _items.RemoveAt(idx);
+ UpdateStatus();
+ }
+
+ private void OnShuffle(object? sender, RoutedEventArgs e)
+ {
+ var rng = new Random();
+ var shuffled = _items.OrderBy(_ => rng.Next()).ToList();
+ _items.Clear();
+ foreach (var item in shuffled)
+ _items.Add(item);
+ UpdateStatus();
+ }
+
+ private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ UpdateStatus();
+ }
+
+ private void UpdateStatus()
+ {
+ StatusText.Text = $"Item: {DemoCarousel.SelectedIndex + 1} / {_items.Count}";
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/CarouselPage/CarouselGalleryAppPage.xaml b/samples/ControlCatalog/Pages/CarouselPage/CarouselGalleryAppPage.xaml
new file mode 100644
index 0000000000..9c7c8e0eab
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselPage/CarouselGalleryAppPage.xaml
@@ -0,0 +1,557 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/CarouselPage/CarouselGalleryAppPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage/CarouselGalleryAppPage.xaml.cs
new file mode 100644
index 0000000000..5c637b5454
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselPage/CarouselGalleryAppPage.xaml.cs
@@ -0,0 +1,101 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+
+namespace ControlCatalog.Pages
+{
+ public partial class CarouselGalleryAppPage : UserControl
+ {
+ private bool _syncing;
+ private Point _dragStart;
+ private bool _isDragging;
+ private const double SwipeThreshold = 50;
+
+ private ScrollViewer? _infoPanel;
+
+ public CarouselGalleryAppPage()
+ {
+ InitializeComponent();
+ _infoPanel = this.FindControl("InfoPanel");
+ HeroCarousel.SelectionChanged += OnHeroSelectionChanged;
+ HeroPager.SelectedIndexChanged += OnPagerIndexChanged;
+ }
+
+ protected override void OnLoaded(RoutedEventArgs e)
+ {
+ base.OnLoaded(e);
+ UpdateInfoPanelVisibility();
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+ if (change.Property == BoundsProperty)
+ UpdateInfoPanelVisibility();
+ }
+
+ private void UpdateInfoPanelVisibility()
+ {
+ if (_infoPanel != null)
+ _infoPanel.IsVisible = Bounds.Width >= 640;
+ }
+
+ private void OnHamburgerClick(object? sender, RoutedEventArgs e)
+ {
+ RootDrawer.IsOpen = !RootDrawer.IsOpen;
+ }
+
+ private void OnHeroSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (_syncing)
+ return;
+ _syncing = true;
+ HeroPager.SelectedPageIndex = HeroCarousel.SelectedIndex;
+ _syncing = false;
+ }
+
+ private void OnPagerIndexChanged(object? sender, PipsPagerSelectedIndexChangedEventArgs e)
+ {
+ if (_syncing)
+ return;
+ _syncing = true;
+ HeroCarousel.SelectedIndex = e.NewIndex;
+ _syncing = false;
+ }
+
+ private void OnDrawerMenuSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ RootDrawer.IsOpen = false;
+ DrawerMenu.SelectedItem = null;
+ }
+
+ private void OnHeroPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ if (!e.GetCurrentPoint(null).Properties.IsLeftButtonPressed)
+ return;
+ _dragStart = e.GetPosition((Visual?)sender);
+ _isDragging = true;
+ }
+
+ private void OnHeroPointerReleased(object? sender, PointerReleasedEventArgs e)
+ {
+ if (!_isDragging)
+ return;
+ _isDragging = false;
+ var delta = e.GetPosition((Visual?)sender).X - _dragStart.X;
+ if (Math.Abs(delta) < SwipeThreshold)
+ return;
+ if (delta < 0)
+ HeroCarousel.Next();
+ else
+ HeroCarousel.Previous();
+ }
+
+ private void OnHeroPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e)
+ {
+ _isDragging = false;
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/CarouselPage/CarouselGesturesPage.xaml b/samples/ControlCatalog/Pages/CarouselPage/CarouselGesturesPage.xaml
new file mode 100644
index 0000000000..7786168d4a
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselPage/CarouselGesturesPage.xaml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/CarouselPage/CarouselGesturesPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage/CarouselGesturesPage.xaml.cs
new file mode 100644
index 0000000000..097dd54966
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselPage/CarouselGesturesPage.xaml.cs
@@ -0,0 +1,59 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+
+namespace ControlCatalog.Pages
+{
+ public partial class CarouselGesturesPage : UserControl
+ {
+ private bool _keyboardEnabled = true;
+
+ public CarouselGesturesPage()
+ {
+ InitializeComponent();
+ DemoCarousel.AddHandler(InputElement.KeyDownEvent, OnKeyDown, handledEventsToo: true);
+ DemoCarousel.SelectionChanged += OnSelectionChanged;
+ DemoCarousel.Loaded += (_, _) => DemoCarousel.Focus();
+ }
+
+ private void OnKeyDown(object? sender, KeyEventArgs e)
+ {
+ if (!_keyboardEnabled)
+ return;
+
+ switch (e.Key)
+ {
+ case Key.Left:
+ case Key.Up:
+ LastActionText.Text = $"Action: Key {e.Key} (Previous)";
+ break;
+ case Key.Right:
+ case Key.Down:
+ LastActionText.Text = $"Action: Key {e.Key} (Next)";
+ break;
+ }
+ }
+
+ private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ StatusText.Text = $"Item: {DemoCarousel.SelectedIndex + 1} / {DemoCarousel.ItemCount}";
+ if (DemoCarousel.IsSwiping)
+ LastActionText.Text = "Action: Swipe";
+ }
+
+ private void OnSwipeEnabledChanged(object? sender, RoutedEventArgs e)
+ {
+ DemoCarousel.IsSwipeEnabled = SwipeCheck.IsChecked == true;
+ }
+
+ private void OnWrapSelectionChanged(object? sender, RoutedEventArgs e)
+ {
+ DemoCarousel.WrapSelection = WrapCheck.IsChecked == true;
+ }
+
+ private void OnKeyboardEnabledChanged(object? sender, RoutedEventArgs e)
+ {
+ _keyboardEnabled = KeyboardCheck.IsChecked == true;
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/CarouselPage/CarouselGettingStartedPage.xaml b/samples/ControlCatalog/Pages/CarouselPage/CarouselGettingStartedPage.xaml
new file mode 100644
index 0000000000..680a65f204
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselPage/CarouselGettingStartedPage.xaml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/CarouselPage/CarouselGettingStartedPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage/CarouselGettingStartedPage.xaml.cs
new file mode 100644
index 0000000000..61aad36df3
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselPage/CarouselGettingStartedPage.xaml.cs
@@ -0,0 +1,40 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+
+namespace ControlCatalog.Pages
+{
+ public partial class CarouselGettingStartedPage : UserControl
+ {
+ public CarouselGettingStartedPage()
+ {
+ InitializeComponent();
+ PreviousButton.Click += OnPrevious;
+ NextButton.Click += OnNext;
+ DemoCarousel.SelectionChanged += OnSelectionChanged;
+ }
+
+ private void OnPrevious(object? sender, RoutedEventArgs e)
+ {
+ DemoCarousel.Previous();
+ UpdateStatus();
+ }
+
+ private void OnNext(object? sender, RoutedEventArgs e)
+ {
+ DemoCarousel.Next();
+ UpdateStatus();
+ }
+
+ private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ UpdateStatus();
+ }
+
+ private void UpdateStatus()
+ {
+ var index = DemoCarousel.SelectedIndex + 1;
+ var count = DemoCarousel.ItemCount;
+ StatusText.Text = $"Item: {index} / {count}";
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/CarouselPage/CarouselMultiItemPage.xaml b/samples/ControlCatalog/Pages/CarouselPage/CarouselMultiItemPage.xaml
new file mode 100644
index 0000000000..23e60f35f2
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselPage/CarouselMultiItemPage.xaml
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/CarouselPage/CarouselMultiItemPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage/CarouselMultiItemPage.xaml.cs
new file mode 100644
index 0000000000..13182e7654
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselPage/CarouselMultiItemPage.xaml.cs
@@ -0,0 +1,47 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Interactivity;
+
+namespace ControlCatalog.Pages
+{
+ public partial class CarouselMultiItemPage : UserControl
+ {
+ public CarouselMultiItemPage()
+ {
+ InitializeComponent();
+ PreviousButton.Click += (_, _) => DemoCarousel.Previous();
+ NextButton.Click += (_, _) => DemoCarousel.Next();
+ DemoCarousel.SelectionChanged += OnSelectionChanged;
+ }
+
+ private void OnViewportFractionChanged(object? sender, RangeBaseValueChangedEventArgs e)
+ {
+ if (DemoCarousel is null)
+ return;
+ var value = Math.Round(e.NewValue, 2);
+ DemoCarousel.ViewportFraction = value;
+ ViewportLabel.Text = value.ToString("0.00");
+ ViewportHint.Text = value >= 1d ? "1.00 — single full item." : $"~{1d / value:0.#} items visible.";
+ }
+
+ private void OnWrapChanged(object? sender, RoutedEventArgs e)
+ {
+ if (DemoCarousel is null)
+ return;
+ DemoCarousel.WrapSelection = WrapCheck.IsChecked == true;
+ }
+
+ private void OnSwipeChanged(object? sender, RoutedEventArgs e)
+ {
+ if (DemoCarousel is null)
+ return;
+ DemoCarousel.IsSwipeEnabled = SwipeCheck.IsChecked == true;
+ }
+
+ private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ StatusText.Text = $"Item: {DemoCarousel.SelectedIndex + 1} / {DemoCarousel.ItemCount}";
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/CarouselPage/CarouselTransitionsPage.xaml b/samples/ControlCatalog/Pages/CarouselPage/CarouselTransitionsPage.xaml
new file mode 100644
index 0000000000..b04ea78ed2
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselPage/CarouselTransitionsPage.xaml
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ None
+ Page Slide
+ Cross Fade
+ Rotate 3D
+ Card Stack
+ Wave Reveal
+ Composite (Slide + Fade)
+
+
+
+
+ Horizontal
+ Vertical
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/CarouselPage/CarouselTransitionsPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage/CarouselTransitionsPage.xaml.cs
new file mode 100644
index 0000000000..2d69ecd4af
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselPage/CarouselTransitionsPage.xaml.cs
@@ -0,0 +1,66 @@
+using System;
+using Avalonia.Animation;
+using Avalonia.Controls;
+using ControlCatalog.Pages.Transitions;
+
+namespace ControlCatalog.Pages
+{
+ public partial class CarouselTransitionsPage : UserControl
+ {
+ public CarouselTransitionsPage()
+ {
+ InitializeComponent();
+ PreviousButton.Click += (_, _) => DemoCarousel.Previous();
+ NextButton.Click += (_, _) => DemoCarousel.Next();
+ TransitionCombo.SelectionChanged += (_, _) => ApplyTransition();
+ OrientationCombo.SelectionChanged += (_, _) => ApplyTransition();
+ }
+
+ private void ApplyTransition()
+ {
+ var axis = OrientationCombo.SelectedIndex == 0 ?
+ PageSlide.SlideAxis.Horizontal :
+ PageSlide.SlideAxis.Vertical;
+ var label = axis == PageSlide.SlideAxis.Horizontal ? "Horizontal" : "Vertical";
+
+ switch (TransitionCombo.SelectedIndex)
+ {
+ case 0:
+ DemoCarousel.PageTransition = null;
+ StatusText.Text = "Transition: None";
+ break;
+ case 1:
+ DemoCarousel.PageTransition = new PageSlide(TimeSpan.FromSeconds(0.25), axis);
+ StatusText.Text = $"Transition: Page Slide ({label})";
+ break;
+ case 2:
+ DemoCarousel.PageTransition = new CrossFade(TimeSpan.FromSeconds(0.25));
+ StatusText.Text = "Transition: Cross Fade";
+ break;
+ case 3:
+ DemoCarousel.PageTransition = new Rotate3DTransition(TimeSpan.FromSeconds(0.5), axis);
+ StatusText.Text = $"Transition: Rotate 3D ({label})";
+ break;
+ case 4:
+ DemoCarousel.PageTransition = new CardStackPageTransition(TimeSpan.FromSeconds(0.5), axis);
+ StatusText.Text = $"Transition: Card Stack ({label})";
+ break;
+ case 5:
+ DemoCarousel.PageTransition = new WaveRevealPageTransition(TimeSpan.FromSeconds(0.8), axis);
+ StatusText.Text = $"Transition: Wave Reveal ({label})";
+ break;
+ case 6:
+ DemoCarousel.PageTransition = new CompositePageTransition
+ {
+ PageTransitions =
+ {
+ new PageSlide(TimeSpan.FromSeconds(0.25), axis),
+ new CrossFade(TimeSpan.FromSeconds(0.25)),
+ }
+ };
+ StatusText.Text = "Transition: Composite (Slide + Fade)";
+ break;
+ }
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/CarouselPage/CarouselVerticalPage.xaml b/samples/ControlCatalog/Pages/CarouselPage/CarouselVerticalPage.xaml
new file mode 100644
index 0000000000..f916e4f526
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselPage/CarouselVerticalPage.xaml
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PageSlide
+ CrossFade
+ None
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/CarouselPage/CarouselVerticalPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage/CarouselVerticalPage.xaml.cs
new file mode 100644
index 0000000000..ac964047c0
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CarouselPage/CarouselVerticalPage.xaml.cs
@@ -0,0 +1,39 @@
+using Avalonia.Animation;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+
+namespace ControlCatalog.Pages
+{
+ public partial class CarouselVerticalPage : UserControl
+ {
+ public CarouselVerticalPage()
+ {
+ InitializeComponent();
+ PreviousButton.Click += (_, _) => DemoCarousel.Previous();
+ NextButton.Click += (_, _) => DemoCarousel.Next();
+ DemoCarousel.SelectionChanged += OnSelectionChanged;
+ TransitionCombo.SelectionChanged += OnTransitionChanged;
+ DemoCarousel.Loaded += (_, _) => DemoCarousel.Focus();
+ }
+
+ private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ StatusText.Text = $"Item: {DemoCarousel.SelectedIndex + 1} / {DemoCarousel.ItemCount}";
+ }
+
+ private void OnTransitionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ DemoCarousel.PageTransition = TransitionCombo.SelectedIndex switch
+ {
+ 1 => new CrossFade(System.TimeSpan.FromSeconds(0.3)),
+ 2 => null,
+ _ => new PageSlide(System.TimeSpan.FromSeconds(0.3), PageSlide.SlideAxis.Vertical),
+ };
+ }
+
+ private void OnWrapSelectionChanged(object? sender, RoutedEventArgs e)
+ {
+ DemoCarousel.WrapSelection = WrapCheck.IsChecked == true;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/VirtualizingCarouselPanel.cs b/src/Avalonia.Controls/VirtualizingCarouselPanel.cs
index 66e717d265..875c2dda5a 100644
--- a/src/Avalonia.Controls/VirtualizingCarouselPanel.cs
+++ b/src/Avalonia.Controls/VirtualizingCarouselPanel.cs
@@ -440,6 +440,13 @@ namespace Avalonia.Controls
TeardownGestureRecognizer();
}
+ protected override void OnItemsControlChanged(ItemsControl? oldValue)
+ {
+ base.OnItemsControlChanged(oldValue);
+
+ RefreshGestureRecognizer();
+ }
+
protected override Size MeasureOverride(Size availableSize)
{
if (UsesViewportFractionLayout())
@@ -948,7 +955,7 @@ namespace Avalonia.Controls
return;
}
- if (_isDragging || _offsetAnimationCts is { IsCancellationRequested: false })
+ if (_isDragging)
return;
var transition = GetTransition();
@@ -976,6 +983,11 @@ namespace Avalonia.Controls
{
ResetViewportTransitionState();
ClearFractionalProgressContext();
+
+ // SyncScrollOffset is blocked during animation and the post-animation layout
+ // still sees a live CTS, so re-sync explicitly in case SelectedIndex changed.
+ if (ItemsControl is Carousel carousel)
+ SyncSelectionOffset(carousel.SelectedIndex);
});
}