diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index c8c496b50c..b6249fe17f 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -161,6 +161,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml new file mode 100644 index 0000000000..b75b5c37c2 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs new file mode 100644 index 0000000000..f42bb10ce9 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerCarouselPage : UserControl +{ + public PipsPagerCarouselPage() + { + InitializeComponent(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml new file mode 100644 index 0000000000..8b9856424d --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml.cs new file mode 100644 index 0000000000..4fc74995bc --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerCustomButtonsPage : UserControl +{ + public PipsPagerCustomButtonsPage() + { + InitializeComponent(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml new file mode 100644 index 0000000000..260536d7ae --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs new file mode 100644 index 0000000000..a9276f11b0 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerCustomColorsPage : UserControl +{ + public PipsPagerCustomColorsPage() + { + InitializeComponent(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml new file mode 100644 index 0000000000..fe748b248d --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs new file mode 100644 index 0000000000..cce9e6c5e5 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerCustomTemplatesPage : UserControl +{ + public PipsPagerCustomTemplatesPage() + { + InitializeComponent(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml new file mode 100644 index 0000000000..a69c101687 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs new file mode 100644 index 0000000000..d97165397a --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs @@ -0,0 +1,29 @@ +using System.Collections.ObjectModel; +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerEventsPage : UserControl +{ + private readonly ObservableCollection _events = new(); + + public PipsPagerEventsPage() + { + InitializeComponent(); + + EventLog.ItemsSource = _events; + + EventPager.PropertyChanged += (_, e) => + { + if (e.Property != PipsPager.SelectedPageIndexProperty) + return; + + var newIndex = (int)e.NewValue!; + StatusText.Text = $"Selected: {newIndex}"; + _events.Insert(0, $"SelectedPageIndex changed to {newIndex}"); + + if (_events.Count > 20) + _events.RemoveAt(_events.Count - 1); + }; + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml new file mode 100644 index 0000000000..5eead2fb31 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs new file mode 100644 index 0000000000..80a1569f30 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerGettingStartedPage : UserControl +{ + public PipsPagerGettingStartedPage() + { + InitializeComponent(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml new file mode 100644 index 0000000000..5cc416d413 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs new file mode 100644 index 0000000000..2dc936b544 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerLargeCollectionPage : UserControl +{ + public PipsPagerLargeCollectionPage() + { + InitializeComponent(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPagerPage.xaml b/samples/ControlCatalog/Pages/PipsPagerPage.xaml new file mode 100644 index 0000000000..54112daae0 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPagerPage.xaml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs new file mode 100644 index 0000000000..8f27cc61f8 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs @@ -0,0 +1,47 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class PipsPagerPage : UserControl + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + ("Getting Started", "First Look", + "Default PipsPager with horizontal and vertical orientation, with and without navigation buttons.", + () => new PipsPagerGettingStartedPage()), + + ("Features", "Carousel Integration", + "Bind SelectedPageIndex to a Carousel's SelectedIndex for two-way synchronized page navigation.", + () => new PipsPagerCarouselPage()), + ("Features", "Large Collections", + "Use MaxVisiblePips to limit visible indicators when the page count is large. Pips scroll automatically.", + () => new PipsPagerLargeCollectionPage()), + ("Features", "Events", + "Monitor SelectedPageIndex changes to react to user navigation.", + () => new PipsPagerEventsPage()), + + ("Appearance", "Custom Colors", + "Override pip indicator colors using resource keys for normal, selected, and hover states.", + () => new PipsPagerCustomColorsPage()), + ("Appearance", "Custom Buttons", + "Replace the default chevron navigation buttons with custom styled buttons.", + () => new PipsPagerCustomButtonsPage()), + ("Appearance", "Custom Templates", + "Override pip item templates to create squares, pills, numbers, or any custom shape.", + () => new PipsPagerCustomTemplatesPage()), + }; + + public PipsPagerPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/PipsPagerAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PipsPagerAutomationPeer.cs new file mode 100644 index 0000000000..b40a9b4159 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/PipsPagerAutomationPeer.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + /// + /// An automation peer for . + /// + public class PipsPagerAutomationPeer : ControlAutomationPeer, ISelectionProvider + { + private ListBox? _pipsList; + + /// + /// Initializes a new instance of the class. + /// + /// The control associated with this peer. + public PipsPagerAutomationPeer(PipsPager owner) : base(owner) + { + owner.SelectedIndexChanged += OnSelectionChanged; + } + + /// + /// Gets the owner as a . + /// + private new PipsPager Owner => (PipsPager)base.Owner; + + /// + public bool CanSelectMultiple => false; + + /// + public bool IsSelectionRequired => true; + + /// + public IReadOnlyList GetSelection() + { + var result = new List(); + var owner = Owner; + + if (owner.SelectedPageIndex >= 0 && owner.SelectedPageIndex < owner.NumberOfPages) + { + _pipsList ??= owner.FindNameScope()?.Find("PART_PipsPagerList"); + + if (_pipsList != null) + { + var container = _pipsList.ContainerFromIndex(owner.SelectedPageIndex); + if (container is Control c) + { + var peer = GetOrCreate(c); + result.Add(peer); + } + } + } + + return result; + } + + /// + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.List; + } + + /// + protected override string GetClassNameCore() + { + return nameof(PipsPager); + } + + /// + protected override string? GetNameCore() + { + var name = base.GetNameCore(); + return string.IsNullOrWhiteSpace(name) ? "Pips Pager" : name; + } + + private void OnSelectionChanged(object? sender, Controls.PipsPagerSelectedIndexChangedEventArgs e) + { + RaisePropertyChangedEvent( + SelectionPatternIdentifiers.SelectionProperty, + e.OldIndex, + e.NewIndex); + } + } +} diff --git a/src/Avalonia.Controls/PipsPager/PipsPager.cs b/src/Avalonia.Controls/PipsPager/PipsPager.cs new file mode 100644 index 0000000000..b976df4826 --- /dev/null +++ b/src/Avalonia.Controls/PipsPager/PipsPager.cs @@ -0,0 +1,662 @@ +using System; +using System.Threading; +using Avalonia.Threading; +using Avalonia.Controls.Metadata; +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Styling; +using System.Collections.Generic; + +namespace Avalonia.Controls +{ + /// + /// Represents a control that lets the user navigate through a paginated collection using a set of pips. + /// + [TemplatePart(PART_PreviousButton, typeof(Button))] + [TemplatePart(PART_NextButton, typeof(Button))] + [TemplatePart(PART_PipsPagerList, typeof(ListBox))] + [PseudoClasses(PC_FirstPage, PC_LastPage, PC_Vertical, PC_Horizontal)] + public class PipsPager : TemplatedControl + { + private const string PART_PreviousButton = "PART_PreviousButton"; + private const string PART_NextButton = "PART_NextButton"; + private const string PART_PipsPagerList = "PART_PipsPagerList"; + + private const string PC_FirstPage = ":first-page"; + private const string PC_LastPage = ":last-page"; + private const string PC_Vertical = ":vertical"; + private const string PC_Horizontal = ":horizontal"; + + private Button? _previousButton; + private Button? _nextButton; + private ListBox? _pipsPagerList; + private bool _scrollPending; + private bool _updatingPagerSize; + private bool _isInitialLoad; + private int _lastSelectedPageIndex; + private CancellationTokenSource? _scrollAnimationCts; + private PipsPagerTemplateSettings _templateSettings = new PipsPagerTemplateSettings(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxVisiblePipsProperty = + AvaloniaProperty.Register(nameof(MaxVisiblePips), 5); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsNextButtonVisibleProperty = + AvaloniaProperty.Register(nameof(IsNextButtonVisible), true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty NumberOfPagesProperty = + AvaloniaProperty.Register(nameof(NumberOfPages)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty OrientationProperty = + AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsPreviousButtonVisibleProperty = + AvaloniaProperty.Register(nameof(IsPreviousButtonVisible), true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty SelectedPageIndexProperty = + AvaloniaProperty.Register(nameof(SelectedPageIndex), + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly DirectProperty TemplateSettingsProperty = + AvaloniaProperty.RegisterDirect(nameof(TemplateSettings), + x => x.TemplateSettings); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PreviousButtonStyleProperty = + AvaloniaProperty.Register(nameof(PreviousButtonStyle)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty NextButtonStyleProperty = + AvaloniaProperty.Register(nameof(NextButtonStyle)); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent SelectedIndexChangedEvent = + RoutedEvent.Register(nameof(SelectedIndexChanged), RoutingStrategies.Bubble); + + /// + /// Occurs when the selected index has changed. + /// + public event EventHandler? SelectedIndexChanged + { + add => AddHandler(SelectedIndexChangedEvent, value); + remove => RemoveHandler(SelectedIndexChangedEvent, value); + } + + static PipsPager() + { + SelectedPageIndexProperty.Changed.AddClassHandler((x, e) => x.OnSelectedPageIndexChanged(e)); + NumberOfPagesProperty.Changed.AddClassHandler((x, e) => x.OnNumberOfPagesChanged(e)); + IsPreviousButtonVisibleProperty.Changed.AddClassHandler((x, e) => x.OnIsPreviousButtonVisibleChanged(e)); + IsNextButtonVisibleProperty.Changed.AddClassHandler((x, e) => x.OnIsNextButtonVisibleChanged(e)); + OrientationProperty.Changed.AddClassHandler((x, e) => x.OnOrientationChanged(e)); + MaxVisiblePipsProperty.Changed.AddClassHandler((x, e) => x.OnMaxVisiblePipsChanged(e)); + } + + /// + /// Initializes a new instance of . + /// + public PipsPager() + { + UpdatePseudoClasses(); + } + + /// + /// Gets or sets the maximum number of visible pips. + /// + public int MaxVisiblePips + { + get => GetValue(MaxVisiblePipsProperty); + set => SetValue(MaxVisiblePipsProperty, value); + } + + /// + /// Gets or sets the visibility of the next button. + /// + public bool IsNextButtonVisible + { + get => GetValue(IsNextButtonVisibleProperty); + set => SetValue(IsNextButtonVisibleProperty, value); + } + + /// + /// Gets or sets the number of pages. + /// + public int NumberOfPages + { + get => GetValue(NumberOfPagesProperty); + set => SetValue(NumberOfPagesProperty, value); + } + + /// + /// Gets or sets the orientation of the pips. + /// + public Orientation Orientation + { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + /// + /// Gets or sets the visibility of the previous button. + /// + public bool IsPreviousButtonVisible + { + get => GetValue(IsPreviousButtonVisibleProperty); + set => SetValue(IsPreviousButtonVisibleProperty, value); + } + + /// + /// Gets or sets the current selected page index. + /// + public int SelectedPageIndex + { + get => GetValue(SelectedPageIndexProperty); + set => SetValue(SelectedPageIndexProperty, value); + } + + /// + /// Gets the template settings. + /// + public PipsPagerTemplateSettings TemplateSettings + { + get => _templateSettings; + private set => SetAndRaise(TemplateSettingsProperty, ref _templateSettings, value); + } + + /// + /// Gets or sets the style for the previous button. + /// + public ControlTheme? PreviousButtonStyle + { + get => GetValue(PreviousButtonStyleProperty); + set => SetValue(PreviousButtonStyleProperty, value); + } + + /// + /// Gets or sets the style for the next button. + /// + public ControlTheme? NextButtonStyle + { + get => GetValue(NextButtonStyleProperty); + set => SetValue(NextButtonStyleProperty, value); + } + + /// + protected override AutomationPeer OnCreateAutomationPeer() + { + return new PipsPagerAutomationPeer(this); + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _scrollAnimationCts?.Cancel(); + _scrollAnimationCts?.Dispose(); + _scrollAnimationCts = null; + _isInitialLoad = true; + + // Unsubscribe from previous button events + if (_previousButton != null) + { + _previousButton.Click -= PreviousButton_Click; + } + + if (_nextButton != null) + { + _nextButton.Click -= NextButton_Click; + } + + // Unsubscribe from previous list events + if (_pipsPagerList != null) + { + _pipsPagerList.SizeChanged -= OnPipsPagerListSizeChanged; + _pipsPagerList.ContainerPrepared -= OnContainerPrepared; + _pipsPagerList.ContainerIndexChanged -= OnContainerIndexChanged; + } + + // Get template parts + _previousButton = e.NameScope.Find + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.Themes.Simple/Controls/PipsPager.xaml b/src/Avalonia.Themes.Simple/Controls/PipsPager.xaml new file mode 100644 index 0000000000..388fc0e4d6 --- /dev/null +++ b/src/Avalonia.Themes.Simple/Controls/PipsPager.xaml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml index 11a25dde9d..e5ba9163b5 100644 --- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml @@ -40,6 +40,7 @@ + diff --git a/tests/Avalonia.Controls.UnitTests/PipsPagerTests.cs b/tests/Avalonia.Controls.UnitTests/PipsPagerTests.cs new file mode 100644 index 0000000000..ffc0469ce3 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/PipsPagerTests.cs @@ -0,0 +1,578 @@ +using Avalonia.Input; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using System.Linq; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class PipsPagerTests : ScopedTestBase + { + [Fact] + public void NumberOfPages_Should_Update_Pips() + { + var target = new PipsPager(); + + target.NumberOfPages = 5; + + Assert.Equal(5, target.TemplateSettings.Pips.Count); + Assert.Equal(1, target.TemplateSettings.Pips[0]); + Assert.Equal(5, target.TemplateSettings.Pips[4]); + } + + [Fact] + public void Decreasing_NumberOfPages_Should_Update_Pips() + { + var target = new PipsPager(); + target.NumberOfPages = 5; + + target.NumberOfPages = 3; + + Assert.Equal(3, target.TemplateSettings.Pips.Count); + } + + [Fact] + public void Decreasing_NumberOfPages_Should_Update_SelectedPageIndex() + { + var target = new PipsPager(); + target.NumberOfPages = 5; + target.SelectedPageIndex = 4; + + target.NumberOfPages = 3; + + Assert.Equal(2, target.SelectedPageIndex); + } + + [Fact] + public void SelectedPageIndex_Should_Be_Clamped_To_Zero() + { + var target = new PipsPager(); + target.NumberOfPages = 5; + + target.SelectedPageIndex = -1; + + Assert.Equal(0, target.SelectedPageIndex); + } + + [Fact] + public void SelectedPageIndex_Change_Should_Raise_Event() + { + var target = new PipsPager(); + target.NumberOfPages = 5; + var raised = false; + target.SelectedIndexChanged += (s, e) => raised = true; + + target.SelectedPageIndex = 2; + + Assert.True(raised); + } + + [Fact] + public void Next_Button_Should_Increment_Index() + { + using var unittestApplication = UnitTestApplication.Start(TestServices.StyledWindow); + + var target = new PipsPager + { + NumberOfPages = 5, + SelectedPageIndex = 1, + IsNextButtonVisible = true, + Template = GetTemplate() + }; + + var root = new TestRoot(target); + target.ApplyTemplate(); + + var nextButton = target.GetVisualDescendants().OfType