Browse Source

[Feature] Expand Page-type navigation: CarouselPage (#20871)

* Added CarouselPage

* Renamed DrawerBreakpointWidth to DrawerBreakpointLength

* More unit tests

* More tests

* Improve Carousel gestures

* Make SelectionChanged a RoutedEvent

* Change TabbedPage IsGestureEnabled to true for consistency

* Allow to clear CurrentPage

* Reuse Tab Icon Path (perf)

* Avoid duplicated ResolvePageAtIndex

* More tests

* A new showcase

* Improve CarouselPage Transitions sample

* More changes

* Expose IsNavigating

* A11y improvements

* Updated showcase samples

* More changes

* Fix vertical carousel page gestures

* More tests

* More changes in showcases

* More changes

* Refactor SwipeGestureRecognizer

* More changes

* More changes

* Added new CommandBar Events sample

* Avoid duplicated subscriptions in CommandBar

* Avoid invalid SelectedIndex values

* Avoid duplicated subscription

* Fix SwipeGestureRecognizer non needed allocation

* Added Easing to stack and modal navigation default transitions

* Preserve NavigationPage modal stack across visual tree detach/reattach

* More fixes and tests

* More changes

* Simplify icon logic

* More fixes, samples and tests

* Fixed tests

* Changes based on feedback

* Updated test

* Fixed tests

* Update API suppressions

---------

Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
pull/20939/head
Javier Suárez 5 months ago
committed by GitHub
parent
commit
af0f61a02b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 60
      api/Avalonia.nupkg.xml
  2. BIN
      samples/ControlCatalog/Assets/Sanctuary/city_bg.jpg
  3. BIN
      samples/ControlCatalog/Assets/Sanctuary/forest_bg.jpg
  4. BIN
      samples/ControlCatalog/Assets/Sanctuary/main_arctic_silence.jpg
  5. BIN
      samples/ControlCatalog/Assets/Sanctuary/main_deep_forest.jpg
  6. BIN
      samples/ControlCatalog/Assets/Sanctuary/main_desert_sands.jpg
  7. BIN
      samples/ControlCatalog/Assets/Sanctuary/main_hero.jpg
  8. BIN
      samples/ControlCatalog/Assets/Sanctuary/mountain_bg.jpg
  9. 9
      samples/ControlCatalog/ControlCatalog.csproj
  10. 5
      samples/ControlCatalog/MainView.xaml
  11. 62
      samples/ControlCatalog/Pages/CarouselDemoPage.xaml.cs
  12. 98
      samples/ControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml
  13. 1068
      samples/ControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml.cs
  14. 115
      samples/ControlCatalog/Pages/CarouselPage/CarouselPageCustomizationPage.xaml
  15. 79
      samples/ControlCatalog/Pages/CarouselPage/CarouselPageCustomizationPage.xaml.cs
  16. 44
      samples/ControlCatalog/Pages/CarouselPage/CarouselPageDataTemplatePage.xaml
  17. 209
      samples/ControlCatalog/Pages/CarouselPage/CarouselPageDataTemplatePage.xaml.cs
  18. 37
      samples/ControlCatalog/Pages/CarouselPage/CarouselPageEventsPage.xaml
  19. 92
      samples/ControlCatalog/Pages/CarouselPage/CarouselPageEventsPage.xaml.cs
  20. 61
      samples/ControlCatalog/Pages/CarouselPage/CarouselPageFirstLookPage.xaml
  21. 35
      samples/ControlCatalog/Pages/CarouselPage/CarouselPageFirstLookPage.xaml.cs
  22. 64
      samples/ControlCatalog/Pages/CarouselPage/CarouselPageGesturePage.xaml
  23. 44
      samples/ControlCatalog/Pages/CarouselPage/CarouselPageGesturePage.xaml.cs
  24. 45
      samples/ControlCatalog/Pages/CarouselPage/CarouselPagePerformancePage.xaml
  25. 103
      samples/ControlCatalog/Pages/CarouselPage/CarouselPagePerformancePage.xaml.cs
  26. 90
      samples/ControlCatalog/Pages/CarouselPage/CarouselPageSelectionPage.xaml
  27. 56
      samples/ControlCatalog/Pages/CarouselPage/CarouselPageSelectionPage.xaml.cs
  28. 65
      samples/ControlCatalog/Pages/CarouselPage/CarouselPageTransitionsPage.xaml
  29. 69
      samples/ControlCatalog/Pages/CarouselPage/CarouselPageTransitionsPage.xaml.cs
  30. 298
      samples/ControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml
  31. 11
      samples/ControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml.cs
  32. 335
      samples/ControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml
  33. 70
      samples/ControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml.cs
  34. 95
      samples/ControlCatalog/Pages/CommandBar/CommandBarEventsPage.xaml
  35. 220
      samples/ControlCatalog/Pages/CommandBar/CommandBarEventsPage.xaml.cs
  36. 1
      samples/ControlCatalog/Pages/CommandBarPage.xaml.cs
  37. 32
      samples/ControlCatalog/Pages/ContentPage/ContentPageCommandBarPage.xaml
  38. 3
      samples/ControlCatalog/Pages/DrawerDemoPage.xaml.cs
  39. 113
      samples/ControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml
  40. 84
      samples/ControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml.cs
  41. 3
      samples/ControlCatalog/Pages/NavigationDemoPage.xaml.cs
  42. 51
      samples/ControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs
  43. 5
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml.cs
  44. 5
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageAttachedMethodsPage.xaml.cs
  45. 5
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageBackButtonPage.xaml.cs
  46. 7
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageEventsPage.xaml
  47. 5
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageEventsPage.xaml.cs
  48. 5
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageFirstLookPage.xaml.cs
  49. 6
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageGesturePage.xaml.cs
  50. 5
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageInteractiveHeaderPage.xaml.cs
  51. 5
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageModalPage.xaml.cs
  52. 8
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageModalTransitionsPage.xaml.cs
  53. 95
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageMvvmNavigation.cs
  54. 86
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageMvvmPage.xaml
  55. 32
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageMvvmPage.xaml.cs
  56. 252
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageMvvmPageFactory.cs
  57. 238
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageMvvmViewModels.cs
  58. 6
      samples/ControlCatalog/Pages/NavigationPage/NavigationPagePassDataPage.xaml.cs
  59. 24
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageScrollAwarePage.xaml.cs
  60. 5
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageStackPage.xaml.cs
  61. 5
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageTitlePage.xaml.cs
  62. 5
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageToolbarPage.xaml.cs
  63. 8
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageTransitionsPage.xaml.cs
  64. 6
      samples/ControlCatalog/Pages/NavigationPage/PulseAppPage.xaml.cs
  65. 35
      samples/ControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml
  66. 43
      samples/ControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml.cs
  67. 7
      samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs
  68. 2
      samples/ControlCatalog/Pages/TabbedDemoPage.xaml.cs
  69. 13
      samples/ControlCatalog/Pages/TabbedPage/TabbedPageDataTemplatePage.xaml
  70. 157
      samples/ControlCatalog/Pages/TabbedPage/TabbedPageDataTemplatePage.xaml.cs
  71. 36
      samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml
  72. 6
      samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml.cs
  73. 1
      src/Avalonia.Base/Animation/PageSlide.cs
  74. 2
      src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs
  75. 26
      src/Avalonia.Controls/Automation/Peers/CarouselPageAutomationPeer.cs
  76. 26
      src/Avalonia.Controls/Automation/Peers/DrawerPageAutomationPeer.cs
  77. 16
      src/Avalonia.Controls/Automation/Peers/TabbedPageAutomationPeer.cs
  78. 3
      src/Avalonia.Controls/Carousel.cs
  79. 2
      src/Avalonia.Controls/CommandBar/CommandBar.cs
  80. 418
      src/Avalonia.Controls/Page/CarouselPage.cs
  81. 23
      src/Avalonia.Controls/Page/ContentPage.cs
  82. 7
      src/Avalonia.Controls/Page/DrawerBehavior.cs
  83. 6
      src/Avalonia.Controls/Page/DrawerClosingEventArgs.cs
  84. 136
      src/Avalonia.Controls/Page/DrawerPage.cs
  85. 5
      src/Avalonia.Controls/Page/INavigation.cs
  86. 102
      src/Avalonia.Controls/Page/MultiPage.cs
  87. 780
      src/Avalonia.Controls/Page/NavigationPage.cs
  88. 22
      src/Avalonia.Controls/Page/Page.cs
  89. 14
      src/Avalonia.Controls/Page/PageNavigationHost.cs
  90. 8
      src/Avalonia.Controls/Page/PageSelectionChangedEventArgs.cs
  91. 47
      src/Avalonia.Controls/Page/SelectingMultiPage.cs
  92. 338
      src/Avalonia.Controls/Page/TabbedPage.cs
  93. 71
      src/Avalonia.Controls/VirtualizingCarouselPanel.cs
  94. 30
      src/Avalonia.Themes.Fluent/Controls/CarouselPage.xaml
  95. 1
      src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
  96. 25
      src/Avalonia.Themes.Fluent/Controls/NavigationPage.xaml
  97. 3
      src/Avalonia.Themes.Fluent/Controls/TabbedPage.xaml
  98. 30
      src/Avalonia.Themes.Simple/Controls/CarouselPage.xaml
  99. 25
      src/Avalonia.Themes.Simple/Controls/NavigationPage.xaml
  100. 1
      src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml

60
api/Avalonia.nupkg.xml

@ -1849,6 +1849,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Controls.DrawerPage.DrawerBreakpointWidthProperty</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Controls.NativeMenuBar.EnableMenuItemClickForwardingProperty</Target>
@ -2029,6 +2035,18 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.DrawerPage.get_DrawerBreakpointWidth</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.DrawerPage.set_DrawerBreakpointWidth(System.Double)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.get_Surfaces</Target>
@ -2065,6 +2083,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.PageSelectionChangedEventArgs.#ctor(Avalonia.Controls.Page,Avalonia.Controls.Page)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Platform.DefaultMenuInteractionHandler.GotFocus(System.Object,Avalonia.Input.GotFocusEventArgs)</Target>
@ -2305,6 +2329,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.TabbedPage.FindNextEnabledTab(System.Int32,System.Int32)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.TabItem.SubscribeToOwnerProperties(Avalonia.AvaloniaObject)</Target>
@ -3463,6 +3493,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Controls.DrawerPage.DrawerBreakpointWidthProperty</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Controls.NativeMenuBar.EnableMenuItemClickForwardingProperty</Target>
@ -3643,6 +3679,18 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.DrawerPage.get_DrawerBreakpointWidth</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.DrawerPage.set_DrawerBreakpointWidth(System.Double)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.get_Surfaces</Target>
@ -3679,6 +3727,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.PageSelectionChangedEventArgs.#ctor(Avalonia.Controls.Page,Avalonia.Controls.Page)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Platform.DefaultMenuInteractionHandler.GotFocus(System.Object,Avalonia.Input.GotFocusEventArgs)</Target>
@ -3919,6 +3973,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.TabbedPage.FindNextEnabledTab(System.Int32,System.Int32)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.TabItem.SubscribeToOwnerProperties(Avalonia.AvaloniaObject)</Target>

BIN
samples/ControlCatalog/Assets/Sanctuary/city_bg.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

BIN
samples/ControlCatalog/Assets/Sanctuary/forest_bg.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

BIN
samples/ControlCatalog/Assets/Sanctuary/main_arctic_silence.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

BIN
samples/ControlCatalog/Assets/Sanctuary/main_deep_forest.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

BIN
samples/ControlCatalog/Assets/Sanctuary/main_desert_sands.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

BIN
samples/ControlCatalog/Assets/Sanctuary/main_hero.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

BIN
samples/ControlCatalog/Assets/Sanctuary/mountain_bg.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

9
samples/ControlCatalog/ControlCatalog.csproj

@ -11,14 +11,7 @@
<AvaloniaXaml Include="**\*.xaml">
<SubType>Designer</SubType>
</AvaloniaXaml>
<AvaloniaResource Include="Assets\*" />
<AvaloniaResource Include="Assets\Fonts\*" />
<AvaloniaResource Include="Assets\Restaurant\*" />
<AvaloniaResource Include="Assets\Pulse\*" />
<AvaloniaResource Include="Assets\Movies\*" />
<AvaloniaResource Include="Assets\CurvedHeader\*" />
<AvaloniaResource Include="Assets\RetroGaming\*" />
<AvaloniaResource Include="Assets\ModernApp\*" />
<AvaloniaResource Include="Assets\**\*" />
</ItemGroup>
<ItemGroup>
<None Remove="Assets\image1.jpg" />

5
samples/ControlCatalog/MainView.xaml

@ -59,6 +59,11 @@
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<pages:CarouselDemoPage />
</TabItem>
<TabItem Header="CarouselPage"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<pages:CarouselDemoPage />
</TabItem>
<TabItem Header="CheckBox">
<pages:CheckBoxPage />

62
samples/ControlCatalog/Pages/CarouselDemoPage.xaml.cs

@ -9,32 +9,70 @@ namespace ControlCatalog.Pages
private static readonly (string Group, string Title, string Description, Func<UserControl> Factory)[] Demos =
{
// Overview
("Overview", "Getting Started",
"Basic Carousel with image items and previous/next navigation buttons.",
() => new CarouselGettingStartedPage()),
("Overview", "First Look",
"Basic CarouselPage with three pages and page indicator.",
() => new CarouselPageFirstLookPage()),
// Populate
("Populate", "Data Templates",
"Bind CarouselPage to an ObservableCollection, add or remove pages at runtime, and switch the page template.",
() => new CarouselPageDataTemplatePage()),
// Appearance
("Appearance", "Customization",
"Switch slide direction between horizontal and vertical with PageSlide. Page indicator dots update on each selection.",
() => new CarouselPageCustomizationPage()),
// Features
("Features", "Transitions",
("Features", "Page Transitions",
"Animate page switches with CrossFade or PageSlide.",
() => new CarouselPageTransitionsPage()),
("Features", "Programmatic Selection",
"Jump to any page programmatically with SelectedIndex and respond to SelectionChanged events.",
() => new CarouselPageSelectionPage()),
("Features", "Gesture & Keyboard",
"Swipe left/right to navigate pages. Toggle IsGestureEnabled and IsKeyboardNavigationEnabled.",
() => new CarouselPageGesturePage()),
("Features", "Events",
"SelectionChanged, NavigatedTo, and NavigatedFrom events. Swipe or navigate to see the live event log.",
() => new CarouselPageEventsPage()),
// Performance
("Performance", "Performance Monitor",
"Track page count, live page instances, and managed heap size. Observe how GC reclaims memory after removing pages.",
() => new CarouselPagePerformancePage()),
// Showcases
("Showcases", "Sanctuary",
"Travel discovery app with 3 full-screen immersive pages. Each page has a real background photo, gradient overlay, and themed content. Built as a 1:1 replica of a Stitch design.",
() => new SanctuaryShowcasePage()),
("Showcases", "Care Companion",
"Healthcare onboarding with CarouselPage (3 pages), then a TabbedPage patient dashboard. Skip or complete onboarding to navigate to the dashboard via RemovePage.",
() => new CareCompanionAppPage()),
// Carousel (ItemsControl) demos
("Carousel", "Getting Started",
"Basic Carousel with image items and previous/next navigation buttons.",
() => new CarouselGettingStartedPage()),
("Carousel", "Transitions",
"Configure page transitions: PageSlide, CrossFade, 3D Rotation, or None.",
() => new CarouselTransitionsPage()),
("Features", "Customization",
("Carousel", "Customization",
"Adjust orientation and transition type to tailor the carousel layout.",
() => new CarouselCustomizationPage()),
("Features", "Gestures & Keyboard",
("Carousel", "Gestures & Keyboard",
"Navigate items via swipe gesture and arrow keys. Toggle each input mode on and off.",
() => new CarouselGesturesPage()),
("Features", "Vertical Orientation",
("Carousel", "Vertical Orientation",
"Carousel with Orientation set to Vertical, navigated with Up/Down keys, swipe, or buttons.",
() => new CarouselVerticalPage()),
("Features", "Multi-Item Peek",
("Carousel", "Multi-Item Peek",
"Adjust ViewportFraction to show multiple items simultaneously with adjacent cards peeking.",
() => new CarouselMultiItemPage()),
("Features", "Data Binding",
("Carousel", "Data Binding",
"Bind Carousel to an ObservableCollection and add, remove, or shuffle items at runtime.",
() => new CarouselDataBindingPage()),
// Showcases
("Showcases", "Curated Gallery",
("Carousel", "Curated Gallery",
"Editorial art gallery app with DrawerPage navigation, hero Carousel with PipsPager dots, and a horizontal peek carousel for collection highlights.",
() => new CarouselGalleryAppPage()),
};

98
samples/ControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml

@ -0,0 +1,98 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CareCompanionAppPage">
<UserControl.Styles>
<Style Selector="PipsPager /template/ ListBox ListBoxItem">
<Setter Property="Width" Value="24" />
<Setter Property="Height" Value="24" />
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="2,0" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="ClipToBounds" Value="False" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template">
<ControlTemplate>
<Grid Background="Transparent">
<Border Name="Pip"
Width="6" Height="6" CornerRadius="3"
HorizontalAlignment="Center" VerticalAlignment="Center"
Background="#d1d5db">
<Border.Transitions>
<Transitions>
<DoubleTransition Property="Width" Duration="0:0:0.2" Easing="CubicEaseOut" />
<DoubleTransition Property="Height" Duration="0:0:0.2" Easing="CubicEaseOut" />
<CornerRadiusTransition Property="CornerRadius" Duration="0:0:0.2" Easing="CubicEaseOut" />
<BrushTransition Property="Background" Duration="0:0:0.2" />
</Transitions>
</Border.Transitions>
</Border>
</Grid>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pointerover /template/ Border#Pip">
<Setter Property="Width" Value="8" />
<Setter Property="Height" Value="8" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Background" Value="#b0b0b0" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected /template/ Border#Pip">
<Setter Property="Width" Value="20" />
<Setter Property="Height" Value="6" />
<Setter Property="CornerRadius" Value="3" />
<Setter Property="Background" Value="#137fec" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected:pointerover /template/ Border#Pip">
<Setter Property="Width" Value="20" />
<Setter Property="Height" Value="6" />
<Setter Property="CornerRadius" Value="3" />
<Setter Property="Background" Value="#0a5bb5" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pressed /template/ Border#Pip">
<Setter Property="Width" Value="6" />
<Setter Property="Height" Value="6" />
<Setter Property="Background" Value="#909090" />
</Style>
</UserControl.Styles>
<DockPanel>
<ScrollViewer x:Name="InfoPanel" DockPanel.Dock="Right" Width="300">
<StackPanel Margin="16" Spacing="16">
<TextBlock Text="Care Companion" FontSize="16" FontWeight="SemiBold"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="A patient-focused healthcare companion app. CarouselPage handles onboarding (swipe or tap Next) with PipsPager as custom pill-shaped page indicators. Tapping Skip or Get Started pushes the TabbedPage dashboard and removes the onboarding from the navigation stack." />
<Separator />
<TextBlock Text="Navigation Flow" FontSize="13" FontWeight="SemiBold" />
<StackPanel Spacing="4">
<TextBlock FontSize="12" TextWrapping="Wrap" Text="1. CarouselPage is the NavigationPage root" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="2. Swipe or tap Next to advance pages" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="3. Skip jumps to Get Started page" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="4. Get Started pushes TabbedPage dashboard" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="5. RemovePage(carousel) cleans the stack" />
</StackPanel>
<Separator />
<TextBlock Text="Design" FontSize="13" FontWeight="SemiBold" />
<StackPanel Spacing="4">
<TextBlock FontSize="12" Text="Primary: #137fec (healthcare blue)" />
<TextBlock FontSize="12" Text="Background: #f6f7f8 (off-white)" />
<TextBlock FontSize="12" Text="Cards: #ffffff with border" />
<TextBlock FontSize="12" Text="Success: #10b981 (streak/done)" />
<TextBlock FontSize="12" Text="Roundness: 12-16px corners" />
</StackPanel>
</StackPanel>
</ScrollViewer>
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1" CornerRadius="8" ClipToBounds="True">
<NavigationPage x:Name="NavPage" />
</Border>
</DockPanel>
</UserControl>

1068
samples/ControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml.cs

File diff suppressed because it is too large

115
samples/ControlCatalog/Pages/CarouselPage/CarouselPageCustomizationPage.xaml

@ -0,0 +1,115 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CarouselPageCustomizationPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="220">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Configuration" FontWeight="SemiBold" FontSize="16"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock Text="Slide Direction" FontWeight="SemiBold" FontSize="13" />
<ComboBox x:Name="OrientationCombo" SelectedIndex="0"
SelectionChanged="OnOrientationChanged"
HorizontalAlignment="Stretch">
<ComboBoxItem Content="Horizontal" />
<ComboBoxItem Content="Vertical" />
</ComboBox>
<Separator />
<TextBlock Text="Navigate" FontWeight="SemiBold" FontSize="13" />
<StackPanel Spacing="6">
<Button Content="Previous" Click="OnPrevious" HorizontalAlignment="Stretch" />
<Button Content="Next" Click="OnNext" HorizontalAlignment="Stretch" />
</StackPanel>
<Separator />
<TextBlock Text="Status" FontWeight="SemiBold" FontSize="13" />
<TextBlock x:Name="StatusText" Text="—" Opacity="0.7" TextWrapping="Wrap" />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1"
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<Border Margin="12"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1"
CornerRadius="6"
ClipToBounds="True">
<Panel>
<CarouselPage x:Name="DemoCarousel">
<ContentPage Header="Sunrise"
Background="#FFFDE68A"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="8">
<Border Width="64" Height="64" CornerRadius="32"
Background="#F59E0B" />
<TextBlock Text="Sunrise" FontSize="28" FontWeight="Bold"
Foreground="#92400E" HorizontalAlignment="Center" />
<TextBlock Text="A new day begins. Warm golden hues fill the horizon."
FontSize="13" Foreground="#92400E" Opacity="0.8"
TextWrapping="Wrap" TextAlignment="Center" MaxWidth="240" />
</StackPanel>
</ContentPage>
<ContentPage Header="Ocean"
Background="#FFBFDBFE"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="8">
<Border Width="64" Height="64" CornerRadius="32"
Background="#3B82F6" />
<TextBlock Text="Ocean" FontSize="28" FontWeight="Bold"
Foreground="#1E3A5F" HorizontalAlignment="Center" />
<TextBlock Text="Vast blue waters stretch beyond what the eye can see."
FontSize="13" Foreground="#1E3A5F" Opacity="0.8"
TextWrapping="Wrap" TextAlignment="Center" MaxWidth="240" />
</StackPanel>
</ContentPage>
<ContentPage Header="Forest"
Background="#FFBBF7D0"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="8">
<Border Width="64" Height="64" CornerRadius="32"
Background="#22C55E" />
<TextBlock Text="Forest" FontSize="28" FontWeight="Bold"
Foreground="#14532D" HorizontalAlignment="Center" />
<TextBlock Text="Ancient trees whisper in the quiet woodland breeze."
FontSize="13" Foreground="#14532D" Opacity="0.8"
TextWrapping="Wrap" TextAlignment="Center" MaxWidth="240" />
</StackPanel>
</ContentPage>
<ContentPage Header="Night"
Background="#1E1B4B"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="8">
<Border Width="64" Height="64" CornerRadius="32"
Background="#6366F1" />
<TextBlock Text="Night" FontSize="28" FontWeight="Bold"
Foreground="#C7D2FE" HorizontalAlignment="Center" />
<TextBlock Text="Stars emerge as the world quiets into peaceful darkness."
FontSize="13" Foreground="#C7D2FE" Opacity="0.8"
TextWrapping="Wrap" TextAlignment="Center" MaxWidth="240" />
</StackPanel>
</ContentPage>
</CarouselPage>
<!-- Page indicator dots -->
<Border VerticalAlignment="Bottom" HorizontalAlignment="Center"
Margin="0,0,0,12" CornerRadius="8" Background="#44000000"
Padding="10,4">
<StackPanel Orientation="Horizontal" Spacing="6">
<Ellipse x:Name="Dot0" Width="8" Height="8" Fill="White" />
<Ellipse x:Name="Dot1" Width="8" Height="8" Fill="White" Opacity="0.4" />
<Ellipse x:Name="Dot2" Width="8" Height="8" Fill="White" Opacity="0.4" />
<Ellipse x:Name="Dot3" Width="8" Height="8" Fill="White" Opacity="0.4" />
</StackPanel>
</Border>
</Panel>
</Border>
</DockPanel>
</UserControl>

79
samples/ControlCatalog/Pages/CarouselPage/CarouselPageCustomizationPage.xaml.cs

@ -0,0 +1,79 @@
using System;
using System.Collections;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public partial class CarouselPageCustomizationPage : UserControl
{
public CarouselPageCustomizationPage()
{
InitializeComponent();
Loaded += OnLoaded;
Unloaded += OnUnloaded;
}
private void OnLoaded(object? sender, RoutedEventArgs e)
{
DemoCarousel.PageTransition = new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Horizontal);
DemoCarousel.SelectionChanged += OnSelectionChanged;
UpdateDots(DemoCarousel.SelectedIndex);
UpdateStatus();
}
private void OnUnloaded(object? sender, RoutedEventArgs e)
{
DemoCarousel.SelectionChanged -= OnSelectionChanged;
}
private void OnSelectionChanged(object? sender, PageSelectionChangedEventArgs e)
{
UpdateDots(DemoCarousel.SelectedIndex);
UpdateStatus();
}
private void OnOrientationChanged(object? sender, SelectionChangedEventArgs e)
{
if (DemoCarousel == null)
return;
var axis = OrientationCombo.SelectedIndex == 1
? PageSlide.SlideAxis.Vertical
: PageSlide.SlideAxis.Horizontal;
DemoCarousel.PageTransition = new PageSlide(TimeSpan.FromMilliseconds(300), axis);
UpdateStatus();
}
private void OnPrevious(object? sender, RoutedEventArgs e)
{
if (DemoCarousel.SelectedIndex > 0)
DemoCarousel.SelectedIndex--;
}
private void OnNext(object? sender, RoutedEventArgs e)
{
var pageCount = (DemoCarousel.Pages as IList)?.Count ?? 0;
if (DemoCarousel.SelectedIndex < pageCount - 1)
DemoCarousel.SelectedIndex++;
}
private void UpdateDots(int selectedIndex)
{
Dot0.Opacity = selectedIndex == 0 ? 1.0 : 0.4;
Dot1.Opacity = selectedIndex == 1 ? 1.0 : 0.4;
Dot2.Opacity = selectedIndex == 2 ? 1.0 : 0.4;
Dot3.Opacity = selectedIndex == 3 ? 1.0 : 0.4;
}
private void UpdateStatus()
{
if (StatusText == null) return;
var pageCount = (DemoCarousel.Pages as IList)?.Count ?? 0;
var axis = OrientationCombo?.SelectedIndex == 1 ? "Vertical" : "Horizontal";
StatusText.Text = $"Page {DemoCarousel.SelectedIndex + 1} of {pageCount} | {axis}";
}
}
}

44
samples/ControlCatalog/Pages/CarouselPage/CarouselPageDataTemplatePage.xaml

@ -0,0 +1,44 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CarouselPageDataTemplatePage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="240">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Data Templates" FontWeight="SemiBold" FontSize="16"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock FontSize="12" Opacity="0.7" TextWrapping="Wrap"
Text="Bind a CarouselPage to a view-model collection, render each item with PageTemplate, and switch templates at runtime." />
<Separator />
<Button Content="Add Page" Click="OnAddPage" HorizontalAlignment="Stretch" />
<Button Content="Remove Last" Click="OnRemovePage" HorizontalAlignment="Stretch" />
<Button Content="Switch Template" Click="OnSwitchTemplate" HorizontalAlignment="Stretch" />
<Separator />
<TextBlock Text="Navigate" FontWeight="SemiBold" FontSize="13" />
<StackPanel Spacing="6">
<Button Content="Previous" Click="OnPrevious" HorizontalAlignment="Stretch" />
<Button Content="Next" Click="OnNext" HorizontalAlignment="Stretch" />
</StackPanel>
<Separator />
<TextBlock x:Name="StatusText" Text="4 pages" Opacity="0.7" />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1"
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<Border Margin="12"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1"
CornerRadius="6"
ClipToBounds="True">
<Panel x:Name="CarouselHost" />
</Border>
</DockPanel>
</UserControl>

209
samples/ControlCatalog/Pages/CarouselPage/CarouselPageDataTemplatePage.xaml.cs

@ -0,0 +1,209 @@
using System.Collections.ObjectModel;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using AvaCarouselPage = Avalonia.Controls.CarouselPage;
namespace ControlCatalog.Pages
{
public partial class CarouselPageDataTemplatePage : UserControl
{
private sealed class CityViewModel
{
public string Name { get; }
public string Color { get; }
public string Description { get; }
public CityViewModel(string name, string color, string description)
{
Name = name;
Color = color;
Description = description;
}
}
private static readonly CityViewModel[] InitialData =
{
new("Tokyo", "#1565C0",
"The neon-lit capital of Japan, where ancient temples meet futuristic skylines."),
new("Amsterdam", "#2E7D32", "A city of canals, bicycles, and world-class museums."),
new("New York", "#6A1B9A", "The city that never sleeps — a cultural and financial powerhouse."),
new("Sydney", "#B71C1C", "Iconic harbour, golden beaches and the world-famous Opera House."),
};
private static readonly CityViewModel[] AddData =
{
new("Paris", "#E65100", "The city of light, love, and the Eiffel Tower."),
new("Barcelona", "#00695C", "Art, architecture, and vibrant street life on the Mediterranean coast."),
new("Kyoto", "#880E4F", "Japan's ancient capital, a living museum of traditional culture."),
};
private readonly ObservableCollection<CityViewModel> _items = new();
private int _addCounter;
private bool _useCardTemplate = true;
private AvaCarouselPage? _carouselPage;
public CarouselPageDataTemplatePage()
{
InitializeComponent();
Loaded += OnLoaded;
}
private void OnLoaded(object? sender, RoutedEventArgs e)
{
if (_carouselPage != null)
return;
foreach (var vm in InitialData)
_items.Add(vm);
_addCounter = InitialData.Length;
_useCardTemplate = true;
_carouselPage = new AvaCarouselPage { ItemsSource = _items, PageTemplate = CreatePageTemplate() };
_carouselPage.SelectionChanged += OnSelectionChanged;
CarouselHost.Children.Add(_carouselPage);
UpdateStatus();
}
private void OnSelectionChanged(object? sender, PageSelectionChangedEventArgs e) => UpdateStatus();
private void OnAddPage(object? sender, RoutedEventArgs e)
{
var idx = _addCounter % AddData.Length;
var vm = AddData[idx];
var suffix = _addCounter >= AddData.Length ? $" {_addCounter / AddData.Length + 1}" : "";
_items.Add(new CityViewModel(vm.Name + suffix, vm.Color, vm.Description));
_addCounter++;
UpdateStatus();
}
private void OnRemovePage(object? sender, RoutedEventArgs e)
{
if (_items.Count > 0)
{
_items.RemoveAt(_items.Count - 1);
UpdateStatus();
}
}
private void OnSwitchTemplate(object? sender, RoutedEventArgs e)
{
if (_carouselPage == null)
return;
_useCardTemplate = !_useCardTemplate;
_carouselPage.PageTemplate = CreatePageTemplate();
}
private void OnPrevious(object? sender, RoutedEventArgs e)
{
if (_carouselPage == null)
return;
if (_carouselPage.SelectedIndex > 0)
_carouselPage.SelectedIndex--;
}
private void OnNext(object? sender, RoutedEventArgs e)
{
if (_carouselPage == null)
return;
if (_carouselPage.SelectedIndex < _items.Count - 1)
_carouselPage.SelectedIndex++;
}
private void UpdateStatus()
{
var count = _items.Count;
var index = _carouselPage?.SelectedIndex ?? -1;
StatusText.Text = count == 0 ? "No pages" : $"Page {index + 1} of {count} (index {index})";
}
private IDataTemplate CreatePageTemplate()
{
return new FuncDataTemplate<CityViewModel>((vm, _) => CreatePage(vm, _useCardTemplate));
}
private static ContentPage CreatePage(CityViewModel? vm, bool useCardTemplate)
{
if (vm is null)
return new ContentPage();
return new ContentPage
{
Header = vm.Name, Content = useCardTemplate ? CreateCardContent(vm) : CreateFeatureContent(vm)
};
}
private static Control CreateCardContent(CityViewModel vm)
{
return new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Spacing = 8,
Children =
{
new TextBlock
{
Text = vm.Name,
FontSize = 28,
FontWeight = FontWeight.Bold,
Foreground = new SolidColorBrush(Color.Parse(vm.Color)),
HorizontalAlignment = HorizontalAlignment.Center
},
new TextBlock
{
Text = vm.Description,
FontSize = 13,
Opacity = 0.7,
TextWrapping = TextWrapping.Wrap,
TextAlignment = TextAlignment.Center,
MaxWidth = 280
}
}
};
}
private static Control CreateFeatureContent(CityViewModel vm)
{
var accent = Color.Parse(vm.Color);
return new Border
{
Background = new SolidColorBrush(accent),
Padding = new Thickness(32),
Child = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Spacing = 12,
Children =
{
new TextBlock
{
Text = vm.Name.ToUpperInvariant(),
FontSize = 34,
FontWeight = FontWeight.Bold,
Foreground = Brushes.White,
HorizontalAlignment = HorizontalAlignment.Center
},
new TextBlock
{
Text = vm.Description,
FontSize = 15,
Foreground = Brushes.White,
Opacity = 0.88,
TextWrapping = TextWrapping.Wrap,
TextAlignment = TextAlignment.Center,
MaxWidth = 320
}
}
}
};
}
}
}

37
samples/ControlCatalog/Pages/CarouselPage/CarouselPageEventsPage.xaml

@ -0,0 +1,37 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CarouselPageEventsPage">
<DockPanel>
<StackPanel DockPanel.Dock="Right" Width="240" Margin="0,0,0,0">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Event Log" FontWeight="SemiBold" FontSize="16"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock Text="Navigate" FontWeight="SemiBold" FontSize="13" />
<StackPanel Spacing="6">
<Button Content="Previous" Click="OnPrevious" HorizontalAlignment="Stretch" />
<Button Content="Next" Click="OnNext" HorizontalAlignment="Stretch" />
</StackPanel>
<Separator />
<Button Content="Clear Log" Click="OnClearLog" HorizontalAlignment="Stretch" />
</StackPanel>
<Border Height="1" Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<ScrollViewer x:Name="LogScrollViewer" VerticalScrollBarVisibility="Auto">
<TextBlock x:Name="EventLog" Margin="12" FontSize="11"
FontFamily="Courier New, Consolas, monospace"
TextWrapping="Wrap" Opacity="0.85" />
</ScrollViewer>
</StackPanel>
<Border DockPanel.Dock="Right" Width="1"
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<Border Margin="12"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1" CornerRadius="6" ClipToBounds="True">
<CarouselPage x:Name="DemoCarousel" />
</Border>
</DockPanel>
</UserControl>

92
samples/ControlCatalog/Pages/CarouselPage/CarouselPageEventsPage.xaml.cs

@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public partial class CarouselPageEventsPage : UserControl
{
private readonly List<string> _log = new();
public CarouselPageEventsPage()
{
InitializeComponent();
Loaded += OnLoaded;
Unloaded += OnUnloaded;
}
private void OnLoaded(object? sender, RoutedEventArgs e)
{
var pageNames = new[] { "Home", "Explore", "Library", "Profile" };
for (int i = 0; i < pageNames.Length; i++)
{
var name = pageNames[i];
var page = new ContentPage
{
Header = name,
Background = NavigationDemoHelper.GetPageBrush(i),
Content = new TextBlock
{
Text = $"{name}",
FontSize = 28,
FontWeight = Avalonia.Media.FontWeight.Bold,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
},
HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Stretch,
VerticalContentAlignment = Avalonia.Layout.VerticalAlignment.Stretch
};
page.NavigatedTo += (_, args) =>
AppendLog($"NavigatedTo: {name} (from {(args.PreviousPage as ContentPage)?.Header ?? ""})");
page.NavigatedFrom += (_, args) =>
AppendLog($"NavigatedFrom: {name} (to {(args.DestinationPage as ContentPage)?.Header ?? ""})");
((Avalonia.Collections.AvaloniaList<Page>)DemoCarousel.Pages!).Add(page);
}
DemoCarousel.SelectionChanged += OnSelectionChanged;
}
private void OnSelectionChanged(object? sender, PageSelectionChangedEventArgs e)
{
AppendLog($"SelectionChanged: {(e.PreviousPage as ContentPage)?.Header ?? ""} → {(e.CurrentPage as ContentPage)?.Header ?? ""}");
}
private void OnPrevious(object? sender, RoutedEventArgs e)
{
if (DemoCarousel.SelectedIndex > 0)
DemoCarousel.SelectedIndex--;
}
private void OnNext(object? sender, RoutedEventArgs e)
{
var pageCount = ((AvaloniaList<Page>)DemoCarousel.Pages!).Count;
if (DemoCarousel.SelectedIndex < pageCount - 1)
DemoCarousel.SelectedIndex++;
}
private void OnUnloaded(object? sender, RoutedEventArgs e)
{
DemoCarousel.SelectionChanged -= OnSelectionChanged;
}
private void OnClearLog(object? sender, RoutedEventArgs e)
{
_log.Clear();
EventLog.Text = string.Empty;
}
private void AppendLog(string message)
{
var timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
_log.Add($"[{timestamp}] {message}");
if (_log.Count > 50)
_log.RemoveAt(0);
EventLog.Text = string.Join(Environment.NewLine, _log);
LogScrollViewer.ScrollToEnd();
}
}
}

61
samples/ControlCatalog/Pages/CarouselPage/CarouselPageFirstLookPage.xaml

@ -0,0 +1,61 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CarouselPageFirstLookPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="220">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Configuration" FontWeight="SemiBold" FontSize="16"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock Text="Navigation" FontWeight="SemiBold" FontSize="13" />
<StackPanel Spacing="6">
<Button Content="Previous" Click="OnPrevious" HorizontalAlignment="Stretch" />
<Button Content="Next" Click="OnNext" HorizontalAlignment="Stretch" />
</StackPanel>
<Separator />
<TextBlock Text="Status" FontWeight="SemiBold" FontSize="13" />
<TextBlock x:Name="StatusText" Text="Page 1 of 3" Opacity="0.7" TextWrapping="Wrap" />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1"
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<Border Margin="12"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1" CornerRadius="6" ClipToBounds="True">
<CarouselPage x:Name="DemoCarousel"
SelectionChanged="OnSelectionChanged">
<ContentPage Header="Welcome">
<StackPanel Margin="24" Spacing="12"
HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="Welcome" FontSize="28" FontWeight="Bold"
HorizontalAlignment="Center" />
<TextBlock Text="Swipe left or use the buttons to navigate."
TextWrapping="Wrap" Opacity="0.7" TextAlignment="Center" MaxWidth="280" />
</StackPanel>
</ContentPage>
<ContentPage Header="Explore">
<StackPanel Margin="24" Spacing="12"
HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="Explore" FontSize="28" FontWeight="Bold"
HorizontalAlignment="Center" />
<TextBlock Text="CarouselPage supports scroll-based and transition-based navigation modes."
TextWrapping="Wrap" Opacity="0.7" TextAlignment="Center" MaxWidth="280" />
</StackPanel>
</ContentPage>
<ContentPage Header="Get Started">
<StackPanel Margin="24" Spacing="12"
HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="Get Started" FontSize="28" FontWeight="Bold"
HorizontalAlignment="Center" />
<TextBlock Text="Use SelectedIndex to jump to any page, or assign a PageTransition for animated switching."
TextWrapping="Wrap" Opacity="0.7" TextAlignment="Center" MaxWidth="280" />
</StackPanel>
</ContentPage>
</CarouselPage>
</Border>
</DockPanel>
</UserControl>

35
samples/ControlCatalog/Pages/CarouselPage/CarouselPageFirstLookPage.xaml.cs

@ -0,0 +1,35 @@
using System.Collections;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public partial class CarouselPageFirstLookPage : UserControl
{
public CarouselPageFirstLookPage()
{
InitializeComponent();
}
private void OnPrevious(object? sender, RoutedEventArgs e)
{
if (DemoCarousel.SelectedIndex > 0)
DemoCarousel.SelectedIndex--;
}
private void OnNext(object? sender, RoutedEventArgs e)
{
var pageCount = (DemoCarousel.Pages as IList)?.Count ?? 0;
if (DemoCarousel.SelectedIndex < pageCount - 1)
DemoCarousel.SelectedIndex++;
}
private void OnSelectionChanged(object? sender, PageSelectionChangedEventArgs e)
{
if (StatusText == null)
return;
var pageCount = (DemoCarousel.Pages as IList)?.Count ?? 0;
StatusText.Text = $"Page {DemoCarousel.SelectedIndex + 1} of {pageCount}";
}
}
}

64
samples/ControlCatalog/Pages/CarouselPage/CarouselPageGesturePage.xaml

@ -0,0 +1,64 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CarouselPageGesturePage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="220">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Configuration" FontWeight="SemiBold" FontSize="16"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock Text="Gesture Navigation" FontWeight="SemiBold" FontSize="13" />
<CheckBox x:Name="GestureCheck" Content="IsGestureEnabled"
IsChecked="True" IsCheckedChanged="OnGestureChanged" />
<Separator />
<TextBlock Text="Keyboard Navigation" FontWeight="SemiBold" FontSize="13" />
<CheckBox x:Name="KeyboardCheck" Content="IsKeyboardNavigationEnabled"
IsChecked="True" IsCheckedChanged="OnKeyboardChanged" />
<Separator />
<TextBlock Text="Hints" FontWeight="SemiBold" FontSize="13" />
<TextBlock Text="• Swipe left/right to navigate (touch)" FontSize="11" Opacity="0.7" TextWrapping="Wrap" />
<TextBlock Text="• Mouse drag (left button) to navigate" FontSize="11" Opacity="0.7" TextWrapping="Wrap" />
<TextBlock Text="• Arrow keys / Home / End when focused" FontSize="11" Opacity="0.7" TextWrapping="Wrap" />
<TextBlock Text="• Mouse wheel scrolls pages" FontSize="11" Opacity="0.7" TextWrapping="Wrap" />
<Separator />
<TextBlock Text="Status" FontWeight="SemiBold" FontSize="13" />
<TextBlock x:Name="StatusText" Text="—" Opacity="0.7" TextWrapping="Wrap" />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1"
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<Border Margin="12"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1" CornerRadius="6" ClipToBounds="True">
<CarouselPage x:Name="DemoCarousel"
SelectionChanged="OnSelectionChanged">
<CarouselPage.PageTransition>
<PageSlide Duration="0:0:0.3" Orientation="Horizontal" />
</CarouselPage.PageTransition>
<ContentPage Header="Page 1" Background="#BBDEFB">
<TextBlock Text="Page 1 — Swipe or drag to navigate"
HorizontalAlignment="Center" VerticalAlignment="Center"
TextWrapping="Wrap" TextAlignment="Center" MaxWidth="200" FontSize="16" />
</ContentPage>
<ContentPage Header="Page 2" Background="#C8E6C9">
<TextBlock Text="Page 2 — Arrow keys also work when focused"
HorizontalAlignment="Center" VerticalAlignment="Center"
TextWrapping="Wrap" TextAlignment="Center" MaxWidth="200" FontSize="16" />
</ContentPage>
<ContentPage Header="Page 3" Background="#FFE0B2">
<TextBlock Text="Page 3 — Disable gestures using the panel"
HorizontalAlignment="Center" VerticalAlignment="Center"
TextWrapping="Wrap" TextAlignment="Center" MaxWidth="200" FontSize="16" />
</ContentPage>
</CarouselPage>
</Border>
</DockPanel>
</UserControl>

44
samples/ControlCatalog/Pages/CarouselPage/CarouselPageGesturePage.xaml.cs

@ -0,0 +1,44 @@
using System.Collections;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public partial class CarouselPageGesturePage : UserControl
{
public CarouselPageGesturePage()
{
InitializeComponent();
Loaded += OnLoaded;
}
private void OnLoaded(object? sender, RoutedEventArgs e) => UpdateStatus();
private void OnGestureChanged(object? sender, RoutedEventArgs e)
{
if (DemoCarousel == null)
return;
DemoCarousel.IsGestureEnabled = GestureCheck.IsChecked == true;
}
private void OnKeyboardChanged(object? sender, RoutedEventArgs e)
{
if (DemoCarousel == null)
return;
DemoCarousel.IsKeyboardNavigationEnabled = KeyboardCheck.IsChecked == true;
}
private void OnSelectionChanged(object? sender, PageSelectionChangedEventArgs e)
{
UpdateStatus();
}
private void UpdateStatus()
{
if (StatusText == null)
return;
var pageCount = (DemoCarousel.Pages as IList)?.Count ?? 0;
StatusText.Text = $"Page {DemoCarousel.SelectedIndex + 1} of {pageCount}";
}
}
}

45
samples/ControlCatalog/Pages/CarouselPage/CarouselPagePerformancePage.xaml

@ -0,0 +1,45 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CarouselPagePerformancePage">
<Grid ColumnDefinitions="*,240">
<Border Grid.Column="0" Margin="12"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1"
CornerRadius="6"
ClipToBounds="True">
<CarouselPage x:Name="DemoCarousel" />
</Border>
<Border Grid.Column="1"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1,0,0,0" Padding="12">
<ScrollViewer>
<StackPanel Spacing="8">
<TextBlock Text="Actions" FontWeight="SemiBold" />
<Button Content="Add 5 Pages" HorizontalAlignment="Stretch" Click="OnAdd5" />
<Button Content="Add 20 Pages" HorizontalAlignment="Stretch" Click="OnAdd20" />
<Button Content="Remove Last 5" HorizontalAlignment="Stretch" Click="OnRemove5" />
<Button Content="Clear All" HorizontalAlignment="Stretch" Click="OnClearAll" />
<Separator />
<Button Content="Force GC" HorizontalAlignment="Stretch" Click="OnForceGC" />
<Button Content="Refresh Stats" HorizontalAlignment="Stretch" Click="OnRefresh" />
<Separator />
<TextBlock Text="Statistics" FontWeight="SemiBold" />
<TextBlock x:Name="PageCountText" Text="Page count: 0" FontSize="12" />
<TextBlock x:Name="LiveCountText" Text="Live instances: 0" FontSize="12" />
<TextBlock x:Name="HeapText" Text="Heap: 0 KB" FontSize="12" />
<TextBlock x:Name="AllocText" Text="Total allocated: 0 KB" FontSize="12" />
<TextBlock x:Name="LastOpTimeText" Text="Last Op: " FontSize="12" />
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
</UserControl>

103
samples/ControlCatalog/Pages/CarouselPage/CarouselPagePerformancePage.xaml.cs

@ -0,0 +1,103 @@
using System;
using System.Collections;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace ControlCatalog.Pages
{
public partial class CarouselPagePerformancePage : UserControl
{
private readonly NavigationPerformanceMonitorHelper _perf = new();
private int _counter;
public CarouselPagePerformancePage()
{
InitializeComponent();
Loaded += OnLoaded;
Unloaded += OnUnloaded;
}
private void OnLoaded(object? sender, RoutedEventArgs e)
{
AddPages(5);
DemoCarousel.SelectionChanged += OnSelectionChanged;
}
private void OnUnloaded(object? sender, RoutedEventArgs e)
{
DemoCarousel.SelectionChanged -= OnSelectionChanged;
}
private void OnSelectionChanged(object? sender, PageSelectionChangedEventArgs e) => RefreshStats();
private void AddPages(int count)
{
var pages = (IList)DemoCarousel.Pages!;
_perf.OpStopwatch.Restart();
for (int i = 0; i < count; i++)
{
var idx = ++_counter;
var page = new ContentPage
{
Header = $"P{idx}",
Content = new TextBlock
{
Text = $"Page {idx}",
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
FontSize = 18,
Opacity = 0.7
},
Tag = new byte[51200],
};
_perf.TrackPage(page);
pages.Add(page);
}
_perf.StopMetrics(LastOpTimeText);
RefreshStats();
}
private void RemovePages(int count)
{
var pages = (IList)DemoCarousel.Pages!;
_perf.OpStopwatch.Restart();
for (int i = 0; i < count && pages.Count > 0; i++)
pages.RemoveAt(pages.Count - 1);
_perf.StopMetrics(LastOpTimeText);
RefreshStats();
}
private void OnAdd5(object? sender, RoutedEventArgs e) => AddPages(5);
private void OnAdd20(object? sender, RoutedEventArgs e) => AddPages(20);
private void OnRemove5(object? sender, RoutedEventArgs e) => RemovePages(5);
private void OnClearAll(object? sender, RoutedEventArgs e)
{
var pages = (IList)DemoCarousel.Pages!;
_perf.OpStopwatch.Restart();
while (pages.Count > 0)
pages.RemoveAt(pages.Count - 1);
_perf.StopMetrics(LastOpTimeText);
RefreshStats();
}
private void OnForceGC(object? sender, RoutedEventArgs e)
{
_perf.ForceGC(RefreshStats);
}
private void OnRefresh(object? sender, RoutedEventArgs e) => RefreshStats();
private void RefreshStats()
{
var pages = (IList)DemoCarousel.Pages!;
PageCountText.Text = $"Page count: {pages.Count}";
LiveCountText.Text = $"Live instances: {_perf.CountLiveInstances()} / {_perf.TotalCreated} tracked";
HeapText.Text = $"Heap: {GC.GetTotalMemory(false) / 1024:N0} KB";
AllocText.Text = $"Total allocated: {GC.GetTotalAllocatedBytes() / 1024:N0} KB";
}
}
}

90
samples/ControlCatalog/Pages/CarouselPage/CarouselPageSelectionPage.xaml

@ -0,0 +1,90 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CarouselPageSelectionPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="220">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Configuration" FontWeight="SemiBold" FontSize="16"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock Text="Jump to Page" FontWeight="SemiBold" FontSize="13" />
<StackPanel Spacing="6">
<Button Content="Go to Page 1" Click="OnGoTo0" HorizontalAlignment="Stretch" />
<Button Content="Go to Page 2" Click="OnGoTo1" HorizontalAlignment="Stretch" />
<Button Content="Go to Page 3" Click="OnGoTo2" HorizontalAlignment="Stretch" />
<Button Content="Go to Page 4" Click="OnGoTo3" HorizontalAlignment="Stretch" />
</StackPanel>
<Separator />
<TextBlock Text="Navigate" FontWeight="SemiBold" FontSize="13" />
<StackPanel Spacing="6">
<Button Content="First" Click="OnFirst" HorizontalAlignment="Stretch" />
<Button Content="Previous" Click="OnPrevious" HorizontalAlignment="Stretch" />
<Button Content="Next" Click="OnNext" HorizontalAlignment="Stretch" />
<Button Content="Last" Click="OnLast" HorizontalAlignment="Stretch" />
</StackPanel>
<Separator />
<TextBlock Text="Status" FontWeight="SemiBold" FontSize="13" />
<TextBlock x:Name="StatusText" Text="—" Opacity="0.7" TextWrapping="Wrap" />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1"
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<Border Margin="12"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1" CornerRadius="6" ClipToBounds="True">
<CarouselPage x:Name="DemoCarousel"
SelectionChanged="OnSelectionChanged">
<ContentPage Header="Onboarding" Background="#BBDEFB">
<StackPanel Margin="24" Spacing="8"
HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="Step 1 of 4" FontSize="14" Opacity="0.6"
HorizontalAlignment="Center" />
<TextBlock Text="Onboarding" FontSize="28" FontWeight="Bold"
HorizontalAlignment="Center" />
<TextBlock Text="Set SelectedIndex to jump directly to any page."
TextWrapping="Wrap" Opacity="0.7" TextAlignment="Center" MaxWidth="260" />
</StackPanel>
</ContentPage>
<ContentPage Header="Features" Background="#C8E6C9">
<StackPanel Margin="24" Spacing="8"
HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="Step 2 of 4" FontSize="14" Opacity="0.6"
HorizontalAlignment="Center" />
<TextBlock Text="Features" FontSize="28" FontWeight="Bold"
HorizontalAlignment="Center" />
<TextBlock Text="Supports scroll, transition, swipe gesture and keyboard navigation."
TextWrapping="Wrap" Opacity="0.7" TextAlignment="Center" MaxWidth="260" />
</StackPanel>
</ContentPage>
<ContentPage Header="Customize" Background="#FFE0B2">
<StackPanel Margin="24" Spacing="8"
HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="Step 3 of 4" FontSize="14" Opacity="0.6"
HorizontalAlignment="Center" />
<TextBlock Text="Customize" FontSize="28" FontWeight="Bold"
HorizontalAlignment="Center" />
<TextBlock Text="Override the ItemsPanel, add any Avalonia Page as a child."
TextWrapping="Wrap" Opacity="0.7" TextAlignment="Center" MaxWidth="260" />
</StackPanel>
</ContentPage>
<ContentPage Header="Ready" Background="#E1BEE7">
<StackPanel Margin="24" Spacing="8"
HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="Step 4 of 4" FontSize="14" Opacity="0.6"
HorizontalAlignment="Center" />
<TextBlock Text="Ready!" FontSize="28" FontWeight="Bold"
HorizontalAlignment="Center" />
<TextBlock Text="You are all set. Use SelectionChanged to react to navigation."
TextWrapping="Wrap" Opacity="0.7" TextAlignment="Center" MaxWidth="260" />
</StackPanel>
</ContentPage>
</CarouselPage>
</Border>
</DockPanel>
</UserControl>

56
samples/ControlCatalog/Pages/CarouselPage/CarouselPageSelectionPage.xaml.cs

@ -0,0 +1,56 @@
using System.Collections;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public partial class CarouselPageSelectionPage : UserControl
{
public CarouselPageSelectionPage()
{
InitializeComponent();
Loaded += (_, _) => UpdateStatus();
}
private void OnGoTo0(object? sender, RoutedEventArgs e) => DemoCarousel.SelectedIndex = 0;
private void OnGoTo1(object? sender, RoutedEventArgs e) => DemoCarousel.SelectedIndex = 1;
private void OnGoTo2(object? sender, RoutedEventArgs e) => DemoCarousel.SelectedIndex = 2;
private void OnGoTo3(object? sender, RoutedEventArgs e) => DemoCarousel.SelectedIndex = 3;
private void OnFirst(object? sender, RoutedEventArgs e) => DemoCarousel.SelectedIndex = 0;
private void OnPrevious(object? sender, RoutedEventArgs e)
{
if (DemoCarousel.SelectedIndex > 0)
DemoCarousel.SelectedIndex--;
}
private void OnNext(object? sender, RoutedEventArgs e)
{
var pageCount = (DemoCarousel.Pages as IList)?.Count ?? 0;
if (DemoCarousel.SelectedIndex < pageCount - 1)
DemoCarousel.SelectedIndex++;
}
private void OnLast(object? sender, RoutedEventArgs e)
{
var pageCount = (DemoCarousel.Pages as IList)?.Count ?? 0;
if (pageCount > 0)
DemoCarousel.SelectedIndex = pageCount - 1;
}
private void OnSelectionChanged(object? sender, PageSelectionChangedEventArgs e)
{
UpdateStatus();
}
private void UpdateStatus()
{
if (StatusText == null)
return;
var pageCount = (DemoCarousel.Pages as IList)?.Count ?? 0;
var header = (DemoCarousel.SelectedPage as ContentPage)?.Header?.ToString() ?? "—";
StatusText.Text = $"Page {DemoCarousel.SelectedIndex + 1} of {pageCount}: {header}";
}
}
}

65
samples/ControlCatalog/Pages/CarouselPage/CarouselPageTransitionsPage.xaml

@ -0,0 +1,65 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CarouselPageTransitionsPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="220">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Configuration" FontWeight="SemiBold" FontSize="16"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock Text="Page Transition" FontWeight="SemiBold" FontSize="13" />
<ComboBox x:Name="TransitionCombo" SelectedIndex="0"
SelectionChanged="OnTransitionChanged"
HorizontalAlignment="Stretch">
<ComboBoxItem Content="None" />
<ComboBoxItem Content="CrossFade" />
<ComboBoxItem Content="Slide Horizontal" />
<ComboBoxItem Content="Slide Vertical" />
<ComboBoxItem Content="Card Stack" />
<ComboBoxItem Content="Wave Reveal" />
</ComboBox>
<Separator />
<TextBlock Text="Navigate" FontWeight="SemiBold" FontSize="13" />
<StackPanel Spacing="6">
<Button Content="Previous" Click="OnPrevious" HorizontalAlignment="Stretch" />
<Button Content="Next" Click="OnNext" HorizontalAlignment="Stretch" />
</StackPanel>
<Separator />
<TextBlock Text="Status" FontWeight="SemiBold" FontSize="13" />
<TextBlock x:Name="StatusText" Text="Page 1 of 4 | Transition: None"
Opacity="0.7" TextWrapping="Wrap" />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1"
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<Border Margin="12"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1" CornerRadius="6" ClipToBounds="True">
<CarouselPage x:Name="DemoCarousel"
SelectionChanged="OnSelectionChanged">
<ContentPage Header="Page 1" Background="#BBDEFB">
<TextBlock Text="Page 1" FontSize="32" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</ContentPage>
<ContentPage Header="Page 2" Background="#C8E6C9">
<TextBlock Text="Page 2" FontSize="32" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</ContentPage>
<ContentPage Header="Page 3" Background="#FFE0B2">
<TextBlock Text="Page 3" FontSize="32" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</ContentPage>
<ContentPage Header="Page 4" Background="#E1BEE7">
<TextBlock Text="Page 4" FontSize="32" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</ContentPage>
</CarouselPage>
</Border>
</DockPanel>
</UserControl>

69
samples/ControlCatalog/Pages/CarouselPage/CarouselPageTransitionsPage.xaml.cs

@ -0,0 +1,69 @@
using System;
using System.Collections;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Interactivity;
using ControlCatalog.Pages.Transitions;
namespace ControlCatalog.Pages
{
public partial class CarouselPageTransitionsPage : UserControl
{
public CarouselPageTransitionsPage()
{
InitializeComponent();
}
private void OnTransitionChanged(object? sender, SelectionChangedEventArgs e)
{
if (DemoCarousel == null)
return;
DemoCarousel.PageTransition = TransitionCombo?.SelectedIndex switch
{
0 => null,
1 => new CrossFade(TimeSpan.FromMilliseconds(300)),
2 => new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Horizontal),
3 => new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Vertical),
4 => new CardStackPageTransition(TimeSpan.FromMilliseconds(400)),
5 => new WaveRevealPageTransition(TimeSpan.FromMilliseconds(600)),
_ => null
};
UpdateStatus();
}
private void OnPrevious(object? sender, RoutedEventArgs e)
{
if (DemoCarousel.SelectedIndex > 0)
DemoCarousel.SelectedIndex--;
}
private void OnNext(object? sender, RoutedEventArgs e)
{
var pageCount = (DemoCarousel.Pages as IList)?.Count ?? 0;
if (DemoCarousel.SelectedIndex < pageCount - 1)
DemoCarousel.SelectedIndex++;
}
private void OnSelectionChanged(object? sender, PageSelectionChangedEventArgs e)
{
UpdateStatus();
}
private void UpdateStatus()
{
if (StatusText == null)
return;
var pageCount = (DemoCarousel.Pages as IList)?.Count ?? 0;
var modeName = DemoCarousel.PageTransition switch
{
null => "None",
CardStackPageTransition => "Card Stack",
WaveRevealPageTransition => "Wave Reveal",
{ } t => t.GetType().Name
};
StatusText.Text = $"Page {DemoCarousel.SelectedIndex + 1} of {pageCount} | Transition: {modeName}";
}
}
}

298
samples/ControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml

@ -0,0 +1,298 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.SanctuaryMainPage">
<UserControl.Styles>
<Style Selector="Button.primary /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#f47b25" />
<Setter Property="CornerRadius" Value="{TemplateBinding CornerRadius}" />
</Style>
<Style Selector="Button.primary:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#e0701f" />
</Style>
<Style Selector="Button.primary:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#c9611a" />
</Style>
<Style Selector="Button.glass /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#18FFFFFF" />
<Setter Property="CornerRadius" Value="{TemplateBinding CornerRadius}" />
</Style>
<Style Selector="Button.glass:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#2AFFFFFF" />
</Style>
<Style Selector="Button.glass:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#10FFFFFF" />
</Style>
</UserControl.Styles>
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="220">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Sanctuary" FontSize="16" FontWeight="SemiBold"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="Main landing page with a full-screen hero card, Featured Escapes section, and a bottom TabbedPage navigation. Navigated to from the onboarding carousel." />
<Separator />
<TextBlock Text="Design" FontSize="13" FontWeight="SemiBold" />
<TextBlock FontSize="12" Text="Theme: Dark (#221710)" />
<TextBlock FontSize="12" Text="Primary: #f47b25 (warm orange)" />
<Separator />
<TextBlock Text="Tabs" FontSize="13" FontWeight="SemiBold" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Home — hero + Featured Escapes cards" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Explore — destination discovery" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Saved — bookmarked destinations" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Profile — account and settings" />
<Separator />
<TextBlock Text="Navigation" FontSize="13" FontWeight="SemiBold" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Reached via 'Get Started' on the Urban Adventures carousel page. Back stack is cleared on arrival." />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1"
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<Grid RowDefinitions="Auto,*" Background="#221710">
<!-- Top Nav Bar -->
<Border Grid.Row="0" Background="#DD221710" Padding="16,10,16,10">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<Panel Width="30" Height="30">
<Ellipse Fill="#f47b25" />
<TextBlock Text="▲" Foreground="White" FontSize="13" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Panel>
<TextBlock Text="Sanctuary" FontSize="18" FontWeight="Bold"
Foreground="White" VerticalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<Border Width="48" Height="48" CornerRadius="24" Background="#18FFFFFF">
<TextBlock Text="⌕" FontSize="20" Foreground="#80FFFFFF"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Border Width="48" Height="48" CornerRadius="24" Background="#33f47b25">
<TextBlock Text="◉" FontSize="19" Foreground="#f47b25"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</StackPanel>
</Grid>
</Border>
<!-- Tabbed content -->
<TabbedPage Grid.Row="1"
TabPlacement="Bottom"
SelectedIndex="0">
<TabbedPage.Resources>
<SolidColorBrush x:Key="TabbedPageTabStripBackground">#221710</SolidColorBrush>
<SolidColorBrush x:Key="TabbedPageTabItemHeaderForegroundSelected">#f47b25</SolidColorBrush>
<SolidColorBrush x:Key="TabbedPageTabItemHeaderForegroundUnselected">#50FFFFFF</SolidColorBrush>
<Thickness x:Key="TabbedPageTabItemHeaderPadding">8,10,8,4</Thickness>
</TabbedPage.Resources>
<!-- Home Tab -->
<ContentPage Header="Home"
Background="#221710">
<ContentPage.Icon>
<StreamGeometry>M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z</StreamGeometry>
</ContentPage.Icon>
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<StackPanel>
<!-- Hero Card -->
<Border Margin="12,12,12,8" CornerRadius="16" ClipToBounds="True" Height="380">
<Border.Background>
<ImageBrush Source="avares://ControlCatalog/Assets/Sanctuary/main_hero.jpg"
Stretch="UniformToFill" />
</Border.Background>
<Grid>
<Border ClipToBounds="True" Margin="-1">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="Transparent" />
<GradientStop Offset="0.45" Color="#55221710" />
<GradientStop Offset="1" Color="#EE221710" />
</LinearGradientBrush>
</Border.Background>
</Border>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Bottom"
Margin="28,0,28,32" Spacing="14">
<StackPanel HorizontalAlignment="Center" Spacing="2">
<TextBlock Text="Find Your" FontSize="40" FontWeight="ExtraBold"
Foreground="White" TextAlignment="Center" />
<TextBlock Text="Sanctuary" FontSize="40" FontWeight="ExtraBold"
Foreground="#f47b25" TextAlignment="Center" />
</StackPanel>
<TextBlock Text="Embark on a curated journey through the world's most serene and breathtaking natural wonders. Escape the noise and rediscover peace."
FontSize="13" Foreground="#CAffffff"
TextAlignment="Center" TextWrapping="Wrap" MaxWidth="280" />
<StackPanel Spacing="10" HorizontalAlignment="Center">
<Button Classes="primary"
Foreground="#221710" FontWeight="Bold" FontSize="14"
CornerRadius="24" Padding="28,14"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center">
<TextBlock Text="Explore the Collection" Foreground="#221710" FontWeight="Bold" />
</Button>
<Button Classes="glass"
Foreground="White" FontWeight="Bold" FontSize="14"
CornerRadius="24" Padding="28,14"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Center">
<TextBlock Text="⊕" Foreground="White" VerticalAlignment="Center" />
<TextBlock Text="View Map" Foreground="White" FontWeight="Bold" VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
</StackPanel>
</Grid>
</Border>
<!-- Featured Escapes Header -->
<Grid Margin="16,8,16,12" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="3">
<TextBlock Text="Featured Escapes" FontSize="20" FontWeight="Bold" Foreground="White" />
<TextBlock Text="Hand-picked destinations for your next retreat"
FontSize="12" Foreground="#80FFFFFF" />
</StackPanel>
<TextBlock Grid.Column="1" Text="See All" FontSize="12" FontWeight="Bold"
Foreground="#f47b25" VerticalAlignment="Bottom" />
</Grid>
<!-- Card 1: Deep Forest -->
<Border Margin="16,0,16,12" CornerRadius="16" ClipToBounds="True" Height="240">
<Border.Background>
<ImageBrush Source="avares://ControlCatalog/Assets/Sanctuary/main_deep_forest.jpg"
Stretch="UniformToFill" />
</Border.Background>
<Grid>
<Border ClipToBounds="True" Margin="-1">
<Border.Background>
<LinearGradientBrush StartPoint="0,1" EndPoint="0,0">
<GradientStop Offset="0" Color="#66000000" />
<GradientStop Offset="0.5" Color="Transparent" />
</LinearGradientBrush>
</Border.Background>
</Border>
<StackPanel VerticalAlignment="Bottom" Margin="16,0,16,16" Spacing="4">
<Border CornerRadius="12" Background="#33f47b25" Padding="8,4" HorizontalAlignment="Left">
<TextBlock Text="FOREST" FontSize="10" FontWeight="Bold" Foreground="#f47b25" />
</Border>
<TextBlock Text="Deep Forest" FontSize="22" FontWeight="Bold" Foreground="White" />
<TextBlock Text="Quiet trails in Oregon, USA" FontSize="12" Foreground="#CCffffff" />
</StackPanel>
</Grid>
</Border>
<!-- Card 2: Arctic Silence -->
<Border Margin="16,0,16,12" CornerRadius="16" ClipToBounds="True" Height="240">
<Border.Background>
<ImageBrush Source="avares://ControlCatalog/Assets/Sanctuary/main_arctic_silence.jpg"
Stretch="UniformToFill" />
</Border.Background>
<Grid>
<Border ClipToBounds="True" Margin="-1">
<Border.Background>
<LinearGradientBrush StartPoint="0,1" EndPoint="0,0">
<GradientStop Offset="0" Color="#66000000" />
<GradientStop Offset="0.5" Color="Transparent" />
</LinearGradientBrush>
</Border.Background>
</Border>
<StackPanel VerticalAlignment="Bottom" Margin="16,0,16,16" Spacing="4">
<Border CornerRadius="12" Background="#33f47b25" Padding="8,4" HorizontalAlignment="Left">
<TextBlock Text="ALPINE" FontSize="10" FontWeight="Bold" Foreground="#f47b25" />
</Border>
<TextBlock Text="Arctic Silence" FontSize="22" FontWeight="Bold" Foreground="White" />
<TextBlock Text="Remote retreats in Svalbard" FontSize="12" Foreground="#CCffffff" />
</StackPanel>
</Grid>
</Border>
<!-- Card 3: Desert Sands -->
<Border Margin="16,0,16,20" CornerRadius="16" ClipToBounds="True" Height="240">
<Border.Background>
<ImageBrush Source="avares://ControlCatalog/Assets/Sanctuary/main_desert_sands.jpg"
Stretch="UniformToFill" />
</Border.Background>
<Grid>
<Border ClipToBounds="True" Margin="-1">
<Border.Background>
<LinearGradientBrush StartPoint="0,1" EndPoint="0,0">
<GradientStop Offset="0" Color="#66000000" />
<GradientStop Offset="0.5" Color="Transparent" />
</LinearGradientBrush>
</Border.Background>
</Border>
<StackPanel VerticalAlignment="Bottom" Margin="16,0,16,16" Spacing="4">
<Border CornerRadius="12" Background="#33f47b25" Padding="8,4" HorizontalAlignment="Left">
<TextBlock Text="DESERT" FontSize="10" FontWeight="Bold" Foreground="#f47b25" />
</Border>
<TextBlock Text="Desert Sands" FontSize="22" FontWeight="Bold" Foreground="White" />
<TextBlock Text="Star gazing in Wadi Rum" FontSize="12" Foreground="#CCffffff" />
</StackPanel>
</Grid>
</Border>
</StackPanel>
</ScrollViewer>
</ContentPage>
<!-- Explore Tab -->
<ContentPage Header="Explore"
Background="#221710">
<ContentPage.Icon>
<StreamGeometry>M12 10.9c-.61 0-1.1.49-1.1 1.1s.49 1.1 1.1 1.1c.61 0 1.1-.49 1.1-1.1s-.49-1.1-1.1-1.1zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm2.19 12.19L6 18l3.81-8.19L18 6l-3.81 8.19z</StreamGeometry>
</ContentPage.Icon>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"
Spacing="12" Margin="32">
<TextBlock Text="✦" FontSize="48" Foreground="#33FFFFFF" HorizontalAlignment="Center" />
<TextBlock Text="Explore" FontSize="20" FontWeight="Bold" Foreground="White"
HorizontalAlignment="Center" />
<TextBlock Text="Discover new destinations around the world."
FontSize="13" Foreground="#80FFFFFF"
TextAlignment="Center" TextWrapping="Wrap" />
</StackPanel>
</ContentPage>
<!-- Saved Tab -->
<ContentPage Header="Saved"
Background="#221710">
<ContentPage.Icon>
<StreamGeometry>M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z</StreamGeometry>
</ContentPage.Icon>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"
Spacing="12" Margin="32">
<TextBlock Text="♡" FontSize="48" Foreground="#33FFFFFF" HorizontalAlignment="Center" />
<TextBlock Text="Saved" FontSize="20" FontWeight="Bold" Foreground="White"
HorizontalAlignment="Center" />
<TextBlock Text="Your saved destinations will appear here."
FontSize="13" Foreground="#80FFFFFF"
TextAlignment="Center" TextWrapping="Wrap" />
</StackPanel>
</ContentPage>
<!-- Profile Tab -->
<ContentPage Header="Profile"
Background="#221710">
<ContentPage.Icon>
<StreamGeometry>M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z</StreamGeometry>
</ContentPage.Icon>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"
Spacing="12" Margin="32">
<TextBlock Text="◉" FontSize="48" Foreground="#33FFFFFF" HorizontalAlignment="Center" />
<TextBlock Text="Profile" FontSize="20" FontWeight="Bold" Foreground="White"
HorizontalAlignment="Center" />
<TextBlock Text="Your profile and settings will appear here."
FontSize="13" Foreground="#80FFFFFF"
TextAlignment="Center" TextWrapping="Wrap" />
</StackPanel>
</ContentPage>
</TabbedPage>
</Grid>
</DockPanel>
</UserControl>

11
samples/ControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml.cs

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace ControlCatalog.Pages;
public partial class SanctuaryMainPage : UserControl
{
public SanctuaryMainPage()
{
InitializeComponent();
}
}

335
samples/ControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml

@ -0,0 +1,335 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.SanctuaryShowcasePage">
<UserControl.Styles>
<!-- Orange primary button -->
<Style Selector="Button.primary /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#f47b25" />
<Setter Property="CornerRadius" Value="{TemplateBinding CornerRadius}" />
</Style>
<Style Selector="Button.primary:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#e0701f" />
</Style>
<Style Selector="Button.primary:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#c9611a" />
</Style>
<!-- Glass button (semi-transparent white) -->
<Style Selector="Button.glass /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#18FFFFFF" />
<Setter Property="CornerRadius" Value="{TemplateBinding CornerRadius}" />
</Style>
<Style Selector="Button.glass:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#2AFFFFFF" />
</Style>
<Style Selector="Button.glass:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#10FFFFFF" />
</Style>
</UserControl.Styles>
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="220">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Sanctuary" FontSize="16" FontWeight="SemiBold"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="A travel discovery app with 3 full-screen immersive pages. Uses PipsPager with custom pill-shaped indicators. Swipe, use arrow keys, tap the CTA buttons, or click the pip indicators to navigate." />
<Separator />
<TextBlock Text="Design" FontSize="13" FontWeight="SemiBold" />
<TextBlock FontSize="12" Text="Theme: Dark (#221710)" />
<TextBlock FontSize="12" Text="Primary: #f47b25 (warm orange)" />
<Separator />
<TextBlock Text="Pages" FontSize="13" FontWeight="SemiBold" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="1. Explore the Unknown — mountain backdrop" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="2. Hidden Sanctuaries — forest scene" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="3. Urban Adventures — neon city" />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1"
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1" CornerRadius="8" ClipToBounds="True" Margin="12">
<Grid>
<CarouselPage x:Name="DemoCarousel" IsGestureEnabled="True">
<!-- Page 1: Explore the Unknown -->
<ContentPage HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch">
<ContentPage.Background>
<ImageBrush Source="avares://ControlCatalog/Assets/Sanctuary/mountain_bg.jpg"
Stretch="UniformToFill" />
</ContentPage.Background>
<Grid>
<Border>
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="Transparent" />
<GradientStop Offset="0.35" Color="#55221710" />
<GradientStop Offset="1" Color="#EE221710" />
</LinearGradientBrush>
</Border.Background>
</Border>
<Grid RowDefinitions="*,Auto">
<StackPanel Grid.Row="1" Margin="28,0,28,44" Spacing="0"
HorizontalAlignment="Center">
<TextBlock Text="Explore the Unknown"
FontSize="36" FontWeight="Bold" Foreground="White"
TextAlignment="Center" TextWrapping="Wrap"
MaxWidth="300" Margin="0,0,0,14" />
<TextBlock Text="Embark on an unforgettable adventure through pristine wilderness and discover the hidden wonders of the world's most majestic peaks."
FontSize="14" Foreground="#CAffffff"
TextAlignment="Center" TextWrapping="Wrap"
MaxWidth="300" Margin="0,0,0,24" />
<Button Classes="primary"
Foreground="White" FontWeight="Bold" FontSize="16"
CornerRadius="24" Padding="24,16"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Click="OnPage1CTA" Margin="0,0,0,48">
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Center">
<TextBlock Text="Start Journey" Foreground="White"
FontWeight="Bold" VerticalAlignment="Center" />
<TextBlock Text="→" Foreground="White" VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
</Grid>
</Grid>
</ContentPage>
<!-- Page 2: Hidden Sanctuaries -->
<ContentPage HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch">
<ContentPage.Background>
<ImageBrush Source="avares://ControlCatalog/Assets/Sanctuary/forest_bg.jpg"
Stretch="UniformToFill" />
</ContentPage.Background>
<Grid>
<Border>
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="#88221710" />
<GradientStop Offset="0.3" Color="Transparent" />
<GradientStop Offset="0.75" Color="#55221710" />
<GradientStop Offset="1" Color="#CC221710" />
</LinearGradientBrush>
</Border.Background>
</Border>
<Grid RowDefinitions="Auto,*,Auto">
<!-- Logo bar -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8"
Margin="20,22,20,0">
<Panel Width="28" Height="28">
<Ellipse Fill="#f47b25" />
<TextBlock Text="▲" Foreground="White" FontSize="12" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Panel>
<TextBlock Text="SANCTUARY" Foreground="White" FontWeight="Bold"
FontSize="15" VerticalAlignment="Center" />
</StackPanel>
<!-- Center content -->
<StackPanel Grid.Row="1" Margin="24,0,24,0" Spacing="18"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Border CornerRadius="20" BorderBrush="#50f47b25" BorderThickness="1"
Background="#1Af47b25" HorizontalAlignment="Center" Padding="14,6">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="◎" Foreground="#f47b25" FontSize="11" VerticalAlignment="Center" />
<TextBlock Text="VOLUME II: SECLUSION" Foreground="#f47b25"
FontSize="11" FontWeight="Bold" />
</StackPanel>
</Border>
<StackPanel Spacing="0" HorizontalAlignment="Center">
<TextBlock Text="Hidden" Foreground="White"
FontSize="54" FontWeight="Bold" TextAlignment="Center" />
<TextBlock Text="Sanctuaries" Foreground="White"
FontSize="54" FontStyle="Italic" FontWeight="Light"
TextAlignment="Center" />
</StackPanel>
<TextBlock Text="Find your peace in the world's most secluded natural wonders. From misty forest groves to still mountain lakes, discover the quiet beauty of nature's best-kept secrets."
Foreground="#CCffffff" FontSize="14"
TextAlignment="Center" TextWrapping="Wrap" MaxWidth="310" />
<StackPanel Spacing="10" HorizontalAlignment="Center">
<Button Classes="primary"
Foreground="#221710" FontWeight="Bold" FontSize="15"
CornerRadius="24" Padding="32,16"
HorizontalContentAlignment="Center" MinWidth="220"
Click="OnPage2CTA">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Discover More" Foreground="#221710"
FontWeight="Bold" VerticalAlignment="Center" />
<TextBlock Text="→" Foreground="#221710" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button Classes="glass"
Foreground="White" FontWeight="Bold" FontSize="15"
CornerRadius="24" Padding="32,16"
HorizontalContentAlignment="Center" MinWidth="220"
Click="OnPage2CTA">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="▷" Foreground="White" VerticalAlignment="Center" />
<TextBlock Text="Experience" Foreground="White"
FontWeight="Bold" VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
</StackPanel>
<!-- Footer: location + social -->
<StackPanel Grid.Row="2" Spacing="14" Margin="0,0,0,44">
<Grid ColumnDefinitions="*,Auto" Margin="20,0,20,0">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="5"
VerticalAlignment="Center">
<TextBlock Text="⊙" Foreground="#80FFFFFF" FontSize="11"
VerticalAlignment="Center" />
<TextBlock Text="NORDIC HIGHLANDS" Foreground="#80FFFFFF"
FontSize="10" FontWeight="SemiBold" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="14">
<TextBlock Text="Instagram" Foreground="#80FFFFFF"
FontSize="10" FontWeight="SemiBold" />
<TextBlock Text="Pinterest" Foreground="#80FFFFFF"
FontSize="10" FontWeight="SemiBold" />
</StackPanel>
</Grid>
</StackPanel>
</Grid>
</Grid>
</ContentPage>
<!-- Page 3: Urban Adventures -->
<ContentPage HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch">
<ContentPage.Background>
<ImageBrush Source="avares://ControlCatalog/Assets/Sanctuary/city_bg.jpg"
Stretch="UniformToFill" />
</ContentPage.Background>
<Grid>
<Border>
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="Transparent" />
<GradientStop Offset="0.35" Color="#55221710" />
<GradientStop Offset="0.6" Color="#99221710" />
<GradientStop Offset="1" Color="#EE221710" />
</LinearGradientBrush>
</Border.Background>
</Border>
<Grid RowDefinitions="*,Auto">
<StackPanel Grid.Row="1" Margin="28,0,28,44" Spacing="0"
HorizontalAlignment="Center">
<TextBlock Text="Urban Adventures"
FontSize="44" FontWeight="Bold" Foreground="White"
TextAlignment="Center" TextWrapping="Wrap"
MaxWidth="320" Margin="0,0,0,14" />
<Border Height="5" Width="88" CornerRadius="3"
Background="#f47b25" HorizontalAlignment="Center"
Margin="0,0,0,18" />
<TextBlock Text="Experience the electric pulse of the city that never sleeps. Explore hidden gems and neon-lit wonders around every corner."
FontSize="14" Foreground="#CAffffff"
TextAlignment="Center" TextWrapping="Wrap"
MaxWidth="300" Margin="0,0,0,24" />
<Button Classes="primary"
Foreground="White" FontWeight="Bold" FontSize="16"
CornerRadius="24" Padding="24,16"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Click="OnPage3CTA" Margin="0,0,0,48">
<TextBlock Text="Get Started" Foreground="White" FontWeight="Bold" />
</Button>
</StackPanel>
</Grid>
</Grid>
</ContentPage>
</CarouselPage>
<PipsPager HorizontalAlignment="Center"
VerticalAlignment="Bottom" Margin="0,0,0,20"
NumberOfPages="3"
SelectedPageIndex="{Binding #DemoCarousel.SelectedIndex}"
IsPreviousButtonVisible="False"
IsNextButtonVisible="False">
<PipsPager.Styles>
<Style Selector="PipsPager /template/ ListBox ListBoxItem">
<Setter Property="Width" Value="38" />
<Setter Property="Height" Value="24" />
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="2,0" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="ClipToBounds" Value="False" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template">
<ControlTemplate>
<Grid Background="Transparent">
<Border Name="Pip"
Width="8" Height="8" CornerRadius="4"
HorizontalAlignment="Center" VerticalAlignment="Center"
Background="#4DFFFFFF">
<Border.Transitions>
<Transitions>
<DoubleTransition Property="Width" Duration="0:0:0.25" Easing="CubicEaseOut" />
<DoubleTransition Property="Height" Duration="0:0:0.25" Easing="CubicEaseOut" />
<CornerRadiusTransition Property="CornerRadius" Duration="0:0:0.25" Easing="CubicEaseOut" />
<BrushTransition Property="Background" Duration="0:0:0.25" />
</Transitions>
</Border.Transitions>
</Border>
</Grid>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pointerover /template/ Border#Pip">
<Setter Property="Width" Value="10" />
<Setter Property="Height" Value="10" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="Background" Value="#80FFFFFF" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected /template/ Border#Pip">
<Setter Property="Width" Value="32" />
<Setter Property="Height" Value="8" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Background" Value="#f47b25" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected:pointerover /template/ Border#Pip">
<Setter Property="Width" Value="32" />
<Setter Property="Height" Value="8" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Background" Value="#e0701f" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pressed /template/ Border#Pip">
<Setter Property="Width" Value="8" />
<Setter Property="Height" Value="8" />
<Setter Property="Background" Value="#f47b25" />
</Style>
</PipsPager.Styles>
</PipsPager>
</Grid>
</Border>
</DockPanel>
</UserControl>

70
samples/ControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml.cs

@ -0,0 +1,70 @@
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.VisualTree;
namespace ControlCatalog.Pages;
public partial class SanctuaryShowcasePage : UserControl
{
public SanctuaryShowcasePage()
{
InitializeComponent();
}
private void OnPage1CTA(object? sender, RoutedEventArgs e)
{
DemoCarousel.SelectedIndex = 1;
}
private void OnPage2CTA(object? sender, RoutedEventArgs e)
{
DemoCarousel.SelectedIndex = 2;
}
private async void OnPage3CTA(object? sender, RoutedEventArgs e)
{
var nav = this.FindAncestorOfType<NavigationPage>();
if (nav == null)
return;
var carouselWrapper = nav.NavigationStack.LastOrDefault();
var headerGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("*, Auto") };
headerGrid.Children.Add(new TextBlock
{
Text = "Sanctuary",
VerticalAlignment = VerticalAlignment.Center
});
var closeIcon = Geometry.Parse(
"M4.397 4.397a1 1 0 0 1 1.414 0L12 10.585l6.19-6.188a1 1 0 0 1 1.414 1.414L13.413 12l6.19 6.189a1 1 0 0 1-1.414 1.414L12 13.413l-6.189 6.19a1 1 0 0 1-1.414-1.414L10.585 12 4.397 5.811a1 1 0 0 1 0-1.414z");
var closeBtn = new Button
{
Content = new PathIcon { Data = closeIcon },
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Padding = new Thickness(8, 4),
VerticalAlignment = VerticalAlignment.Center
};
Grid.SetColumn(closeBtn, 1);
headerGrid.Children.Add(closeBtn);
closeBtn.Click += async (_, _) => await nav.PopAsync(null);
var mainPage = new ContentPage
{
Header = headerGrid,
Content = new SanctuaryMainPage()
};
NavigationPage.SetHasBackButton(mainPage, false);
await nav.PushAsync(mainPage);
if (carouselWrapper != null)
{
nav.RemovePage(carouselWrapper);
}
}
}

95
samples/ControlCatalog/Pages/CommandBar/CommandBarEventsPage.xaml

@ -0,0 +1,95 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CommandBarEventsPage">
<UserControl.Resources>
<StreamGeometry x:Key="AddIcon">M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z</StreamGeometry>
<StreamGeometry x:Key="SaveIcon">M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z</StreamGeometry>
<StreamGeometry x:Key="ShareIcon">M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z</StreamGeometry>
<StreamGeometry x:Key="ExportIcon">M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M16,11V18.1L13.9,16L11.1,18.8L8.3,16L11.1,13.2L9,11.1L16,11Z</StreamGeometry>
<StreamGeometry x:Key="DeleteIcon">M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z</StreamGeometry>
</UserControl.Resources>
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="280">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Actions" FontWeight="SemiBold" FontSize="16"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<CheckBox x:Name="IsOpenCheck"
Content="IsOpen"
IsCheckedChanged="OnIsOpenChanged" />
<Button Content="+ Add Primary"
HorizontalAlignment="Stretch"
Click="OnAddPrimary" />
<Button Content="- Remove Primary"
HorizontalAlignment="Stretch"
Click="OnRemovePrimary" />
<Button Content="+ Add Secondary"
HorizontalAlignment="Stretch"
Click="OnAddSecondary" />
<Button Content="- Remove Secondary"
HorizontalAlignment="Stretch"
Click="OnRemoveSecondary" />
<Button Content="Clear Log"
HorizontalAlignment="Stretch"
Click="OnClearLog" />
<Separator />
<TextBlock Text="State" FontWeight="SemiBold" />
<TextBlock x:Name="StateText"
FontSize="12"
Opacity="0.7"
TextWrapping="Wrap" />
<Separator />
<TextBlock Text="About" FontWeight="SemiBold" />
<TextBlock FontSize="12" Opacity="0.7" TextWrapping="Wrap"
Text="Opening/Opened fire when the overflow opens. Closing/Closed fire when it closes. The log also records item clicks and command execution while the state panel reflects IsOpen, HasSecondaryCommands, and IsOverflowButtonVisible in real time." />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1" Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<ScrollViewer>
<StackPanel Spacing="12" Margin="12,12,12,0">
<TextBlock Classes="h2">Observe CommandBar overflow events, item invocation, and state changes while opening, closing, and editing the primary and secondary command sets.</TextBlock>
<CommandBar x:Name="DemoBar"
OverflowButtonVisibility="Auto">
<CommandBar.PrimaryCommands>
<AppBarButton Label="New"><AppBarButton.Icon><PathIcon Data="{StaticResource AddIcon}" /></AppBarButton.Icon></AppBarButton>
<AppBarButton Label="Save"><AppBarButton.Icon><PathIcon Data="{StaticResource SaveIcon}" /></AppBarButton.Icon></AppBarButton>
<AppBarButton Label="Share"><AppBarButton.Icon><PathIcon Data="{StaticResource ShareIcon}" /></AppBarButton.Icon></AppBarButton>
</CommandBar.PrimaryCommands>
<CommandBar.SecondaryCommands>
<AppBarButton Label="Export"><AppBarButton.Icon><PathIcon Data="{StaticResource ExportIcon}" /></AppBarButton.Icon></AppBarButton>
<AppBarButton Label="Delete"><AppBarButton.Icon><PathIcon Data="{StaticResource DeleteIcon}" /></AppBarButton.Icon></AppBarButton>
</CommandBar.SecondaryCommands>
</CommandBar>
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1"
CornerRadius="6"
Padding="10">
<StackPanel Spacing="6">
<TextBlock Text="Event Log" FontWeight="SemiBold" />
<TextBlock x:Name="EventLogText"
FontFamily="Consolas, Menlo, monospace"
FontSize="12"
TextWrapping="Wrap"
MinHeight="140"
Opacity="0.8"
Text="Ready" />
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</DockPanel>
</UserControl>

220
samples/ControlCatalog/Pages/CommandBar/CommandBarEventsPage.xaml.cs

@ -0,0 +1,220 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using MiniMvvm;
namespace ControlCatalog.Pages
{
public partial class CommandBarEventsPage : UserControl
{
private readonly List<string> _log = new();
private int _primaryCount = 3;
private int _secondaryCount = 2;
public CommandBarEventsPage()
{
InitializeComponent();
Loaded += OnLoaded;
Unloaded += OnUnloaded;
}
private void OnLoaded(object? sender, RoutedEventArgs e)
{
DemoBar.Opening += OnOpening;
DemoBar.Opened += OnOpened;
DemoBar.Closing += OnClosing;
DemoBar.Closed += OnClosed;
DemoBar.PropertyChanged += OnBarPropertyChanged;
AttachItemHandlers(DemoBar.PrimaryCommands);
AttachItemHandlers(DemoBar.SecondaryCommands);
AppendLog("Ready");
RefreshState();
}
private void OnUnloaded(object? sender, RoutedEventArgs e)
{
DemoBar.Opening -= OnOpening;
DemoBar.Opened -= OnOpened;
DemoBar.Closing -= OnClosing;
DemoBar.Closed -= OnClosed;
DemoBar.PropertyChanged -= OnBarPropertyChanged;
DetachItemHandlers(DemoBar.PrimaryCommands);
DetachItemHandlers(DemoBar.SecondaryCommands);
}
private void OnIsOpenChanged(object? sender, RoutedEventArgs e)
{
DemoBar.IsOpen = IsOpenCheck.IsChecked == true;
RefreshState();
}
private void OnAddPrimary(object? sender, RoutedEventArgs e)
{
_primaryCount++;
var button = CreateButton($"Primary {_primaryCount}");
DemoBar.PrimaryCommands.Add(button);
AppendLog($"Primary +, {DemoBar.PrimaryCommands.Count}");
RefreshState();
}
private void OnRemovePrimary(object? sender, RoutedEventArgs e)
{
RemoveLastCommand(DemoBar.PrimaryCommands, "Primary");
}
private void OnAddSecondary(object? sender, RoutedEventArgs e)
{
_secondaryCount++;
var button = CreateButton($"Secondary {_secondaryCount}");
DemoBar.SecondaryCommands.Add(button);
AppendLog($"Secondary +, {DemoBar.SecondaryCommands.Count}");
RefreshState();
}
private void OnRemoveSecondary(object? sender, RoutedEventArgs e)
{
RemoveLastCommand(DemoBar.SecondaryCommands, "Secondary");
}
private void OnClearLog(object? sender, RoutedEventArgs e)
{
_log.Clear();
EventLogText.Text = "Log cleared";
}
private void OnOpening(object? sender, RoutedEventArgs e)
{
AppendLog("Opening");
RefreshState();
}
private void OnOpened(object? sender, RoutedEventArgs e)
{
AppendLog("Opened");
RefreshState();
}
private void OnClosing(object? sender, RoutedEventArgs e)
{
AppendLog("Closing");
RefreshState();
}
private void OnClosed(object? sender, RoutedEventArgs e)
{
AppendLog("Closed");
RefreshState();
}
private void OnBarPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == CommandBar.IsOpenProperty
|| e.Property == CommandBar.HasSecondaryCommandsProperty
|| e.Property == CommandBar.IsOverflowButtonVisibleProperty)
{
RefreshState();
}
}
private void RefreshState()
{
StateText.Text =
$"IsOpen: {DemoBar.IsOpen}\n" +
$"HasSecondaryCommands: {DemoBar.HasSecondaryCommands}\n" +
$"IsOverflowButtonVisible: {DemoBar.IsOverflowButtonVisible}\n" +
$"Primary: {DemoBar.PrimaryCommands.Count}\n" +
$"Secondary: {DemoBar.SecondaryCommands.Count}\n" +
$"OverflowItems: {DemoBar.OverflowItems.Count}";
IsOpenCheck.IsChecked = DemoBar.IsOpen;
}
private void OnCommandItemClick(object? sender, RoutedEventArgs e)
{
if (sender is AppBarButton button)
AppendLog($"Click, {button.Label}, {DescribePlacement(button)}");
}
private AppBarButton CreateButton(string label)
{
var button = new AppBarButton
{
Label = label,
Icon = new PathIcon
{
Data = StreamGeometry.Parse("M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z")
}
};
AttachItemHandler(button);
return button;
}
private void AttachItemHandlers(IEnumerable<ICommandBarElement> items)
{
foreach (var item in items)
AttachItemHandler(item);
}
private void DetachItemHandlers(IEnumerable<ICommandBarElement> items)
{
foreach (var item in items)
{
if (item is AppBarButton button)
button.Click -= OnCommandItemClick;
}
}
private void AttachItemHandler(ICommandBarElement item)
{
if (item is not AppBarButton button)
return;
button.Click -= OnCommandItemClick;
button.Click += OnCommandItemClick;
button.Command = MiniCommand.Create(() => AppendLog($"Command, {button.Label}, {DescribePlacement(button)}"));
}
private void RemoveLastCommand(IList<ICommandBarElement> items, string bucketName)
{
if (items.Count == 0)
return;
var item = items[^1];
var label = item is AppBarButton button ? button.Label ?? "(unnamed)" : item.GetType().Name;
if (item is AppBarButton appBarButton)
appBarButton.Click -= OnCommandItemClick;
items.RemoveAt(items.Count - 1);
AppendLog($"{bucketName} -, {label}, {items.Count}");
RefreshState();
}
private static string DescribePlacement(AppBarButton button)
{
return button.IsInOverflow ? "overflow" : "primary";
}
private void AppendLog(string message)
{
_log.Add(message);
if (_log.Count > 12)
_log.RemoveAt(0);
EventLogText.Text = string.Join("\n", _log.Select((entry, index) => $"{index + 1,2}. {entry}"));
}
}
}

1
samples/ControlCatalog/Pages/CommandBarPage.xaml.cs

@ -22,6 +22,7 @@ namespace ControlCatalog.Pages
// Features
("Features", "Overflow Menu", "Secondary commands appear in an overflow popup. Configure visibility and sticky behavior.", () => new CommandBarOverflowPage()),
("Features", "Dynamic Overflow", "IsDynamicOverflowEnabled moves primary commands to overflow as space shrinks.", () => new CommandBarDynamicOverflowPage()),
("Features", "Events & State", "Observe Opening, Opened, Closing, and Closed while tracking IsOpen, HasSecondaryCommands, and IsOverflowButtonVisible.", () => new CommandBarEventsPage()),
};
public CommandBarPage()

32
samples/ControlCatalog/Pages/ContentPage/ContentPageCommandBarPage.xaml

@ -34,12 +34,32 @@
<Separator />
<TextBlock Text="About" FontWeight="SemiBold" />
<TextBlock Text="• NavigationPage.SetTopCommandBar(page, bar): places a CommandBar inside the navigation bar area at the top."
FontSize="12" Opacity="0.7" TextWrapping="Wrap" />
<TextBlock Text="• NavigationPage.SetBottomCommandBar(page, bar): places a CommandBar below the page content at the bottom."
FontSize="12" Opacity="0.7" TextWrapping="Wrap" />
<TextBlock Text="• Each ContentPage has its own CommandBar. It is replaced when navigating between pages."
FontSize="12" Opacity="0.7" TextWrapping="Wrap" />
<StackPanel Spacing="6">
<Grid ColumnDefinitions="Auto,*">
<TextBlock Text="•" FontSize="12" Opacity="0.7" Margin="0,0,6,0" />
<TextBlock Grid.Column="1"
Text="NavigationPage.SetTopCommandBar(page, bar): places a CommandBar inside the navigation bar area at the top."
FontSize="12"
Opacity="0.7"
TextWrapping="Wrap" />
</Grid>
<Grid ColumnDefinitions="Auto,*">
<TextBlock Text="•" FontSize="12" Opacity="0.7" Margin="0,0,6,0" />
<TextBlock Grid.Column="1"
Text="NavigationPage.SetBottomCommandBar(page, bar): places a CommandBar below the page content at the bottom."
FontSize="12"
Opacity="0.7"
TextWrapping="Wrap" />
</Grid>
<Grid ColumnDefinitions="Auto,*">
<TextBlock Text="•" FontSize="12" Opacity="0.7" Margin="0,0,6,0" />
<TextBlock Grid.Column="1"
Text="Each ContentPage has its own CommandBar. It is replaced when navigating between pages."
FontSize="12"
Opacity="0.7"
TextWrapping="Wrap" />
</Grid>
</StackPanel>
</StackPanel>
</ScrollViewer>

3
samples/ControlCatalog/Pages/DrawerDemoPage.xaml.cs

@ -19,6 +19,9 @@ namespace ControlCatalog.Pages
("Features", "Compact Rail",
"CompactOverlay and CompactInline layout modes: a narrow icon rail is always visible and expands on open. Adjust rail width and open pane width.",
() => new DrawerPageCompactPage()),
("Features", "Breakpoint",
"DrawerBreakpointLength: below the threshold the drawer switches to Overlay mode automatically. Resize the window or adjust the slider to see the layout switch in real time.",
() => new DrawerPageBreakpointPage()),
("Features", "Events",
"Opened, Closing, and Closed drawer events plus NavigatedTo and NavigatedFrom page lifecycle events. Enable 'Cancel next close' to prevent the drawer from closing.",
() => new DrawerPageEventsPage()),

113
samples/ControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml

@ -0,0 +1,113 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.DrawerPageBreakpointPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="240">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Configuration" FontWeight="SemiBold" FontSize="16"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock Text="DrawerBreakpointLength" FontSize="13" FontWeight="SemiBold" />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="When set, the drawer automatically switches layout based on the available size (width for Left/Right, height for Top/Bottom)." />
<Border Background="{DynamicResource SystemControlBackgroundBaseLowBrush}"
CornerRadius="4" Padding="8">
<StackPanel Spacing="4">
<Grid ColumnDefinitions="18,*">
<PathIcon Grid.Column="0" Width="12" Height="12" Opacity="0.7" VerticalAlignment="Top" Margin="0,1,0,0"
Data="M19,11H7.83L12.42,6.41L11,5L4,12L11,19L12.41,17.59L7.83,13H19V11Z" />
<TextBlock Grid.Column="1" FontSize="11" TextWrapping="Wrap"
Text="Below breakpoint → Overlay (hamburger button, drawer closes)" />
</Grid>
<Grid ColumnDefinitions="18,*">
<PathIcon Grid.Column="0" Width="12" Height="12" Opacity="0.7" VerticalAlignment="Top" Margin="0,1,0,0"
Data="M5,13H16.17L11.58,17.59L13,19L20,12L13,5L11.59,6.41L16.17,11H5V13Z" />
<TextBlock Grid.Column="1" FontSize="11" TextWrapping="Wrap"
Text="Above breakpoint → configured layout, drawer opens automatically" />
</Grid>
</StackPanel>
</Border>
<TextBlock Text="Breakpoint value" FontSize="12" Opacity="0.7" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Slider x:Name="BreakpointSlider" Minimum="100" Maximum="900" Value="500"
Width="150" ValueChanged="OnBreakpointChanged" />
<TextBlock x:Name="BreakpointText" Text="500" VerticalAlignment="Center" />
</StackPanel>
<Separator />
<TextBlock Text="Layout above breakpoint" FontSize="13" FontWeight="SemiBold" />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="The DrawerLayoutBehavior used when width exceeds the breakpoint." />
<ComboBox x:Name="LayoutCombo" SelectedIndex="0"
SelectionChanged="OnLayoutChanged" HorizontalAlignment="Stretch">
<ComboBoxItem Content="Split" />
<ComboBoxItem Content="CompactInline" />
<ComboBoxItem Content="CompactOverlay" />
</ComboBox>
<Separator />
<TextBlock Text="Status" FontWeight="SemiBold" FontSize="13" />
<TextBlock x:Name="WidthText" Text="Width: —" Opacity="0.7" />
<TextBlock x:Name="ModeText" Text="Mode: Overlay" Opacity="0.7" TextWrapping="Wrap" />
<TextBlock TextWrapping="Wrap" FontSize="11" Opacity="0.6"
Text="Resize the window to see the layout switch automatically." />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1" Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<Border Margin="12"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1"
CornerRadius="6"
ClipToBounds="True">
<DrawerPage x:Name="DemoDrawer"
DrawerLayoutBehavior="Split"
DrawerBreakpointLength="500"
DrawerLength="200">
<DrawerPage.Drawer>
<StackPanel Margin="8" Spacing="4">
<Button HorizontalAlignment="Stretch" Background="Transparent"
Click="OnMenuItemClick" Tag="Home">
<StackPanel Orientation="Horizontal" Spacing="8">
<PathIcon Width="16" Height="16"
Data="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" />
<TextBlock Text="Home" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button HorizontalAlignment="Stretch" Background="Transparent"
Click="OnMenuItemClick" Tag="Profile">
<StackPanel Orientation="Horizontal" Spacing="8">
<PathIcon Width="16" Height="16"
Data="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z" />
<TextBlock Text="Profile" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button HorizontalAlignment="Stretch" Background="Transparent"
Click="OnMenuItemClick" Tag="Settings">
<StackPanel Orientation="Horizontal" Spacing="8">
<PathIcon Width="16" Height="16"
Data="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.04 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.04 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" />
<TextBlock Text="Settings" VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
</DrawerPage.Drawer>
<DrawerPage.Content>
<ContentPage x:Name="DetailPage" Header="Home">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="8">
<TextBlock x:Name="DetailTitleText" Text="Home" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" />
<TextBlock Text="Select an item from the drawer." FontSize="13" Opacity="0.7"
TextWrapping="Wrap" TextAlignment="Center" MaxWidth="260" />
</StackPanel>
</ContentPage>
</DrawerPage.Content>
</DrawerPage>
</Border>
</DockPanel>
</UserControl>

84
samples/ControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml.cs

@ -0,0 +1,84 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public partial class DrawerPageBreakpointPage : UserControl
{
private bool _isLoaded;
public DrawerPageBreakpointPage()
{
InitializeComponent();
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
_isLoaded = true;
DemoDrawer.PropertyChanged += OnDrawerPropertyChanged;
UpdateStatus();
}
protected override void OnUnloaded(RoutedEventArgs e)
{
base.OnUnloaded(e);
DemoDrawer.PropertyChanged -= OnDrawerPropertyChanged;
}
private void OnDrawerPropertyChanged(object? sender, Avalonia.AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == DrawerPage.BoundsProperty)
UpdateStatus();
}
private void OnBreakpointChanged(object? sender, RangeBaseValueChangedEventArgs e)
{
if (!_isLoaded)
return;
var value = (int)e.NewValue;
DemoDrawer.DrawerBreakpointLength = value;
BreakpointText.Text = value.ToString();
UpdateStatus();
}
private void OnLayoutChanged(object? sender, SelectionChangedEventArgs e)
{
if (!_isLoaded)
return;
DemoDrawer.DrawerLayoutBehavior = LayoutCombo.SelectedIndex switch
{
0 => DrawerLayoutBehavior.Split,
1 => DrawerLayoutBehavior.CompactInline,
2 => DrawerLayoutBehavior.CompactOverlay,
_ => DrawerLayoutBehavior.Split
};
UpdateStatus();
}
private void OnMenuItemClick(object? sender, RoutedEventArgs e)
{
if (!_isLoaded || sender is not Button button)
return;
var item = button.Tag?.ToString() ?? "Home";
DetailTitleText.Text = item;
DetailPage.Header = item;
if (DemoDrawer.DrawerLayoutBehavior != DrawerLayoutBehavior.Split)
DemoDrawer.IsOpen = false;
}
private void UpdateStatus()
{
var isVertical = DemoDrawer.DrawerPlacement == DrawerPlacement.Top ||
DemoDrawer.DrawerPlacement == DrawerPlacement.Bottom;
var length = isVertical ? DemoDrawer.Bounds.Height : DemoDrawer.Bounds.Width;
var breakpoint = DemoDrawer.DrawerBreakpointLength;
WidthText.Text = $"{(isVertical ? "Height" : "Width")}: {(int)length} px";
var isOverlay = breakpoint > 0 && length > 0 && length < breakpoint;
ModeText.Text = isOverlay ?
"Mode: Overlay (below breakpoint)" :
$"Mode: {DemoDrawer.DrawerLayoutBehavior} (above breakpoint)";
}
}
}

3
samples/ControlCatalog/Pages/NavigationDemoPage.xaml.cs

@ -28,6 +28,9 @@ namespace ControlCatalog.Pages
// Data
("Data", "Pass Data", "Pass data during navigation via constructor arguments or DataContext.",
() => new NavigationPagePassDataPage()),
("Data", "MVVM Navigation",
"Keep navigation decisions in view models by routing NavigationPage push and pop operations through a small INavigationService.",
() => new NavigationPageMvvmPage()),
// Features
("Features", "Attached Methods",

51
samples/ControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs

@ -59,6 +59,26 @@ public partial class LAvenirAppPage : UserControl
_infoPanel.IsVisible = Bounds.Width >= 650;
}
void ApplyRootNavigationBarAppearance()
{
if (_navPage == null)
return;
_navPage.Background = new SolidColorBrush(BgLight);
_navPage.Resources["NavigationBarBackground"] = new SolidColorBrush(BgLight);
_navPage.Resources["NavigationBarForeground"] = new SolidColorBrush(TextDark);
}
void ApplyDetailNavigationBarAppearance()
{
if (_navPage == null)
return;
_navPage.Background = new SolidColorBrush(BgDark);
_navPage.Resources["NavigationBarBackground"] = new SolidColorBrush(BgDark);
_navPage.Resources["NavigationBarForeground"] = Brushes.White;
}
TabbedPage BuildMenuTabbedPage()
{
var tp = new TabbedPage
@ -92,6 +112,7 @@ public partial class LAvenirAppPage : UserControl
VerticalAlignment = VerticalAlignment.Center,
TextAlignment = TextAlignment.Center,
};
ApplyRootNavigationBarAppearance();
NavigationPage.SetTopCommandBar(tp, new Button
{
@ -119,7 +140,7 @@ public partial class LAvenirAppPage : UserControl
Content = menuView,
Background = new SolidColorBrush(BgLight),
Header = "Menu",
Icon = "M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2v7zm5-3v8h2.5v8H21V2c-2.76 0-5 2.24-5 4z",
Icon = Geometry.Parse("M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2v7zm5-3v8h2.5v8H21V2c-2.76 0-5 2.24-5 4z"),
};
var reservationsPage = new ContentPage
@ -127,7 +148,7 @@ public partial class LAvenirAppPage : UserControl
Content = new LAvenirReservationsView(),
Background = new SolidColorBrush(BgLight),
Header = "Reservations",
Icon = "M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM9 10H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2z",
Icon = Geometry.Parse("M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM9 10H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2z"),
};
var profilePage = new ContentPage
@ -135,7 +156,7 @@ public partial class LAvenirAppPage : UserControl
Content = new LAvenirProfileView(),
Background = new SolidColorBrush(BgLight),
Header = "Profile",
Icon = "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z",
Icon = Geometry.Parse("M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"),
};
tp.Pages = new ObservableCollection<Page> { menuPage, reservationsPage, profilePage };
@ -144,7 +165,8 @@ public partial class LAvenirAppPage : UserControl
async void PushDishDetail(string name, string price, string description, string imageFile)
{
if (_navPage == null) return;
if (_navPage == null)
return;
var detail = new ContentPage
{
@ -153,22 +175,19 @@ public partial class LAvenirAppPage : UserControl
Header = name,
};
NavigationPage.SetBottomCommandBar(detail, BuildFloatingBar(price));
_navPage.Background = new SolidColorBrush(BgDark);
_navPage.Resources["NavigationBarBackground"] = new SolidColorBrush(BgDark);
_navPage.Resources["NavigationBarForeground"] = Brushes.White;
detail.NavigatedFrom += (_, _) =>
detail.Navigating += args =>
{
if (_navPage != null)
{
_navPage.Background = new SolidColorBrush(BgLight);
_navPage.Resources["NavigationBarBackground"] = new SolidColorBrush(BgLight);
_navPage.Resources["NavigationBarForeground"] = new SolidColorBrush(TextDark);
}
if (args.NavigationType == NavigationType.Pop)
ApplyRootNavigationBarAppearance();
return Task.CompletedTask;
};
ApplyDetailNavigationBarAppearance();
await _navPage.PushAsync(detail);
if (!ReferenceEquals(_navPage.CurrentPage, detail))
ApplyRootNavigationBarAppearance();
}
Border BuildFloatingBar(string price)

5
samples/ControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml.cs

@ -8,6 +8,7 @@ namespace ControlCatalog.Pages
{
public partial class NavigationPageAppearancePage : UserControl
{
private bool _initialized;
private int _pageCount;
private int _backButtonStyle;
@ -19,6 +20,10 @@ namespace ControlCatalog.Pages
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
if (_initialized)
return;
_initialized = true;
await DemoNav.PushAsync(NavigationDemoHelper.MakePage("Appearance", "Change bar properties using the options panel.", 0), null);
}

5
samples/ControlCatalog/Pages/NavigationPage/NavigationPageAttachedMethodsPage.xaml.cs

@ -8,6 +8,7 @@ namespace ControlCatalog.Pages
{
public partial class NavigationPageAttachedMethodsPage : UserControl
{
private bool _initialized;
private int _pageCount;
public NavigationPageAttachedMethodsPage()
@ -18,6 +19,10 @@ namespace ControlCatalog.Pages
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
if (_initialized)
return;
_initialized = true;
await DemoNav.PushAsync(new ContentPage
{
Header = "Root Page",

5
samples/ControlCatalog/Pages/NavigationPage/NavigationPageBackButtonPage.xaml.cs

@ -8,6 +8,7 @@ namespace ControlCatalog.Pages
{
public partial class NavigationPageBackButtonPage : UserControl
{
private bool _initialized;
private int _pushCount;
public NavigationPageBackButtonPage()
@ -18,6 +19,10 @@ namespace ControlCatalog.Pages
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
if (_initialized)
return;
_initialized = true;
DemoNav.Pushed += (s, ev) => AddLog($"Pushed: \"{ev.Page?.Header}\"");
DemoNav.Popped += (s, ev) => AddLog($"Popped: \"{ev.Page?.Header}\"");

7
samples/ControlCatalog/Pages/NavigationPage/NavigationPageEventsPage.xaml

@ -51,6 +51,13 @@
</ScrollViewer>
</Border>
<Separator />
<!-- IsNavigating indicator -->
<TextBlock Text="Status" FontSize="13" FontWeight="SemiBold" />
<TextBlock Text="{Binding IsNavigating, ElementName=DemoNav, StringFormat='IsNavigating: {0}'}"
FontSize="12" />
<Separator />
<TextBlock Text="NavigationPage exposes events for push, pop, insert, remove, and modal operations. Pages also fire NavigatedTo and NavigatedFrom on each navigation."
TextWrapping="Wrap" FontSize="12" Opacity="0.7" />

5
samples/ControlCatalog/Pages/NavigationPage/NavigationPageEventsPage.xaml.cs

@ -7,6 +7,7 @@ namespace ControlCatalog.Pages
{
public partial class NavigationPageEventsPage : UserControl
{
private bool _initialized;
private int _pageCount;
public NavigationPageEventsPage()
@ -17,6 +18,10 @@ namespace ControlCatalog.Pages
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
if (_initialized)
return;
_initialized = true;
DemoNav.Pushed += (s, ev) => AddLog($"Pushed → {ev.Page?.Header}");
DemoNav.Popped += (s, ev) => AddLog($"Popped ← {ev.Page?.Header}");
DemoNav.PoppedToRoot += (s, ev) => AddLog("PoppedToRoot");

5
samples/ControlCatalog/Pages/NavigationPage/NavigationPageFirstLookPage.xaml.cs

@ -6,6 +6,7 @@ namespace ControlCatalog.Pages
{
public partial class NavigationPageFirstLookPage : UserControl
{
private bool _initialized;
private int _pageCount;
public NavigationPageFirstLookPage()
@ -16,6 +17,10 @@ namespace ControlCatalog.Pages
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
if (_initialized)
return;
_initialized = true;
await DemoNav.PushAsync(NavigationDemoHelper.MakePage("Home", "Welcome!\nUse the buttons to push and pop pages.", 0), null);
UpdateStatus();
}

6
samples/ControlCatalog/Pages/NavigationPage/NavigationPageGesturePage.xaml.cs

@ -7,6 +7,8 @@ namespace ControlCatalog.Pages
{
public partial class NavigationPageGesturePage : UserControl
{
private bool _initialized;
public NavigationPageGesturePage()
{
InitializeComponent();
@ -16,6 +18,10 @@ namespace ControlCatalog.Pages
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
if (_initialized)
return;
_initialized = true;
await DemoNav.PushAsync(NavigationDemoHelper.MakePage("Page 1", "← Drag from the left edge to go back", 0), null);
await DemoNav.PushAsync(NavigationDemoHelper.MakePage("Page 2", "← Drag from the left edge to go back", 1), null);
await DemoNav.PushAsync(NavigationDemoHelper.MakePage("Page 3", "← Drag from the left edge to go back", 2), null);

5
samples/ControlCatalog/Pages/NavigationPage/NavigationPageInteractiveHeaderPage.xaml.cs

@ -37,6 +37,7 @@ namespace ControlCatalog.Pages
];
private readonly ObservableCollection<ContactItem> _filteredItems = new(AllContacts);
private bool _initialized;
private string _searchText = "";
public NavigationPageInteractiveHeaderPage()
@ -47,6 +48,10 @@ namespace ControlCatalog.Pages
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
if (_initialized)
return;
_initialized = true;
var headerGrid = new Grid
{
ColumnDefinitions = new ColumnDefinitions("*, Auto"),

5
samples/ControlCatalog/Pages/NavigationPage/NavigationPageModalPage.xaml.cs

@ -7,6 +7,7 @@ namespace ControlCatalog.Pages
{
public partial class NavigationPageModalPage : UserControl
{
private bool _initialized;
private int _modalCount;
public NavigationPageModalPage()
@ -17,6 +18,10 @@ namespace ControlCatalog.Pages
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
if (_initialized)
return;
_initialized = true;
await DemoNav.PushAsync(NavigationDemoHelper.MakePage("Home", "Use Push Modal to show a modal on top.", 0), null);
}

8
samples/ControlCatalog/Pages/NavigationPage/NavigationPageModalTransitionsPage.xaml.cs

@ -20,6 +20,7 @@ namespace ControlCatalog.Pages
];
private int _modalCount;
private bool _initialized;
public NavigationPageModalTransitionsPage()
{
@ -29,6 +30,13 @@ namespace ControlCatalog.Pages
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
if (_initialized)
{
UpdateTransition();
return;
}
_initialized = true;
await DemoNav.PushAsync(new ContentPage
{
Header = "Modal Transitions",

95
samples/ControlCatalog/Pages/NavigationPage/NavigationPageMvvmNavigation.cs

@ -0,0 +1,95 @@
using System;
using System.Threading.Tasks;
using Avalonia.Controls;
using MiniMvvm;
namespace ControlCatalog.Pages
{
internal interface ISampleNavigationService
{
event EventHandler<NavigationStateChangedEventArgs>? StateChanged;
Task NavigateToAsync(ViewModelBase viewModel);
Task GoBackAsync();
Task PopToRootAsync();
}
internal interface ISamplePageFactory
{
ContentPage CreatePage(ViewModelBase viewModel);
}
internal sealed class NavigationStateChangedEventArgs : EventArgs
{
public NavigationStateChangedEventArgs(string currentPageHeader, int navigationDepth, string lastAction)
{
CurrentPageHeader = currentPageHeader;
NavigationDepth = navigationDepth;
LastAction = lastAction;
}
public string CurrentPageHeader { get; }
public int NavigationDepth { get; }
public string LastAction { get; }
}
internal sealed class SampleNavigationService : ISampleNavigationService
{
private readonly NavigationPage _navigationPage;
private readonly ISamplePageFactory _pageFactory;
public SampleNavigationService(NavigationPage navigationPage, ISamplePageFactory pageFactory)
{
_navigationPage = navigationPage;
_pageFactory = pageFactory;
_navigationPage.Pushed += (_, e) => PublishState($"Pushed {e.Page?.Header}");
_navigationPage.Popped += (_, e) => PublishState($"Popped {e.Page?.Header}");
_navigationPage.PoppedToRoot += (_, _) => PublishState("Popped to root");
}
public event EventHandler<NavigationStateChangedEventArgs>? StateChanged;
public async Task NavigateToAsync(ViewModelBase viewModel)
{
var page = _pageFactory.CreatePage(viewModel);
await _navigationPage.PushAsync(page);
}
public async Task GoBackAsync()
{
if (_navigationPage.NavigationStack.Count <= 1)
{
PublishState("Already at the root page");
return;
}
await _navigationPage.PopAsync();
}
public async Task PopToRootAsync()
{
if (_navigationPage.NavigationStack.Count <= 1)
{
PublishState("Already at the root page");
return;
}
await _navigationPage.PopToRootAsync();
}
private void PublishState(string lastAction)
{
var header = _navigationPage.CurrentPage?.Header?.ToString() ?? "None";
StateChanged?.Invoke(this, new NavigationStateChangedEventArgs(
header,
_navigationPage.NavigationStack.Count,
lastAction));
}
}
}

86
samples/ControlCatalog/Pages/NavigationPage/NavigationPageMvvmPage.xaml

@ -0,0 +1,86 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pages="using:ControlCatalog.Pages"
x:Class="ControlCatalog.Pages.NavigationPageMvvmPage"
x:DataType="pages:NavigationPageMvvmShellViewModel">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="300">
<StackPanel Margin="12" Spacing="12">
<TextBlock Text="MVVM Pattern"
FontSize="16"
FontWeight="SemiBold"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock TextWrapping="Wrap"
FontSize="12"
Opacity="0.75"
Text="This sample keeps NavigationPage in the view, sends commands from view models, routes push or pop operations through ISampleNavigationService, and resolves pages in a separate SamplePageFactory." />
<Separator />
<TextBlock Text="Selected Project"
FontSize="13"
FontWeight="SemiBold" />
<ListBox ItemsSource="{Binding Projects}"
SelectedItem="{Binding SelectedProject, Mode=TwoWay}"
MaxHeight="180">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="pages:ProjectCardViewModel">
<StackPanel Margin="0,2" Spacing="2">
<TextBlock Text="{Binding Name}"
FontWeight="SemiBold" />
<TextBlock Text="{Binding Owner}"
FontSize="11"
Opacity="0.65" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="Open Selected Project"
Command="{Binding OpenSelectedProjectCommand}" />
<Button Content="Back"
Command="{Binding GoBackCommand}" />
<Button Content="Pop To Root"
Command="{Binding PopToRootCommand}" />
<Separator />
<TextBlock Text="Navigation State"
FontSize="13"
FontWeight="SemiBold" />
<TextBlock Text="{Binding CurrentPageHeader, StringFormat=Current page: {0}}"
Opacity="0.75" />
<TextBlock Text="{Binding NavigationDepth, StringFormat=Stack depth: {0}}"
Opacity="0.75" />
<TextBlock Text="{Binding LastAction, StringFormat=Last action: {0}}"
Opacity="0.75"
TextWrapping="Wrap" />
<Separator />
<TextBlock Text="Selected Details"
FontSize="13"
FontWeight="SemiBold" />
<TextBlock Text="{Binding SelectedProject.Status, StringFormat=Status: {0}}"
Opacity="0.75" />
<TextBlock Text="{Binding SelectedProject.NextMilestone, StringFormat=Next milestone: {0}}"
Opacity="0.75"
TextWrapping="Wrap" />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right"
Width="1"
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<Border Margin="12"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1"
CornerRadius="6"
ClipToBounds="True">
<NavigationPage x:Name="DemoNav" />
</Border>
</DockPanel>
</UserControl>

32
samples/ControlCatalog/Pages/NavigationPage/NavigationPageMvvmPage.xaml.cs

@ -0,0 +1,32 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public partial class NavigationPageMvvmPage : UserControl
{
private readonly NavigationPageMvvmShellViewModel _viewModel;
private bool _initialized;
public NavigationPageMvvmPage()
{
InitializeComponent();
ISamplePageFactory pageFactory = new SamplePageFactory();
ISampleNavigationService navigationService = new SampleNavigationService(DemoNav, pageFactory);
_viewModel = new NavigationPageMvvmShellViewModel(navigationService);
DataContext = _viewModel;
Loaded += OnLoaded;
}
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
if (_initialized)
return;
_initialized = true;
await _viewModel.InitializeAsync();
}
}
}

252
samples/ControlCatalog/Pages/NavigationPage/NavigationPageMvvmPageFactory.cs

@ -0,0 +1,252 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.Media;
using MiniMvvm;
namespace ControlCatalog.Pages
{
internal sealed class SamplePageFactory : ISamplePageFactory
{
public ContentPage CreatePage(ViewModelBase viewModel) =>
viewModel switch
{
WorkspaceViewModel workspace => CreateWorkspacePage(workspace),
ProjectDetailViewModel detail => CreateProjectDetailPage(detail),
ProjectActivityViewModel activity => CreateProjectActivityPage(activity),
_ => throw new InvalidOperationException($"Unsupported view model: {viewModel.GetType().Name}")
};
private static ContentPage CreateWorkspacePage(WorkspaceViewModel viewModel)
{
var stack = new StackPanel
{
Margin = new Thickness(20),
Spacing = 14,
};
stack.Children.Add(new TextBlock
{
Text = viewModel.Title,
FontSize = 24,
FontWeight = FontWeight.Bold,
});
stack.Children.Add(new TextBlock
{
Text = viewModel.Description,
FontSize = 13,
Opacity = 0.75,
TextWrapping = TextWrapping.Wrap,
});
stack.Children.Add(new ItemsControl
{
ItemsSource = viewModel.Projects,
ItemTemplate = new FuncDataTemplate<ProjectCardViewModel>((item, _) =>
{
if (item == null)
return new TextBlock();
var accentBrush = new SolidColorBrush(item.AccentColor);
var statusBadge = new Border
{
Background = accentBrush,
CornerRadius = new CornerRadius(999),
Padding = new Thickness(10, 4),
Child = new TextBlock
{
Text = item.Status,
Foreground = Brushes.White,
FontSize = 11,
FontWeight = FontWeight.SemiBold,
}
};
DockPanel.SetDock(statusBadge, Dock.Right);
var header = new DockPanel();
header.Children.Add(statusBadge);
header.Children.Add(new TextBlock
{
Text = item.Name,
FontSize = 17,
FontWeight = FontWeight.SemiBold,
});
return new Border
{
Background = new SolidColorBrush(Color.FromArgb(20, item.AccentColor.R, item.AccentColor.G, item.AccentColor.B)),
BorderBrush = accentBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(14),
Margin = new Thickness(0, 0, 0, 8),
Child = new StackPanel
{
Spacing = 8,
Children =
{
header,
new TextBlock
{
Text = item.Summary,
FontSize = 13,
Opacity = 0.72,
TextWrapping = TextWrapping.Wrap,
},
new TextBlock
{
Text = $"Owner: {item.Owner} • Next: {item.NextMilestone}",
FontSize = 12,
Opacity = 0.6,
TextWrapping = TextWrapping.Wrap,
},
new Button
{
Content = "Open Project",
HorizontalAlignment = HorizontalAlignment.Left,
Command = item.OpenCommand,
}
}
}
};
})
});
var page = new ContentPage
{
Header = "Workspace",
Content = new ScrollViewer { Content = stack },
HorizontalContentAlignment = HorizontalAlignment.Stretch,
VerticalContentAlignment = VerticalAlignment.Stretch,
};
NavigationPage.SetHasBackButton(page, false);
return page;
}
private static ContentPage CreateProjectDetailPage(ProjectDetailViewModel viewModel)
{
var accentBrush = new SolidColorBrush(viewModel.AccentColor);
var panel = new StackPanel
{
Margin = new Thickness(24, 20),
Spacing = 12,
};
panel.Children.Add(new Border
{
Background = accentBrush,
CornerRadius = new CornerRadius(999),
Padding = new Thickness(12, 5),
HorizontalAlignment = HorizontalAlignment.Left,
Child = new TextBlock
{
Text = viewModel.Status,
Foreground = Brushes.White,
FontSize = 11,
FontWeight = FontWeight.SemiBold,
}
});
panel.Children.Add(new TextBlock
{
Text = viewModel.Name,
FontSize = 26,
FontWeight = FontWeight.Bold,
});
panel.Children.Add(new TextBlock
{
Text = viewModel.Summary,
FontSize = 14,
Opacity = 0.78,
TextWrapping = TextWrapping.Wrap,
});
panel.Children.Add(new TextBlock
{
Text = $"Owner: {viewModel.Owner}",
FontSize = 13,
Opacity = 0.68,
});
panel.Children.Add(new TextBlock
{
Text = $"Next milestone: {viewModel.NextMilestone}",
FontSize = 13,
Opacity = 0.68,
TextWrapping = TextWrapping.Wrap,
});
panel.Children.Add(new Separator { Margin = new Thickness(0, 4) });
panel.Children.Add(new TextBlock
{
Text = "This page is resolved by SamplePageFactory from a ProjectDetailViewModel. The view model only requests navigation through ISampleNavigationService.",
FontSize = 12,
Opacity = 0.7,
TextWrapping = TextWrapping.Wrap,
});
panel.Children.Add(new Button
{
Content = "Open Activity",
HorizontalAlignment = HorizontalAlignment.Left,
Command = viewModel.OpenActivityCommand,
});
return new ContentPage
{
Header = viewModel.Name,
Background = new SolidColorBrush(Color.FromArgb(18, viewModel.AccentColor.R, viewModel.AccentColor.G, viewModel.AccentColor.B)),
Content = new ScrollViewer { Content = panel },
HorizontalContentAlignment = HorizontalAlignment.Stretch,
VerticalContentAlignment = VerticalAlignment.Stretch,
};
}
private static ContentPage CreateProjectActivityPage(ProjectActivityViewModel viewModel)
{
var panel = new StackPanel
{
Margin = new Thickness(24, 20),
Spacing = 10,
};
panel.Children.Add(new TextBlock
{
Text = "Activity Timeline",
FontSize = 24,
FontWeight = FontWeight.Bold,
});
panel.Children.Add(new TextBlock
{
Text = $"Recent updates for {viewModel.Name}. This page was opened from a command on the detail view model.",
FontSize = 13,
Opacity = 0.74,
TextWrapping = TextWrapping.Wrap,
});
foreach (var item in viewModel.Items)
{
panel.Children.Add(new Border
{
Background = new SolidColorBrush(Color.FromArgb(14, viewModel.AccentColor.R, viewModel.AccentColor.G, viewModel.AccentColor.B)),
BorderBrush = new SolidColorBrush(Color.FromArgb(80, viewModel.AccentColor.R, viewModel.AccentColor.G, viewModel.AccentColor.B)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(6),
Padding = new Thickness(12, 10),
Child = new TextBlock
{
Text = item,
FontSize = 13,
TextWrapping = TextWrapping.Wrap,
}
});
}
return new ContentPage
{
Header = "Activity",
Content = new ScrollViewer { Content = panel },
HorizontalContentAlignment = HorizontalAlignment.Stretch,
VerticalContentAlignment = VerticalAlignment.Stretch,
};
}
}
}

238
samples/ControlCatalog/Pages/NavigationPage/NavigationPageMvvmViewModels.cs

@ -0,0 +1,238 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Media;
using MiniMvvm;
namespace ControlCatalog.Pages
{
public sealed class NavigationPageMvvmShellViewModel : ViewModelBase
{
private readonly ISampleNavigationService _navigationService;
private string _currentPageHeader = "Not initialized";
private string _lastAction = "Waiting for first load";
private int _navigationDepth;
private ProjectCardViewModel? _selectedProject;
internal NavigationPageMvvmShellViewModel(ISampleNavigationService navigationService)
{
_navigationService = navigationService;
_navigationService.StateChanged += OnStateChanged;
Workspace = new WorkspaceViewModel(CreateProjects(navigationService));
SelectedProject = Workspace.Projects[0];
OpenSelectedProjectCommand = MiniCommand.CreateFromTask(OpenSelectedProjectAsync);
GoBackCommand = MiniCommand.CreateFromTask(_navigationService.GoBackAsync);
PopToRootCommand = MiniCommand.CreateFromTask(_navigationService.PopToRootAsync);
}
internal WorkspaceViewModel Workspace { get; }
public IReadOnlyList<ProjectCardViewModel> Projects => Workspace.Projects;
public MiniCommand OpenSelectedProjectCommand { get; }
public MiniCommand GoBackCommand { get; }
public MiniCommand PopToRootCommand { get; }
public string CurrentPageHeader
{
get => _currentPageHeader;
set => this.RaiseAndSetIfChanged(ref _currentPageHeader, value);
}
public int NavigationDepth
{
get => _navigationDepth;
set => this.RaiseAndSetIfChanged(ref _navigationDepth, value);
}
public string LastAction
{
get => _lastAction;
set => this.RaiseAndSetIfChanged(ref _lastAction, value);
}
public ProjectCardViewModel? SelectedProject
{
get => _selectedProject;
set => this.RaiseAndSetIfChanged(ref _selectedProject, value);
}
public Task InitializeAsync() => _navigationService.NavigateToAsync(Workspace);
private async Task OpenSelectedProjectAsync()
{
if (SelectedProject == null)
return;
await SelectedProject.OpenCommandAsync();
}
private void OnStateChanged(object? sender, NavigationStateChangedEventArgs e)
{
CurrentPageHeader = e.CurrentPageHeader;
NavigationDepth = e.NavigationDepth;
LastAction = e.LastAction;
}
private static IReadOnlyList<ProjectCardViewModel> CreateProjects(ISampleNavigationService navigationService) =>
new[]
{
new ProjectCardViewModel(
"Release Radar",
"Marta Collins",
"Ready for QA",
"Coordinate the 11.0 release checklist and lock down the final regression window.",
"Freeze build on Friday",
Color.Parse("#0063B1"),
navigationService,
new[]
{
"Release notes draft updated with accessibility fixes.",
"Package validation finished for desktop artifacts.",
"Remaining task, confirm browser smoke test coverage."
}),
new ProjectCardViewModel(
"Support Console",
"Jae Kim",
"Active Sprint",
"Consolidate customer incidents into a triage board and route them to platform owners.",
"Triage review in 2 hours",
Color.Parse("#0F7B0F"),
navigationService,
new[]
{
"Five customer reports grouped under input routing.",
"Hotfix candidate approved for preview branch.",
"Awaiting macOS verification on native embed scenarios."
}),
new ProjectCardViewModel(
"Docs Refresh",
"Anika Patel",
"Needs Review",
"Refresh navigation samples and walkthrough docs so the gallery matches the current API.",
"Sample review tomorrow",
Color.Parse("#8E562E"),
navigationService,
new[]
{
"NavigationPage sample matrix reviewed with design.",
"MVVM walkthrough draft linked from the docs backlog.",
"Outstanding task, capture one more screenshot for drawer navigation."
}),
};
}
internal sealed class WorkspaceViewModel : ViewModelBase
{
public WorkspaceViewModel(IReadOnlyList<ProjectCardViewModel> projects)
{
Projects = projects;
}
public string Title => "Team Workspace";
public string Description =>
"Each card is a project view model with its own command. The command asks ISampleNavigationService to navigate with the next view model, and SamplePageFactory resolves the matching ContentPage.";
public IReadOnlyList<ProjectCardViewModel> Projects { get; }
}
public sealed class ProjectCardViewModel : ViewModelBase
{
private readonly ISampleNavigationService _navigationService;
internal ProjectCardViewModel(
string name,
string owner,
string status,
string summary,
string nextMilestone,
Color accentColor,
ISampleNavigationService navigationService,
IReadOnlyList<string> activityItems)
{
Name = name;
Owner = owner;
Status = status;
Summary = summary;
NextMilestone = nextMilestone;
AccentColor = accentColor;
ActivityItems = activityItems;
_navigationService = navigationService;
OpenCommand = MiniCommand.CreateFromTask(OpenCommandAsync);
}
public string Name { get; }
public string Owner { get; }
public string Status { get; }
public string Summary { get; }
public string NextMilestone { get; }
public Color AccentColor { get; }
public IReadOnlyList<string> ActivityItems { get; }
public MiniCommand OpenCommand { get; }
public Task OpenCommandAsync()
{
return _navigationService.NavigateToAsync(new ProjectDetailViewModel(this, _navigationService));
}
}
internal sealed class ProjectDetailViewModel : ViewModelBase
{
private readonly ProjectCardViewModel _project;
private readonly ISampleNavigationService _navigationService;
public ProjectDetailViewModel(ProjectCardViewModel project, ISampleNavigationService navigationService)
{
_project = project;
_navigationService = navigationService;
OpenActivityCommand = MiniCommand.CreateFromTask(OpenActivityAsync);
}
public string Name => _project.Name;
public string Owner => _project.Owner;
public string Status => _project.Status;
public string Summary => _project.Summary;
public string NextMilestone => _project.NextMilestone;
public Color AccentColor => _project.AccentColor;
public MiniCommand OpenActivityCommand { get; }
private Task OpenActivityAsync()
{
return _navigationService.NavigateToAsync(new ProjectActivityViewModel(_project));
}
}
internal sealed class ProjectActivityViewModel : ViewModelBase
{
public ProjectActivityViewModel(ProjectCardViewModel project)
{
Name = project.Name;
AccentColor = project.AccentColor;
Items = project.ActivityItems;
}
public string Name { get; }
public Color AccentColor { get; }
public IReadOnlyList<string> Items { get; }
}
}

6
samples/ControlCatalog/Pages/NavigationPage/NavigationPagePassDataPage.xaml.cs

@ -19,6 +19,7 @@ namespace ControlCatalog.Pages
new("Emma Brown", "UX Researcher", "Germany", Color.Parse("#F44336")),
};
private bool _initialized;
private bool _isLoaded;
public NavigationPagePassDataPage()
@ -31,6 +32,11 @@ namespace ControlCatalog.Pages
{
_isLoaded = true;
if (_initialized)
return;
_initialized = true;
DemoNav.Pushed += (s, ev) => AppendNavigationLog($"Pushed → {ev.Page?.Header}");
DemoNav.Popped += (s, ev) => AppendNavigationLog($"Popped ← {ev.Page?.Header}");

24
samples/ControlCatalog/Pages/NavigationPage/NavigationPageScrollAwarePage.xaml.cs

@ -21,8 +21,10 @@ namespace ControlCatalog.Pages
}
private IDisposable? _scrollSubscription;
private ScrollViewer? _scrollViewer;
private double _lastScrollY;
private double _currentTranslateY;
private bool _initialized;
public NavigationPageScrollAwarePage()
{
@ -33,12 +35,21 @@ namespace ControlCatalog.Pages
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
var scrollViewer = new ScrollViewer { Content = BuildLongContent() };
if (_initialized)
{
if (_scrollViewer != null)
Dispatcher.UIThread.Post(() => AttachScrollWatcher(_scrollViewer), DispatcherPriority.Loaded);
return;
}
_initialized = true;
_scrollViewer = new ScrollViewer { Content = BuildLongContent() };
var rootPage = new ContentPage
{
Header = "Scroll to Hide Bar",
Content = scrollViewer,
Content = _scrollViewer,
HorizontalContentAlignment = HorizontalAlignment.Stretch,
VerticalContentAlignment = VerticalAlignment.Stretch,
};
@ -46,13 +57,18 @@ namespace ControlCatalog.Pages
NavigationPage.SetBarLayoutBehavior(rootPage, BarLayoutBehavior.Overlay);
await DemoNav.PushAsync(rootPage, null);
Dispatcher.UIThread.Post(() => AttachScrollWatcher(scrollViewer), DispatcherPriority.Loaded);
Dispatcher.UIThread.Post(() => AttachScrollWatcher(_scrollViewer), DispatcherPriority.Loaded);
}
private void OnUnloaded(object? sender, RoutedEventArgs e) => DetachScrollWatcher();
private void AttachScrollWatcher(ScrollViewer sv)
private void AttachScrollWatcher(ScrollViewer? sv)
{
if (sv == null)
return;
DetachScrollWatcher();
var navBar = DemoNav.GetVisualDescendants().OfType<Border>()
.FirstOrDefault(b => b.Name == "PART_NavigationBar");
if (navBar == null)

5
samples/ControlCatalog/Pages/NavigationPage/NavigationPageStackPage.xaml.cs

@ -9,6 +9,7 @@ namespace ControlCatalog.Pages
{
public partial class NavigationPageStackPage : UserControl
{
private bool _initialized;
private int _pageCount;
public NavigationPageStackPage()
@ -19,6 +20,10 @@ namespace ControlCatalog.Pages
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
if (_initialized)
return;
_initialized = true;
DemoNav.Pushed += (s, ev) => RefreshStack();
DemoNav.Popped += (s, ev) => RefreshStack();
DemoNav.PoppedToRoot += (s, ev) => RefreshStack();

5
samples/ControlCatalog/Pages/NavigationPage/NavigationPageTitlePage.xaml.cs

@ -8,6 +8,7 @@ namespace ControlCatalog.Pages
{
public partial class NavigationPageTitlePage : UserControl
{
private bool _initialized;
private int _pageCount;
public NavigationPageTitlePage()
@ -18,6 +19,10 @@ namespace ControlCatalog.Pages
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
if (_initialized)
return;
_initialized = true;
await DemoNav.PushAsync(NavigationDemoHelper.MakePage("Home", "Choose a header type and tap 'Push'.", 0), null);
StatusText.Text = "Current: Home";
}

5
samples/ControlCatalog/Pages/NavigationPage/NavigationPageToolbarPage.xaml.cs

@ -7,6 +7,7 @@ namespace ControlCatalog.Pages
{
public partial class NavigationPageToolbarPage : UserControl
{
private bool _initialized;
private int _pageCount;
private int _itemCount;
private ContentPage? _rootPage;
@ -20,6 +21,10 @@ namespace ControlCatalog.Pages
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
if (_initialized)
return;
_initialized = true;
_rootPage = NavigationDemoHelper.MakePage("CommandBar Demo",
"Use the panel to add CommandBar items.\nTop items appear inside the navigation bar.\nBottom items appear as a separate bar.", 0);
ApplyPosition();

8
samples/ControlCatalog/Pages/NavigationPage/NavigationPageTransitionsPage.xaml.cs

@ -8,6 +8,7 @@ namespace ControlCatalog.Pages
{
public partial class NavigationPageTransitionsPage : UserControl
{
private bool _initialized;
private int _pageCount;
public NavigationPageTransitionsPage()
@ -18,6 +19,13 @@ namespace ControlCatalog.Pages
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
if (_initialized)
{
UpdateTransition();
return;
}
_initialized = true;
await DemoNav.PushAsync(NavigationDemoHelper.MakePage("Transitions", "Choose a transition type and push pages.", 0), null);
UpdateTransition();
}

6
samples/ControlCatalog/Pages/NavigationPage/PulseAppPage.xaml.cs

@ -99,7 +99,7 @@ public partial class PulseAppPage : UserControl
Content = homeView,
Background = new SolidColorBrush(BgDashboard),
Header = "Home",
Icon = "M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z",
Icon = Geometry.Parse("M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"),
};
var workoutsPage = new ContentPage
@ -107,7 +107,7 @@ public partial class PulseAppPage : UserControl
Content = new PulseWorkoutsView(),
Background = new SolidColorBrush(BgDashboard),
Header = "Workouts",
Icon = "M20.57 14.86L22 13.43 20.57 12 17 15.57 8.43 7 12 3.43 10.57 2 9.14 3.43 7.71 2 5.57 4.14 4.14 2.71 2.71 4.14l1.43 1.43L2 7.71l1.43 1.43L2 10.57 3.43 12 7 8.43 15.57 17 12 20.57 13.43 22l1.43-1.43L16.29 22l2.14-2.14 1.43 1.43 1.43-1.43-1.43-1.43L22 16.29z",
Icon = Geometry.Parse("M20.57 14.86L22 13.43 20.57 12 17 15.57 8.43 7 12 3.43 10.57 2 9.14 3.43 7.71 2 5.57 4.14 4.14 2.71 2.71 4.14l1.43 1.43L2 7.71l1.43 1.43L2 10.57 3.43 12 7 8.43 15.57 17 12 20.57 13.43 22l1.43-1.43L16.29 22l2.14-2.14 1.43 1.43 1.43-1.43-1.43-1.43L22 16.29z"),
};
var profilePage = new ContentPage
@ -115,7 +115,7 @@ public partial class PulseAppPage : UserControl
Content = new PulseProfileView(),
Background = new SolidColorBrush(BgDashboard),
Header = "Profile",
Icon = "M12 2C9.243 2 7 4.243 7 7s2.243 5 5 5 5-2.243 5-5-2.243-5-5-5zM12 14c-5.523 0-10 3.582-10 8a1 1 0 001 1h18a1 1 0 001-1c0-4.418-4.477-8-10-8z",
Icon = Geometry.Parse("M12 2C9.243 2 7 4.243 7 7s2.243 5 5 5 5-2.243 5-5-2.243-5-5-5zM12 14c-5.523 0-10 3.582-10 8a1 1 0 001 1h18a1 1 0 001-1c0-4.418-4.477-8-10-8z"),
};
tp.Pages = new ObservableCollection<Page> { homePage, workoutsPage, profilePage };

35
samples/ControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml

@ -48,6 +48,7 @@
<UserControl.Styles>
<!-- Nav back button: square bordered box -->
<Style Selector="NavigationPage /template/ Button#PART_BackButton">
<Setter Property="Margin" Value="4,0,0,0"/>
<Setter Property="Width" Value="32"/>
<Setter Property="Height" Value="32"/>
<Setter Property="Padding" Value="0"/>
@ -55,22 +56,43 @@
<Setter Property="BorderBrush" Value="{StaticResource RetroCyan}"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="Background" Value="{StaticResource RetroSurface}"/>
<Setter Property="Foreground" Value="{StaticResource RetroCyan}"/>
</Style>
<Style Selector="NavigationPage /template/ Button#PART_BackButton PathIcon">
<Style Selector="NavigationPage /template/ Button#PART_BackButton:pointerover">
<Setter Property="Background" Value="#3d2b6e"/>
<Setter Property="BorderBrush" Value="{StaticResource RetroCyan}"/>
<Setter Property="Foreground" Value="{StaticResource RetroCyan}"/>
</Style>
<Style Selector="NavigationPage /template/ Button#PART_BackButton:pressed">
<Setter Property="Background" Value="#1e1035"/>
<Setter Property="BorderBrush" Value="{StaticResource RetroCyan}"/>
<Setter Property="Foreground" Value="{StaticResource RetroCyan}"/>
</Style>
<Style Selector="NavigationPage /template/ Path#PART_BackButtonDefaultIcon">
<Setter Property="Fill" Value="{StaticResource RetroCyan}"/>
<Setter Property="Width" Value="16"/>
<Setter Property="Height" Value="16"/>
</Style>
<Style Selector="NavigationPage /template/ Button#PART_BackButton:pointerover Path#PART_BackButtonDefaultIcon">
<Setter Property="Fill" Value="{StaticResource RetroCyan}"/>
</Style>
<Style Selector="NavigationPage /template/ Button#PART_BackButton:pressed Path#PART_BackButtonDefaultIcon">
<Setter Property="Fill" Value="{StaticResource RetroCyan}"/>
</Style>
<Style Selector="NavigationPage /template/ Button#PART_BackButton /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#cc2d1b4e"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource RetroCyan}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="Padding" Value="0"/>
</Style>
<Style Selector="NavigationPage /template/ Button#PART_BackButton:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#cc3d2b6e"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource RetroCyan}"/>
</Style>
<Style Selector="NavigationPage /template/ Button#PART_BackButton:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#cc1e1035"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource RetroCyan}"/>
</Style>
<!-- Primary purple button (INSERT COIN TO PLAY) -->
@ -153,10 +175,5 @@
<Style Selector="Button.retro-fab:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#cccc00"/>
</Style>
<!-- Nav bar back button: left margin so it doesn't hug the edge -->
<Style Selector="NavigationPage /template/ Button#PART_BackButton">
<Setter Property="Margin" Value="4,0,0,0"/>
</Style>
</UserControl.Styles>
</UserControl>

43
samples/ControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml.cs

@ -51,11 +51,30 @@ public partial class RetroGamingAppPage : UserControl
_infoPanel.IsVisible = Bounds.Width >= 650;
}
void ApplyHomeNavigationBarAppearance()
{
if (_nav == null)
return;
_nav.Resources["NavigationBarBackground"] = new SolidColorBrush(SurfaceColor);
_nav.Resources["NavigationBarForeground"] = new SolidColorBrush(CyanColor);
}
void ApplyDetailNavigationBarAppearance()
{
if (_nav == null)
return;
_nav.Resources["NavigationBarBackground"] = Brushes.Transparent;
_nav.Resources["NavigationBarForeground"] = new SolidColorBrush(CyanColor);
}
ContentPage BuildHomePage()
{
var page = new ContentPage { Background = new SolidColorBrush(BgColor) };
page.Header = BuildPixelArcadeLogo();
NavigationPage.SetTopCommandBar(page, BuildNavBarRight());
ApplyHomeNavigationBarAppearance();
var panel = new Panel();
panel.Children.Add(BuildHomeTabbedPage());
@ -174,7 +193,7 @@ public partial class RetroGamingAppPage : UserControl
var homeTab = new ContentPage
{
Header = "Home",
Icon = "M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z",
Icon = Geometry.Parse("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z"),
Background = new SolidColorBrush(BgColor),
Content = homeView,
};
@ -185,7 +204,7 @@ public partial class RetroGamingAppPage : UserControl
var gamesTab = new ContentPage
{
Header = "Games",
Icon = "M7.97,16L5,19C4.67,19.3 4.23,19.5 3.75,19.5A1.75,1.75 0 0,1 2,17.75V17.5L3,10.12C3.21,7.81 5.14,6 7.5,6H16.5C18.86,6 20.79,7.81 21,10.12L22,17.5V17.75A1.75,1.75 0 0,1 20.25,19.5C19.77,19.5 19.33,19.3 19,19L16.03,16H7.97M7,9V11H5V13H7V15H9V13H11V11H9V9H7M14.5,12A1.5,1.5 0 0,0 13,13.5A1.5,1.5 0 0,0 14.5,15A1.5,1.5 0 0,0 16,13.5A1.5,1.5 0 0,0 14.5,12M17.5,9A1.5,1.5 0 0,0 16,10.5A1.5,1.5 0 0,0 17.5,12A1.5,1.5 0 0,0 19,10.5A1.5,1.5 0 0,0 17.5,9Z",
Icon = Geometry.Parse("M7.97,16L5,19C4.67,19.3 4.23,19.5 3.75,19.5A1.75,1.75 0 0,1 2,17.75V17.5L3,10.12C3.21,7.81 5.14,6 7.5,6H16.5C18.86,6 20.79,7.81 21,10.12L22,17.5V17.75A1.75,1.75 0 0,1 20.25,19.5C19.77,19.5 19.33,19.3 19,19L16.03,16H7.97M7,9V11H5V13H7V15H9V13H11V11H9V9H7M14.5,12A1.5,1.5 0 0,0 13,13.5A1.5,1.5 0 0,0 14.5,15A1.5,1.5 0 0,0 16,13.5A1.5,1.5 0 0,0 14.5,12M17.5,9A1.5,1.5 0 0,0 16,10.5A1.5,1.5 0 0,0 17.5,12A1.5,1.5 0 0,0 19,10.5A1.5,1.5 0 0,0 17.5,9Z"),
Background = new SolidColorBrush(BgColor),
Content = gamesView,
};
@ -193,7 +212,7 @@ public partial class RetroGamingAppPage : UserControl
var favTab = new ContentPage
{
Header = "Favorites",
Icon = "M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z",
Icon = Geometry.Parse("M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z"),
Background = new SolidColorBrush(BgColor),
Content = new RetroGamingFavoritesView(),
};
@ -201,7 +220,7 @@ public partial class RetroGamingAppPage : UserControl
var profileTab = new ContentPage
{
Header = "Profile",
Icon = "M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z",
Icon = Geometry.Parse("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z"),
Background = new SolidColorBrush(BgColor),
Content = new RetroGamingProfileView(),
};
@ -260,7 +279,8 @@ public partial class RetroGamingAppPage : UserControl
async void PushDetailPage(string gameTitle)
{
if (_nav == null) return;
if (_nav == null)
return;
var detailView = new RetroGamingDetailView(gameTitle);
@ -271,8 +291,13 @@ public partial class RetroGamingAppPage : UserControl
};
NavigationPage.SetBarLayoutBehavior(page, BarLayoutBehavior.Overlay);
page.NavigatedTo += (_, _) => { if (_nav != null) _nav.Resources["NavigationBarBackground"] = Brushes.Transparent; };
page.NavigatedFrom += (_, _) => { if (_nav != null) _nav.Resources["NavigationBarBackground"] = new SolidColorBrush(SurfaceColor); };
page.Navigating += args =>
{
if (args.NavigationType == NavigationType.Pop)
ApplyHomeNavigationBarAppearance();
return Task.CompletedTask;
};
var cmdBar = new StackPanel
{
@ -301,6 +326,10 @@ public partial class RetroGamingAppPage : UserControl
cmdBar.Children.Add(shareBtn);
NavigationPage.SetTopCommandBar(page, cmdBar);
ApplyDetailNavigationBarAppearance();
await _nav.PushAsync(page);
if (!ReferenceEquals(_nav.CurrentPage, page))
ApplyHomeNavigationBarAppearance();
}
}

7
samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs

@ -31,6 +31,13 @@ namespace ControlCatalog.Pages
("Appearance", "Custom Templates",
"Override pip item templates to create squares, pills, numbers, or any custom shape.",
() => new PipsPagerCustomTemplatesPage()),
("Showcases", "Care Companion",
"A health care onboarding flow using PipsPager as the page indicator for a CarouselPage.",
() => new CareCompanionAppPage()),
("Showcases", "Sanctuary",
"A travel discovery app using PipsPager as the page indicator for a CarouselPage.",
() => new SanctuaryShowcasePage()),
};
public PipsPagerPage()

2
samples/ControlCatalog/Pages/TabbedDemoPage.xaml.cs

@ -18,7 +18,7 @@ namespace ControlCatalog.Pages
"Populate a TabbedPage by adding ContentPage objects directly to the Pages collection.",
() => new TabbedPageCollectionPage()),
("Populate", "Data Templates",
"Populate a TabbedPage with a data collection and a custom PageTemplate to render each item.",
"Bind TabbedPage to an ObservableCollection, add or remove tabs at runtime, and switch the page template.",
() => new TabbedPageDataTemplatePage()),
// Appearance

13
samples/ControlCatalog/Pages/TabbedPage/TabbedPageDataTemplatePage.xaml

@ -8,16 +8,25 @@
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock FontSize="12" Opacity="0.7" TextWrapping="Wrap"
Text="Bind a TabbedPage to a data collection and use PageTemplate to render each item as a ContentPage." />
Text="Bind a TabbedPage to a data collection, render each item with PageTemplate, and switch templates at runtime." />
<Separator />
<Button Content="Add Category" Click="OnAddCategory" HorizontalAlignment="Stretch" />
<Button Content="Remove Last" Click="OnRemoveCategory" HorizontalAlignment="Stretch" />
<Button Content="Switch Template" Click="OnSwitchTemplate" HorizontalAlignment="Stretch" />
<Separator />
<TextBlock x:Name="StatusText" Text="3 categories" Opacity="0.7" />
<TextBlock Text="Navigate" FontWeight="SemiBold" FontSize="13" />
<StackPanel Spacing="6">
<Button Content="Previous" Click="OnPrevious" HorizontalAlignment="Stretch" />
<Button Content="Next" Click="OnNext" HorizontalAlignment="Stretch" />
</StackPanel>
<Separator />
<TextBlock x:Name="StatusText" Text="3 tabs" Opacity="0.7" />
</StackPanel>
</ScrollViewer>

157
samples/ControlCatalog/Pages/TabbedPage/TabbedPageDataTemplatePage.xaml.cs

@ -1,5 +1,7 @@
using System.Collections.ObjectModel;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
@ -8,18 +10,32 @@ namespace ControlCatalog.Pages
{
public partial class TabbedPageDataTemplatePage : UserControl
{
private static readonly (string Name, string Color)[] CategoryData =
private sealed class CategoryViewModel
{
("Electronics", "#1565C0"),
("Books", "#2E7D32"),
("Clothing", "#6A1B9A"),
public string Name { get; }
public string Color { get; }
public CategoryViewModel(string name, string color)
{
Name = name;
Color = color;
}
}
private static readonly CategoryViewModel[] InitialData =
{
new("Electronics", "#1565C0"), new("Books", "#2E7D32"), new("Clothing", "#6A1B9A"),
};
private static readonly string[] AddNames = { "Sports", "Music", "Garden", "Toys", "Food" };
private static readonly string[] AddColors = { "#E53935", "#F57C00", "#00796B", "#E91E63", "#3F51B5" };
private static readonly CategoryViewModel[] AddData =
{
new("Sports", "#E53935"), new("Music", "#F57C00"), new("Garden", "#00796B"), new("Toys", "#E91E63"),
new("Food", "#3F51B5")
};
private readonly ObservableCollection<Page> _pages = new();
private readonly ObservableCollection<CategoryViewModel> _items = new();
private int _addCounter;
private bool _useDetailTemplate = true;
private TabbedPage? _tabbedPage;
public TabbedPageDataTemplatePage()
@ -30,48 +46,99 @@ namespace ControlCatalog.Pages
private void OnLoaded(object? sender, RoutedEventArgs e)
{
foreach (var (name, color) in CategoryData)
_pages.Add(CreatePage(name, color));
if (_tabbedPage != null)
return;
_addCounter = CategoryData.Length;
foreach (var vm in InitialData)
_items.Add(vm);
_addCounter = InitialData.Length;
_useDetailTemplate = true;
_tabbedPage = new TabbedPage
{
TabPlacement = TabPlacement.Top,
Pages = _pages
TabPlacement = TabPlacement.Top, ItemsSource = _items, PageTemplate = CreatePageTemplate()
};
_tabbedPage.SelectionChanged += OnSelectionChanged;
TabbedPageHost.Children.Add(_tabbedPage);
UpdateStatus();
}
private void OnAddCategory(object? sender, RoutedEventArgs e)
{
var idx = _addCounter % AddNames.Length;
var name = AddNames[idx] + (_addCounter >= AddNames.Length ? $" {_addCounter / AddNames.Length + 1}" : "");
_pages.Add(CreatePage(name, AddColors[idx]));
var idx = _addCounter % AddData.Length;
var vm = AddData[idx];
var suffix = _addCounter >= AddData.Length ? $" {_addCounter / AddData.Length + 1}" : "";
_items.Add(new CategoryViewModel(vm.Name + suffix, vm.Color));
_addCounter++;
UpdateStatus();
}
private void OnRemoveCategory(object? sender, RoutedEventArgs e)
{
if (_pages.Count > 0)
if (_items.Count > 0)
{
_pages.RemoveAt(_pages.Count - 1);
_items.RemoveAt(_items.Count - 1);
UpdateStatus();
}
}
private void OnSelectionChanged(object? sender, PageSelectionChangedEventArgs e) => UpdateStatus();
private void OnSwitchTemplate(object? sender, RoutedEventArgs e)
{
if (_tabbedPage == null)
return;
_useDetailTemplate = !_useDetailTemplate;
_tabbedPage.PageTemplate = CreatePageTemplate();
UpdateStatus();
}
private void OnPrevious(object? sender, RoutedEventArgs e)
{
if (_tabbedPage == null)
return;
if (_tabbedPage.SelectedIndex > 0)
_tabbedPage.SelectedIndex--;
}
private void OnNext(object? sender, RoutedEventArgs e)
{
if (_tabbedPage == null)
return;
if (_tabbedPage.SelectedIndex < _items.Count - 1)
_tabbedPage.SelectedIndex++;
}
private void UpdateStatus()
{
StatusText.Text = $"{_pages.Count} categor{(_pages.Count == 1 ? "y" : "ies")}";
var count = _items.Count;
var index = _tabbedPage?.SelectedIndex ?? -1;
StatusText.Text = count == 0 ? "No tabs" : $"Tab {index + 1} of {count} (index {index})";
}
private static ContentPage CreatePage(string name, string color) => new()
private IDataTemplate CreatePageTemplate()
{
Header = name,
Content = new StackPanel
return new FuncDataTemplate<CategoryViewModel>((vm, _) => CreatePage(vm, _useDetailTemplate));
}
private static ContentPage CreatePage(CategoryViewModel? vm, bool useDetailTemplate)
{
if (vm is null)
return new ContentPage();
return new ContentPage
{
Header = vm.Name, Content = useDetailTemplate ? CreateDetailContent(vm) : CreateShowcaseContent(vm)
};
}
private static Control CreateDetailContent(CategoryViewModel vm)
{
return new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
@ -80,15 +147,15 @@ namespace ControlCatalog.Pages
{
new TextBlock
{
Text = name,
Text = vm.Name,
FontSize = 24,
FontWeight = FontWeight.SemiBold,
Foreground = new SolidColorBrush(Color.Parse(color)),
Foreground = new SolidColorBrush(Color.Parse(vm.Color)),
HorizontalAlignment = HorizontalAlignment.Center
},
new TextBlock
{
Text = $"Tab for category: {name}",
Text = $"Tab for category: {vm.Name}",
FontSize = 13,
Opacity = 0.7,
TextWrapping = TextWrapping.Wrap,
@ -96,7 +163,45 @@ namespace ControlCatalog.Pages
MaxWidth = 280
}
}
}
};
};
}
private static Control CreateShowcaseContent(CategoryViewModel vm)
{
var accent = Color.Parse(vm.Color);
return new Border
{
Margin = new Thickness(24),
CornerRadius = new CornerRadius(18),
Background = new SolidColorBrush(accent),
Padding = new Thickness(28),
Child = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Spacing = 10,
Children =
{
new TextBlock
{
Text = vm.Name,
FontSize = 28,
FontWeight = FontWeight.Bold,
Foreground = Brushes.White,
HorizontalAlignment = HorizontalAlignment.Center
},
new TextBlock
{
Text = "Template switched at runtime",
FontSize = 14,
Foreground = Brushes.White,
Opacity = 0.9,
HorizontalAlignment = HorizontalAlignment.Center
}
}
}
};
}
}
}

36
samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml

@ -29,27 +29,21 @@
CornerRadius="6"
ClipToBounds="True">
<TabbedPage x:Name="DemoTabs" TabPlacement="Bottom">
<ContentPage Header="Browse">
<NavigationPage x:Name="BrowseNav">
<NavigationPage.Resources>
<SolidColorBrush x:Key="NavigationBarBackground" Color="Transparent" />
</NavigationPage.Resources>
</NavigationPage>
</ContentPage>
<ContentPage Header="Search">
<NavigationPage x:Name="SearchNav">
<NavigationPage.Resources>
<SolidColorBrush x:Key="NavigationBarBackground" Color="Transparent" />
</NavigationPage.Resources>
</NavigationPage>
</ContentPage>
<ContentPage Header="Account">
<NavigationPage x:Name="AccountNav">
<NavigationPage.Resources>
<SolidColorBrush x:Key="NavigationBarBackground" Color="Transparent" />
</NavigationPage.Resources>
</NavigationPage>
</ContentPage>
<NavigationPage x:Name="BrowseNav" Header="Browse">
<NavigationPage.Resources>
<SolidColorBrush x:Key="NavigationBarBackground" Color="Transparent" />
</NavigationPage.Resources>
</NavigationPage>
<NavigationPage x:Name="SearchNav" Header="Search">
<NavigationPage.Resources>
<SolidColorBrush x:Key="NavigationBarBackground" Color="Transparent" />
</NavigationPage.Resources>
</NavigationPage>
<NavigationPage x:Name="AccountNav" Header="Account">
<NavigationPage.Resources>
<SolidColorBrush x:Key="NavigationBarBackground" Color="Transparent" />
</NavigationPage.Resources>
</NavigationPage>
</TabbedPage>
</Border>
</DockPanel>

6
samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml.cs

@ -7,6 +7,8 @@ namespace ControlCatalog.Pages
{
public partial class TabbedPageWithNavigationPage : UserControl
{
private bool _initialized;
public TabbedPageWithNavigationPage()
{
InitializeComponent();
@ -15,6 +17,10 @@ namespace ControlCatalog.Pages
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
if (_initialized)
return;
_initialized = true;
await BrowseNav.PushAsync(CreateListPage("Browse", "Items", BrowseNav), null);
await SearchNav.PushAsync(CreateListPage("Search", "Results", SearchNav), null);
await AccountNav.PushAsync(CreateListPage("Account", "Options", AccountNav), null);

1
src/Avalonia.Base/Animation/PageSlide.cs

@ -210,6 +210,7 @@ namespace Avalonia.Animation
visual.RenderTransform = null;
}
/// <summary>
/// Gets the common visual parent of the two control.
/// </summary>

2
src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs

@ -7,7 +7,7 @@ using Avalonia.Styling;
namespace Avalonia.Animation;
public class Rotate3DTransition: PageSlide
public class Rotate3DTransition : PageSlide
{
private const double SidePeekAngle = 24.0;
private const double FarPeekAngle = 38.0;

26
src/Avalonia.Controls/Automation/Peers/CarouselPageAutomationPeer.cs

@ -0,0 +1,26 @@
using Avalonia.Controls;
namespace Avalonia.Automation.Peers;
public class CarouselPageAutomationPeer : ControlAutomationPeer
{
public CarouselPageAutomationPeer(CarouselPage owner)
: base(owner)
{
}
public new CarouselPage Owner => (CarouselPage)base.Owner;
protected override AutomationControlType GetAutomationControlTypeCore()
=> AutomationControlType.Pane;
protected override string? GetNameCore()
{
var result = base.GetNameCore();
if (string.IsNullOrEmpty(result))
result = Owner.Header?.ToString();
return result;
}
}

26
src/Avalonia.Controls/Automation/Peers/DrawerPageAutomationPeer.cs

@ -1,16 +1,24 @@
using Avalonia.Automation.Provider;
using Avalonia.Controls;
namespace Avalonia.Automation.Peers;
public class DrawerPageAutomationPeer : ControlAutomationPeer
public class DrawerPageAutomationPeer : ControlAutomationPeer,
IExpandCollapseProvider
{
public DrawerPageAutomationPeer(DrawerPage owner)
: base(owner)
{
owner.PropertyChanged += OwnerPropertyChanged;
}
public new DrawerPage Owner => (DrawerPage)base.Owner;
public ExpandCollapseState ExpandCollapseState => ToState(Owner.IsOpen);
public bool ShowsMenu => false;
public void Collapse() => Owner.IsOpen = false;
public void Expand() => Owner.IsOpen = true;
protected override AutomationControlType GetAutomationControlTypeCore()
=> AutomationControlType.Pane;
@ -23,4 +31,20 @@ public class DrawerPageAutomationPeer : ControlAutomationPeer
return result;
}
private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == DrawerPage.IsOpenProperty)
{
RaisePropertyChangedEvent(
ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty,
ToState(e.GetOldValue<bool>()),
ToState(e.GetNewValue<bool>()));
}
}
private static ExpandCollapseState ToState(bool value)
{
return value ? ExpandCollapseState.Expanded : ExpandCollapseState.Collapsed;
}
}

16
src/Avalonia.Controls/Automation/Peers/TabbedPageAutomationPeer.cs

@ -21,6 +21,22 @@ public class TabbedPageAutomationPeer : ControlAutomationPeer
if (string.IsNullOrEmpty(result))
result = Owner.Header?.ToString();
var index = Owner.SelectedIndex;
var tabCount = GetTabCount();
if (index >= 0 && tabCount > 0)
{
var header = Owner.SelectedPage?.Header?.ToString();
var position = $"Tab {index + 1} of {tabCount}";
var tabName = string.IsNullOrEmpty(header) ? position : $"{position}: {header}";
return string.IsNullOrEmpty(result) ? tabName : $"{result} {tabName}";
}
return result;
}
private int GetTabCount()
{
return Owner.GetTabCount();
}
}

3
src/Avalonia.Controls/Carousel.cs

@ -199,6 +199,9 @@ namespace Avalonia.Controls
{
base.OnApplyTemplate(e);
_scroller = e.NameScope.Find<IScrollable>("PART_ScrollViewer");
if (ItemsPanelRoot is VirtualizingCarouselPanel panel)
panel.RefreshGestureRecognizer();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)

2
src/Avalonia.Controls/CommandBar/CommandBar.cs

@ -142,11 +142,9 @@ namespace Avalonia.Controls
OverflowItems = new ReadOnlyObservableCollection<ICommandBarElement>(_overflowItems);
var primaryCommands = new ObservableCollection<ICommandBarElement>();
primaryCommands.CollectionChanged += OnPrimaryCommandsChanged;
SetCurrentValue(PrimaryCommandsProperty, (IList<ICommandBarElement>)primaryCommands);
var secondaryCommands = new ObservableCollection<ICommandBarElement>();
secondaryCommands.CollectionChanged += OnSecondaryCommandsChanged;
SetCurrentValue(SecondaryCommandsProperty, (IList<ICommandBarElement>)secondaryCommands);
SizeChanged += CommandBar_SizeChanged;

418
src/Avalonia.Controls/Page/CarouselPage.cs

@ -0,0 +1,418 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Animation;
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Collections;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Threading;
namespace Avalonia.Controls
{
/// <summary>
/// A page that displays its child pages in a horizontally scrollable carousel,
/// with optional animated page transitions.
/// </summary>
[TemplatePart("PART_Carousel", typeof(Carousel))]
public class CarouselPage : SelectingMultiPage
{
private Carousel? _carousel;
private static readonly FuncTemplate<Panel?> DefaultPanel =
new FuncTemplate<Panel?>(() => new VirtualizingCarouselPanel());
/// <summary>
/// Defines the <see cref="ItemsPanel"/> property.
/// </summary>
public static readonly StyledProperty<ITemplate<Panel?>> ItemsPanelProperty =
ItemsControl.ItemsPanelProperty.AddOwner<CarouselPage>();
/// <summary>
/// Defines the <see cref="PageTransition"/> property.
/// </summary>
public static readonly StyledProperty<IPageTransition?> PageTransitionProperty =
AvaloniaProperty.Register<CarouselPage, IPageTransition?>(nameof(PageTransition));
/// <summary>
/// Defines the <see cref="IsGestureEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsGestureEnabledProperty =
AvaloniaProperty.Register<CarouselPage, bool>(nameof(IsGestureEnabled), true);
/// <summary>
/// Defines the <see cref="IsKeyboardNavigationEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsKeyboardNavigationEnabledProperty =
AvaloniaProperty.Register<CarouselPage, bool>(nameof(IsKeyboardNavigationEnabled), true);
static CarouselPage()
{
ItemsPanelProperty.OverrideDefaultValue<CarouselPage>(DefaultPanel);
FocusableProperty.OverrideDefaultValue<CarouselPage>(true);
}
public CarouselPage()
{
SetCurrentValue(PagesProperty, new AvaloniaList<Page>());
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
AddHandler(PointerWheelChangedEvent, OnPointerWheelChanged, RoutingStrategies.Bubble);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
RemoveHandler(PointerWheelChangedEvent, OnPointerWheelChanged);
}
/// <summary>
/// Gets or sets the items panel template used to arrange page items.
/// </summary>
public ITemplate<Panel?> ItemsPanel
{
get => GetValue(ItemsPanelProperty);
set => SetValue(ItemsPanelProperty, value);
}
/// <summary>
/// Gets or sets the animated page transition used when the selected page changes.
/// </summary>
public IPageTransition? PageTransition
{
get => GetValue(PageTransitionProperty);
set => SetValue(PageTransitionProperty, value);
}
/// <summary>
/// Gets or sets whether swipe and scroll gestures can be used to navigate between pages.
/// </summary>
public bool IsGestureEnabled
{
get => GetValue(IsGestureEnabledProperty);
set => SetValue(IsGestureEnabledProperty, value);
}
/// <summary>
/// Gets or sets whether keyboard shortcuts (arrow keys, Home/End) can be used to navigate between pages.
/// </summary>
public bool IsKeyboardNavigationEnabled
{
get => GetValue(IsKeyboardNavigationEnabledProperty);
set => SetValue(IsKeyboardNavigationEnabledProperty, value);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (_carousel != null)
{
_carousel.SelectionChanged -= OnCarouselSelectionChanged;
_carousel.ContainerPrepared -= OnCarouselContainerPrepared;
}
_carousel = e.NameScope.Find<Carousel>("PART_Carousel");
if (_carousel != null)
{
_carousel.SelectionChanged += OnCarouselSelectionChanged;
_carousel.ContainerPrepared += OnCarouselContainerPrepared;
_carousel.PageTransition = PageTransition;
_carousel.ItemsPanel = ItemsPanel;
_carousel.ItemTemplate = PageTemplate;
_carousel.IsSwipeEnabled = IsGestureEnabled;
_carousel.ItemsSource = (IEnumerable?)ItemsSource ?? Pages;
if (SelectedIndex >= 0)
{
_carousel.SelectedIndex = SelectedIndex;
}
UpdateActivePage();
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == PageTransitionProperty && _carousel != null)
_carousel.PageTransition = change.GetNewValue<IPageTransition?>();
else if (change.Property == ItemsPanelProperty && _carousel != null)
_carousel.ItemsPanel = change.GetNewValue<ITemplate<Panel?>>();
else if (change.Property == PageTemplateProperty && _carousel != null)
{
_carousel.ItemTemplate = change.GetNewValue<IDataTemplate?>();
if (ItemsSource != null)
UpdateActivePage();
}
else if (change.Property == IsGestureEnabledProperty && _carousel != null)
_carousel.IsSwipeEnabled = change.GetNewValue<bool>();
else if (change.Property == ItemsSourceProperty && _carousel != null)
{
_carousel.ItemsSource = change.GetNewValue<IEnumerable?>() ?? Pages;
UpdateActivePage();
}
else if (change.Property == PagesProperty && ItemsSource == null && _carousel != null)
_carousel.ItemsSource = change.GetNewValue<IEnumerable<Page>?>();
}
protected override void UpdateActivePage(NavigationType navigationType)
{
if (_carousel != null)
{
var index = _carousel.SelectedIndex;
if (index >= 0)
{
UpdateSelection(index, navigationType);
}
else if (GetPageCount() > 0)
{
ApplySelectedIndex(0);
}
}
else if (GetPageCount() > 0)
{
var index = CoercePreTemplateSelectedIndex(SelectedIndex);
if (ItemsSource != null)
{
StoreSelectedIndex(index);
}
else
{
var page = ResolvePageAtIndex(index);
if (index != SelectedIndex || !ReferenceEquals(SelectedPage, page))
CommitSelection(index, page, navigationType);
}
}
}
protected override void ApplySelectedIndex(int index)
{
if (_carousel != null)
{
_carousel.SelectedIndex = index;
}
else
{
var pageCount = GetPageCount();
if (pageCount > 0)
{
var coercedIndex = CoercePreTemplateSelectedIndex(index);
if (ItemsSource != null)
{
StoreSelectedIndex(coercedIndex);
}
else
{
var newPage = ResolvePageAtIndex(coercedIndex);
CommitSelection(coercedIndex, newPage);
}
}
else
{
// Preserve preselection until pages exist.
StoreSelectedIndex(index);
}
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.Handled || !IsKeyboardNavigationEnabled)
return;
bool isRtl = IsRightToLeft;
bool next = isRtl ? e.Key == Key.Left : e.Key == Key.Right;
bool prev = isRtl ? e.Key == Key.Right : e.Key == Key.Left;
var pageCount = GetPageCount();
if (next || e.Key == Key.Down)
{
if (SelectedIndex < pageCount - 1)
{
ApplySelectedIndex(SelectedIndex + 1);
e.Handled = true;
}
}
else if (prev || e.Key == Key.Up)
{
if (SelectedIndex > 0)
{
ApplySelectedIndex(SelectedIndex - 1);
e.Handled = true;
}
}
else if (e.Key == Key.Home)
{
if (pageCount > 0 && SelectedIndex != 0)
{
ApplySelectedIndex(0);
e.Handled = true;
}
}
else if (e.Key == Key.End)
{
if (pageCount > 0 && SelectedIndex != pageCount - 1)
{
ApplySelectedIndex(pageCount - 1);
e.Handled = true;
}
}
}
private void OnCarouselSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_carousel == null)
return;
var newIndex = _carousel.SelectedIndex;
var newPage = ResolveDisplayedPageAtIndex(newIndex);
if (newIndex == SelectedIndex && ReferenceEquals(newPage, SelectedPage))
return;
UpdateSelection(newIndex, NavigationType.Replace);
}
private void OnCarouselContainerPrepared(object? sender, ContainerPreparedEventArgs e)
{
if (_carousel == null || e.Index != _carousel.SelectedIndex)
return;
Dispatcher.UIThread.Post(
() =>
{
if (_carousel != null && _carousel.SelectedIndex == e.Index)
UpdateSelection(e.Index, NavigationType.Replace);
},
DispatcherPriority.Loaded);
}
private void OnPointerWheelChanged(object? sender, PointerWheelEventArgs e)
{
if (!IsGestureEnabled)
return;
bool isRtl = IsRightToLeft;
var pageCount = GetPageCount();
bool goNext = e.Delta.Y < 0 || (isRtl ? e.Delta.X < 0 : e.Delta.X > 0);
bool goPrev = e.Delta.Y > 0 || (isRtl ? e.Delta.X > 0 : e.Delta.X < 0);
if (goNext && SelectedIndex < pageCount - 1)
{
ApplySelectedIndex(SelectedIndex + 1);
e.Handled = true;
}
else if (goPrev && SelectedIndex > 0)
{
ApplySelectedIndex(SelectedIndex - 1);
e.Handled = true;
}
}
protected override AutomationPeer OnCreateAutomationPeer() => new CarouselPageAutomationPeer(this);
private void UpdateAccessibilityName(int index, int pageCount, Page? page)
{
var header = page?.Header?.ToString();
var position = pageCount > 0 ? $"Page {index + 1} of {pageCount}" : string.Empty;
var name = string.IsNullOrEmpty(header) ? position : string.IsNullOrEmpty(position) ? header : $"{position}: {header}";
// CarouselPageAutomationPeer.GetNameCore reads this via base.GetNameCore(), which returns
// AutomationProperties.Name when set. Position and header are encoded here rather than in the
// peer so that the name stays current without requiring the peer to re-query the carousel state.
AutomationProperties.SetName(this, name);
}
private bool IsRightToLeft => FlowDirection == Media.FlowDirection.RightToLeft;
private int GetPageCount()
{
if (_carousel != null)
return _carousel.ItemCount;
var source = (IEnumerable?)ItemsSource ?? Pages;
if (source is ICollection nonGenericCol)
return nonGenericCol.Count;
if (source is ICollection<Page> col)
return col.Count;
if (source != null)
{
int count = 0;
foreach (var _ in source)
count++;
return count;
}
return 0;
}
private int CoercePreTemplateSelectedIndex(int index)
{
var pageCount = GetPageCount();
if (pageCount <= 0)
return index;
return (uint)index < (uint)pageCount ? index : 0;
}
private void UpdateSelection(int index, NavigationType navigationType)
{
var page = ResolveDisplayedPageAtIndex(index);
var pageCount = GetPageCount();
if (page == null && ItemsSource != null)
{
StoreSelectedIndex(index);
UpdateAccessibilityName(index, pageCount, null);
return;
}
CommitSelection(index, page, navigationType);
UpdateAccessibilityName(index, pageCount, page);
}
private Page? ResolveDisplayedPageAtIndex(int index)
{
if (index < 0)
return null;
if (_carousel?.ContainerFromIndex(index) is { } container)
return TryGetPageFromContainer(container);
return ItemsSource == null ? ResolvePageAtIndex(index) : null;
}
private static Page? TryGetPageFromContainer(Control container)
{
if (container is Page page)
return page;
if (container is ContentPresenter presenter)
{
if (presenter.Child == null)
presenter.UpdateChild();
return presenter.Child as Page;
}
if (container is ContentControl contentControl)
return contentControl.Content as Page;
return null;
}
}
}

23
src/Avalonia.Controls/Page/ContentPage.cs

@ -1,3 +1,4 @@
using System;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
@ -25,7 +26,14 @@ namespace Avalonia.Controls
/// Defines the <see cref="Content"/> property.
/// </summary>
public static readonly StyledProperty<object?> ContentProperty =
ContentControl.ContentProperty.AddOwner<ContentPage>();
ContentControl.ContentProperty.AddOwner<ContentPage>(new StyledPropertyMetadata<object?>(
coerce: (_, val) =>
{
if (val is Page)
throw new InvalidOperationException(
"A Page cannot be used as the content of a ContentPage. Use a MultiPage subclass such as NavigationPage, TabbedPage, DrawerPage, or CarouselPage to host child pages.");
return val;
}));
/// <summary>
/// Defines the <see cref="ContentTemplate"/> property.
@ -89,6 +97,14 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets the page content.
/// </summary>
/// <remarks>
/// The content must not be another <see cref="Page"/>. Use a <see cref="MultiPage"/>
/// implementation such as <see cref="NavigationPage"/>, <see cref="TabbedPage"/>,
/// <see cref="DrawerPage"/>, or <see cref="CarouselPage"/> to host child pages.
/// </remarks>
/// <exception cref="InvalidOperationException">
/// Thrown when the assigned value is a <see cref="Page"/>.
/// </exception>
[Content]
[DependsOn(nameof(ContentTemplate))]
public object? Content
@ -172,8 +188,6 @@ namespace Avalonia.Controls
: Padding;
_contentPresenter.InvalidateMeasure();
if (Content is Page childPage)
childPage.SafeAreaPadding = Padding.GetRemainingSafeAreaPadding(SafeAreaPadding);
}
}
@ -182,9 +196,6 @@ namespace Avalonia.Controls
if (e.OldValue is ILogical oldChild)
LogicalChildren.Remove(oldChild);
if (e.OldValue is Page oldPage)
oldPage.SafeAreaPadding = default;
if (e.NewValue is ILogical newChild)
LogicalChildren.Add(newChild);
}

7
src/Avalonia.Controls/Page/DrawerBehavior.cs

@ -16,10 +16,17 @@ namespace Avalonia.Controls
/// <summary>
/// The drawer is permanently open and cannot be closed.
/// </summary>
/// <remarks>
/// Setting this value forces <see cref="DrawerPage.IsOpen"/> to <see langword="true"/>.
/// </remarks>
Locked,
/// <summary>
/// The drawer is hidden and cannot be opened.
/// </summary>
/// <remarks>
/// Setting this value forces <see cref="DrawerPage.IsOpen"/> to <see langword="false"/>.
/// The <see cref="DrawerPage.Closing"/> event is not raised.
/// </remarks>
Disabled
}
}

6
src/Avalonia.Controls/Page/DrawerClosingEventArgs.cs

@ -7,7 +7,11 @@ namespace Avalonia.Controls
/// </summary>
public class DrawerClosingEventArgs : RoutedEventArgs
{
internal DrawerClosingEventArgs(RoutedEvent routedEvent) : base(routedEvent) { }
/// <summary>
/// Initializes a new instance of the <see cref="DrawerClosingEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event data.</param>
public DrawerClosingEventArgs(RoutedEvent routedEvent) : base(routedEvent) { }
/// <summary>
/// Gets or sets a value indicating whether the closing should be cancelled.

136
src/Avalonia.Controls/Page/DrawerPage.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Metadata;
@ -85,10 +86,10 @@ namespace Avalonia.Controls
validate: ValidateLength);
/// <summary>
/// Defines the <see cref="DrawerBreakpointWidth"/> property.
/// Defines the <see cref="DrawerBreakpointLength"/> property.
/// </summary>
public static readonly StyledProperty<double> DrawerBreakpointWidthProperty =
AvaloniaProperty.Register<DrawerPage, double>(nameof(DrawerBreakpointWidth), 0d);
public static readonly StyledProperty<double> DrawerBreakpointLengthProperty =
AvaloniaProperty.Register<DrawerPage, double>(nameof(DrawerBreakpointLength), 0d);
/// <summary>
/// Defines the <see cref="IsGestureEnabled"/> property.
@ -200,6 +201,7 @@ namespace Avalonia.Controls
public static readonly StyledProperty<SplitViewDisplayMode> DisplayModeProperty =
SplitView.DisplayModeProperty.AddOwner<DrawerPage>();
private ContentPresenter? _contentPresenter;
private ContentPresenter? _drawerPresenter;
private ContentPresenter? _drawerHeaderPresenter;
@ -293,7 +295,6 @@ namespace Avalonia.Controls
public DrawerPage()
{
GestureRecognizers.Add(_swipeRecognizer);
AddHandler(PointerPressedEvent, OnSwipePointerPressed, handledEventsToo: true);
UpdateSwipeRecognizerAxes();
}
@ -321,6 +322,15 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets whether the drawer pane is currently open.
/// </summary>
/// <remarks>
/// Setting this property to <see langword="true"/> while <see cref="DrawerBehavior"/> is
/// <see cref="DrawerBehavior.Disabled"/> is a no-op; the value is coerced back to
/// <see langword="false"/>. When closing programmatically, the <see cref="Closing"/> event
/// is raised and can be cancelled; if cancelled, the property reverts to
/// <see langword="true"/>. The <see cref="Closing"/> event is not raised when the drawer
/// is forced closed because <see cref="DrawerBehavior"/> is set to
/// <see cref="DrawerBehavior.Disabled"/>.
/// </remarks>
public bool IsOpen
{
get => GetValue(IsOpenProperty);
@ -346,12 +356,12 @@ namespace Avalonia.Controls
}
/// <summary>
/// Gets or sets the width threshold for switching to overlay mode.
/// Gets or sets the size threshold for switching to overlay mode. Set to 0 to disable.
/// </summary>
public double DrawerBreakpointWidth
public double DrawerBreakpointLength
{
get => GetValue(DrawerBreakpointWidthProperty);
set => SetValue(DrawerBreakpointWidthProperty, value);
get => GetValue(DrawerBreakpointLengthProperty);
set => SetValue(DrawerBreakpointLengthProperty, value);
}
/// <summary>
@ -520,8 +530,7 @@ namespace Avalonia.Controls
{
base.OnApplyTemplate(e);
if (_backdrop != null)
_backdrop.PointerPressed -= OnBackdropPressed;
DetachBackdropPointerPressed();
_contentPresenter = e.NameScope.Find<ContentPresenter>("PART_ContentPresenter");
_drawerPresenter = e.NameScope.Find<ContentPresenter>("PART_DrawerPresenter");
@ -538,7 +547,12 @@ namespace Avalonia.Controls
UpdateIconPresenters();
if (_backdrop != null)
_backdrop.PointerPressed += OnBackdropPressed;
{
if (IsAttachedToVisualTree)
AttachBackdropPointerPressed();
AutomationProperties.SetAccessibilityView(_backdrop, AccessibilityView.Raw);
}
ApplyForeground(_drawerHeaderPresenter, DrawerHeaderForeground);
ApplyForeground(_drawerFooterPresenter, DrawerFooterForeground);
@ -568,6 +582,7 @@ namespace Avalonia.Controls
if (change.Property == ContentProperty)
{
_hasHadFirstPage = false;
_navBarVisibleSub?.Dispose();
_navBarVisibleSub = null;
@ -606,11 +621,14 @@ namespace Avalonia.Controls
}
else if (change.Property == DrawerBehaviorProperty ||
change.Property == DrawerLayoutBehaviorProperty ||
change.Property == DrawerBreakpointWidthProperty)
change.Property == DrawerBreakpointLengthProperty)
{
UpdateSplitViewDisplayMode();
if (change.Property == DrawerBehaviorProperty && Content is NavigationPage nav)
nav.SetDrawerPage(this);
}
else if (change.Property == BoundsProperty && DrawerBreakpointWidth > 0)
else if (change.Property == BoundsProperty && DrawerBreakpointLength > 0)
{
UpdateSplitViewDisplayMode();
}
@ -647,9 +665,10 @@ namespace Avalonia.Controls
base.OnAttachedToVisualTree(e);
AddHandler(InputElement.SwipeGestureEvent, OnSwipeGesture);
AddHandler(PointerPressedEvent, OnSwipePointerPressed, handledEventsToo: true);
if (_backdrop != null)
_backdrop.PointerPressed += OnBackdropPressed;
AttachBackdropPointerPressed();
RestoreNavigationState();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
@ -657,10 +676,27 @@ namespace Avalonia.Controls
base.OnDetachedFromVisualTree(e);
RemoveHandler(InputElement.SwipeGestureEvent, OnSwipeGesture);
RemoveHandler(PointerPressedEvent, OnSwipePointerPressed);
if (_backdrop != null)
_backdrop.PointerPressed -= OnBackdropPressed;
DetachBackdropPointerPressed();
ClearNavigationState();
}
private void RestoreNavigationState()
{
if (Content is not NavigationPage nav)
return;
_navBarVisibleSub?.Dispose();
nav.SetDrawerPage(this);
_navBarVisibleSub = nav.GetObservable(NavigationPage.IsNavBarEffectivelyVisibleProperty)
.Subscribe(new AnonymousObserver<bool>(_ => UpdateDetailNavBarVisiblePseudoClass()));
UpdateDetailNavBarVisiblePseudoClass();
}
private void ClearNavigationState()
{
_navBarVisibleSub?.Dispose();
_navBarVisibleSub = null;
@ -668,6 +704,19 @@ namespace Avalonia.Controls
nav.SetDrawerPage(null);
}
private void AttachBackdropPointerPressed()
{
if (_backdrop != null)
_backdrop.PointerPressed += OnBackdropPressed;
}
private void DetachBackdropPointerPressed()
{
if (_backdrop != null)
_backdrop.PointerPressed -= OnBackdropPressed;
}
private void UpdateSwipeRecognizerAxes()
{
_swipeRecognizer.CanVerticallySwipe = IsVerticalPlacement;
@ -875,7 +924,7 @@ namespace Avalonia.Controls
if (_splitView == null)
return;
if (DrawerBreakpointWidth > 0 && previousMode != mode)
if (DrawerBreakpointLength > 0 && previousMode != mode)
{
if (mode == SplitViewDisplayMode.Inline)
{
@ -903,9 +952,13 @@ namespace Avalonia.Controls
return SplitViewDisplayMode.Overlay;
}
var breakpoint = DrawerBreakpointWidth;
if (!IsVerticalPlacement && breakpoint > 0 && Bounds.Width > 0 && Bounds.Width < breakpoint)
return SplitViewDisplayMode.Overlay;
var breakpoint = DrawerBreakpointLength;
if (breakpoint > 0)
{
var length = IsVerticalPlacement ? Bounds.Height : Bounds.Width;
if (length > 0 && length < breakpoint)
return SplitViewDisplayMode.Overlay;
}
switch (DrawerLayoutBehavior)
{
@ -963,40 +1016,15 @@ namespace Avalonia.Controls
_bottomPaneIconPresenter.Content = CreateIconContent(DrawerIcon);
}
private static object? CreateIconContent(object? icon)
internal static object? CreateIconContent(object? icon) => icon switch
{
// Non-visual data (Geometry, IImage, string, etc.) can be shared across presenters.
if (icon is not Control)
return icon;
// For Control-typed icons, create an independent copy per presenter to avoid
// the "already has a visual parent" exception when the same instance is used
// in multiple ContentPresenters simultaneously.
if (icon is PathIcon pathIcon)
{
var clone = new PathIcon
{
Data = pathIcon.Data,
Width = pathIcon.Width,
Height = pathIcon.Height,
};
if (pathIcon.IsSet(PathIcon.ForegroundProperty))
clone.Foreground = pathIcon.Foreground;
if (pathIcon.IsSet(PathIcon.OpacityProperty))
clone.Opacity = pathIcon.Opacity;
if (pathIcon.IsSet(PathIcon.RenderTransformProperty))
clone.RenderTransform = pathIcon.RenderTransform;
if (pathIcon.IsSet(PathIcon.RenderTransformOriginProperty))
clone.RenderTransformOrigin = pathIcon.RenderTransformOrigin;
return clone;
}
// For other Control subtypes, return null to avoid a crash.
// Users should pass non-Control icon data instead.
return null;
}
ITemplate<Control> template => template.Build(),
Geometry g => new PathIcon { Data = g },
PathIcon pi => new PathIcon { Data = pi.Data },
DrawingImage { Drawing: GeometryDrawing { Geometry: { } gd } } => new PathIcon { Data = gd },
IImage image => new Image { Source = image },
_ => null
};
private void ApplyDrawerBackground()
{

5
src/Avalonia.Controls/Page/INavigation.cs

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Animation;
@ -117,11 +118,15 @@ namespace Avalonia.Controls
/// Inserts <paramref name="page"/> immediately before <paramref name="before"/> in the stack.
/// Does not change the currently visible page.
/// </summary>
/// <exception cref="ArgumentNullException"><paramref name="page"/> or <paramref name="before"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException"><paramref name="before"/> is not in the navigation stack, or <paramref name="page"/> is already hosted by this navigation page.</exception>
void InsertPage(Page page, Page before);
/// <summary>
/// Removes <paramref name="page"/> from the navigation stack without animation.
/// If <paramref name="page"/> is not in the stack the call is a no-op.
/// </summary>
/// <exception cref="ArgumentNullException"><paramref name="page"/> is <see langword="null"/>.</exception>
void RemovePage(Page page);
}
}

102
src/Avalonia.Controls/Page/MultiPage.cs

@ -5,7 +5,6 @@ using System.Collections.Specialized;
using Avalonia.Controls.Templates;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@ -20,6 +19,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IEnumerable<Page>?> PagesProperty =
AvaloniaProperty.Register<MultiPage, IEnumerable<Page>?>(nameof(Pages));
/// <summary>
/// Defines the <see cref="ItemsSource"/> property.
/// </summary>
public static readonly StyledProperty<IEnumerable?> ItemsSourceProperty =
AvaloniaProperty.Register<MultiPage, IEnumerable?>(nameof(ItemsSource));
/// <summary>
/// Defines the <see cref="PageTemplate"/> property.
/// </summary>
@ -43,6 +48,18 @@ namespace Avalonia.Controls
set => SetValue(PagesProperty, value);
}
/// <summary>
/// Gets or sets a view-model collection to bind to. Use together with
/// <see cref="PageTemplate"/> to convert each item into a <see cref="Page"/>.
/// When set, takes precedence over <see cref="Pages"/> as the item source for
/// the inner tab or carousel control.
/// </summary>
public IEnumerable? ItemsSource
{
get => GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
/// <summary>
/// Gets or sets the data template used to create pages from view-model items.
/// </summary>
@ -68,18 +85,18 @@ namespace Avalonia.Controls
if (change.Property == PagesProperty)
{
if (ReferenceEquals(change.OldValue, change.NewValue))
return;
if (change.OldValue is INotifyCollectionChanged oldNotifyCollection)
oldNotifyCollection.CollectionChanged -= NotifyCollection_CollectionChanged;
LogicalChildren.Clear();
if (change.NewValue is IEnumerable<Page> newPages)
if (change.NewValue is IEnumerable<Page> newItems)
{
foreach (var page in newPages)
{
if (page is ILogical logical)
LogicalChildren.Add(logical);
}
foreach (var page in newItems)
LogicalChildren.Add(page);
}
if (change.NewValue is INotifyCollectionChanged newNotifyCollection)
@ -87,6 +104,8 @@ namespace Avalonia.Controls
if (change.NewValue != null)
UpdateActivePage();
else
SetCurrentValue(CurrentPageProperty, null);
}
else if (change.Property == CurrentPageProperty)
CurrentPageChanged?.Invoke(this, EventArgs.Empty);
@ -103,6 +122,24 @@ namespace Avalonia.Controls
/// Called when the active child page changes.
/// </summary>
/// <param name="navigationType">The reason for the page change.</param>
/// <remarks>
/// The base class calls this method in the following situations, passing the corresponding
/// <see cref="NavigationType"/> value:
/// <list type="bullet">
/// <item><description>
/// <see cref="NavigationType.Replace"/>. The <see cref="Pages"/> collection was
/// assigned, a new item was added, or the active page changed for any reason not
/// covered below.
/// </description></item>
/// <item><description>
/// <see cref="NavigationType.Remove"/>. The currently active page was removed from
/// the <see cref="Pages"/> collection or the collection was reset and the active page
/// is no longer present. Subclasses should select a replacement page.
/// </description></item>
/// </list>
/// Subclasses may also call this method directly with any <see cref="NavigationType"/>
/// value appropriate for their own navigation model.
/// </remarks>
protected virtual void UpdateActivePage(NavigationType navigationType) { }
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
@ -132,33 +169,8 @@ namespace Avalonia.Controls
LogicalChildren.Clear();
if (Pages != null)
{
foreach (var item in Pages)
{
if (item is ILogical logical)
LogicalChildren.Add(logical);
}
}
break;
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Replace:
if (e.OldItems != null)
{
foreach (var old in e.OldItems)
{
if (old is ILogical logical)
LogicalChildren.Remove(logical);
}
}
if (e.NewItems != null)
{
int insertIdx = e.NewStartingIndex >= 0 ? e.NewStartingIndex : LogicalChildren.Count;
foreach (var newItem in e.NewItems)
{
if (newItem is ILogical logical)
LogicalChildren.Insert(insertIdx++, logical);
}
foreach (var page in Pages)
LogicalChildren.Add(page);
}
break;
@ -167,8 +179,8 @@ namespace Avalonia.Controls
{
foreach (var old in e.OldItems)
{
if (old is ILogical logical)
LogicalChildren.Remove(logical);
if (old is Page page)
LogicalChildren.Remove(page);
}
}
@ -177,8 +189,8 @@ namespace Avalonia.Controls
int insertIdx = e.NewStartingIndex >= 0 ? e.NewStartingIndex : LogicalChildren.Count;
foreach (var newItem in e.NewItems)
{
if (newItem is ILogical logical)
LogicalChildren.Insert(insertIdx++, logical);
if (newItem is Page page)
LogicalChildren.Insert(insertIdx++, page);
}
}
break;
@ -196,14 +208,22 @@ namespace Avalonia.Controls
{
if (e.OldItems != null)
foreach (var old in e.OldItems)
if (ReferenceEquals(old, current)) { currentRemoved = true; break; }
if (ReferenceEquals(old, current))
{
currentRemoved = true;
break;
}
}
else if (e.Action == NotifyCollectionChangedAction.Reset)
{
currentRemoved = true;
if (Pages != null)
foreach (var item in Pages)
if (ReferenceEquals(item, current)) { currentRemoved = false; break; }
foreach (var page in Pages)
if (ReferenceEquals(page, current))
{
currentRemoved = false;
break;
}
}
if (currentRemoved)

780
src/Avalonia.Controls/Page/NavigationPage.cs

File diff suppressed because it is too large

22
src/Avalonia.Controls/Page/Page.cs

@ -147,6 +147,14 @@ namespace Avalonia.Controls
/// <summary>
/// Occurs when the page is about to be navigated from.
/// </summary>
/// <remarks>
/// Each subscriber is awaited in turn. Set <see cref="NavigatingFromEventArgs.Cancel"/> to
/// <see langword="true"/> to abort the navigation; remaining subscribers are not invoked once
/// cancellation is requested. If a subscriber throws an exception, the exception propagates
/// to the calling navigation method (such as <see cref="NavigationPage.PushAsync(Page)"/>)
/// and the navigation is aborted. Subscribers should use try/catch internally if they need
/// guaranteed cancellation semantics regardless of errors.
/// </remarks>
public event Func<NavigatingFromEventArgs, Task>? Navigating;
/// <summary>
@ -162,6 +170,11 @@ namespace Avalonia.Controls
/// <summary>
/// Called when the page is about to be navigated from.
/// </summary>
/// <remarks>
/// Setting <see cref="NavigatingFromEventArgs.Cancel"/> to <see langword="true"/> here
/// prevents the <see cref="Navigating"/> async handlers from running and aborts the
/// navigation. This method is called before the <see cref="Navigating"/> event.
/// </remarks>
protected virtual void OnNavigatingFrom(NavigatingFromEventArgs args) { }
/// <summary>
@ -181,11 +194,18 @@ namespace Avalonia.Controls
{
OnNavigatingFrom(args);
if (args.Cancel)
return;
var navigating = Navigating;
if (navigating != null)
{
foreach (Func<NavigatingFromEventArgs, Task> handler in navigating.GetInvocationList())
{
await handler(args);
if (args.Cancel)
return;
}
}
}
@ -201,7 +221,7 @@ namespace Avalonia.Controls
UpdateContentSafeAreaPadding();
if (change.Property == HeaderProperty)
AutomationProperties.SetName(this, change.NewValue as string ?? string.Empty);
AutomationProperties.SetName(this, change.GetNewValue<object?>() as string ?? string.Empty);
}
protected override void OnLoaded(RoutedEventArgs e)

14
src/Avalonia.Controls/Page/PageNavigationHost.cs

@ -129,12 +129,14 @@ namespace Avalonia.Controls
private void ContentPresenter_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == ContentPresenter.ChildProperty
&& e.NewValue is Page page
&& _insetManager != null)
{
page.SafeAreaPadding = _insetManager.SafeAreaPadding;
}
if (e.Property != ContentPresenter.ChildProperty)
return;
if (e.GetOldValue<object?>() is Page oldPage)
oldPage.SafeAreaPadding = default;
if (e.GetNewValue<object?>() is Page newPage && _insetManager != null)
newPage.SafeAreaPadding = _insetManager.SafeAreaPadding;
}
private void TopLevel_BackRequested(object? sender, RoutedEventArgs e)

8
src/Avalonia.Controls/Page/PageSelectionChangedEventArgs.cs

@ -1,18 +1,20 @@
using System;
using Avalonia.Interactivity;
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for a page selection-changed event.
/// </summary>
public class PageSelectionChangedEventArgs : EventArgs
public class PageSelectionChangedEventArgs : RoutedEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="PageSelectionChangedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with this event args instance.</param>
/// <param name="previousPage">The page that was selected before the change, or <see langword="null"/> if no page was selected.</param>
/// <param name="currentPage">The page that is now selected, or <see langword="null"/> if selection was cleared.</param>
public PageSelectionChangedEventArgs(Page? previousPage, Page? currentPage)
public PageSelectionChangedEventArgs(RoutedEvent routedEvent, Page? previousPage, Page? currentPage)
: base(routedEvent)
{
PreviousPage = previousPage;
CurrentPage = currentPage;

47
src/Avalonia.Controls/Page/SelectingMultiPage.cs

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using Avalonia.Interactivity;
namespace Avalonia.Controls
{
@ -24,6 +26,14 @@ namespace Avalonia.Controls
nameof(SelectedPage),
o => o._selectedPage);
/// <summary>
/// Defines the <see cref="SelectionChanged"/> routed event.
/// </summary>
public static readonly RoutedEvent<PageSelectionChangedEventArgs> SelectionChangedEvent =
RoutedEvent.Register<SelectingMultiPage, PageSelectionChangedEventArgs>(
nameof(SelectionChanged),
RoutingStrategies.Bubble);
private int _selectedIndex = -1;
private Page? _selectedPage;
@ -44,11 +54,21 @@ namespace Avalonia.Controls
/// <summary>
/// Raised when the selected page changes.
/// </summary>
public event EventHandler<PageSelectionChangedEventArgs>? SelectionChanged;
public event EventHandler<PageSelectionChangedEventArgs>? SelectionChanged
{
add => AddHandler(SelectionChangedEvent, value);
remove => RemoveHandler(SelectionChangedEvent, value);
}
/// <summary>
/// Commits a selection change and fires lifecycle events on the outgoing and incoming pages.
/// </summary>
/// <remarks>
/// Raises <see cref="SelectionChanged"/>, then <see cref="Page.NavigatedFrom"/> on the outgoing
/// page and <see cref="Page.NavigatedTo"/> on the incoming page. If the incoming and outgoing
/// pages are the same object no events are fired. Subclasses should call this method rather
/// than modifying selection state directly.
/// </remarks>
protected void CommitSelection(int newIndex, Page? newPage, NavigationType navigationType = NavigationType.Replace)
{
var previousPage = _selectedPage;
@ -57,7 +77,7 @@ namespace Avalonia.Controls
SetCurrentValue(CurrentPageProperty, newPage);
if (!ReferenceEquals(previousPage, newPage))
{
SelectionChanged?.Invoke(this, new PageSelectionChangedEventArgs(previousPage, newPage));
RaiseEvent(new PageSelectionChangedEventArgs(SelectionChangedEvent, previousPage, newPage));
if (previousPage != null)
{
@ -83,5 +103,28 @@ namespace Avalonia.Controls
{
SetAndRaise(SelectedIndexProperty, ref _selectedIndex, index);
}
/// <summary>
/// Returns the page at <paramref name="index"/> from <see cref="MultiPage.Pages"/>,
/// or <see langword="null"/> if the index is out of range.
/// </summary>
protected Page? ResolvePageAtIndex(int index)
{
if (Pages is IList<Page> list)
return (uint)index < (uint)list.Count ? list[index] : null;
if (Pages != null)
{
int i = 0;
foreach (var page in Pages)
{
if (i == index)
return page;
i++;
}
}
return null;
}
}
}

338
src/Avalonia.Controls/Page/TabbedPage.cs

@ -24,8 +24,10 @@ namespace Avalonia.Controls
public class TabbedPage : SelectingMultiPage
{
private TabControl? _tabControl;
private bool _ignoringDisabledSelection;
private readonly Dictionary<TabItem, Page> _containerPageMap = new();
private readonly Dictionary<Page, TabItem> _pageContainerMap = new();
private readonly HashSet<Page> _templateCreatedPages = new(ReferenceEqualityComparer.Instance);
private int _lastSwipeGestureId;
private readonly SwipeGestureRecognizer _swipeRecognizer = new SwipeGestureRecognizer
{
@ -84,15 +86,20 @@ namespace Avalonia.Controls
public static void SetIsTabEnabled(Page page, bool value) =>
page.SetValue(IsTabEnabledProperty, value);
private static readonly bool s_isMobilePlatform = OperatingSystem.IsAndroid() || OperatingSystem.IsIOS();
static TabbedPage()
{
FocusableProperty.OverrideDefaultValue<TabbedPage>(true);
}
/// <summary>
/// Initializes a new instance of the <see cref="TabbedPage"/> class.
/// </summary>
public TabbedPage()
{
SetCurrentValue(PagesProperty, new AvaloniaList<Page>());
Focusable = true;
GestureRecognizers.Add(_swipeRecognizer);
AddHandler(InputElement.SwipeGestureEvent, OnSwipeGesture);
UpdateSwipeRecognizerAxes();
}
@ -117,6 +124,11 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets whether swipe gestures can be used to navigate between tabs.
/// </summary>
/// <remarks>
/// Defaults to <see langword="false"/> because tab strips do not respond to swipe gestures
/// on most platforms (iOS, desktop). Enable this only when the host platform and the
/// content inside each tab page do not conflict with horizontal swipe input.
/// </remarks>
public bool IsGestureEnabled
{
get => GetValue(IsGestureEnabledProperty);
@ -141,6 +153,18 @@ namespace Avalonia.Controls
set => SetValue(IndicatorTemplateProperty, value);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
AddHandler(InputElement.SwipeGestureEvent, OnSwipeGesture);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
RemoveHandler(InputElement.SwipeGestureEvent, OnSwipeGesture);
}
protected override void ApplySelectedIndex(int index)
{
if (_tabControl != null)
@ -162,13 +186,10 @@ namespace Avalonia.Controls
_tabControl.SelectionChanged -= TabControl_SelectionChanged;
_tabControl.ContainerPrepared -= OnContainerPrepared;
_tabControl.ContainerClearing -= OnContainerClearing;
foreach (var page in _containerPageMap.Values)
page.PropertyChanged -= OnPagePropertyChanged;
_containerPageMap.Clear();
_pageContainerMap.Clear();
}
ClearContainerPages();
_tabControl = e.NameScope.Find<TabControl>("PART_TabControl");
if (_tabControl != null)
@ -176,6 +197,7 @@ namespace Avalonia.Controls
_tabControl.SelectionChanged += TabControl_SelectionChanged;
_tabControl.ContainerPrepared += OnContainerPrepared;
_tabControl.ContainerClearing += OnContainerClearing;
_tabControl.ItemsSource = (IEnumerable?)ItemsSource ?? Pages;
if (SelectedIndex >= 0)
_tabControl.SelectedIndex = SelectedIndex;
@ -187,7 +209,10 @@ namespace Avalonia.Controls
ApplyIndicatorTemplate();
UpdateActivePage();
Dispatcher.UIThread.Post(SyncAllTabEnabledStates, DispatcherPriority.Loaded);
var capturedTabControl = _tabControl;
Dispatcher.UIThread.Post(
() => SyncAllTabEnabledStates(capturedTabControl),
DispatcherPriority.Loaded);
}
}
@ -206,6 +231,12 @@ namespace Avalonia.Controls
ApplyIndicatorTemplate();
else if (change.Property == IsGestureEnabledProperty)
_swipeRecognizer.IsEnabled = change.GetNewValue<bool>();
else if (change.Property == ItemsSourceProperty && _tabControl != null)
_tabControl.ItemsSource = change.GetNewValue<IEnumerable?>() ?? Pages;
else if (change.Property == PagesProperty && ItemsSource == null && _tabControl != null)
_tabControl.ItemsSource = change.GetNewValue<IEnumerable<Page>?>();
else if (change.Property == PageTemplateProperty && ItemsSource != null && _tabControl != null)
RebuildTemplateCreatedPages();
}
private TabPlacement ResolveTabPlacement()
@ -213,9 +244,7 @@ namespace Avalonia.Controls
if (TabPlacement != TabPlacement.Auto)
return TabPlacement;
return OperatingSystem.IsAndroid() || OperatingSystem.IsIOS()
? TabPlacement.Bottom
: TabPlacement.Top;
return s_isMobilePlatform ? TabPlacement.Bottom : TabPlacement.Top;
}
private void ApplyTabPlacement()
@ -248,15 +277,13 @@ namespace Avalonia.Controls
_tabControl.IndicatorTemplate = IndicatorTemplate;
}
private bool _ignoringDisabledSelection;
private void TabControl_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_ignoringDisabledSelection)
return;
int newIndex = _tabControl!.SelectedIndex;
var newPage = _tabControl.SelectedItem as Page ?? ResolvePageAtIndex(newIndex);
var newPage = ResolvePageAtIndex(newIndex);
if (newPage != null && !GetIsTabEnabled(newPage))
{
@ -276,51 +303,58 @@ namespace Avalonia.Controls
if (target == SelectedIndex)
return;
CommitSelection(target, ResolvePageAtIndex(target), NavigationType.Replace);
CommitSelectionIfResolved(target, NavigationType.Replace);
UpdateContentSafeAreaPadding();
return;
}
CommitSelection(newIndex, newPage, NavigationType.Replace);
CommitSelectionIfResolved(newIndex, NavigationType.Replace);
UpdateContentSafeAreaPadding();
}
private void OnContainerPrepared(object? sender, ContainerPreparedEventArgs e)
{
if (e.Container is not TabItem tabItem) return;
if (Pages is not IList pages || e.Index >= pages.Count) return;
var item = pages[e.Index];
Page? page;
if (e.Container is not TabItem tabItem)
return;
if (item is Page directPage)
{
page = directPage;
}
else
{
// Data-template mode: build the page and use it directly as the tab's content.
page = PageTemplate?.Build(item) as Page;
if (page == null) return;
tabItem.Content = page;
}
var page = PreparePageForContainer(tabItem, e.Index);
if (page == null)
return;
tabItem.IsEnabled = GetIsTabEnabled(page);
tabItem.Header = page.Header;
tabItem.Icon = CreateIconControl(page.Icon);
tabItem.Icon = CreateIconContent(page.Icon);
_containerPageMap[tabItem] = page;
_pageContainerMap[page] = tabItem;
page.PropertyChanged += OnPagePropertyChanged;
if (e.Index == (_tabControl?.SelectedIndex ?? -1))
UpdateActivePage();
}
private void OnContainerClearing(object? sender, ContainerClearingEventArgs e)
{
if (e.Container is TabItem tabItem && _containerPageMap.Remove(tabItem, out var page))
if (e.Container is TabItem tabItem)
ClearContainerPage(tabItem);
}
private void RebuildTemplateCreatedPages()
{
if (_tabControl == null || ItemsSource == null)
return;
for (int i = 0; i < _tabControl.ItemCount; i++)
{
_pageContainerMap.Remove(page);
page.PropertyChanged -= OnPagePropertyChanged;
if (_tabControl.ContainerFromIndex(i) is not TabItem tabItem)
continue;
var page = PreparePageForContainer(tabItem, i);
if (page == null)
continue;
tabItem.IsEnabled = GetIsTabEnabled(page);
tabItem.Header = page.Header;
tabItem.Icon = CreateIconContent(page.Icon);
}
UpdateActivePage();
}
private void OnPagePropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
@ -331,7 +365,7 @@ namespace Avalonia.Controls
if (e.Property == Page.IconProperty)
{
if (_pageContainerMap.TryGetValue(page, out var tabItem))
tabItem.Icon = CreateIconControl(page.Icon);
tabItem.Icon = CreateIconContent(page.Icon);
}
else if (e.Property == Page.HeaderProperty)
{
@ -347,14 +381,16 @@ namespace Avalonia.Controls
/// <summary>
/// Creates a visual control from a page icon value.
/// </summary>
internal static Control? CreateIconControl(object? icon)
internal static Control? CreateIconContent(object? icon)
{
if (icon is ITemplate<Control> template)
return template.Build();
Geometry? geometry = icon switch
{
Geometry g => g,
PathIcon pi => pi.Data,
DrawingImage { Drawing: GeometryDrawing { Geometry: { } gd } } => gd,
string s when !string.IsNullOrEmpty(s) => Geometry.Parse(s),
_ => null
};
@ -375,87 +411,108 @@ namespace Avalonia.Controls
}
if (icon is IImage image)
{
return new Image
{
HorizontalAlignment = HorizontalAlignment.Center,
Source = image,
};
}
return new Image { Source = image };
return null;
}
private int FindNearestEnabledTab(int disabledIndex)
{
if (Pages is not IList pages) return -1;
int count = pages.Count;
int count = GetTabCount();
for (int dist = 1; dist < count; dist++)
{
int p = disabledIndex - dist;
if (p >= 0 && pages[p] is Page pp && GetIsTabEnabled(pp)) return p;
if (p >= 0 && IsTabEnabledAtIndex(p))
return p;
int n = disabledIndex + dist;
if (n < count && pages[n] is Page np && GetIsTabEnabled(np)) return n;
if (n < count && IsTabEnabledAtIndex(n))
return n;
}
return -1;
}
protected internal int FindNextEnabledTab(int start, int direction)
private protected int FindNextEnabledTab(int start, int direction)
{
if (Pages is not IList pages) return -1;
int count = pages.Count;
int count = GetTabCount();
int i = start;
while (i >= 0 && i < count)
{
if (pages[i] is Page page && GetIsTabEnabled(page)) return i;
if (IsTabEnabledAtIndex(i))
return i;
i += direction;
}
return -1;
}
private Page? ResolvePageAtIndex(int index)
internal int GetTabCount()
{
if (_tabControl != null)
return _tabControl.ItemCount;
var source = (IEnumerable?)ItemsSource ?? Pages;
if (source is ICollection collection)
return collection.Count;
if (source == null)
return 0;
int count = 0;
foreach (var _ in source)
count++;
return count;
}
private bool IsTabEnabledAtIndex(int index)
{
if (_tabControl?.ContainerFromIndex(index) is TabItem ti && _containerPageMap.TryGetValue(ti, out var p))
return p;
if (Pages is IList pages && (uint)index < (uint)pages.Count)
return pages[index] as Page;
return null;
return GetIsTabEnabled(p);
if (ResolvePageAtIndex(index) is Page page)
return GetIsTabEnabled(page);
return true;
}
private void SyncTabEnabledState(Page page)
private new Page? ResolvePageAtIndex(int index)
{
if (_tabControl == null || Pages is not IList pages)
return;
if (_tabControl?.ContainerFromIndex(index) is TabItem ti && _containerPageMap.TryGetValue(ti, out var p))
return p;
for (int i = 0; i < pages.Count; i++)
if (_tabControl != null)
{
if (!ReferenceEquals(pages[i], page))
continue;
var itemsView = _tabControl.ItemsView;
if ((uint)index < (uint)itemsView.Count)
return itemsView[index] as Page;
}
if (_tabControl.ContainerFromIndex(i) is TabItem tabItem)
tabItem.IsEnabled = GetIsTabEnabled(page);
if (_tabControl?.ContainerFromIndex(index) is TabItem { Content: Page page })
return page;
if (!GetIsTabEnabled(page) && i == _tabControl.SelectedIndex)
{
int target = FindNearestEnabledTab(i);
if (target >= 0 && target != i)
_tabControl.SelectedIndex = target;
}
return base.ResolvePageAtIndex(index);
}
private void SyncTabEnabledState(Page page)
{
if (_tabControl == null || !_pageContainerMap.TryGetValue(page, out var tabItem))
return;
tabItem.IsEnabled = GetIsTabEnabled(page);
if (!GetIsTabEnabled(page) && ReferenceEquals(tabItem, _tabControl.ContainerFromIndex(_tabControl.SelectedIndex)))
{
int i = _tabControl.SelectedIndex;
int target = FindNearestEnabledTab(i);
if (target >= 0 && target != i)
_tabControl.SelectedIndex = target;
}
}
private void SyncAllTabEnabledStates()
private void SyncAllTabEnabledStates(TabControl tabControl)
{
if (_tabControl == null || Pages is not IList pages)
return;
for (int i = 0; i < pages.Count; i++)
for (int i = 0; i < tabControl.ItemCount; i++)
{
if (_tabControl.ContainerFromIndex(i) is TabItem tabItem &&
if (tabControl.ContainerFromIndex(i) is TabItem tabItem &&
_containerPageMap.TryGetValue(tabItem, out var page))
{
tabItem.IsEnabled = GetIsTabEnabled(page);
@ -468,11 +525,123 @@ namespace Avalonia.Controls
if (_tabControl != null)
{
int index = _tabControl.SelectedIndex;
CommitSelection(index, _tabControl.SelectedItem as Page ?? ResolvePageAtIndex(index), navigationType);
CommitSelectionIfResolved(index, navigationType);
UpdateContentSafeAreaPadding();
}
}
private Page? PreparePageForContainer(TabItem tabItem, int index)
{
ClearContainerPage(tabItem);
Page? page;
if (ItemsSource != null)
{
if (!TryGetItemAtIndex(index, out var item))
return null;
page = PageTemplate?.Build(item) as Page;
tabItem.Content = page;
if (page == null)
return null;
_templateCreatedPages.Add(page);
LogicalChildren.Add(page);
}
else
{
if (!TryGetItemAtIndex(index, out var item))
return null;
page = item as Page;
if (page == null)
return null;
tabItem.Content = page;
}
_containerPageMap[tabItem] = page;
_pageContainerMap[page] = tabItem;
page.PropertyChanged += OnPagePropertyChanged;
return page;
}
private void ClearContainerPages()
{
foreach (var page in _containerPageMap.Values)
page.PropertyChanged -= OnPagePropertyChanged;
foreach (var page in _templateCreatedPages)
LogicalChildren.Remove(page);
_containerPageMap.Clear();
_pageContainerMap.Clear();
_templateCreatedPages.Clear();
}
private void ClearContainerPage(TabItem tabItem)
{
if (!_containerPageMap.Remove(tabItem, out var page))
return;
_pageContainerMap.Remove(page);
page.PropertyChanged -= OnPagePropertyChanged;
if (_templateCreatedPages.Remove(page))
LogicalChildren.Remove(page);
}
private bool TryGetItemAtIndex(int index, out object? item)
{
if (_tabControl != null)
{
var itemsView = _tabControl.ItemsView;
if ((uint)index < (uint)itemsView.Count)
{
item = itemsView[index];
return true;
}
}
var source = (IEnumerable?)ItemsSource ?? Pages;
if (source != null)
{
int currentIndex = 0;
foreach (var candidate in source)
{
if (currentIndex == index)
{
item = candidate;
return true;
}
currentIndex++;
}
}
item = null;
return false;
}
private void CommitSelectionIfResolved(int index, NavigationType navigationType)
{
var page = ResolvePageAtIndex(index);
if (page == null &&
ItemsSource != null &&
index >= 0 &&
_tabControl?.ContainerFromIndex(index) == null)
{
StoreSelectedIndex(index);
return;
}
CommitSelection(index, page, navigationType);
}
protected override void UpdateContentSafeAreaPadding()
{
base.UpdateContentSafeAreaPadding();
@ -531,7 +700,8 @@ namespace Avalonia.Controls
_ => 0
};
if (delta == 0) return;
if (delta == 0)
return;
int next = FindNextEnabledTab(_tabControl.SelectedIndex + delta, delta);
if (next >= 0)

71
src/Avalonia.Controls/VirtualizingCarouselPanel.cs

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
@ -111,7 +111,7 @@ namespace Avalonia.Controls
Size IScrollable.Extent => Extent;
Size IScrollable.Viewport => Viewport;
Vector IScrollable.Offset
Vector IScrollable.Offset
{
get => _offset;
set => SetOffset(value);
@ -428,6 +428,14 @@ namespace Avalonia.Controls
_offsetAnimationCts = null;
}
protected override void OnItemsControlChanged(ItemsControl? oldValue)
{
base.OnItemsControlChanged(oldValue);
// ItemsPresenter attaches the panel to the visual tree before calling Attach(ItemsControl),
// so OnAttachedToVisualTree fires with a null ItemsControl; re-run setup here.
RefreshGestureRecognizer();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
@ -440,13 +448,6 @@ namespace Avalonia.Controls
TeardownGestureRecognizer();
}
protected override void OnItemsControlChanged(ItemsControl? oldValue)
{
base.OnItemsControlChanged(oldValue);
RefreshGestureRecognizer();
}
protected override Size MeasureOverride(Size availableSize)
{
if (UsesViewportFractionLayout())
@ -461,7 +462,7 @@ namespace Avalonia.Controls
private Size MeasureSinglePageOverride(Size availableSize)
{
var items = Items;
var index = (int)_offset.X;
var index = (ItemsControl as Carousel)?.SelectedIndex ?? (int)_offset.X;
CompleteFinishedTransitionIfNeeded();
@ -497,8 +498,7 @@ namespace Avalonia.Controls
_realized = null;
_realizedIndex = -1;
}
// Get or create an element for the new item.
if (index >= 0 && index < items.Count)
{
_realized = GetOrCreateElement(items, index);
@ -538,14 +538,10 @@ namespace Avalonia.Controls
_realized is { } to &&
GetTransition() is { } transition)
{
_transition = new CancellationTokenSource();
var transitionCts = new CancellationTokenSource();
_transition = transitionCts;
var forward = (_realizedIndex > _transitionFromIndex);
if (Items.Count > 2)
{
forward = forward || (_transitionFromIndex == Items.Count - 1 && _realizedIndex == 0);
forward = forward && !(_transitionFromIndex == 0 && _realizedIndex == Items.Count - 1);
}
var forward = _realizedIndex > _transitionFromIndex;
_transitionTask = RunTransitionAsync(_transition, _transitionFrom, to, forward, transition);
}
@ -736,26 +732,20 @@ namespace Avalonia.Controls
break;
case NotifyCollectionChangedAction.Replace:
if (e.OldStartingIndex < 0)
{
goto case NotifyCollectionChangedAction.Reset;
}
Remove(e.OldStartingIndex, e.OldItems!.Count);
Add(e.NewStartingIndex, e.NewItems!.Count);
break;
case NotifyCollectionChangedAction.Move:
if (e.OldStartingIndex < 0)
{
goto case NotifyCollectionChangedAction.Reset;
}
Remove(e.OldStartingIndex, e.OldItems!.Count);
var insertIndex = e.NewStartingIndex;
if (e.NewStartingIndex > e.OldStartingIndex)
{
insertIndex -= e.OldItems.Count - 1;
}
Add(insertIndex, e.NewItems!.Count);
break;
@ -766,6 +756,7 @@ namespace Avalonia.Controls
_realized = null;
_realizedIndex = -1;
}
break;
}
@ -776,25 +767,25 @@ namespace Avalonia.Controls
{
Debug.Assert(ItemContainerGenerator is not null);
var e = GetRealizedElement(index);
var element = GetRealizedElement(index);
if (e is null)
if (element is null)
{
var item = items[index];
var generator = ItemContainerGenerator!;
if (generator.NeedsContainer(item, index, out var recycleKey))
{
e = GetRecycledElement(item, index, recycleKey) ??
element = GetRecycledElement(item, index, recycleKey) ??
CreateElement(item, index, recycleKey);
}
else
{
e = GetItemAsOwnContainer(item, index);
element = GetItemAsOwnContainer(item, index);
}
}
return e;
return element;
}
private Control? GetRealizedElement(int index)
@ -876,19 +867,17 @@ namespace Avalonia.Controls
{
return;
}
else
{
ItemContainerGenerator.ClearItemContainer(element);
_recyclePool ??= new();
if (!_recyclePool.TryGetValue(recycleKey, out var pool))
{
pool = new();
_recyclePool.Add(recycleKey, pool);
}
ItemContainerGenerator.ClearItemContainer(element);
_recyclePool ??= new();
pool.Push(element);
if (!_recyclePool.TryGetValue(recycleKey, out var pool))
{
pool = new();
_recyclePool.Add(recycleKey, pool);
}
pool.Push(element);
}
private IPageTransition? GetTransition() => (ItemsControl as Carousel)?.PageTransition;
@ -983,7 +972,6 @@ 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)
@ -1007,7 +995,6 @@ namespace Avalonia.Controls
{
CanHorizontallySwipe = _swipeAxis != PageSlide.SlideAxis.Vertical,
CanVerticallySwipe = _swipeAxis != PageSlide.SlideAxis.Horizontal,
IsMouseEnabled = true,
};
GestureRecognizers.Add(_swipeGestureRecognizer);

30
src/Avalonia.Themes.Fluent/Controls/CarouselPage.xaml

@ -0,0 +1,30 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Width="400" Height="300">
<CarouselPage>
<ContentPage Header="Page 1">
<TextBlock Text="First page" HorizontalAlignment="Center" VerticalAlignment="Center" />
</ContentPage>
<ContentPage Header="Page 2">
<TextBlock Text="Second page" HorizontalAlignment="Center" VerticalAlignment="Center" />
</ContentPage>
</CarouselPage>
</Border>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type CarouselPage}" TargetType="CarouselPage">
<Setter Property="Background" Value="{DynamicResource SolidBackgroundFillColorBaseBrush}" />
<Setter Property="Template">
<ControlTemplate>
<Carousel Name="PART_Carousel"
Background="{TemplateBinding Background}"
ItemTemplate="{TemplateBinding PageTemplate}"
PageTransition="{TemplateBinding PageTransition}"
ItemsPanel="{TemplateBinding ItemsPanel}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

1
src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml

@ -9,6 +9,7 @@
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/AdornerLayer.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/Button.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/CommandBar.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/CarouselPage.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/ContentPage.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/DrawerPage.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/NavigationPage.xaml" />

25
src/Avalonia.Themes.Fluent/Controls/NavigationPage.xaml

@ -18,12 +18,26 @@
<Setter Property="Background" Value="Transparent" />
<Setter Property="PageTransition">
<Setter.Value>
<PageSlide Duration="0:0:0.3" Orientation="Horizontal" FillMode="Forward" />
<PageSlide Duration="0:0:0.3" Orientation="Horizontal" FillMode="Forward">
<PageSlide.SlideInEasing>
<SplineEasing X1="0.16" Y1="1" X2="0.3" Y2="1" />
</PageSlide.SlideInEasing>
<PageSlide.SlideOutEasing>
<SplineEasing X1="0.2" Y1="0.85" X2="0.3" Y2="1" />
</PageSlide.SlideOutEasing>
</PageSlide>
</Setter.Value>
</Setter>
<Setter Property="ModalTransition">
<Setter.Value>
<PageSlide Duration="0:0:0.25" Orientation="Vertical" FillMode="Forward" />
<PageSlide Duration="0:0:0.25" Orientation="Vertical" FillMode="Forward">
<PageSlide.SlideInEasing>
<SplineEasing X1="0.16" Y1="1" X2="0.3" Y2="1" />
</PageSlide.SlideInEasing>
<PageSlide.SlideOutEasing>
<SplineEasing X1="0.2" Y1="0.85" X2="0.3" Y2="1" />
</PageSlide.SlideOutEasing>
</PageSlide>
</Setter.Value>
</Setter>
<Setter Property="Template">
@ -107,16 +121,15 @@
ToolTip.Tip="{x:Null}">
<Panel Background="Transparent">
<Path Data="{DynamicResource NavigationPageBackIcon}"
x:Name="PART_BackButtonDefaultIcon"
Fill="{DynamicResource NavigationBarForeground}"
Width="24"
Height="24"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsVisible="{Binding CurrentPage?.(NavigationPage.BackButtonContent), RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static ObjectConverters.IsNull}, FallbackValue=False}" />
VerticalAlignment="Center" />
<ContentPresenter
Content="{Binding CurrentPage?.(NavigationPage.BackButtonContent), RelativeSource={RelativeSource TemplatedParent}, FallbackValue={x:Null}}"
IsVisible="{Binding CurrentPage?.(NavigationPage.BackButtonContent), RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
x:Name="PART_BackButtonContentPresenter"
VerticalAlignment="Center"
HorizontalAlignment="Center"/>
</Panel>

3
src/Avalonia.Themes.Fluent/Controls/TabbedPage.xaml

@ -253,8 +253,7 @@
Theme="{StaticResource TabbedPageTabControlTheme}"
ItemContainerTheme="{StaticResource TabbedPageTabItemTheme}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ItemsSource="{TemplateBinding Pages}" />
VerticalAlignment="Stretch" />
</Panel>
</ControlTemplate>
</Setter>

30
src/Avalonia.Themes.Simple/Controls/CarouselPage.xaml

@ -0,0 +1,30 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Width="400" Height="300">
<CarouselPage>
<ContentPage Header="Page 1">
<TextBlock Text="First page" HorizontalAlignment="Center" VerticalAlignment="Center" />
</ContentPage>
<ContentPage Header="Page 2">
<TextBlock Text="Second page" HorizontalAlignment="Center" VerticalAlignment="Center" />
</ContentPage>
</CarouselPage>
</Border>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type CarouselPage}" TargetType="CarouselPage">
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}" />
<Setter Property="Template">
<ControlTemplate>
<Carousel Name="PART_Carousel"
Background="{TemplateBinding Background}"
ItemTemplate="{TemplateBinding PageTemplate}"
PageTransition="{TemplateBinding PageTransition}"
ItemsPanel="{TemplateBinding ItemsPanel}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

25
src/Avalonia.Themes.Simple/Controls/NavigationPage.xaml

@ -17,12 +17,26 @@
<Setter Property="Background" Value="Transparent" />
<Setter Property="PageTransition">
<Setter.Value>
<PageSlide Duration="0:0:0.3" Orientation="Horizontal" FillMode="Forward" />
<PageSlide Duration="0:0:0.3" Orientation="Horizontal" FillMode="Forward">
<PageSlide.SlideInEasing>
<SplineEasing X1="0.16" Y1="1" X2="0.3" Y2="1" />
</PageSlide.SlideInEasing>
<PageSlide.SlideOutEasing>
<SplineEasing X1="0.2" Y1="0.85" X2="0.3" Y2="1" />
</PageSlide.SlideOutEasing>
</PageSlide>
</Setter.Value>
</Setter>
<Setter Property="ModalTransition">
<Setter.Value>
<PageSlide Duration="0:0:0.25" Orientation="Vertical" FillMode="Forward" />
<PageSlide Duration="0:0:0.25" Orientation="Vertical" FillMode="Forward">
<PageSlide.SlideInEasing>
<SplineEasing X1="0.16" Y1="1" X2="0.3" Y2="1" />
</PageSlide.SlideInEasing>
<PageSlide.SlideOutEasing>
<SplineEasing X1="0.2" Y1="0.85" X2="0.3" Y2="1" />
</PageSlide.SlideOutEasing>
</PageSlide>
</Setter.Value>
</Setter>
<Setter Property="Template">
@ -101,16 +115,15 @@
ToolTip.Tip="{x:Null}">
<Panel Background="Transparent">
<Path Data="{DynamicResource NavigationPageBackIcon}"
x:Name="PART_BackButtonDefaultIcon"
Fill="{DynamicResource NavigationBarForeground}"
Width="24"
Height="24"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsVisible="{Binding CurrentPage?.(NavigationPage.BackButtonContent), RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static ObjectConverters.IsNull}, FallbackValue=False}" />
VerticalAlignment="Center" />
<ContentPresenter
Content="{Binding CurrentPage?.(NavigationPage.BackButtonContent), RelativeSource={RelativeSource TemplatedParent}, FallbackValue={x:Null}}"
IsVisible="{Binding CurrentPage?.(NavigationPage.BackButtonContent), RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
x:Name="PART_BackButtonContentPresenter"
VerticalAlignment="Center"
HorizontalAlignment="Center"/>
</Panel>

1
src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml

@ -75,6 +75,7 @@
<MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/TextSelectionHandle.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/CommandBar.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/ContentPage.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/CarouselPage.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/DrawerPage.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/NavigationPage.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/TabbedPage.xaml" />

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save