Browse Source

Merge branch 'master' into render-pooling

pull/20885/head
Matt 6 days ago
committed by GitHub
parent
commit
20a56bc08c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 312
      api/Avalonia.nupkg.xml
  2. 2
      global.json
  3. 6
      samples/ControlCatalog/MainView.xaml
  4. 11
      samples/ControlCatalog/Pages/CarouselDemoPage.xaml
  5. 53
      samples/ControlCatalog/Pages/CarouselDemoPage.xaml.cs
  6. 117
      samples/ControlCatalog/Pages/CarouselPage.xaml
  7. 116
      samples/ControlCatalog/Pages/CarouselPage.xaml.cs
  8. 119
      samples/ControlCatalog/Pages/CarouselPage/CarouselCustomizationPage.xaml
  9. 48
      samples/ControlCatalog/Pages/CarouselPage/CarouselCustomizationPage.xaml.cs
  10. 60
      samples/ControlCatalog/Pages/CarouselPage/CarouselDataBindingPage.xaml
  11. 95
      samples/ControlCatalog/Pages/CarouselPage/CarouselDataBindingPage.xaml.cs
  12. 557
      samples/ControlCatalog/Pages/CarouselPage/CarouselGalleryAppPage.xaml
  13. 101
      samples/ControlCatalog/Pages/CarouselPage/CarouselGalleryAppPage.xaml.cs
  14. 93
      samples/ControlCatalog/Pages/CarouselPage/CarouselGesturesPage.xaml
  15. 59
      samples/ControlCatalog/Pages/CarouselPage/CarouselGesturesPage.xaml.cs
  16. 74
      samples/ControlCatalog/Pages/CarouselPage/CarouselGettingStartedPage.xaml
  17. 40
      samples/ControlCatalog/Pages/CarouselPage/CarouselGettingStartedPage.xaml.cs
  18. 140
      samples/ControlCatalog/Pages/CarouselPage/CarouselMultiItemPage.xaml
  19. 47
      samples/ControlCatalog/Pages/CarouselPage/CarouselMultiItemPage.xaml.cs
  20. 97
      samples/ControlCatalog/Pages/CarouselPage/CarouselTransitionsPage.xaml
  21. 66
      samples/ControlCatalog/Pages/CarouselPage/CarouselTransitionsPage.xaml.cs
  22. 132
      samples/ControlCatalog/Pages/CarouselPage/CarouselVerticalPage.xaml
  23. 39
      samples/ControlCatalog/Pages/CarouselPage/CarouselVerticalPage.xaml.cs
  24. 13
      samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs
  25. 13
      samples/ControlCatalog/Pages/DrawerPage/DrawerPageFirstLookPage.xaml.cs
  26. 13
      samples/ControlCatalog/Pages/NavigationPage/NavigationPageGesturePage.xaml.cs
  27. 13
      samples/ControlCatalog/Pages/TabbedPage/TabbedPageGesturePage.xaml.cs
  28. 447
      samples/ControlCatalog/Pages/Transitions/CardStackPageTransition.cs
  29. 380
      samples/ControlCatalog/Pages/Transitions/WaveRevealPageTransition.cs
  30. 145
      samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.Designer.cs
  31. 25
      samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.cs
  32. 3
      samples/interop/WindowsInteropTest/Program.cs
  33. 1
      samples/interop/WindowsInteropTest/WindowsInteropTest.csproj
  34. 32
      src/Avalonia.Base/Animation/CompositePageTransition.cs
  35. 84
      src/Avalonia.Base/Animation/CrossFade.cs
  36. 39
      src/Avalonia.Base/Animation/IProgressPageTransition.cs
  37. 57
      src/Avalonia.Base/Animation/PageSlide.cs
  38. 12
      src/Avalonia.Base/Animation/PageTransitionItem.cs
  39. 161
      src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs
  40. 37
      src/Avalonia.Base/Input/FindNextElementOptions.cs
  41. 169
      src/Avalonia.Base/Input/FocusManager.cs
  42. 18
      src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs
  43. 265
      src/Avalonia.Base/Input/GestureRecognizers/SwipeGestureRecognizer.cs
  44. 2
      src/Avalonia.Base/Input/Gestures.cs
  45. 63
      src/Avalonia.Base/Input/IFocusManager.cs
  46. 16
      src/Avalonia.Base/Input/InputElement.Gestures.cs
  47. 4
      src/Avalonia.Base/Input/InputElement.cs
  48. 29
      src/Avalonia.Base/Input/Navigation/XYFocusOptions.cs
  49. 28
      src/Avalonia.Base/Input/SwipeDirection.cs
  50. 73
      src/Avalonia.Base/Input/SwipeGestureEventArgs.cs
  51. 4
      src/Avalonia.Base/Layout/Layoutable.cs
  52. 4
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs
  53. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs
  54. 6
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs
  55. 2
      src/Avalonia.Controls/Border.cs
  56. 178
      src/Avalonia.Controls/Carousel.cs
  57. 4
      src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs
  58. 2
      src/Avalonia.Controls/Decorator.cs
  59. 7
      src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs
  60. 23
      src/Avalonia.Controls/Page/DrawerPage.cs
  61. 24
      src/Avalonia.Controls/Page/NavigationPage.cs
  62. 2
      src/Avalonia.Controls/Page/Page.cs
  63. 17
      src/Avalonia.Controls/Page/TabbedPage.cs
  64. 4
      src/Avalonia.Controls/PresentationSource/PresentationSource.cs
  65. 2
      src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
  66. 28
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  67. 5
      src/Avalonia.Controls/Primitives/OverlayLayer.cs
  68. 2
      src/Avalonia.Controls/Primitives/SelectionHandleType.cs
  69. 37
      src/Avalonia.Controls/Primitives/TextSearch.cs
  70. 58
      src/Avalonia.Controls/Primitives/VisualLayerManager.cs
  71. 16
      src/Avalonia.Controls/TopLevel.cs
  72. 1282
      src/Avalonia.Controls/VirtualizingCarouselPanel.cs
  73. 19
      src/Avalonia.Controls/Window.cs
  74. 2
      src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml
  75. 2
      src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml
  76. 2
      src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml
  77. 2
      src/Avalonia.Themes.Fluent/Controls/Window.xaml
  78. 2
      src/Avalonia.Themes.Simple/Controls/EmbeddableControlRoot.xaml
  79. 2
      src/Avalonia.Themes.Simple/Controls/OverlayPopupHost.xaml
  80. 2
      src/Avalonia.Themes.Simple/Controls/PopupRoot.xaml
  81. 2
      src/Avalonia.Themes.Simple/Controls/Window.xaml
  82. 4
      src/Avalonia.X11/ActivityTrackingHelper.cs
  83. 6
      src/Avalonia.X11/Clipboard/ClipboardDataFormatHelper.cs
  84. 3
      src/Avalonia.X11/Clipboard/ClipboardReadSession.cs
  85. 6
      src/Avalonia.X11/Clipboard/X11Clipboard.cs
  86. 6
      src/Avalonia.X11/Screens/X11Screen.Providers.cs
  87. 2
      src/Avalonia.X11/TransparencyHelper.cs
  88. 147
      src/Avalonia.X11/X11Atoms.cs
  89. 6
      src/Avalonia.X11/X11Globals.cs
  90. 5
      src/Avalonia.X11/X11IconLoader.cs
  91. 20
      src/Avalonia.X11/X11Window.cs
  92. 2
      src/Avalonia.X11/XLib.cs
  93. 6
      src/Avalonia.X11/XResources.cs
  94. 15
      src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs
  95. 16
      src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs
  96. 1
      src/Headless/Avalonia.Headless/IHeadlessWindow.cs
  97. 4
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs
  98. 48
      src/Windows/Avalonia.Win32.Interoperability/WinForms/WinFormsAvaloniaMessageFilter.cs
  99. 8
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  100. 3
      src/Windows/Avalonia.Win32/WindowImpl.cs

312
api/Avalonia.nupkg.xml

@ -409,6 +409,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.Primitives.SelectionHandleType</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.Remote.RemoteServer</Target>
@ -883,6 +889,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.Primitives.SelectionHandleType</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.Remote.RemoteServer</Target>
@ -979,6 +991,18 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.CrossAxisCancelThresholdProperty</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.EdgeSizeProperty</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Input.HoldingState.Cancelled</Target>
@ -1105,12 +1129,72 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.#ctor(Avalonia.Input.IInputElement)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.ClearFocus</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.ClearFocusOnElementRemoved(Avalonia.Input.IInputElement,Avalonia.Visual)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.FindNextElement(Avalonia.Input.NavigationDirection)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.get_CrossAxisCancelThreshold</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.get_EdgeSize</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.set_CrossAxisCancelThreshold(System.Double)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.set_EdgeSize(System.Double)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.HoldingRoutedEventArgs.#ctor(Avalonia.Input.HoldingState,Avalonia.Point,Avalonia.Input.PointerType)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.ClearFocus</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.IInputRoot.get_KeyboardNavigationHandler</Target>
@ -1363,6 +1447,18 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.SwipeGestureEventArgs.#ctor(System.Int32,Avalonia.Input.SwipeDirection,Avalonia.Vector,Avalonia.Point)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.SwipeGestureEventArgs.get_StartPoint</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.TextInput.TextInputMethodClient.ShowInputPanel</Target>
@ -2077,12 +2173,36 @@
<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.Primitives.TextSearch.GetText(Avalonia.Interactivity.Interactive)</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.Primitives.TextSearch.GetTextBinding(Avalonia.Interactivity.Interactive)</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.Primitives.TextSearch.SetText(Avalonia.Controls.Control,System.String)</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.Primitives.TextSearch.SetText(Avalonia.Interactivity.Interactive,System.String)</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.Primitives.TextSearch.SetTextBinding(Avalonia.Interactivity.Interactive,Avalonia.Data.BindingBase)</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.Primitives.ToggleButton.add_Checked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs})</Target>
@ -2149,6 +2269,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.Primitives.VisualLayerManager.get_IsPopup</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.Primitives.VisualLayerManager.get_LightDismissOverlayLayer</Target>
@ -2167,6 +2293,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.Primitives.VisualLayerManager.set_IsPopup(System.Boolean)</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.Screens.ScreenFromWindow(Avalonia.Platform.IWindowBaseImpl)</Target>
@ -2473,6 +2605,18 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.CrossAxisCancelThresholdProperty</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.EdgeSizeProperty</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Input.HoldingState.Cancelled</Target>
@ -2599,12 +2743,72 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.#ctor(Avalonia.Input.IInputElement)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.ClearFocus</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.ClearFocusOnElementRemoved(Avalonia.Input.IInputElement,Avalonia.Visual)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.FindNextElement(Avalonia.Input.NavigationDirection)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.get_CrossAxisCancelThreshold</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.get_EdgeSize</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.set_CrossAxisCancelThreshold(System.Double)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.set_EdgeSize(System.Double)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.HoldingRoutedEventArgs.#ctor(Avalonia.Input.HoldingState,Avalonia.Point,Avalonia.Input.PointerType)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.ClearFocus</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.IInputRoot.get_KeyboardNavigationHandler</Target>
@ -2857,6 +3061,18 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.SwipeGestureEventArgs.#ctor(System.Int32,Avalonia.Input.SwipeDirection,Avalonia.Vector,Avalonia.Point)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.SwipeGestureEventArgs.get_StartPoint</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.TextInput.TextInputMethodClient.ShowInputPanel</Target>
@ -3571,12 +3787,36 @@
<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.Primitives.TextSearch.GetText(Avalonia.Interactivity.Interactive)</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.Primitives.TextSearch.GetTextBinding(Avalonia.Interactivity.Interactive)</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.Primitives.TextSearch.SetText(Avalonia.Controls.Control,System.String)</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.Primitives.TextSearch.SetText(Avalonia.Interactivity.Interactive,System.String)</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.Primitives.TextSearch.SetTextBinding(Avalonia.Interactivity.Interactive,Avalonia.Data.BindingBase)</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.Primitives.ToggleButton.add_Checked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs})</Target>
@ -3643,6 +3883,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.Primitives.VisualLayerManager.get_IsPopup</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.Primitives.VisualLayerManager.get_LightDismissOverlayLayer</Target>
@ -3661,6 +3907,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.Primitives.VisualLayerManager.set_IsPopup(System.Boolean)</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.Screens.ScreenFromWindow(Avalonia.Platform.IWindowBaseImpl)</Target>
@ -4015,6 +4267,36 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindFirstFocusableElement</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindLastFocusableElement</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindNextElement(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.Focus(Avalonia.Input.IInputElement,Avalonia.Input.NavigationMethod,Avalonia.Input.KeyModifiers)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IKeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers,System.Nullable{Avalonia.Input.KeyDeviceType})</Target>
@ -4303,6 +4585,36 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindFirstFocusableElement</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindLastFocusableElement</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindNextElement(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.Focus(Avalonia.Input.IInputElement,Avalonia.Input.NavigationMethod,Avalonia.Input.KeyModifiers)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IKeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers,System.Nullable{Avalonia.Input.KeyDeviceType})</Target>

2
global.json

@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.101",
"version": "10.0.201",
"rollForward": "latestFeature"
},
"test": {

6
samples/ControlCatalog/MainView.xaml

@ -54,8 +54,10 @@
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<pages:CommandBarPage />
</TabItem>
<TabItem Header="Carousel">
<pages:CarouselPage />
<TabItem Header="Carousel"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<pages:CarouselDemoPage />
</TabItem>
<TabItem Header="CheckBox">

11
samples/ControlCatalog/Pages/CarouselDemoPage.xaml

@ -0,0 +1,11 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CarouselDemoPage">
<NavigationPage x:Name="SampleNav">
<NavigationPage.Styles>
<Style Selector="NavigationPage#SampleNav /template/ Border#PART_NavigationBar">
<Setter Property="Background" Value="Transparent" />
</Style>
</NavigationPage.Styles>
</NavigationPage>
</UserControl>

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

@ -0,0 +1,53 @@
using System;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public partial class CarouselDemoPage : UserControl
{
private static readonly (string Group, string Title, string Description, Func<UserControl> Factory)[] Demos =
{
// Overview
("Overview", "Getting Started",
"Basic Carousel with image items and previous/next navigation buttons.",
() => new CarouselGettingStartedPage()),
// Features
("Features", "Transitions",
"Configure page transitions: PageSlide, CrossFade, 3D Rotation, or None.",
() => new CarouselTransitionsPage()),
("Features", "Customization",
"Adjust orientation and transition type to tailor the carousel layout.",
() => new CarouselCustomizationPage()),
("Features", "Gestures & Keyboard",
"Navigate items via swipe gesture and arrow keys. Toggle each input mode on and off.",
() => new CarouselGesturesPage()),
("Features", "Vertical Orientation",
"Carousel with Orientation set to Vertical, navigated with Up/Down keys, swipe, or buttons.",
() => new CarouselVerticalPage()),
("Features", "Multi-Item Peek",
"Adjust ViewportFraction to show multiple items simultaneously with adjacent cards peeking.",
() => new CarouselMultiItemPage()),
("Features", "Data Binding",
"Bind Carousel to an ObservableCollection and add, remove, or shuffle items at runtime.",
() => new CarouselDataBindingPage()),
// Showcases
("Showcases", "Curated Gallery",
"Editorial art gallery app with DrawerPage navigation, hero Carousel with PipsPager dots, and a horizontal peek carousel for collection highlights.",
() => new CarouselGalleryAppPage()),
};
public CarouselDemoPage()
{
InitializeComponent();
Loaded += OnLoaded;
}
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null);
}
}
}

117
samples/ControlCatalog/Pages/CarouselPage.xaml

@ -1,44 +1,117 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CarouselPage">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h2">An items control that displays its items as pages that fill the control.</TextBlock>
<StackPanel Orientation="Vertical" Spacing="4" HorizontalAlignment="Stretch">
<TextBlock Classes="h2">A swipeable items control that can reveal adjacent pages with ViewportFraction.</TextBlock>
<Grid ColumnDefinitions="Auto,*,Auto"
MaxWidth="660"
<Grid Name="layoutGrid" ColumnDefinitions="Auto,*,Auto" RowDefinitions="*,Auto,*"
MaxWidth="760"
HorizontalAlignment="Stretch" Margin="0 16 0 0">
<Button Name="left" Grid.Column="0" VerticalAlignment="Center" Padding="10,20" Margin="4">
<Path Data="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" Fill="Black"/>
<Button Name="left" Grid.Column="0" Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Center" Padding="10,20" Margin="4">
<Path Name="leftArrow" Data="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" Fill="Black"/>
</Button>
<Carousel Name="carousel" Grid.Column="1">
<Carousel Name="carousel" Grid.Column="1" Grid.Row="1" Background="Transparent" Height="400" Focusable="True" ViewportFraction="1.0">
<Carousel.PageTransition>
<PageSlide Duration="0.25" Orientation="Horizontal" />
</Carousel.PageTransition>
<Image Source="/Assets/delicate-arch-896885_640.jpg"/>
<Image Source="/Assets/hirsch-899118_640.jpg"/>
<Image Source="/Assets/maple-leaf-888807_640.jpg"/>
<Border Margin="14,12" CornerRadius="18" ClipToBounds="True">
<Grid>
<Image Source="/Assets/delicate-arch-896885_640.jpg" Stretch="UniformToFill"/>
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12">
<TextBlock Text="Item 1: Delicate Arch" Foreground="White" HorizontalAlignment="Center" FontWeight="SemiBold"/>
</Border>
</Grid>
</Border>
<Border Margin="14,12" CornerRadius="18" ClipToBounds="True">
<Grid>
<Image Source="/Assets/hirsch-899118_640.jpg" Stretch="UniformToFill"/>
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12">
<TextBlock Text="Item 2: Hirsch" Foreground="White" HorizontalAlignment="Center" FontWeight="SemiBold"/>
</Border>
</Grid>
</Border>
<Border Margin="14,12" CornerRadius="18" ClipToBounds="True">
<Grid>
<Image Source="/Assets/maple-leaf-888807_640.jpg" Stretch="UniformToFill"/>
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12">
<TextBlock Text="Item 3: Maple Leaf" Foreground="White" HorizontalAlignment="Center" FontWeight="SemiBold"/>
</Border>
</Grid>
</Border>
</Carousel>
<Button Name="right" Grid.Column="2" VerticalAlignment="Center" Padding="10,20" Margin="4">
<Path Data="M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z" Fill="Black"/>
<Button Name="right" Grid.Column="2" Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Center" Padding="10,20" Margin="4">
<Path Name="rightArrow" Data="M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z" Fill="Black"/>
</Button>
</Grid>
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock VerticalAlignment="Center">Transition</TextBlock>
<ComboBox Name="transition" SelectedIndex="1" VerticalAlignment="Center">
<Separator Margin="0 4"/>
<Grid ColumnDefinitions="160,420" RowDefinitions="Auto, Auto, Auto" RowSpacing="8"
Margin="0 4" MaxWidth="580" HorizontalAlignment="Left">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center">Transition</TextBlock>
<ComboBox SelectedIndex="1" Name="transition" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Stretch">
<ComboBoxItem>None</ComboBoxItem>
<ComboBoxItem>Slide</ComboBoxItem>
<ComboBoxItem>Crossfade</ComboBoxItem>
<ComboBoxItem>3D Rotation</ComboBoxItem>
<ComboBoxItem>Page Slide</ComboBoxItem>
<ComboBoxItem>Cross Fade</ComboBoxItem>
<ComboBoxItem>Rotate 3D</ComboBoxItem>
<ComboBoxItem>Card Stack</ComboBoxItem>
<ComboBoxItem>Wave Reveal</ComboBoxItem>
<ComboBoxItem>Composite (Slide + Fade)</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock VerticalAlignment="Center">Orientation</TextBlock>
<ComboBox Name="orientation" SelectedIndex="0" VerticalAlignment="Center">
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center">Orientation</TextBlock>
<ComboBox Name="orientation" Grid.Row="1" Grid.Column="1" SelectedIndex="0" HorizontalAlignment="Stretch">
<ComboBoxItem>Horizontal</ComboBoxItem>
<ComboBoxItem>Vertical</ComboBoxItem>
</ComboBox>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center">Viewport Fraction</TextBlock>
<StackPanel Grid.Row="2" Grid.Column="1" Spacing="4" HorizontalAlignment="Stretch">
<Grid ColumnDefinitions="*,56" ColumnSpacing="12">
<Slider Name="viewportFraction"
Minimum="0.33"
Maximum="1"
Value="1.0"
TickFrequency="0.01"
HorizontalAlignment="Stretch" />
<TextBlock Name="viewportFractionIndicator"
Grid.Column="1"
HorizontalAlignment="Stretch"
TextAlignment="Right"
VerticalAlignment="Center"
FontWeight="SemiBold">1.00</TextBlock>
</Grid>
<TextBlock Name="viewportFractionHint"
HorizontalAlignment="Stretch"
Opacity="0.75"
TextWrapping="Wrap"
Text="Values below 1 reveal adjacent pages." />
</StackPanel>
</Grid>
<Separator Margin="0 8"/>
<StackPanel Orientation="Horizontal" Spacing="24" Margin="0 4" MaxWidth="580" HorizontalAlignment="Left">
<CheckBox Name="wrapSelection">Wrap Selection</CheckBox>
<CheckBox Name="swipeEnabled">Swipe Enabled</CheckBox>
</StackPanel>
<Separator Margin="0 8"/>
<StackPanel Spacing="12" MaxWidth="580" HorizontalAlignment="Left">
<StackPanel Orientation="Horizontal" Spacing="24">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock>Total Items:</TextBlock>
<TextBlock Name="itemsCountIndicator" FontWeight="Bold">0</TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock>Selected Index:</TextBlock>
<TextBlock Name="selectedIndexIndicator" FontWeight="Bold">0</TextBlock>
</StackPanel>
</StackPanel>
</StackPanel>
</StackPanel>

116
samples/ControlCatalog/Pages/CarouselPage.xaml.cs

@ -1,6 +1,9 @@
using System;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using ControlCatalog.Pages.Transitions;
namespace ControlCatalog.Pages
{
@ -9,28 +12,137 @@ namespace ControlCatalog.Pages
public CarouselPage()
{
InitializeComponent();
left.Click += (s, e) => carousel.Previous();
right.Click += (s, e) => carousel.Next();
transition.SelectionChanged += TransitionChanged;
orientation.SelectionChanged += TransitionChanged;
viewportFraction.ValueChanged += ViewportFractionChanged;
wrapSelection.IsChecked = carousel.WrapSelection;
wrapSelection.IsCheckedChanged += (s, e) =>
{
carousel.WrapSelection = wrapSelection.IsChecked ?? false;
UpdateButtonState();
};
swipeEnabled.IsChecked = carousel.IsSwipeEnabled;
swipeEnabled.IsCheckedChanged += (s, e) =>
{
carousel.IsSwipeEnabled = swipeEnabled.IsChecked ?? false;
};
carousel.PropertyChanged += (s, e) =>
{
if (e.Property == SelectingItemsControl.SelectedIndexProperty)
{
UpdateButtonState();
}
else if (e.Property == Carousel.ViewportFractionProperty)
{
UpdateViewportFractionDisplay();
}
};
carousel.ViewportFraction = viewportFraction.Value;
UpdateButtonState();
UpdateViewportFractionDisplay();
}
private void UpdateButtonState()
{
itemsCountIndicator.Text = carousel.ItemCount.ToString();
selectedIndexIndicator.Text = carousel.SelectedIndex.ToString();
var wrap = carousel.WrapSelection;
left.IsEnabled = wrap || carousel.SelectedIndex > 0;
right.IsEnabled = wrap || carousel.SelectedIndex < carousel.ItemCount - 1;
}
private void ViewportFractionChanged(object? sender, RangeBaseValueChangedEventArgs e)
{
carousel.ViewportFraction = Math.Round(e.NewValue, 2);
UpdateViewportFractionDisplay();
}
private void UpdateViewportFractionDisplay()
{
var value = carousel.ViewportFraction;
viewportFractionIndicator.Text = value.ToString("0.00");
var pagesInView = 1d / value;
viewportFractionHint.Text = value >= 1d
? "1.00 shows a single full page."
: $"{pagesInView:0.##} pages fit in view. Try 0.80 for peeking or 0.33 for three full items.";
}
private void TransitionChanged(object? sender, SelectionChangedEventArgs e)
{
var isVertical = orientation.SelectedIndex == 1;
var axis = isVertical ? PageSlide.SlideAxis.Vertical : PageSlide.SlideAxis.Horizontal;
switch (transition.SelectedIndex)
{
case 0:
carousel.PageTransition = null;
break;
case 1:
carousel.PageTransition = new PageSlide(TimeSpan.FromSeconds(0.25), orientation.SelectedIndex == 0 ? PageSlide.SlideAxis.Horizontal : PageSlide.SlideAxis.Vertical);
carousel.PageTransition = new PageSlide(TimeSpan.FromSeconds(0.25), axis);
break;
case 2:
carousel.PageTransition = new CrossFade(TimeSpan.FromSeconds(0.25));
break;
case 3:
carousel.PageTransition = new Rotate3DTransition(TimeSpan.FromSeconds(0.5), orientation.SelectedIndex == 0 ? PageSlide.SlideAxis.Horizontal : PageSlide.SlideAxis.Vertical);
carousel.PageTransition = new Rotate3DTransition(TimeSpan.FromSeconds(0.5), axis);
break;
case 4:
carousel.PageTransition = new CardStackPageTransition(TimeSpan.FromSeconds(0.5), axis);
break;
case 5:
carousel.PageTransition = new WaveRevealPageTransition(TimeSpan.FromSeconds(0.8), axis);
break;
case 6:
carousel.PageTransition = new CompositePageTransition
{
PageTransitions =
{
new PageSlide(TimeSpan.FromSeconds(0.25), axis),
new CrossFade(TimeSpan.FromSeconds(0.25)),
}
};
break;
}
UpdateLayoutForOrientation(isVertical);
}
private void UpdateLayoutForOrientation(bool isVertical)
{
if (isVertical)
{
Grid.SetColumn(left, 1);
Grid.SetRow(left, 0);
Grid.SetColumn(right, 1);
Grid.SetRow(right, 2);
left.Padding = new Thickness(20, 10);
right.Padding = new Thickness(20, 10);
leftArrow.RenderTransform = new Avalonia.Media.RotateTransform(90);
rightArrow.RenderTransform = new Avalonia.Media.RotateTransform(90);
}
else
{
Grid.SetColumn(left, 0);
Grid.SetRow(left, 1);
Grid.SetColumn(right, 2);
Grid.SetRow(right, 1);
left.Padding = new Thickness(10, 20);
right.Padding = new Thickness(10, 20);
leftArrow.RenderTransform = null;
rightArrow.RenderTransform = null;
}
}
}

119
samples/ControlCatalog/Pages/CarouselPage/CarouselCustomizationPage.xaml

@ -0,0 +1,119 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CarouselCustomizationPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="260">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Configuration" FontWeight="SemiBold" FontSize="16"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock Text="Navigation" FontWeight="SemiBold" FontSize="13" />
<Button x:Name="PreviousButton"
Content="Previous"
HorizontalAlignment="Stretch" />
<Button x:Name="NextButton"
Content="Next"
HorizontalAlignment="Stretch" />
<Separator />
<TextBlock Text="Orientation" FontWeight="SemiBold" FontSize="13" />
<ComboBox x:Name="OrientationCombo"
HorizontalAlignment="Stretch"
SelectedIndex="0">
<ComboBoxItem>Horizontal</ComboBoxItem>
<ComboBoxItem>Vertical</ComboBoxItem>
</ComboBox>
<TextBlock Text="Viewport Fraction" FontWeight="SemiBold" FontSize="13" />
<Grid ColumnDefinitions="*,48" ColumnSpacing="8">
<Slider x:Name="ViewportSlider"
Minimum="0.33"
Maximum="1.0"
Value="1.0"
TickFrequency="0.01"
HorizontalAlignment="Stretch" />
<TextBlock x:Name="ViewportLabel"
Grid.Column="1"
Text="1.00"
VerticalAlignment="Center"
HorizontalAlignment="Right"
FontWeight="SemiBold" />
</Grid>
<TextBlock x:Name="ViewportHint"
Text="1.00 shows a single full page."
FontSize="11"
Opacity="0.6"
TextWrapping="Wrap" />
<Separator />
<TextBlock Text="Options" FontWeight="SemiBold" FontSize="14" />
<CheckBox x:Name="WrapSelectionCheck"
Content="Wrap Selection"
IsChecked="False"
IsCheckedChanged="OnWrapSelectionChanged" />
<CheckBox x:Name="SwipeEnabledCheck"
Content="Swipe Enabled"
IsChecked="False"
IsCheckedChanged="OnSwipeEnabledChanged" />
<Separator />
<TextBlock Text="Status" FontWeight="SemiBold" FontSize="14" />
<TextBlock x:Name="StatusText"
Text="Orientation: Horizontal"
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">
<Carousel x:Name="DemoCarousel" Height="300">
<Carousel.PageTransition>
<PageSlide Duration="0.25" Orientation="Horizontal" />
</Carousel.PageTransition>
<Border Margin="14,12" CornerRadius="12" ClipToBounds="True">
<Grid>
<Image Source="/Assets/delicate-arch-896885_640.jpg" Stretch="UniformToFill" />
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12">
<TextBlock Text="Item 1: Delicate Arch" Foreground="White"
HorizontalAlignment="Center" FontWeight="SemiBold" />
</Border>
</Grid>
</Border>
<Border Margin="14,12" CornerRadius="12" ClipToBounds="True">
<Grid>
<Image Source="/Assets/hirsch-899118_640.jpg" Stretch="UniformToFill" />
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12">
<TextBlock Text="Item 2: Hirsch" Foreground="White"
HorizontalAlignment="Center" FontWeight="SemiBold" />
</Border>
</Grid>
</Border>
<Border Margin="14,12" CornerRadius="12" ClipToBounds="True">
<Grid>
<Image Source="/Assets/maple-leaf-888807_640.jpg" Stretch="UniformToFill" />
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12">
<TextBlock Text="Item 3: Maple Leaf" Foreground="White"
HorizontalAlignment="Center" FontWeight="SemiBold" />
</Border>
</Grid>
</Border>
</Carousel>
</Border>
</DockPanel>
</UserControl>

48
samples/ControlCatalog/Pages/CarouselPage/CarouselCustomizationPage.xaml.cs

@ -0,0 +1,48 @@
using System;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public partial class CarouselCustomizationPage : UserControl
{
public CarouselCustomizationPage()
{
InitializeComponent();
PreviousButton.Click += (_, _) => DemoCarousel.Previous();
NextButton.Click += (_, _) => DemoCarousel.Next();
OrientationCombo.SelectionChanged += (_, _) => ApplyOrientation();
ViewportSlider.ValueChanged += OnViewportFractionChanged;
}
private void ApplyOrientation()
{
var horizontal = OrientationCombo.SelectedIndex == 0;
var axis = horizontal ? PageSlide.SlideAxis.Horizontal : PageSlide.SlideAxis.Vertical;
DemoCarousel.PageTransition = new PageSlide(TimeSpan.FromSeconds(0.25), axis);
StatusText.Text = $"Orientation: {(horizontal ? "Horizontal" : "Vertical")}";
}
private void OnViewportFractionChanged(object? sender, RangeBaseValueChangedEventArgs e)
{
var value = Math.Round(e.NewValue, 2);
DemoCarousel.ViewportFraction = value;
ViewportLabel.Text = value.ToString("0.00");
ViewportHint.Text = value >= 1d ?
"1.00 shows a single full page." :
$"{1d / value:0.##} pages fit in view. Try 0.80 for peeking.";
}
private void OnWrapSelectionChanged(object? sender, RoutedEventArgs e)
{
DemoCarousel.WrapSelection = WrapSelectionCheck.IsChecked == true;
}
private void OnSwipeEnabledChanged(object? sender, RoutedEventArgs e)
{
DemoCarousel.IsSwipeEnabled = SwipeEnabledCheck.IsChecked == true;
}
}
}

60
samples/ControlCatalog/Pages/CarouselPage/CarouselDataBindingPage.xaml

@ -0,0 +1,60 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pages="clr-namespace:ControlCatalog.Pages"
x:Class="ControlCatalog.Pages.CarouselDataBindingPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="260">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Collection" FontWeight="SemiBold" FontSize="16"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock Text="Navigation" FontWeight="SemiBold" FontSize="13" />
<Button x:Name="PreviousButton" Content="Previous" HorizontalAlignment="Stretch" />
<Button x:Name="NextButton" Content="Next" HorizontalAlignment="Stretch" />
<Separator />
<TextBlock Text="Modify Items" FontWeight="SemiBold" FontSize="13" />
<TextBlock TextWrapping="Wrap" FontSize="11" Opacity="0.6"
Text="The Carousel is bound to an ObservableCollection. Changes reflect immediately." />
<Button x:Name="AddButton" Content="Add Item" HorizontalAlignment="Stretch" />
<Button x:Name="RemoveButton" Content="Remove Current" HorizontalAlignment="Stretch" />
<Button x:Name="ShuffleButton" Content="Shuffle" HorizontalAlignment="Stretch" />
<Separator />
<TextBlock Text="Status" FontWeight="SemiBold" FontSize="14" />
<TextBlock x:Name="StatusText" Text="Item: 1 / 4"
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">
<Carousel x:Name="DemoCarousel"
Height="280"
IsSwipeEnabled="True">
<Carousel.PageTransition>
<CrossFade Duration="0.3" />
</Carousel.PageTransition>
<Carousel.ItemTemplate>
<DataTemplate x:DataType="pages:CarouselCardItem">
<Border CornerRadius="14" Margin="14,12" ClipToBounds="True"
Background="{Binding Background}">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="8">
<TextBlock Text="{Binding Number}" FontSize="52" FontWeight="Bold"
Foreground="White" HorizontalAlignment="Center" LetterSpacing="-2" />
<TextBlock Text="{Binding Title}" FontSize="15" FontWeight="SemiBold"
Foreground="{Binding Accent}" HorizontalAlignment="Center" />
</StackPanel>
</Border>
</DataTemplate>
</Carousel.ItemTemplate>
</Carousel>
</Border>
</DockPanel>
</UserControl>

95
samples/ControlCatalog/Pages/CarouselPage/CarouselDataBindingPage.xaml.cs

@ -0,0 +1,95 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
namespace ControlCatalog.Pages
{
public class CarouselCardItem
{
public string Number { get; set; } = "";
public string Title { get; set; } = "";
public IBrush Background { get; set; } = Brushes.Gray;
public IBrush Accent { get; set; } = Brushes.White;
}
public partial class CarouselDataBindingPage : UserControl
{
private static readonly (string Title, string Color, string Accent)[] Palette =
{
("Neon Pulse", "#3525CD", "#C3C0FF"), ("Ephemeral Blue", "#0891B2", "#BAF0FA"),
("Forest Forms", "#059669", "#A7F3D0"), ("Golden Hour", "#D97706", "#FDE68A"),
("Crimson Wave", "#BE185D", "#FBCFE8"), ("Stone Age", "#57534E", "#D6D3D1"),
};
private readonly ObservableCollection<CarouselCardItem> _items = new();
private int _addCounter;
public CarouselDataBindingPage()
{
InitializeComponent();
DemoCarousel.ItemsSource = _items;
DemoCarousel.SelectionChanged += OnSelectionChanged;
for (var i = 0; i < 4; i++)
AppendItem();
PreviousButton.Click += (_, _) => DemoCarousel.Previous();
NextButton.Click += (_, _) => DemoCarousel.Next();
AddButton.Click += OnAddItem;
RemoveButton.Click += OnRemoveCurrent;
ShuffleButton.Click += OnShuffle;
UpdateStatus();
}
private void AppendItem()
{
var (title, color, accent) = Palette[_addCounter % Palette.Length];
_items.Add(new CarouselCardItem
{
Number = $"{_items.Count + 1:D2}",
Title = title,
Background = new SolidColorBrush(Color.Parse(color)),
Accent = new SolidColorBrush(Color.Parse(accent)),
});
_addCounter++;
}
private void OnAddItem(object? sender, RoutedEventArgs e)
{
AppendItem();
UpdateStatus();
}
private void OnRemoveCurrent(object? sender, RoutedEventArgs e)
{
if (_items.Count == 0)
return;
var idx = Math.Clamp(DemoCarousel.SelectedIndex, 0, _items.Count - 1);
_items.RemoveAt(idx);
UpdateStatus();
}
private void OnShuffle(object? sender, RoutedEventArgs e)
{
var rng = new Random();
var shuffled = _items.OrderBy(_ => rng.Next()).ToList();
_items.Clear();
foreach (var item in shuffled)
_items.Add(item);
UpdateStatus();
}
private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
UpdateStatus();
}
private void UpdateStatus()
{
StatusText.Text = $"Item: {DemoCarousel.SelectedIndex + 1} / {_items.Count}";
}
}
}

557
samples/ControlCatalog/Pages/CarouselPage/CarouselGalleryAppPage.xaml

@ -0,0 +1,557 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CarouselGalleryAppPage"
Background="#F8F9FB">
<UserControl.Resources>
<!-- White pip colors for the hero dark background -->
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForeground" Color="#7FFFFFFF" />
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundPointerOver" Color="#BFFFFFFF" />
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundPressed" Color="#BFFFFFFF" />
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundSelected" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundDisabled" Color="#3FFFFFFF" />
<SolidColorBrush x:Key="PipsPagerNavigationButtonForeground" Color="#7FFFFFFF" />
<SolidColorBrush x:Key="PipsPagerNavigationButtonForegroundPointerOver" Color="#BFFFFFFF" />
<SolidColorBrush x:Key="PipsPagerNavigationButtonForegroundPressed" Color="#BFFFFFFF" />
</UserControl.Resources>
<DockPanel>
<!-- Right info panel — visible when width >= 640px -->
<ScrollViewer x:Name="InfoPanel" DockPanel.Dock="Right" Width="290" IsVisible="False">
<StackPanel Margin="16" Spacing="16">
<TextBlock Text="Curated Gallery" FontSize="16" FontWeight="SemiBold"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="Art gallery editorial app showcasing a full-bleed hero Carousel synced with a pill-shaped PipsPager, a peek Collection Highlights scroll list, Curators' Choice cards, and a Join the Circle subscription section. Navigation via DrawerPage side menu." />
<Separator />
<TextBlock Text="Navigation Flow" FontSize="13" FontWeight="SemiBold" />
<StackPanel Spacing="4">
<TextBlock FontSize="12" TextWrapping="Wrap" Text="1. DrawerPage: root, side placement" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="2. Hamburger overlaid on hero opens the drawer pane" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="3. Hero: full-bleed Carousel + PipsPager (pill dots)" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="4. PipsPager synced bidirectionally with Carousel" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="5. Mouse drag on hero navigates carousel slides" />
</StackPanel>
<Separator />
<TextBlock Text="Key Code" FontSize="13" FontWeight="SemiBold" />
<Border Background="{DynamicResource SystemControlBackgroundBaseLowBrush}"
CornerRadius="4" Padding="8">
<TextBlock FontFamily="Cascadia Code,Consolas,Menlo,monospace"
FontSize="10" TextWrapping="Wrap"
Text="HeroCarousel.SelectionChanged&#xA; += OnHeroSelectionChanged;&#xA;HeroPager.SelectedIndexChanged&#xA; += OnPagerIndexChanged;&#xA;&#xA;// Bidirectional sync guard&#xA;if (_syncing) return;&#xA;_syncing = true;&#xA;HeroPager.SelectedPageIndex&#xA; = HeroCarousel.SelectedIndex;&#xA;_syncing = false;" />
</Border>
</StackPanel>
</ScrollViewer>
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1" CornerRadius="8" ClipToBounds="True">
<DrawerPage x:Name="RootDrawer"
DrawerLength="260"
IsGestureEnabled="True">
<DrawerPage.Styles>
<!-- Hide the DrawerPage built-in top bar so only our custom hero overlay bar is shown -->
<Style Selector="DrawerPage#RootDrawer /template/ Border#PART_TopBar">
<Setter Property="IsVisible" Value="False" />
</Style>
</DrawerPage.Styles>
<!-- Drawer header -->
<DrawerPage.DrawerHeader>
<Border Background="#3525CD" Padding="20,32,20,20">
<StackPanel Spacing="4">
<TextBlock Text="CURATED"
FontSize="20"
FontWeight="Bold"
Foreground="White"
LetterSpacing="-0.4" />
<TextBlock Text="The Digital Gallery"
FontSize="12"
Foreground="#C3C0FF"
Opacity="0.85" />
</StackPanel>
</Border>
</DrawerPage.DrawerHeader>
<!-- Drawer menu -->
<DrawerPage.Drawer>
<StackPanel Background="#F8F9FB">
<ListBox x:Name="DrawerMenu"
Background="Transparent"
SelectionChanged="OnDrawerMenuSelectionChanged">
<ListBoxItem Padding="20,14">
<StackPanel Orientation="Horizontal" Spacing="16">
<Path Data="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"
Fill="#3525CD" Width="20" Height="20" Stretch="Uniform" />
<TextBlock Text="Discover" FontSize="15" FontWeight="SemiBold"
Foreground="#191C1E" VerticalAlignment="Center" />
</StackPanel>
</ListBoxItem>
<ListBoxItem Padding="20,14">
<StackPanel Orientation="Horizontal" Spacing="16">
<Path Data="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"
Fill="#464555" Width="20" Height="20" Stretch="Uniform" />
<TextBlock Text="Collection" FontSize="15"
Foreground="#464555" VerticalAlignment="Center" />
</StackPanel>
</ListBoxItem>
<ListBoxItem Padding="20,14">
<StackPanel Orientation="Horizontal" Spacing="16">
<Path Data="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 14l-5-5 1.41-1.41L12 14.17l7.59-7.59L21 8l-9 9z"
Fill="#464555" Width="20" Height="20" Stretch="Uniform" />
<TextBlock Text="Archive" FontSize="15"
Foreground="#464555" VerticalAlignment="Center" />
</StackPanel>
</ListBoxItem>
<ListBoxItem Padding="20,14">
<StackPanel Orientation="Horizontal" Spacing="16">
<Path Data="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"
Fill="#464555" Width="20" Height="20" Stretch="Uniform" />
<TextBlock Text="Profile" FontSize="15"
Foreground="#464555" VerticalAlignment="Center" />
</StackPanel>
</ListBoxItem>
</ListBox>
<Separator Margin="20,8" />
<StackPanel Margin="20,8" Spacing="0">
<TextBlock Text="EXHIBITIONS"
FontSize="11"
FontWeight="Bold"
Foreground="#777587"
LetterSpacing="1.2"
Margin="0,0,0,16" />
<Grid ColumnDefinitions="4,*" Margin="0,0,0,14">
<Border Grid.Column="0" Width="4" Height="38"
CornerRadius="2" Background="#3525CD"
VerticalAlignment="Center" Margin="0,0,14,0" />
<StackPanel Grid.Column="1" Spacing="2" VerticalAlignment="Center">
<TextBlock Text="Neon Pulse" FontSize="14" FontWeight="SemiBold"
Foreground="#191C1E" />
<TextBlock Text="Opens March 20" FontSize="11" Foreground="#777587" />
</StackPanel>
</Grid>
<Grid ColumnDefinitions="4,*" Margin="0,0,0,14">
<Border Grid.Column="0" Width="4" Height="38"
CornerRadius="2" Background="#4F46E5"
VerticalAlignment="Center" Margin="0,0,14,0" />
<StackPanel Grid.Column="1" Spacing="2" VerticalAlignment="Center">
<TextBlock Text="Fragmented Forms" FontSize="14" FontWeight="SemiBold"
Foreground="#191C1E" />
<TextBlock Text="Now Open" FontSize="11" Foreground="#4F46E5" />
</StackPanel>
</Grid>
<Grid ColumnDefinitions="4,*">
<Border Grid.Column="0" Width="4" Height="38"
CornerRadius="2" Background="#B84B00"
VerticalAlignment="Center" Margin="0,0,14,0" />
<StackPanel Grid.Column="1" Spacing="2" VerticalAlignment="Center">
<TextBlock Text="The Digital Horizon" FontSize="14" FontWeight="SemiBold"
Foreground="#191C1E" />
<TextBlock Text="Closing Soon" FontSize="11" Foreground="#B84B00" />
</StackPanel>
</Grid>
</StackPanel>
</StackPanel>
</DrawerPage.Drawer>
<!-- Main content: hero carousel IS the header -->
<DrawerPage.Content>
<Grid RowDefinitions="Auto,*">
<!-- Row 0: Hero carousel header — also handles mouse drag for swipe navigation -->
<Grid Height="320"
PointerPressed="OnHeroPointerPressed"
PointerReleased="OnHeroPointerReleased"
PointerCaptureLost="OnHeroPointerCaptureLost">
<!-- Full-bleed hero carousel -->
<Carousel x:Name="HeroCarousel"
IsSwipeEnabled="True">
<Carousel.PageTransition>
<PageSlide Duration="0.35" Orientation="Horizontal" />
</Carousel.PageTransition>
<!-- Hero 1 -->
<Grid>
<Image Source="/Assets/ModernApp/gallery_city.jpg" Stretch="UniformToFill" />
<Border>
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#88000000" Offset="0" />
<GradientStop Color="#00000000" Offset="0.35" />
<GradientStop Color="#00000000" Offset="0.55" />
<GradientStop Color="#CC000000" Offset="1" />
</LinearGradientBrush>
</Border.Background>
</Border>
<StackPanel VerticalAlignment="Bottom" Margin="20,0,20,44" Spacing="4">
<TextBlock Text="FEATURED EXHIBITION"
FontSize="11" FontWeight="Bold"
Foreground="#C3C0FF" LetterSpacing="1.5" />
<TextBlock Text="Neon Pulse: The New Abstract"
FontSize="22" FontWeight="Bold"
Foreground="White" TextWrapping="Wrap" LetterSpacing="-0.4" />
</StackPanel>
</Grid>
<!-- Hero 2 -->
<Grid>
<Image Source="/Assets/ModernApp/gallery_alpine.jpg" Stretch="UniformToFill" />
<Border>
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#88000000" Offset="0" />
<GradientStop Color="#00000000" Offset="0.35" />
<GradientStop Color="#00000000" Offset="0.55" />
<GradientStop Color="#CC000000" Offset="1" />
</LinearGradientBrush>
</Border.Background>
</Border>
<StackPanel VerticalAlignment="Bottom" Margin="20,0,20,44" Spacing="4">
<TextBlock Text="NOW OPEN"
FontSize="11" FontWeight="Bold"
Foreground="#C3C0FF" LetterSpacing="1.5" />
<TextBlock Text="Fragmented Forms: Sculpture Today"
FontSize="22" FontWeight="Bold"
Foreground="White" TextWrapping="Wrap" LetterSpacing="-0.4" />
</StackPanel>
</Grid>
<!-- Hero 3 -->
<Grid>
<Image Source="/Assets/ModernApp/gallery_venice.jpg" Stretch="UniformToFill" />
<Border>
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#88000000" Offset="0" />
<GradientStop Color="#00000000" Offset="0.35" />
<GradientStop Color="#00000000" Offset="0.55" />
<GradientStop Color="#CC000000" Offset="1" />
</LinearGradientBrush>
</Border.Background>
</Border>
<StackPanel VerticalAlignment="Bottom" Margin="20,0,20,44" Spacing="4">
<TextBlock Text="CLOSING SOON"
FontSize="11" FontWeight="Bold"
Foreground="#FFCDD2" LetterSpacing="1.5" />
<TextBlock Text="The Digital Horizon: Web3 &amp; Generative Art"
FontSize="22" FontWeight="Bold"
Foreground="White" TextWrapping="Wrap" LetterSpacing="-0.4" />
</StackPanel>
</Grid>
</Carousel>
<!-- PipsPager overlaid near bottom of hero — pill-shaped, no nav arrows -->
<PipsPager x:Name="HeroPager"
NumberOfPages="3"
IsPreviousButtonVisible="False"
IsNextButtonVisible="False"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Margin="0,0,0,18">
<PipsPager.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="#7FFFFFFF">
<Border.Transitions>
<Transitions>
<DoubleTransition Property="Width" 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="#BFFFFFFF" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected /template/ Border#Pip">
<Setter Property="Width" Value="22" />
<Setter Property="Height" Value="6" />
<Setter Property="CornerRadius" Value="3" />
<Setter Property="Background" Value="#FFFFFFFF" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected:pointerover /template/ Border#Pip">
<Setter Property="Width" Value="22" />
<Setter Property="Height" Value="6" />
<Setter Property="CornerRadius" Value="3" />
<Setter Property="Background" Value="#E8FFFFFF" />
</Style>
</PipsPager.Styles>
</PipsPager>
<!-- Top bar overlaid on hero -->
<Grid ColumnDefinitions="Auto,*,Auto"
VerticalAlignment="Top"
Margin="4,8,4,0">
<Button Grid.Column="0"
Background="Transparent"
BorderThickness="0"
Padding="12,8"
Click="OnHamburgerClick">
<Path Data="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"
Fill="White" Width="22" Height="22" Stretch="Uniform" />
</Button>
<TextBlock Grid.Column="1"
Text="Curated"
FontSize="18"
FontWeight="Bold"
Foreground="White"
VerticalAlignment="Center"
HorizontalAlignment="Center"
LetterSpacing="-0.3" />
<Button Grid.Column="2"
Background="Transparent"
BorderThickness="0"
Padding="12,8">
<Path Data="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
Fill="White" Width="22" Height="22" Stretch="Uniform" />
</Button>
</Grid>
</Grid>
<!-- Row 1: Scrollable body -->
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel>
<!-- Collection Highlights -->
<StackPanel Margin="0,28,0,0">
<Grid ColumnDefinitions="*,Auto" Margin="20,0,20,16">
<TextBlock Text="Collection Highlights"
FontSize="18" FontWeight="Bold"
Foreground="#191C1E" LetterSpacing="-0.3" />
<TextBlock Grid.Column="1"
Text="SEE ALL"
FontSize="12" FontWeight="Bold"
Foreground="#3525CD" LetterSpacing="0.8"
VerticalAlignment="Center" />
</Grid>
<ScrollViewer HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Disabled"
Margin="20,0,0,0">
<StackPanel Orientation="Horizontal" Spacing="10">
<Border Width="180" Height="210" CornerRadius="12" ClipToBounds="True">
<Grid>
<Image Source="/Assets/ModernApp/gallery_paris.jpg" Stretch="UniformToFill" />
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12,10">
<StackPanel Spacing="2">
<TextBlock Text="SCULPTURE" FontSize="10" FontWeight="Bold"
Foreground="#C3C0FF" LetterSpacing="1" />
<TextBlock Text="Fragmented Grace" FontSize="13"
FontWeight="SemiBold" Foreground="White" />
</StackPanel>
</Border>
</Grid>
</Border>
<Border Width="180" Height="210" CornerRadius="12" ClipToBounds="True">
<Grid>
<Image Source="/Assets/ModernApp/gallery_bay.jpg" Stretch="UniformToFill" />
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12,10">
<StackPanel Spacing="2">
<TextBlock Text="OIL PAINTING" FontSize="10" FontWeight="Bold"
Foreground="#C3C0FF" LetterSpacing="1" />
<TextBlock Text="Ephemeral Blue" FontSize="13"
FontWeight="SemiBold" Foreground="White" />
</StackPanel>
</Border>
</Grid>
</Border>
<Border Width="180" Height="210" CornerRadius="12" ClipToBounds="True">
<Grid>
<Image Source="/Assets/ModernApp/gallery_tropical.jpg" Stretch="UniformToFill" />
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12,10">
<StackPanel Spacing="2">
<TextBlock Text="TEXTILE" FontSize="10" FontWeight="Bold"
Foreground="#C3C0FF" LetterSpacing="1" />
<TextBlock Text="Interwoven Lines" FontSize="13"
FontWeight="SemiBold" Foreground="White" />
</StackPanel>
</Border>
</Grid>
</Border>
<Border Width="180" Height="210" CornerRadius="12" ClipToBounds="True">
<Grid>
<Image Source="/Assets/ModernApp/gallery_alpine.jpg" Stretch="UniformToFill" />
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12,10">
<StackPanel Spacing="2">
<TextBlock Text="PHOTOGRAPHY" FontSize="10" FontWeight="Bold"
Foreground="#C3C0FF" LetterSpacing="1" />
<TextBlock Text="Silent Mountains" FontSize="13"
FontWeight="SemiBold" Foreground="White" />
</StackPanel>
</Border>
</Grid>
</Border>
<!-- Padding card to reveal peek of last item -->
<Border Width="20" Height="210" />
</StackPanel>
</ScrollViewer>
</StackPanel>
<!-- Curators' Choice -->
<StackPanel Margin="20,32,20,0" Spacing="12">
<TextBlock Text="Curators' Choice"
FontSize="20" FontWeight="Bold"
Foreground="#191C1E" HorizontalAlignment="Center"
LetterSpacing="-0.3" />
<TextBlock Text="Hand-picked selections from our global network of artists."
FontSize="13" Foreground="#777587"
HorizontalAlignment="Center" TextAlignment="Center"
TextWrapping="Wrap" Margin="0,0,0,8" />
<!-- Two-column layout: large card left, two stacked badge cards right -->
<Grid ColumnDefinitions="*,130">
<!-- Left: main feature card -->
<Border Grid.Column="0" Background="White" CornerRadius="16"
Padding="20" Margin="0,0,10,0"
BoxShadow="0 2 16 0 #12191C1E">
<StackPanel Spacing="10">
<Path Data="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5z"
Fill="#3525CD" Width="22" Height="22" Stretch="Uniform"
HorizontalAlignment="Left" />
<TextBlock Text="The Digital Horizon"
FontSize="17" FontWeight="Bold"
Foreground="#191C1E" TextWrapping="Wrap" />
<TextBlock Text="Exploring Web3 and Generative Art"
FontSize="13" Foreground="#777587" TextWrapping="Wrap" />
<Button Content="EXPLORE"
Margin="0,10,0,0" Padding="20,11"
FontSize="11" FontWeight="Bold" LetterSpacing="0.8"
CornerRadius="22" Foreground="White" HorizontalAlignment="Left">
<Button.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="#3525CD" Offset="0" />
<GradientStop Color="#4F46E5" Offset="1" />
</LinearGradientBrush>
</Button.Background>
</Button>
</StackPanel>
</Border>
<!-- Right: two stacked badge cards -->
<StackPanel Grid.Column="1" Spacing="10">
<Border Background="White" CornerRadius="16"
BoxShadow="0 2 16 0 #12191C1E"
Padding="12">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"
Spacing="8" Margin="0,12">
<Path Data="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14H9V8h2v8zm4 0h-2V8h2v8z"
Fill="#B84B00" Width="28" Height="28" Stretch="Uniform"
HorizontalAlignment="Center" />
<TextBlock Text="TRENDING"
FontSize="10" FontWeight="Bold"
Foreground="#B84B00" LetterSpacing="1"
HorizontalAlignment="Center" />
</StackPanel>
</Border>
<Border Background="#EDEEF0" CornerRadius="16" Padding="12">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"
Spacing="8" Margin="0,12">
<Path Data="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1.41 14.06L6.17 11.64l1.42-1.41 2.99 3 6.01-6.01 1.42 1.41-7.42 7.43z"
Fill="#464555" Width="28" Height="28" Stretch="Uniform"
HorizontalAlignment="Center" />
<TextBlock Text="VERIFIED"
FontSize="10" FontWeight="Bold"
Foreground="#464555" LetterSpacing="1"
HorizontalAlignment="Center" />
</StackPanel>
</Border>
</StackPanel>
</Grid>
</StackPanel>
<!-- Join the Circle -->
<Border Margin="20,32,20,32" CornerRadius="20" Padding="24" ClipToBounds="True">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#3525CD" Offset="0" />
<GradientStop Color="#4F46E5" Offset="1" />
</LinearGradientBrush>
</Border.Background>
<StackPanel Spacing="10">
<TextBlock Text="Join the Circle"
FontSize="20" FontWeight="Bold"
Foreground="White" LetterSpacing="-0.3" />
<TextBlock Text="Receive exclusive invitations to private viewings and new drop alerts."
FontSize="13" Foreground="#C3C0FF"
TextWrapping="Wrap" Opacity="0.9" LineHeight="20" />
<TextBox PlaceholderText="Your email address"
Margin="0,6,0,0"
CornerRadius="12"
BorderThickness="1"
Padding="14,12"
Foreground="White"
PlaceholderForeground="#9896D8">
<TextBox.Resources>
<SolidColorBrush x:Key="TextControlBackground" Color="#3C38B5" />
<SolidColorBrush x:Key="TextControlBackgroundPointerOver" Color="#4440BE" />
<SolidColorBrush x:Key="TextControlBackgroundFocused" Color="#3C38B5" />
<SolidColorBrush x:Key="TextControlBorderBrush" Color="#5552C8" />
<SolidColorBrush x:Key="TextControlBorderBrushPointerOver" Color="#7370D8" />
<SolidColorBrush x:Key="TextControlBorderBrushFocused" Color="#8B88E8" />
</TextBox.Resources>
</TextBox>
<Button Content="SUBSCRIBE"
Margin="0,2,0,0" Padding="24,12"
FontSize="12" FontWeight="Bold" LetterSpacing="1"
CornerRadius="24" Foreground="#3525CD" Background="White"
HorizontalAlignment="Left" />
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</DrawerPage.Content>
</DrawerPage>
</Border>
</DockPanel>
</UserControl>

101
samples/ControlCatalog/Pages/CarouselPage/CarouselGalleryAppPage.xaml.cs

@ -0,0 +1,101 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public partial class CarouselGalleryAppPage : UserControl
{
private bool _syncing;
private Point _dragStart;
private bool _isDragging;
private const double SwipeThreshold = 50;
private ScrollViewer? _infoPanel;
public CarouselGalleryAppPage()
{
InitializeComponent();
_infoPanel = this.FindControl<ScrollViewer>("InfoPanel");
HeroCarousel.SelectionChanged += OnHeroSelectionChanged;
HeroPager.SelectedIndexChanged += OnPagerIndexChanged;
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
UpdateInfoPanelVisibility();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == BoundsProperty)
UpdateInfoPanelVisibility();
}
private void UpdateInfoPanelVisibility()
{
if (_infoPanel != null)
_infoPanel.IsVisible = Bounds.Width >= 640;
}
private void OnHamburgerClick(object? sender, RoutedEventArgs e)
{
RootDrawer.IsOpen = !RootDrawer.IsOpen;
}
private void OnHeroSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_syncing)
return;
_syncing = true;
HeroPager.SelectedPageIndex = HeroCarousel.SelectedIndex;
_syncing = false;
}
private void OnPagerIndexChanged(object? sender, PipsPagerSelectedIndexChangedEventArgs e)
{
if (_syncing)
return;
_syncing = true;
HeroCarousel.SelectedIndex = e.NewIndex;
_syncing = false;
}
private void OnDrawerMenuSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
RootDrawer.IsOpen = false;
DrawerMenu.SelectedItem = null;
}
private void OnHeroPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(null).Properties.IsLeftButtonPressed)
return;
_dragStart = e.GetPosition((Visual?)sender);
_isDragging = true;
}
private void OnHeroPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (!_isDragging)
return;
_isDragging = false;
var delta = e.GetPosition((Visual?)sender).X - _dragStart.X;
if (Math.Abs(delta) < SwipeThreshold)
return;
if (delta < 0)
HeroCarousel.Next();
else
HeroCarousel.Previous();
}
private void OnHeroPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e)
{
_isDragging = false;
}
}
}

93
samples/ControlCatalog/Pages/CarouselPage/CarouselGesturesPage.xaml

@ -0,0 +1,93 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CarouselGesturesPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="260">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Configuration" FontWeight="SemiBold" FontSize="16"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock Text="Options" FontWeight="SemiBold" FontSize="13" />
<CheckBox x:Name="SwipeCheck"
Content="Swipe Gesture"
IsChecked="True"
IsCheckedChanged="OnSwipeEnabledChanged" />
<CheckBox x:Name="WrapCheck"
Content="Wrap Selection"
IsChecked="False"
IsCheckedChanged="OnWrapSelectionChanged" />
<CheckBox x:Name="KeyboardCheck"
Content="Keyboard Navigation"
IsChecked="True"
IsCheckedChanged="OnKeyboardEnabledChanged" />
<Separator />
<TextBlock Text="Status" FontWeight="SemiBold" FontSize="14" />
<TextBlock x:Name="StatusText"
Text="Item: 1 / 3"
Opacity="0.7"
TextWrapping="Wrap" />
<TextBlock x:Name="LastActionText"
Text="Action: —"
Opacity="0.7"
TextWrapping="Wrap" />
<TextBlock Text="Swipe left/right or use arrow keys to navigate."
FontSize="11"
Opacity="0.5"
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">
<Carousel x:Name="DemoCarousel"
Focusable="True"
IsSwipeEnabled="True"
Height="300">
<Carousel.PageTransition>
<PageSlide Duration="0.25" Orientation="Horizontal" />
</Carousel.PageTransition>
<Border Margin="14,12" CornerRadius="12" ClipToBounds="True">
<Grid>
<Image Source="/Assets/delicate-arch-896885_640.jpg" Stretch="UniformToFill" />
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12">
<TextBlock Text="Item 1: Delicate Arch" Foreground="White"
HorizontalAlignment="Center" FontWeight="SemiBold" />
</Border>
</Grid>
</Border>
<Border Margin="14,12" CornerRadius="12" ClipToBounds="True">
<Grid>
<Image Source="/Assets/hirsch-899118_640.jpg" Stretch="UniformToFill" />
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12">
<TextBlock Text="Item 2: Hirsch" Foreground="White"
HorizontalAlignment="Center" FontWeight="SemiBold" />
</Border>
</Grid>
</Border>
<Border Margin="14,12" CornerRadius="12" ClipToBounds="True">
<Grid>
<Image Source="/Assets/maple-leaf-888807_640.jpg" Stretch="UniformToFill" />
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12">
<TextBlock Text="Item 3: Maple Leaf" Foreground="White"
HorizontalAlignment="Center" FontWeight="SemiBold" />
</Border>
</Grid>
</Border>
</Carousel>
</Border>
</DockPanel>
</UserControl>

59
samples/ControlCatalog/Pages/CarouselPage/CarouselGesturesPage.xaml.cs

@ -0,0 +1,59 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public partial class CarouselGesturesPage : UserControl
{
private bool _keyboardEnabled = true;
public CarouselGesturesPage()
{
InitializeComponent();
DemoCarousel.AddHandler(InputElement.KeyDownEvent, OnKeyDown, handledEventsToo: true);
DemoCarousel.SelectionChanged += OnSelectionChanged;
DemoCarousel.Loaded += (_, _) => DemoCarousel.Focus();
}
private void OnKeyDown(object? sender, KeyEventArgs e)
{
if (!_keyboardEnabled)
return;
switch (e.Key)
{
case Key.Left:
case Key.Up:
LastActionText.Text = $"Action: Key {e.Key} (Previous)";
break;
case Key.Right:
case Key.Down:
LastActionText.Text = $"Action: Key {e.Key} (Next)";
break;
}
}
private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
StatusText.Text = $"Item: {DemoCarousel.SelectedIndex + 1} / {DemoCarousel.ItemCount}";
if (DemoCarousel.IsSwiping)
LastActionText.Text = "Action: Swipe";
}
private void OnSwipeEnabledChanged(object? sender, RoutedEventArgs e)
{
DemoCarousel.IsSwipeEnabled = SwipeCheck.IsChecked == true;
}
private void OnWrapSelectionChanged(object? sender, RoutedEventArgs e)
{
DemoCarousel.WrapSelection = WrapCheck.IsChecked == true;
}
private void OnKeyboardEnabledChanged(object? sender, RoutedEventArgs e)
{
_keyboardEnabled = KeyboardCheck.IsChecked == true;
}
}
}

74
samples/ControlCatalog/Pages/CarouselPage/CarouselGettingStartedPage.xaml

@ -0,0 +1,74 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CarouselGettingStartedPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="260">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Configuration" FontWeight="SemiBold" FontSize="16"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock Text="Navigation" FontWeight="SemiBold" FontSize="13" />
<Button x:Name="PreviousButton"
Content="Previous"
HorizontalAlignment="Stretch" />
<Button x:Name="NextButton"
Content="Next"
HorizontalAlignment="Stretch" />
<Separator />
<TextBlock Text="Status" FontWeight="SemiBold" FontSize="14" />
<TextBlock x:Name="StatusText"
Text="Item: 1 / 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">
<Carousel x:Name="DemoCarousel" Height="300" IsSwipeEnabled="True">
<Carousel.PageTransition>
<PageSlide Duration="0.25" Orientation="Horizontal" />
</Carousel.PageTransition>
<Border Margin="14,12" CornerRadius="12" ClipToBounds="True">
<Grid>
<Image Source="/Assets/delicate-arch-896885_640.jpg" Stretch="UniformToFill" />
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12">
<TextBlock Text="Item 1: Delicate Arch" Foreground="White"
HorizontalAlignment="Center" FontWeight="SemiBold" />
</Border>
</Grid>
</Border>
<Border Margin="14,12" CornerRadius="12" ClipToBounds="True">
<Grid>
<Image Source="/Assets/hirsch-899118_640.jpg" Stretch="UniformToFill" />
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12">
<TextBlock Text="Item 2: Hirsch" Foreground="White"
HorizontalAlignment="Center" FontWeight="SemiBold" />
</Border>
</Grid>
</Border>
<Border Margin="14,12" CornerRadius="12" ClipToBounds="True">
<Grid>
<Image Source="/Assets/maple-leaf-888807_640.jpg" Stretch="UniformToFill" />
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12">
<TextBlock Text="Item 3: Maple Leaf" Foreground="White"
HorizontalAlignment="Center" FontWeight="SemiBold" />
</Border>
</Grid>
</Border>
</Carousel>
</Border>
</DockPanel>
</UserControl>

40
samples/ControlCatalog/Pages/CarouselPage/CarouselGettingStartedPage.xaml.cs

@ -0,0 +1,40 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public partial class CarouselGettingStartedPage : UserControl
{
public CarouselGettingStartedPage()
{
InitializeComponent();
PreviousButton.Click += OnPrevious;
NextButton.Click += OnNext;
DemoCarousel.SelectionChanged += OnSelectionChanged;
}
private void OnPrevious(object? sender, RoutedEventArgs e)
{
DemoCarousel.Previous();
UpdateStatus();
}
private void OnNext(object? sender, RoutedEventArgs e)
{
DemoCarousel.Next();
UpdateStatus();
}
private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
UpdateStatus();
}
private void UpdateStatus()
{
var index = DemoCarousel.SelectedIndex + 1;
var count = DemoCarousel.ItemCount;
StatusText.Text = $"Item: {index} / {count}";
}
}
}

140
samples/ControlCatalog/Pages/CarouselPage/CarouselMultiItemPage.xaml

@ -0,0 +1,140 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CarouselMultiItemPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="260">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Configuration" FontWeight="SemiBold" FontSize="16"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock Text="Navigation" FontWeight="SemiBold" FontSize="13" />
<Button x:Name="PreviousButton" Content="Previous" HorizontalAlignment="Stretch" />
<Button x:Name="NextButton" Content="Next" HorizontalAlignment="Stretch" />
<Separator />
<TextBlock Text="Viewport Fraction" FontWeight="SemiBold" FontSize="13" />
<TextBlock TextWrapping="Wrap" FontSize="11" Opacity="0.6"
Text="Values below 1.0 show adjacent items peeking into the viewport." />
<Grid ColumnDefinitions="*,48" ColumnSpacing="8">
<Slider x:Name="ViewportSlider"
Minimum="0.2" Maximum="1.0" Value="0.5"
TickFrequency="0.01" IsSnapToTickEnabled="True"
HorizontalAlignment="Stretch"
ValueChanged="OnViewportFractionChanged" />
<TextBlock x:Name="ViewportLabel" Grid.Column="1"
Text="0.50" VerticalAlignment="Center"
HorizontalAlignment="Right" FontWeight="SemiBold" />
</Grid>
<TextBlock x:Name="ViewportHint"
Text="~2 items visible."
FontSize="11" Opacity="0.6" TextWrapping="Wrap" />
<Separator />
<TextBlock Text="Options" FontWeight="SemiBold" FontSize="13" />
<CheckBox x:Name="WrapCheck" Content="Wrap Selection" IsChecked="False"
IsCheckedChanged="OnWrapChanged" />
<CheckBox x:Name="SwipeCheck" Content="Swipe / Drag" IsChecked="True"
IsCheckedChanged="OnSwipeChanged" />
<Separator />
<TextBlock Text="Status" FontWeight="SemiBold" FontSize="14" />
<TextBlock x:Name="StatusText" Text="Item: 1 / 5"
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">
<Carousel x:Name="DemoCarousel"
Height="280"
ViewportFraction="0.5"
IsSwipeEnabled="True">
<Carousel.PageTransition>
<PageSlide Duration="0.3" Orientation="Horizontal" />
</Carousel.PageTransition>
<Border Margin="6,12" CornerRadius="14" ClipToBounds="True">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#3525CD" Offset="0" />
<GradientStop Color="#6B5CE7" Offset="1" />
</LinearGradientBrush>
</Border.Background>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="8">
<TextBlock Text="01" FontSize="52" FontWeight="Bold" Foreground="White"
HorizontalAlignment="Center" LetterSpacing="-2" />
<TextBlock Text="Neon Pulse" FontSize="15" FontWeight="SemiBold"
Foreground="#C3C0FF" HorizontalAlignment="Center" />
</StackPanel>
</Border>
<Border Margin="6,12" CornerRadius="14" ClipToBounds="True">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#0891B2" Offset="0" />
<GradientStop Color="#06B6D4" Offset="1" />
</LinearGradientBrush>
</Border.Background>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="8">
<TextBlock Text="02" FontSize="52" FontWeight="Bold" Foreground="White"
HorizontalAlignment="Center" LetterSpacing="-2" />
<TextBlock Text="Ephemeral Blue" FontSize="15" FontWeight="SemiBold"
Foreground="#BAF0FA" HorizontalAlignment="Center" />
</StackPanel>
</Border>
<Border Margin="6,12" CornerRadius="14" ClipToBounds="True">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#059669" Offset="0" />
<GradientStop Color="#10B981" Offset="1" />
</LinearGradientBrush>
</Border.Background>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="8">
<TextBlock Text="03" FontSize="52" FontWeight="Bold" Foreground="White"
HorizontalAlignment="Center" LetterSpacing="-2" />
<TextBlock Text="Forest Forms" FontSize="15" FontWeight="SemiBold"
Foreground="#A7F3D0" HorizontalAlignment="Center" />
</StackPanel>
</Border>
<Border Margin="6,12" CornerRadius="14" ClipToBounds="True">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#D97706" Offset="0" />
<GradientStop Color="#F59E0B" Offset="1" />
</LinearGradientBrush>
</Border.Background>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="8">
<TextBlock Text="04" FontSize="52" FontWeight="Bold" Foreground="White"
HorizontalAlignment="Center" LetterSpacing="-2" />
<TextBlock Text="Golden Hour" FontSize="15" FontWeight="SemiBold"
Foreground="#FDE68A" HorizontalAlignment="Center" />
</StackPanel>
</Border>
<Border Margin="6,12" CornerRadius="14" ClipToBounds="True">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#BE185D" Offset="0" />
<GradientStop Color="#EC4899" Offset="1" />
</LinearGradientBrush>
</Border.Background>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="8">
<TextBlock Text="05" FontSize="52" FontWeight="Bold" Foreground="White"
HorizontalAlignment="Center" LetterSpacing="-2" />
<TextBlock Text="Crimson Wave" FontSize="15" FontWeight="SemiBold"
Foreground="#FBCFE8" HorizontalAlignment="Center" />
</StackPanel>
</Border>
</Carousel>
</Border>
</DockPanel>
</UserControl>

47
samples/ControlCatalog/Pages/CarouselPage/CarouselMultiItemPage.xaml.cs

@ -0,0 +1,47 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public partial class CarouselMultiItemPage : UserControl
{
public CarouselMultiItemPage()
{
InitializeComponent();
PreviousButton.Click += (_, _) => DemoCarousel.Previous();
NextButton.Click += (_, _) => DemoCarousel.Next();
DemoCarousel.SelectionChanged += OnSelectionChanged;
}
private void OnViewportFractionChanged(object? sender, RangeBaseValueChangedEventArgs e)
{
if (DemoCarousel is null)
return;
var value = Math.Round(e.NewValue, 2);
DemoCarousel.ViewportFraction = value;
ViewportLabel.Text = value.ToString("0.00");
ViewportHint.Text = value >= 1d ? "1.00 — single full item." : $"~{1d / value:0.#} items visible.";
}
private void OnWrapChanged(object? sender, RoutedEventArgs e)
{
if (DemoCarousel is null)
return;
DemoCarousel.WrapSelection = WrapCheck.IsChecked == true;
}
private void OnSwipeChanged(object? sender, RoutedEventArgs e)
{
if (DemoCarousel is null)
return;
DemoCarousel.IsSwipeEnabled = SwipeCheck.IsChecked == true;
}
private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
StatusText.Text = $"Item: {DemoCarousel.SelectedIndex + 1} / {DemoCarousel.ItemCount}";
}
}
}

97
samples/ControlCatalog/Pages/CarouselPage/CarouselTransitionsPage.xaml

@ -0,0 +1,97 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CarouselTransitionsPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="260">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Configuration" FontWeight="SemiBold" FontSize="16"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock Text="Navigation" FontWeight="SemiBold" FontSize="13" />
<Button x:Name="PreviousButton"
Content="Previous"
HorizontalAlignment="Stretch" />
<Button x:Name="NextButton"
Content="Next"
HorizontalAlignment="Stretch" />
<Separator />
<TextBlock Text="Transition" FontWeight="SemiBold" FontSize="13" />
<ComboBox x:Name="TransitionCombo"
HorizontalAlignment="Stretch"
SelectedIndex="1">
<ComboBoxItem>None</ComboBoxItem>
<ComboBoxItem>Page Slide</ComboBoxItem>
<ComboBoxItem>Cross Fade</ComboBoxItem>
<ComboBoxItem>Rotate 3D</ComboBoxItem>
<ComboBoxItem>Card Stack</ComboBoxItem>
<ComboBoxItem>Wave Reveal</ComboBoxItem>
<ComboBoxItem>Composite (Slide + Fade)</ComboBoxItem>
</ComboBox>
<TextBlock Text="Orientation" FontWeight="SemiBold" FontSize="13" />
<ComboBox x:Name="OrientationCombo"
HorizontalAlignment="Stretch"
SelectedIndex="0">
<ComboBoxItem>Horizontal</ComboBoxItem>
<ComboBoxItem>Vertical</ComboBoxItem>
</ComboBox>
<Separator />
<TextBlock Text="Status" FontWeight="SemiBold" FontSize="14" />
<TextBlock x:Name="StatusText"
Text="Transition: Page Slide"
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">
<Carousel x:Name="DemoCarousel" Height="300">
<Carousel.PageTransition>
<PageSlide Duration="0.25" Orientation="Horizontal" />
</Carousel.PageTransition>
<Border Margin="14,12" CornerRadius="12" ClipToBounds="True">
<Grid>
<Image Source="/Assets/delicate-arch-896885_640.jpg" Stretch="UniformToFill" />
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12">
<TextBlock Text="Item 1: Delicate Arch" Foreground="White"
HorizontalAlignment="Center" FontWeight="SemiBold" />
</Border>
</Grid>
</Border>
<Border Margin="14,12" CornerRadius="12" ClipToBounds="True">
<Grid>
<Image Source="/Assets/hirsch-899118_640.jpg" Stretch="UniformToFill" />
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12">
<TextBlock Text="Item 2: Hirsch" Foreground="White"
HorizontalAlignment="Center" FontWeight="SemiBold" />
</Border>
</Grid>
</Border>
<Border Margin="14,12" CornerRadius="12" ClipToBounds="True">
<Grid>
<Image Source="/Assets/maple-leaf-888807_640.jpg" Stretch="UniformToFill" />
<Border Background="#80000000" VerticalAlignment="Bottom" Padding="12">
<TextBlock Text="Item 3: Maple Leaf" Foreground="White"
HorizontalAlignment="Center" FontWeight="SemiBold" />
</Border>
</Grid>
</Border>
</Carousel>
</Border>
</DockPanel>
</UserControl>

66
samples/ControlCatalog/Pages/CarouselPage/CarouselTransitionsPage.xaml.cs

@ -0,0 +1,66 @@
using System;
using Avalonia.Animation;
using Avalonia.Controls;
using ControlCatalog.Pages.Transitions;
namespace ControlCatalog.Pages
{
public partial class CarouselTransitionsPage : UserControl
{
public CarouselTransitionsPage()
{
InitializeComponent();
PreviousButton.Click += (_, _) => DemoCarousel.Previous();
NextButton.Click += (_, _) => DemoCarousel.Next();
TransitionCombo.SelectionChanged += (_, _) => ApplyTransition();
OrientationCombo.SelectionChanged += (_, _) => ApplyTransition();
}
private void ApplyTransition()
{
var axis = OrientationCombo.SelectedIndex == 0 ?
PageSlide.SlideAxis.Horizontal :
PageSlide.SlideAxis.Vertical;
var label = axis == PageSlide.SlideAxis.Horizontal ? "Horizontal" : "Vertical";
switch (TransitionCombo.SelectedIndex)
{
case 0:
DemoCarousel.PageTransition = null;
StatusText.Text = "Transition: None";
break;
case 1:
DemoCarousel.PageTransition = new PageSlide(TimeSpan.FromSeconds(0.25), axis);
StatusText.Text = $"Transition: Page Slide ({label})";
break;
case 2:
DemoCarousel.PageTransition = new CrossFade(TimeSpan.FromSeconds(0.25));
StatusText.Text = "Transition: Cross Fade";
break;
case 3:
DemoCarousel.PageTransition = new Rotate3DTransition(TimeSpan.FromSeconds(0.5), axis);
StatusText.Text = $"Transition: Rotate 3D ({label})";
break;
case 4:
DemoCarousel.PageTransition = new CardStackPageTransition(TimeSpan.FromSeconds(0.5), axis);
StatusText.Text = $"Transition: Card Stack ({label})";
break;
case 5:
DemoCarousel.PageTransition = new WaveRevealPageTransition(TimeSpan.FromSeconds(0.8), axis);
StatusText.Text = $"Transition: Wave Reveal ({label})";
break;
case 6:
DemoCarousel.PageTransition = new CompositePageTransition
{
PageTransitions =
{
new PageSlide(TimeSpan.FromSeconds(0.25), axis),
new CrossFade(TimeSpan.FromSeconds(0.25)),
}
};
StatusText.Text = "Transition: Composite (Slide + Fade)";
break;
}
}
}
}

132
samples/ControlCatalog/Pages/CarouselPage/CarouselVerticalPage.xaml

@ -0,0 +1,132 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CarouselVerticalPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="260">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Configuration" FontWeight="SemiBold" FontSize="16"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock Text="Navigation" FontWeight="SemiBold" FontSize="13" />
<Button x:Name="PreviousButton" Content="Up" HorizontalAlignment="Stretch" />
<Button x:Name="NextButton" Content="Down" HorizontalAlignment="Stretch" />
<Separator />
<TextBlock Text="Transition" FontWeight="SemiBold" FontSize="13" />
<ComboBox x:Name="TransitionCombo"
HorizontalAlignment="Stretch"
SelectedIndex="0">
<ComboBoxItem>PageSlide</ComboBoxItem>
<ComboBoxItem>CrossFade</ComboBoxItem>
<ComboBoxItem>None</ComboBoxItem>
</ComboBox>
<Separator />
<TextBlock Text="Options" FontWeight="SemiBold" FontSize="13" />
<CheckBox x:Name="WrapCheck"
Content="Wrap Selection"
IsChecked="False"
IsCheckedChanged="OnWrapSelectionChanged" />
<Separator />
<TextBlock Text="Status" FontWeight="SemiBold" FontSize="14" />
<TextBlock x:Name="StatusText"
Text="Item: 1 / 4"
Opacity="0.7"
TextWrapping="Wrap" />
<TextBlock Text="Use Up/Down arrow keys or buttons to navigate."
FontSize="11"
Opacity="0.5"
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">
<Carousel x:Name="DemoCarousel"
Focusable="True"
IsSwipeEnabled="True">
<Carousel.PageTransition>
<PageSlide Duration="0.3" Orientation="Vertical" />
</Carousel.PageTransition>
<Border Margin="14,12" CornerRadius="12">
<Border.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Color="#1A1A2E" Offset="0" />
<GradientStop Color="#3525CD" Offset="1" />
</LinearGradientBrush>
</Border.Background>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="10">
<TextBlock Text="01" FontSize="64" FontWeight="Bold"
Foreground="White" HorizontalAlignment="Center" LetterSpacing="-2" />
<TextBlock Text="Neon Pulse" FontSize="18" FontWeight="SemiBold"
Foreground="#C3C0FF" HorizontalAlignment="Center" />
<TextBlock Text="Slide down to explore" FontSize="12"
Foreground="#80FFFFFF" HorizontalAlignment="Center" />
</StackPanel>
</Border>
<Border Margin="14,12" CornerRadius="12">
<Border.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Color="#0C1A1F" Offset="0" />
<GradientStop Color="#0891B2" Offset="1" />
</LinearGradientBrush>
</Border.Background>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="10">
<TextBlock Text="02" FontSize="64" FontWeight="Bold"
Foreground="White" HorizontalAlignment="Center" LetterSpacing="-2" />
<TextBlock Text="Ephemeral Blue" FontSize="18" FontWeight="SemiBold"
Foreground="#BAF0FA" HorizontalAlignment="Center" />
<TextBlock Text="Vertical PageSlide in action" FontSize="12"
Foreground="#80FFFFFF" HorizontalAlignment="Center" />
</StackPanel>
</Border>
<Border Margin="14,12" CornerRadius="12">
<Border.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Color="#0A1F18" Offset="0" />
<GradientStop Color="#059669" Offset="1" />
</LinearGradientBrush>
</Border.Background>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="10">
<TextBlock Text="03" FontSize="64" FontWeight="Bold"
Foreground="White" HorizontalAlignment="Center" LetterSpacing="-2" />
<TextBlock Text="Forest Forms" FontSize="18" FontWeight="SemiBold"
Foreground="#A7F3D0" HorizontalAlignment="Center" />
<TextBlock Text="Swipe up or down on touch screens" FontSize="12"
Foreground="#80FFFFFF" HorizontalAlignment="Center" />
</StackPanel>
</Border>
<Border Margin="14,12" CornerRadius="12">
<Border.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Color="#1F1208" Offset="0" />
<GradientStop Color="#D97706" Offset="1" />
</LinearGradientBrush>
</Border.Background>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="10">
<TextBlock Text="04" FontSize="64" FontWeight="Bold"
Foreground="White" HorizontalAlignment="Center" LetterSpacing="-2" />
<TextBlock Text="Golden Hour" FontSize="18" FontWeight="SemiBold"
Foreground="#FDE68A" HorizontalAlignment="Center" />
<TextBlock Text="Switch transitions in the panel" FontSize="12"
Foreground="#80FFFFFF" HorizontalAlignment="Center" />
</StackPanel>
</Border>
</Carousel>
</Border>
</DockPanel>
</UserControl>

39
samples/ControlCatalog/Pages/CarouselPage/CarouselVerticalPage.xaml.cs

@ -0,0 +1,39 @@
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public partial class CarouselVerticalPage : UserControl
{
public CarouselVerticalPage()
{
InitializeComponent();
PreviousButton.Click += (_, _) => DemoCarousel.Previous();
NextButton.Click += (_, _) => DemoCarousel.Next();
DemoCarousel.SelectionChanged += OnSelectionChanged;
TransitionCombo.SelectionChanged += OnTransitionChanged;
DemoCarousel.Loaded += (_, _) => DemoCarousel.Focus();
}
private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
StatusText.Text = $"Item: {DemoCarousel.SelectedIndex + 1} / {DemoCarousel.ItemCount}";
}
private void OnTransitionChanged(object? sender, SelectionChangedEventArgs e)
{
DemoCarousel.PageTransition = TransitionCombo.SelectedIndex switch
{
1 => new CrossFade(System.TimeSpan.FromSeconds(0.3)),
2 => null,
_ => new PageSlide(System.TimeSpan.FromSeconds(0.3), PageSlide.SlideAxis.Vertical),
};
}
private void OnWrapSelectionChanged(object? sender, RoutedEventArgs e)
{
DemoCarousel.WrapSelection = WrapCheck.IsChecked == true;
}
}
}

13
samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs

@ -1,5 +1,7 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Interactivity;
using Avalonia.Media;
@ -22,6 +24,7 @@ namespace ControlCatalog.Pages
public DrawerPageCustomizationPage()
{
InitializeComponent();
EnableMouseSwipeGesture(DemoDrawer);
}
protected override void OnLoaded(RoutedEventArgs e)
@ -188,5 +191,15 @@ namespace ControlCatalog.Pages
if (DemoDrawer.DrawerBehavior != DrawerBehavior.Locked)
DemoDrawer.IsOpen = false;
}
private static void EnableMouseSwipeGesture(Control control)
{
var recognizer = control.GestureRecognizers
.OfType<SwipeGestureRecognizer>()
.FirstOrDefault();
if (recognizer is not null)
recognizer.IsMouseEnabled = true;
}
}
}

13
samples/ControlCatalog/Pages/DrawerPage/DrawerPageFirstLookPage.xaml.cs

@ -1,4 +1,6 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
@ -8,6 +10,7 @@ namespace ControlCatalog.Pages
public DrawerPageFirstLookPage()
{
InitializeComponent();
EnableMouseSwipeGesture(DemoDrawer);
}
protected override void OnLoaded(RoutedEventArgs e)
@ -61,5 +64,15 @@ namespace ControlCatalog.Pages
{
StatusText.Text = $"Drawer: {(DemoDrawer.IsOpen ? "Open" : "Closed")}";
}
private static void EnableMouseSwipeGesture(Control control)
{
var recognizer = control.GestureRecognizers
.OfType<SwipeGestureRecognizer>()
.FirstOrDefault();
if (recognizer is not null)
recognizer.IsMouseEnabled = true;
}
}
}

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

@ -1,4 +1,6 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
@ -8,6 +10,7 @@ namespace ControlCatalog.Pages
public NavigationPageGesturePage()
{
InitializeComponent();
EnableMouseSwipeGesture(DemoNav);
Loaded += OnLoaded;
}
@ -43,5 +46,15 @@ namespace ControlCatalog.Pages
{
StatusText.Text = $"Depth: {DemoNav.StackDepth}";
}
private static void EnableMouseSwipeGesture(Control control)
{
var recognizer = control.GestureRecognizers
.OfType<SwipeGestureRecognizer>()
.FirstOrDefault();
if (recognizer is not null)
recognizer.IsMouseEnabled = true;
}
}
}

13
samples/ControlCatalog/Pages/TabbedPage/TabbedPageGesturePage.xaml.cs

@ -1,4 +1,6 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Input.GestureRecognizers;
namespace ControlCatalog.Pages
{
@ -7,6 +9,7 @@ namespace ControlCatalog.Pages
public TabbedPageGesturePage()
{
InitializeComponent();
EnableMouseSwipeGesture(DemoTabs);
}
private void OnGestureEnabledChanged(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
@ -26,5 +29,15 @@ namespace ControlCatalog.Pages
_ => TabPlacement.Top
};
}
private static void EnableMouseSwipeGesture(Control control)
{
var recognizer = control.GestureRecognizers
.OfType<SwipeGestureRecognizer>()
.FirstOrDefault();
if (recognizer is not null)
recognizer.IsMouseEnabled = true;
}
}
}

447
samples/ControlCatalog/Pages/Transitions/CardStackPageTransition.cs

@ -0,0 +1,447 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Media;
using Avalonia.Styling;
namespace ControlCatalog.Pages.Transitions;
/// <summary>
/// Transitions between two pages with a card-stack effect:
/// the top page moves/rotates away while the next page scales up underneath.
/// </summary>
public class CardStackPageTransition : PageSlide
{
private const double ViewportLiftScale = 0.03;
private const double ViewportPromotionScale = 0.02;
private const double ViewportDepthOpacityFalloff = 0.08;
private const double SidePeekAngle = 4.0;
private const double FarPeekAngle = 7.0;
/// <summary>
/// Initializes a new instance of the <see cref="CardStackPageTransition"/> class.
/// </summary>
public CardStackPageTransition()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CardStackPageTransition"/> class.
/// </summary>
/// <param name="duration">The duration of the animation.</param>
/// <param name="orientation">The axis on which the animation should occur.</param>
public CardStackPageTransition(TimeSpan duration, PageSlide.SlideAxis orientation = PageSlide.SlideAxis.Horizontal)
: base(duration, orientation)
{
}
/// <summary>
/// Gets or sets the maximum rotation angle (degrees) applied to the top card.
/// </summary>
public double MaxSwipeAngle { get; set; } = 15.0;
/// <summary>
/// Gets or sets the scale reduction applied to the back card (0.05 = 5%).
/// </summary>
public double BackCardScale { get; set; } = 0.05;
/// <summary>
/// Gets or sets the vertical offset (pixels) applied to the back card.
/// </summary>
public double BackCardOffset { get; set; } = 0.0;
/// <inheritdoc />
public override async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
var tasks = new List<Task>();
var parent = GetVisualParent(from, to);
var distance = Orientation == PageSlide.SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height;
var translateProperty = Orientation == PageSlide.SlideAxis.Horizontal ? TranslateTransform.XProperty : TranslateTransform.YProperty;
var rotationTarget = Orientation == PageSlide.SlideAxis.Horizontal ? (forward ? -MaxSwipeAngle : MaxSwipeAngle) : 0.0;
var startScale = 1.0 - BackCardScale;
if (from != null)
{
var (rotate, translate) = EnsureTopTransforms(from);
rotate.Angle = 0;
translate.X = 0;
translate.Y = 0;
from.Opacity = 1;
from.ZIndex = 1;
var animation = new Animation
{
Easing = SlideOutEasing,
Duration = Duration,
FillMode = FillMode,
Children =
{
new KeyFrame
{
Setters =
{
new Setter(translateProperty, 0d),
new Setter(RotateTransform.AngleProperty, 0d)
},
Cue = new Cue(0d)
},
new KeyFrame
{
Setters =
{
new Setter(translateProperty, forward ? -distance : distance),
new Setter(RotateTransform.AngleProperty, rotationTarget)
},
Cue = new Cue(1d)
}
}
};
tasks.Add(animation.RunAsync(from, cancellationToken));
}
if (to != null)
{
var (scale, translate) = EnsureBackTransforms(to);
scale.ScaleX = startScale;
scale.ScaleY = startScale;
translate.X = 0;
translate.Y = BackCardOffset;
to.IsVisible = true;
to.Opacity = 1;
to.ZIndex = 0;
var animation = new Animation
{
Easing = SlideInEasing,
Duration = Duration,
FillMode = FillMode,
Children =
{
new KeyFrame
{
Setters =
{
new Setter(ScaleTransform.ScaleXProperty, startScale),
new Setter(ScaleTransform.ScaleYProperty, startScale),
new Setter(TranslateTransform.YProperty, BackCardOffset)
},
Cue = new Cue(0d)
},
new KeyFrame
{
Setters =
{
new Setter(ScaleTransform.ScaleXProperty, 1d),
new Setter(ScaleTransform.ScaleYProperty, 1d),
new Setter(TranslateTransform.YProperty, 0d)
},
Cue = new Cue(1d)
}
}
};
tasks.Add(animation.RunAsync(to, cancellationToken));
}
await Task.WhenAll(tasks);
if (from != null && !cancellationToken.IsCancellationRequested)
{
from.IsVisible = false;
}
if (!cancellationToken.IsCancellationRequested && to != null)
{
var (scale, translate) = EnsureBackTransforms(to);
scale.ScaleX = 1;
scale.ScaleY = 1;
translate.X = 0;
translate.Y = 0;
}
}
/// <inheritdoc />
public override void Update(
double progress,
Visual? from,
Visual? to,
bool forward,
double pageLength,
IReadOnlyList<PageTransitionItem> visibleItems)
{
if (visibleItems.Count > 0)
{
UpdateVisibleItems(progress, from, to, forward, pageLength, visibleItems);
return;
}
if (from is null && to is null)
return;
var parent = GetVisualParent(from, to);
var size = parent.Bounds.Size;
var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal;
var distance = pageLength > 0
? pageLength
: (isHorizontal ? size.Width : size.Height);
var rotationTarget = isHorizontal ? (forward ? -MaxSwipeAngle : MaxSwipeAngle) : 0.0;
var startScale = 1.0 - BackCardScale;
if (from != null)
{
var (rotate, translate) = EnsureTopTransforms(from);
if (isHorizontal)
{
translate.X = forward ? -distance * progress : distance * progress;
translate.Y = 0;
}
else
{
translate.X = 0;
translate.Y = forward ? -distance * progress : distance * progress;
}
rotate.Angle = rotationTarget * progress;
from.IsVisible = true;
from.Opacity = 1;
from.ZIndex = 1;
}
if (to != null)
{
var (scale, translate) = EnsureBackTransforms(to);
var currentScale = startScale + (1.0 - startScale) * progress;
var currentOffset = BackCardOffset * (1.0 - progress);
scale.ScaleX = currentScale;
scale.ScaleY = currentScale;
if (isHorizontal)
{
translate.X = 0;
translate.Y = currentOffset;
}
else
{
translate.X = currentOffset;
translate.Y = 0;
}
to.IsVisible = true;
to.Opacity = 1;
to.ZIndex = 0;
}
}
/// <inheritdoc />
public override void Reset(Visual visual)
{
visual.RenderTransform = null;
visual.RenderTransformOrigin = default;
visual.Opacity = 1;
visual.ZIndex = 0;
}
private void UpdateVisibleItems(
double progress,
Visual? from,
Visual? to,
bool forward,
double pageLength,
IReadOnlyList<PageTransitionItem> visibleItems)
{
var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal;
var rotationTarget = isHorizontal
? (forward ? -MaxSwipeAngle : MaxSwipeAngle)
: 0.0;
var stackOffset = GetViewportStackOffset(pageLength);
var lift = Math.Sin(Math.Clamp(progress, 0.0, 1.0) * Math.PI);
foreach (var item in visibleItems)
{
var visual = item.Visual;
var (rotate, scale, translate) = EnsureViewportTransforms(visual);
var depth = GetViewportDepth(item.ViewportCenterOffset);
var scaleValue = Math.Max(0.84, 1.0 - (BackCardScale * depth));
var stackValue = stackOffset * depth;
var baseOpacity = Math.Max(0.8, 1.0 - (ViewportDepthOpacityFalloff * depth));
var restingAngle = isHorizontal ? GetViewportRestingAngle(item.ViewportCenterOffset) : 0.0;
rotate.Angle = restingAngle;
scale.ScaleX = scaleValue;
scale.ScaleY = scaleValue;
translate.X = 0;
translate.Y = 0;
if (ReferenceEquals(visual, from))
{
rotate.Angle = restingAngle + (rotationTarget * progress);
stackValue -= stackOffset * 0.2 * lift;
baseOpacity = Math.Min(1.0, baseOpacity + 0.08);
}
if (ReferenceEquals(visual, to))
{
var promotedScale = Math.Min(1.0, scaleValue + (ViewportLiftScale * lift) + (ViewportPromotionScale * progress));
scale.ScaleX = promotedScale;
scale.ScaleY = promotedScale;
rotate.Angle = restingAngle * (1.0 - progress);
stackValue = Math.Max(0.0, stackValue - (stackOffset * (0.45 + (0.2 * lift)) * progress));
baseOpacity = Math.Min(1.0, baseOpacity + (0.12 * lift));
}
if (isHorizontal)
translate.Y = stackValue;
else
translate.X = stackValue;
visual.IsVisible = true;
visual.Opacity = baseOpacity;
visual.ZIndex = GetViewportZIndex(item.ViewportCenterOffset, visual, from, to);
}
}
private static (RotateTransform rotate, TranslateTransform translate) EnsureTopTransforms(Visual visual)
{
if (visual.RenderTransform is TransformGroup group &&
group.Children.Count == 2 &&
group.Children[0] is RotateTransform rotateTransform &&
group.Children[1] is TranslateTransform translateTransform)
{
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
return (rotateTransform, translateTransform);
}
var rotate = new RotateTransform();
var translate = new TranslateTransform();
visual.RenderTransform = new TransformGroup
{
Children =
{
rotate,
translate
}
};
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
return (rotate, translate);
}
private static (ScaleTransform scale, TranslateTransform translate) EnsureBackTransforms(Visual visual)
{
if (visual.RenderTransform is TransformGroup group &&
group.Children.Count == 2 &&
group.Children[0] is ScaleTransform scaleTransform &&
group.Children[1] is TranslateTransform translateTransform)
{
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
return (scaleTransform, translateTransform);
}
var scale = new ScaleTransform();
var translate = new TranslateTransform();
visual.RenderTransform = new TransformGroup
{
Children =
{
scale,
translate
}
};
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
return (scale, translate);
}
private static (RotateTransform rotate, ScaleTransform scale, TranslateTransform translate) EnsureViewportTransforms(Visual visual)
{
if (visual.RenderTransform is TransformGroup group &&
group.Children.Count == 3 &&
group.Children[0] is RotateTransform rotateTransform &&
group.Children[1] is ScaleTransform scaleTransform &&
group.Children[2] is TranslateTransform translateTransform)
{
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
return (rotateTransform, scaleTransform, translateTransform);
}
var rotate = new RotateTransform();
var scale = new ScaleTransform(1, 1);
var translate = new TranslateTransform();
visual.RenderTransform = new TransformGroup
{
Children =
{
rotate,
scale,
translate
}
};
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
return (rotate, scale, translate);
}
private double GetViewportStackOffset(double pageLength)
{
if (BackCardOffset > 0)
return BackCardOffset;
return Math.Clamp(pageLength * 0.045, 10.0, 18.0);
}
private static double GetViewportDepth(double offsetFromCenter)
{
var distance = Math.Abs(offsetFromCenter);
if (distance <= 1.0)
return distance;
if (distance <= 2.0)
return 1.0 + ((distance - 1.0) * 0.8);
return 1.8;
}
private static double GetViewportRestingAngle(double offsetFromCenter)
{
var sign = Math.Sign(offsetFromCenter);
if (sign == 0)
return 0;
var distance = Math.Abs(offsetFromCenter);
if (distance <= 1.0)
return sign * Lerp(0.0, SidePeekAngle, distance);
if (distance <= 2.0)
return sign * Lerp(SidePeekAngle, FarPeekAngle, distance - 1.0);
return sign * FarPeekAngle;
}
private static double Lerp(double from, double to, double t)
{
return from + ((to - from) * Math.Clamp(t, 0.0, 1.0));
}
private static int GetViewportZIndex(double offsetFromCenter, Visual visual, Visual? from, Visual? to)
{
if (ReferenceEquals(visual, from))
return 5;
if (ReferenceEquals(visual, to))
return 4;
var distance = Math.Abs(offsetFromCenter);
if (distance < 0.5)
return 4;
if (distance < 1.5)
return 3;
return 2;
}
}

380
samples/ControlCatalog/Pages/Transitions/WaveRevealPageTransition.cs

@ -0,0 +1,380 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Media;
namespace ControlCatalog.Pages.Transitions;
/// <summary>
/// Transitions between two pages using a wave clip that reveals the next page.
/// </summary>
public class WaveRevealPageTransition : PageSlide
{
/// <summary>
/// Initializes a new instance of the <see cref="WaveRevealPageTransition"/> class.
/// </summary>
public WaveRevealPageTransition()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WaveRevealPageTransition"/> class.
/// </summary>
/// <param name="duration">The duration of the animation.</param>
/// <param name="orientation">The axis on which the animation should occur.</param>
public WaveRevealPageTransition(TimeSpan duration, PageSlide.SlideAxis orientation = PageSlide.SlideAxis.Horizontal)
: base(duration, orientation)
{
}
/// <summary>
/// Gets or sets the maximum wave bulge (pixels) along the movement axis.
/// </summary>
public double MaxBulge { get; set; } = 120.0;
/// <summary>
/// Gets or sets the bulge factor along the movement axis (0-1).
/// </summary>
public double BulgeFactor { get; set; } = 0.35;
/// <summary>
/// Gets or sets the bulge factor along the cross axis (0-1).
/// </summary>
public double CrossBulgeFactor { get; set; } = 0.3;
/// <summary>
/// Gets or sets a cross-axis offset (pixels) to shift the wave center.
/// </summary>
public double WaveCenterOffset { get; set; } = 0.0;
/// <summary>
/// Gets or sets how strongly the wave center follows the provided offset.
/// </summary>
public double CenterSensitivity { get; set; } = 1.0;
/// <summary>
/// Gets or sets the bulge exponent used to shape the wave (1.0 = linear).
/// Higher values tighten the bulge; lower values broaden it.
/// </summary>
public double BulgeExponent { get; set; } = 1.0;
/// <summary>
/// Gets or sets the easing applied to the wave progress (clip only).
/// </summary>
public Easing WaveEasing { get; set; } = new CubicEaseOut();
/// <inheritdoc />
public override async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
if (to != null)
{
to.IsVisible = true;
to.ZIndex = 1;
}
if (from != null)
{
from.ZIndex = 0;
}
await AnimateProgress(0.0, 1.0, from, to, forward, cancellationToken);
if (to != null && !cancellationToken.IsCancellationRequested)
{
to.Clip = null;
}
if (from != null && !cancellationToken.IsCancellationRequested)
{
from.IsVisible = false;
}
}
/// <inheritdoc />
public override void Update(
double progress,
Visual? from,
Visual? to,
bool forward,
double pageLength,
IReadOnlyList<PageTransitionItem> visibleItems)
{
if (visibleItems.Count > 0)
{
UpdateVisibleItems(from, to, forward, pageLength, visibleItems);
return;
}
if (from is null && to is null)
return;
var parent = GetVisualParent(from, to);
var size = parent.Bounds.Size;
var centerOffset = WaveCenterOffset * CenterSensitivity;
var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal;
if (to != null)
{
to.IsVisible = progress > 0.0;
to.ZIndex = 1;
to.Opacity = 1;
if (progress >= 1.0)
{
to.Clip = null;
}
else
{
var waveProgress = WaveEasing?.Ease(progress) ?? progress;
var clip = LiquidSwipeClipper.CreateWavePath(
waveProgress,
size,
centerOffset,
forward,
isHorizontal,
MaxBulge,
BulgeFactor,
CrossBulgeFactor,
BulgeExponent);
to.Clip = clip;
}
}
if (from != null)
{
from.IsVisible = true;
from.ZIndex = 0;
from.Opacity = 1;
}
}
private void UpdateVisibleItems(
Visual? from,
Visual? to,
bool forward,
double pageLength,
IReadOnlyList<PageTransitionItem> visibleItems)
{
if (from is null && to is null)
return;
var parent = GetVisualParent(from, to);
var size = parent.Bounds.Size;
var centerOffset = WaveCenterOffset * CenterSensitivity;
var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal;
var resolvedPageLength = pageLength > 0
? pageLength
: (isHorizontal ? size.Width : size.Height);
foreach (var item in visibleItems)
{
var visual = item.Visual;
visual.IsVisible = true;
visual.Opacity = 1;
visual.Clip = null;
visual.ZIndex = ReferenceEquals(visual, to) ? 1 : 0;
if (!ReferenceEquals(visual, to))
continue;
var visibleFraction = GetVisibleFraction(item.ViewportCenterOffset, size, resolvedPageLength, isHorizontal);
if (visibleFraction >= 1.0)
continue;
visual.Clip = LiquidSwipeClipper.CreateWavePath(
visibleFraction,
size,
centerOffset,
forward,
isHorizontal,
MaxBulge,
BulgeFactor,
CrossBulgeFactor,
BulgeExponent);
}
}
private static double GetVisibleFraction(double offsetFromCenter, Size viewportSize, double pageLength, bool isHorizontal)
{
if (pageLength <= 0)
return 1.0;
var viewportLength = isHorizontal ? viewportSize.Width : viewportSize.Height;
if (viewportLength <= 0)
return 0.0;
var viewportUnits = viewportLength / pageLength;
var edgePeek = Math.Max(0.0, (viewportUnits - 1.0) / 2.0);
return Math.Clamp(1.0 + edgePeek - Math.Abs(offsetFromCenter), 0.0, 1.0);
}
/// <inheritdoc />
public override void Reset(Visual visual)
{
visual.Clip = null;
visual.ZIndex = 0;
visual.Opacity = 1;
}
private async Task AnimateProgress(
double from,
double to,
Visual? fromVisual,
Visual? toVisual,
bool forward,
CancellationToken cancellationToken)
{
var parent = GetVisualParent(fromVisual, toVisual);
var pageLength = Orientation == PageSlide.SlideAxis.Horizontal
? parent.Bounds.Width
: parent.Bounds.Height;
var durationMs = Math.Max(Duration.TotalMilliseconds * Math.Abs(to - from), 50);
var startTicks = Stopwatch.GetTimestamp();
var tickFreq = Stopwatch.Frequency;
while (!cancellationToken.IsCancellationRequested)
{
var elapsedMs = (Stopwatch.GetTimestamp() - startTicks) * 1000.0 / tickFreq;
var t = Math.Clamp(elapsedMs / durationMs, 0.0, 1.0);
var eased = SlideInEasing?.Ease(t) ?? t;
var progress = from + (to - from) * eased;
Update(progress, fromVisual, toVisual, forward, pageLength, Array.Empty<PageTransitionItem>());
if (t >= 1.0)
break;
await Task.Delay(16, cancellationToken);
}
if (!cancellationToken.IsCancellationRequested)
{
Update(to, fromVisual, toVisual, forward, pageLength, Array.Empty<PageTransitionItem>());
}
}
private static class LiquidSwipeClipper
{
public static Geometry CreateWavePath(
double progress,
Size size,
double waveCenterOffset,
bool forward,
bool isHorizontal,
double maxBulge,
double bulgeFactor,
double crossBulgeFactor,
double bulgeExponent)
{
var width = size.Width;
var height = size.Height;
if (progress <= 0)
return new RectangleGeometry(new Rect(0, 0, 0, 0));
if (progress >= 1)
return new RectangleGeometry(new Rect(0, 0, width, height));
if (width <= 0 || height <= 0)
return new RectangleGeometry(new Rect(0, 0, 0, 0));
var mainLength = isHorizontal ? width : height;
var crossLength = isHorizontal ? height : width;
var wavePhase = Math.Sin(progress * Math.PI);
var bulgeProgress = bulgeExponent == 1.0 ? wavePhase : Math.Pow(wavePhase, bulgeExponent);
var revealedLength = mainLength * progress;
var bulgeMain = Math.Min(mainLength * bulgeFactor, maxBulge) * bulgeProgress;
bulgeMain = Math.Min(bulgeMain, revealedLength * 0.45);
var bulgeCross = crossLength * crossBulgeFactor;
var waveCenter = crossLength / 2 + waveCenterOffset;
waveCenter = Math.Clamp(waveCenter, bulgeCross, crossLength - bulgeCross);
var geometry = new StreamGeometry();
using (var context = geometry.Open())
{
if (isHorizontal)
{
if (forward)
{
var waveX = width * (1 - progress);
context.BeginFigure(new Point(width, 0), true);
context.LineTo(new Point(waveX, 0));
context.CubicBezierTo(
new Point(waveX, waveCenter - bulgeCross),
new Point(waveX - bulgeMain, waveCenter - bulgeCross * 0.5),
new Point(waveX - bulgeMain, waveCenter));
context.CubicBezierTo(
new Point(waveX - bulgeMain, waveCenter + bulgeCross * 0.5),
new Point(waveX, waveCenter + bulgeCross),
new Point(waveX, height));
context.LineTo(new Point(width, height));
context.EndFigure(true);
}
else
{
var waveX = width * progress;
context.BeginFigure(new Point(0, 0), true);
context.LineTo(new Point(waveX, 0));
context.CubicBezierTo(
new Point(waveX, waveCenter - bulgeCross),
new Point(waveX + bulgeMain, waveCenter - bulgeCross * 0.5),
new Point(waveX + bulgeMain, waveCenter));
context.CubicBezierTo(
new Point(waveX + bulgeMain, waveCenter + bulgeCross * 0.5),
new Point(waveX, waveCenter + bulgeCross),
new Point(waveX, height));
context.LineTo(new Point(0, height));
context.EndFigure(true);
}
}
else
{
if (forward)
{
var waveY = height * (1 - progress);
context.BeginFigure(new Point(0, height), true);
context.LineTo(new Point(0, waveY));
context.CubicBezierTo(
new Point(waveCenter - bulgeCross, waveY),
new Point(waveCenter - bulgeCross * 0.5, waveY - bulgeMain),
new Point(waveCenter, waveY - bulgeMain));
context.CubicBezierTo(
new Point(waveCenter + bulgeCross * 0.5, waveY - bulgeMain),
new Point(waveCenter + bulgeCross, waveY),
new Point(width, waveY));
context.LineTo(new Point(width, height));
context.EndFigure(true);
}
else
{
var waveY = height * progress;
context.BeginFigure(new Point(0, 0), true);
context.LineTo(new Point(0, waveY));
context.CubicBezierTo(
new Point(waveCenter - bulgeCross, waveY),
new Point(waveCenter - bulgeCross * 0.5, waveY + bulgeMain),
new Point(waveCenter, waveY + bulgeMain));
context.CubicBezierTo(
new Point(waveCenter + bulgeCross * 0.5, waveY + bulgeMain),
new Point(waveCenter + bulgeCross, waveY),
new Point(width, waveY));
context.LineTo(new Point(width, 0));
context.EndFigure(true);
}
}
}
return geometry;
}
}
}

145
samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.Designer.cs

@ -30,87 +30,88 @@ namespace WindowsInteropTest
/// </summary>
private void InitializeComponent()
{
this.button1 = new System.Windows.Forms.Button();
this.monthCalendar1 = new System.Windows.Forms.MonthCalendar();
this.groupBox1 = new System.Windows.Forms.GroupBox();
this.groupBox2 = new System.Windows.Forms.GroupBox();
this.avaloniaHost = new WinFormsAvaloniaControlHost();
this.groupBox1.SuspendLayout();
this.groupBox2.SuspendLayout();
this.SuspendLayout();
//
// button1
//
this.button1.Location = new System.Drawing.Point(28, 29);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(164, 73);
this.button1.TabIndex = 0;
this.button1.Text = "button1";
this.button1.UseVisualStyleBackColor = true;
//
OpenWindowButton = new System.Windows.Forms.Button();
monthCalendar1 = new System.Windows.Forms.MonthCalendar();
groupBox1 = new System.Windows.Forms.GroupBox();
groupBox2 = new System.Windows.Forms.GroupBox();
avaloniaHost = new Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost();
groupBox1.SuspendLayout();
groupBox2.SuspendLayout();
SuspendLayout();
//
// OpenWindowButton
//
OpenWindowButton.Location = new System.Drawing.Point(33, 33);
OpenWindowButton.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
OpenWindowButton.Name = "OpenWindowButton";
OpenWindowButton.Size = new System.Drawing.Size(191, 84);
OpenWindowButton.TabIndex = 0;
OpenWindowButton.Text = "Open Avalonia Window";
OpenWindowButton.UseVisualStyleBackColor = true;
OpenWindowButton.Click += OpenWindowButton_Click;
//
// monthCalendar1
//
this.monthCalendar1.Location = new System.Drawing.Point(28, 114);
this.monthCalendar1.Name = "monthCalendar1";
this.monthCalendar1.TabIndex = 1;
//
//
monthCalendar1.Location = new System.Drawing.Point(33, 132);
monthCalendar1.Margin = new System.Windows.Forms.Padding(10);
monthCalendar1.Name = "monthCalendar1";
monthCalendar1.TabIndex = 1;
//
// groupBox1
//
this.groupBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)));
this.groupBox1.Controls.Add(this.button1);
this.groupBox1.Controls.Add(this.monthCalendar1);
this.groupBox1.Location = new System.Drawing.Point(12, 12);
this.groupBox1.Name = "groupBox1";
this.groupBox1.Size = new System.Drawing.Size(227, 418);
this.groupBox1.TabIndex = 2;
this.groupBox1.TabStop = false;
this.groupBox1.Text = "WinForms";
//
//
groupBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left));
groupBox1.Controls.Add(OpenWindowButton);
groupBox1.Controls.Add(monthCalendar1);
groupBox1.Location = new System.Drawing.Point(14, 14);
groupBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
groupBox1.Name = "groupBox1";
groupBox1.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
groupBox1.Size = new System.Drawing.Size(265, 482);
groupBox1.TabIndex = 2;
groupBox1.TabStop = false;
groupBox1.Text = "WinForms";
//
// groupBox2
//
this.groupBox2.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.groupBox2.Controls.Add(this.avaloniaHost);
this.groupBox2.Location = new System.Drawing.Point(245, 12);
this.groupBox2.Name = "groupBox2";
this.groupBox2.Size = new System.Drawing.Size(501, 418);
this.groupBox2.TabIndex = 3;
this.groupBox2.TabStop = false;
this.groupBox2.Text = "Avalonia";
//
//
groupBox2.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right));
groupBox2.Controls.Add(avaloniaHost);
groupBox2.Location = new System.Drawing.Point(286, 14);
groupBox2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
groupBox2.Name = "groupBox2";
groupBox2.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
groupBox2.Size = new System.Drawing.Size(584, 482);
groupBox2.TabIndex = 3;
groupBox2.TabStop = false;
groupBox2.Text = "Avalonia";
//
// avaloniaHost
//
this.avaloniaHost.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.avaloniaHost.Content = null;
this.avaloniaHost.Location = new System.Drawing.Point(6, 19);
this.avaloniaHost.Name = "avaloniaHost";
this.avaloniaHost.Size = new System.Drawing.Size(489, 393);
this.avaloniaHost.TabIndex = 0;
this.avaloniaHost.Text = "avaloniaHost";
//
//
avaloniaHost.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right));
avaloniaHost.Location = new System.Drawing.Point(7, 22);
avaloniaHost.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
avaloniaHost.Name = "avaloniaHost";
avaloniaHost.Size = new System.Drawing.Size(570, 453);
avaloniaHost.TabIndex = 0;
avaloniaHost.Text = "avaloniaHost";
//
// EmbedToWinFormsDemo
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(758, 442);
this.Controls.Add(this.groupBox2);
this.Controls.Add(this.groupBox1);
this.MinimumSize = new System.Drawing.Size(600, 400);
this.Name = "EmbedToWinFormsDemo";
this.Text = "EmbedToWinFormsDemo";
this.groupBox1.ResumeLayout(false);
this.groupBox2.ResumeLayout(false);
this.ResumeLayout(false);
//
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
ClientSize = new System.Drawing.Size(884, 510);
Controls.Add(groupBox2);
Controls.Add(groupBox1);
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
MinimumSize = new System.Drawing.Size(697, 456);
Text = "EmbedToWinFormsDemo";
groupBox1.ResumeLayout(false);
groupBox2.ResumeLayout(false);
ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Button OpenWindowButton;
private System.Windows.Forms.MonthCalendar monthCalendar1;
private System.Windows.Forms.GroupBox groupBox1;
private System.Windows.Forms.GroupBox groupBox2;

25
samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.cs

@ -1,5 +1,10 @@
using System.Windows.Forms;
using System;
using System.Windows.Forms;
using ControlCatalog;
using AvaloniaButton = Avalonia.Controls.Button;
using AvaloniaStackPanel = Avalonia.Controls.StackPanel;
using AvaloniaTextBox = Avalonia.Controls.TextBox;
using AvaloniaWindow = Avalonia.Controls.Window;
namespace WindowsInteropTest
{
@ -10,5 +15,23 @@ namespace WindowsInteropTest
InitializeComponent();
avaloniaHost.Content = new MainView();
}
private void OpenWindowButton_Click(object sender, EventArgs e)
{
var window = new AvaloniaWindow
{
Width = 300,
Height = 300,
Content = new AvaloniaStackPanel
{
Children =
{
new AvaloniaButton { Content = "Button" },
new AvaloniaTextBox { Text = "Text" }
}
}
};
window.Show();
}
}
}

3
samples/interop/WindowsInteropTest/Program.cs

@ -1,6 +1,7 @@
using System;
using ControlCatalog;
using Avalonia;
using Avalonia.Win32.Interoperability;
namespace WindowsInteropTest
{
@ -14,9 +15,11 @@ namespace WindowsInteropTest
{
System.Windows.Forms.Application.EnableVisualStyles();
System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false);
System.Windows.Forms.Application.AddMessageFilter(new WinFormsAvaloniaMessageFilter());
AppBuilder.Configure<App>()
.UseWin32()
.UseSkia()
.UseHarfBuzz()
.SetupWithoutStarting();
System.Windows.Forms.Application.Run(new EmbedToWinFormsDemo());
}

1
samples/interop/WindowsInteropTest/WindowsInteropTest.csproj

@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\HarfBuzz\Avalonia.HarfBuzz\Avalonia.HarfBuzz.csproj" />
<ProjectReference Include="..\..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\..\..\src\Windows\Avalonia.Win32.Interoperability\Avalonia.Win32.Interoperability.csproj" />
<ProjectReference Include="..\..\ControlCatalog\ControlCatalog.csproj" />

32
src/Avalonia.Base/Animation/CompositePageTransition.cs

@ -28,7 +28,7 @@ namespace Avalonia.Animation
/// </code>
/// </para>
/// </remarks>
public class CompositePageTransition : IPageTransition
public class CompositePageTransition : IPageTransition, IProgressPageTransition
{
/// <summary>
/// Gets or sets the transitions to be executed. Can be defined from XAML.
@ -44,5 +44,35 @@ namespace Avalonia.Animation
.ToArray();
return Task.WhenAll(transitionTasks);
}
/// <inheritdoc />
public void Update(
double progress,
Visual? from,
Visual? to,
bool forward,
double pageLength,
IReadOnlyList<PageTransitionItem> visibleItems)
{
foreach (var transition in PageTransitions)
{
if (transition is IProgressPageTransition progressive)
{
progressive.Update(progress, from, to, forward, pageLength, visibleItems);
}
}
}
/// <inheritdoc />
public void Reset(Visual visual)
{
foreach (var transition in PageTransitions)
{
if (transition is IProgressPageTransition progressive)
{
progressive.Reset(visual);
}
}
}
}
}

84
src/Avalonia.Base/Animation/CrossFade.cs

@ -12,8 +12,13 @@ namespace Avalonia.Animation
/// <summary>
/// Defines a cross-fade animation between two <see cref="Visual"/>s.
/// </summary>
public class CrossFade : IPageTransition
public class CrossFade : IPageTransition, IProgressPageTransition
{
private const double SidePeekOpacity = 0.72;
private const double FarPeekOpacity = 0.42;
private const double OutgoingDip = 0.22;
private const double IncomingBoost = 0.12;
private const double PassiveDip = 0.05;
private readonly Animation _fadeOutAnimation;
private readonly Animation _fadeInAnimation;
@ -182,5 +187,82 @@ namespace Avalonia.Animation
{
return Start(from, to, cancellationToken);
}
/// <inheritdoc/>
public void Update(
double progress,
Visual? from,
Visual? to,
bool forward,
double pageLength,
IReadOnlyList<PageTransitionItem> visibleItems)
{
if (visibleItems.Count > 0)
{
UpdateVisibleItems(progress, from, to, visibleItems);
return;
}
if (from != null)
from.Opacity = 1 - progress;
if (to != null)
{
to.IsVisible = true;
to.Opacity = progress;
}
}
/// <inheritdoc/>
public void Reset(Visual visual)
{
visual.Opacity = 1;
}
private static void UpdateVisibleItems(
double progress,
Visual? from,
Visual? to,
IReadOnlyList<PageTransitionItem> visibleItems)
{
var emphasis = Math.Sin(Math.Clamp(progress, 0.0, 1.0) * Math.PI);
foreach (var item in visibleItems)
{
item.Visual.IsVisible = true;
var opacity = GetOpacityForOffset(item.ViewportCenterOffset);
if (ReferenceEquals(item.Visual, from))
{
opacity = Math.Max(FarPeekOpacity, opacity - (OutgoingDip * emphasis));
}
else if (ReferenceEquals(item.Visual, to))
{
opacity = Math.Min(1.0, opacity + (IncomingBoost * emphasis));
}
else
{
opacity = Math.Max(FarPeekOpacity, opacity - (PassiveDip * emphasis));
}
item.Visual.Opacity = opacity;
}
}
private static double GetOpacityForOffset(double offsetFromCenter)
{
var distance = Math.Abs(offsetFromCenter);
if (distance <= 1.0)
return Lerp(1.0, SidePeekOpacity, distance);
if (distance <= 2.0)
return Lerp(SidePeekOpacity, FarPeekOpacity, distance - 1.0);
return FarPeekOpacity;
}
private static double Lerp(double from, double to, double t)
{
return from + ((to - from) * Math.Clamp(t, 0.0, 1.0));
}
}
}

39
src/Avalonia.Base/Animation/IProgressPageTransition.cs

@ -0,0 +1,39 @@
using System.Collections.Generic;
using Avalonia.VisualTree;
namespace Avalonia.Animation
{
/// <summary>
/// An <see cref="IPageTransition"/> that supports progress-driven updates.
/// </summary>
/// <remarks>
/// Transitions implementing this interface can be driven by a normalized progress value
/// (0.0 to 1.0) during swipe gestures or programmatic animations, rather than running
/// as a timed animation via <see cref="IPageTransition.Start"/>.
/// </remarks>
public interface IProgressPageTransition : IPageTransition
{
/// <summary>
/// Updates the transition to reflect the given progress.
/// </summary>
/// <param name="progress">The normalized progress value from 0.0 (start) to 1.0 (complete).</param>
/// <param name="from">The visual being transitioned away from. May be null.</param>
/// <param name="to">The visual being transitioned to. May be null.</param>
/// <param name="forward">Whether the transition direction is forward (next) or backward (previous).</param>
/// <param name="pageLength">The size of a page along the transition axis.</param>
/// <param name="visibleItems">The currently visible realized pages, if more than one page is visible.</param>
void Update(
double progress,
Visual? from,
Visual? to,
bool forward,
double pageLength,
IReadOnlyList<PageTransitionItem> visibleItems);
/// <summary>
/// Resets any visual state applied to the given visual by this transition.
/// </summary>
/// <param name="visual">The visual to reset.</param>
void Reset(Visual visual);
}
}

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

@ -12,7 +12,7 @@ namespace Avalonia.Animation
/// <summary>
/// Transitions between two pages by sliding them horizontally or vertically.
/// </summary>
public class PageSlide : IPageTransition
public class PageSlide : IPageTransition, IProgressPageTransition
{
/// <summary>
/// The axis on which the PageSlide should occur
@ -50,12 +50,12 @@ namespace Avalonia.Animation
/// Gets the orientation of the animation.
/// </summary>
public SlideAxis Orientation { get; set; }
/// <summary>
/// Gets or sets element entrance easing.
/// </summary>
public Easing SlideInEasing { get; set; } = new LinearEasing();
/// <summary>
/// Gets or sets element exit easing.
/// </summary>
@ -152,8 +152,6 @@ namespace Avalonia.Animation
if (from != null)
{
// Hide BEFORE resetting transform so there is no single-frame flash
// where the element snaps back to position 0 while still visible.
from.IsVisible = false;
if (FillMode != FillMode.None)
from.RenderTransform = null;
@ -163,6 +161,55 @@ namespace Avalonia.Animation
to.RenderTransform = null;
}
/// <inheritdoc/>
public virtual void Update(
double progress,
Visual? from,
Visual? to,
bool forward,
double pageLength,
IReadOnlyList<PageTransitionItem> visibleItems)
{
if (visibleItems.Count > 0)
return;
if (from is null && to is null)
return;
var parent = GetVisualParent(from, to);
var distance = pageLength > 0
? pageLength
: (Orientation == SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height);
var offset = distance * progress;
if (from != null)
{
if (from.RenderTransform is not TranslateTransform ft)
from.RenderTransform = ft = new TranslateTransform();
if (Orientation == SlideAxis.Horizontal)
ft.X = forward ? -offset : offset;
else
ft.Y = forward ? -offset : offset;
}
if (to != null)
{
to.IsVisible = true;
if (to.RenderTransform is not TranslateTransform tt)
to.RenderTransform = tt = new TranslateTransform();
if (Orientation == SlideAxis.Horizontal)
tt.X = forward ? distance - offset : -(distance - offset);
else
tt.Y = forward ? distance - offset : -(distance - offset);
}
}
/// <inheritdoc/>
public virtual void Reset(Visual visual)
{
visual.RenderTransform = null;
}
/// <summary>
/// Gets the common visual parent of the two control.
/// </summary>

12
src/Avalonia.Base/Animation/PageTransitionItem.cs

@ -0,0 +1,12 @@
using Avalonia.VisualTree;
namespace Avalonia.Animation
{
/// <summary>
/// Describes a single visible page within a carousel viewport.
/// </summary>
public readonly record struct PageTransitionItem(
int Index,
Visual Visual,
double ViewportCenterOffset);
}

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

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Media;
@ -8,6 +9,8 @@ namespace Avalonia.Animation;
public class Rotate3DTransition: PageSlide
{
private const double SidePeekAngle = 24.0;
private const double FarPeekAngle = 38.0;
/// <summary>
/// Creates a new instance of the <see cref="Rotate3DTransition"/>
@ -20,7 +23,7 @@ public class Rotate3DTransition: PageSlide
{
Depth = depth;
}
/// <summary>
/// Defines the depth of the 3D Effect. If null, depth will be calculated automatically from the width or height
/// of the common parent of the visual being rotated.
@ -28,12 +31,12 @@ public class Rotate3DTransition: PageSlide
public double? Depth { get; set; }
/// <summary>
/// Creates a new instance of the <see cref="Rotate3DTransition"/>
/// Initializes a new instance of the <see cref="Rotate3DTransition"/> class.
/// </summary>
public Rotate3DTransition() { }
/// <inheritdoc />
public override async Task Start(Visual? @from, Visual? to, bool forward, CancellationToken cancellationToken)
public override async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
@ -49,11 +52,12 @@ public class Rotate3DTransition: PageSlide
_ => throw new ArgumentOutOfRangeException()
};
var depthSetter = new Setter {Property = Rotate3DTransform.DepthProperty, Value = Depth ?? center};
var centerZSetter = new Setter {Property = Rotate3DTransform.CenterZProperty, Value = -center / 2};
var depthSetter = new Setter { Property = Rotate3DTransform.DepthProperty, Value = Depth ?? center };
var centerZSetter = new Setter { Property = Rotate3DTransform.CenterZProperty, Value = -center / 2 };
KeyFrame CreateKeyFrame(double cue, double rotation, int zIndex, bool isVisible = true) =>
new() {
KeyFrame CreateKeyFrame(double cue, double rotation, int zIndex, bool isVisible = true) =>
new()
{
Setters =
{
new Setter { Property = rotateProperty, Value = rotation },
@ -71,7 +75,7 @@ public class Rotate3DTransition: PageSlide
{
Easing = SlideOutEasing,
Duration = Duration,
FillMode = FillMode.Forward,
FillMode = FillMode,
Children =
{
CreateKeyFrame(0d, 0d, 2),
@ -90,7 +94,7 @@ public class Rotate3DTransition: PageSlide
{
Easing = SlideInEasing,
Duration = Duration,
FillMode = FillMode.Forward,
FillMode = FillMode,
Children =
{
CreateKeyFrame(0d, 90d * (forward ? 1 : -1), 1),
@ -107,10 +111,8 @@ public class Rotate3DTransition: PageSlide
if (!cancellationToken.IsCancellationRequested)
{
if (to != null)
{
to.ZIndex = 2;
}
if (from != null)
{
from.IsVisible = false;
@ -118,4 +120,139 @@ public class Rotate3DTransition: PageSlide
}
}
}
/// <inheritdoc/>
public override void Update(
double progress,
Visual? from,
Visual? to,
bool forward,
double pageLength,
IReadOnlyList<PageTransitionItem> visibleItems)
{
if (visibleItems.Count > 0)
{
UpdateVisibleItems(progress, from, to, pageLength, visibleItems);
return;
}
if (from is null && to is null)
return;
var parent = GetVisualParent(from, to);
var center = pageLength > 0
? pageLength
: (Orientation == SlideAxis.Vertical ? parent.Bounds.Height : parent.Bounds.Width);
var depth = Depth ?? center;
var sign = forward ? 1.0 : -1.0;
if (from != null)
{
if (from.RenderTransform is not Rotate3DTransform ft)
from.RenderTransform = ft = new Rotate3DTransform();
ft.Depth = depth;
ft.CenterZ = -center / 2;
from.ZIndex = progress < 0.5 ? 2 : 1;
if (Orientation == SlideAxis.Horizontal)
ft.AngleY = -sign * 90.0 * progress;
else
ft.AngleX = -sign * 90.0 * progress;
}
if (to != null)
{
to.IsVisible = true;
if (to.RenderTransform is not Rotate3DTransform tt)
to.RenderTransform = tt = new Rotate3DTransform();
tt.Depth = depth;
tt.CenterZ = -center / 2;
to.ZIndex = progress < 0.5 ? 1 : 2;
if (Orientation == SlideAxis.Horizontal)
tt.AngleY = sign * 90.0 * (1.0 - progress);
else
tt.AngleX = sign * 90.0 * (1.0 - progress);
}
}
private void UpdateVisibleItems(
double progress,
Visual? from,
Visual? to,
double pageLength,
IReadOnlyList<PageTransitionItem> visibleItems)
{
var anchor = from ?? to ?? visibleItems[0].Visual;
if (anchor.VisualParent is not Visual parent)
return;
var center = pageLength > 0
? pageLength
: (Orientation == SlideAxis.Vertical ? parent.Bounds.Height : parent.Bounds.Width);
var depth = Depth ?? center;
var angleStrength = Math.Sin(Math.Clamp(progress, 0.0, 1.0) * Math.PI);
foreach (var item in visibleItems)
{
var visual = item.Visual;
visual.IsVisible = true;
visual.ZIndex = GetZIndex(item.ViewportCenterOffset);
if (visual.RenderTransform is not Rotate3DTransform transform)
visual.RenderTransform = transform = new Rotate3DTransform();
transform.Depth = depth;
transform.CenterZ = -center / 2;
var angle = GetAngleForOffset(item.ViewportCenterOffset) * angleStrength;
if (Orientation == SlideAxis.Horizontal)
{
transform.AngleY = angle;
transform.AngleX = 0;
}
else
{
transform.AngleX = angle;
transform.AngleY = 0;
}
}
}
private static double GetAngleForOffset(double offsetFromCenter)
{
var sign = Math.Sign(offsetFromCenter);
if (sign == 0)
return 0;
var distance = Math.Abs(offsetFromCenter);
if (distance <= 1.0)
return sign * Lerp(0.0, SidePeekAngle, distance);
if (distance <= 2.0)
return sign * Lerp(SidePeekAngle, FarPeekAngle, distance - 1.0);
return sign * FarPeekAngle;
}
private static int GetZIndex(double offsetFromCenter)
{
var distance = Math.Abs(offsetFromCenter);
if (distance < 0.5)
return 3;
if (distance < 1.5)
return 2;
return 1;
}
private static double Lerp(double from, double to, double t)
{
return from + ((to - from) * Math.Clamp(t, 0.0, 1.0));
}
/// <inheritdoc/>
public override void Reset(Visual visual)
{
visual.RenderTransform = null;
visual.ZIndex = 0;
}
}

37
src/Avalonia.Base/Input/FindNextElementOptions.cs

@ -6,12 +6,49 @@ using System.Threading.Tasks;
namespace Avalonia.Input
{
/// <summary>
/// Provides options to customize the behavior when identifying the next element to focus
/// during a navigation operation.
/// </summary>
public sealed class FindNextElementOptions
{
/// <summary>
/// Gets or sets the root <see cref="InputElement"/> within which the search for the next
/// focusable element will be conducted.
/// </summary>
/// <remarks>
/// This property defines the boundary for focus navigation operations. It determines the root element
/// in the visual tree under which the focusable item search is performed. If not specified, the search
/// will default to the current scope.
/// </remarks>
public InputElement? SearchRoot { get; init; }
/// <summary>
/// Gets or sets the rectangular region within the visual hierarchy that will be excluded
/// from consideration during focus navigation.
/// </summary>
public Rect ExclusionRect { get; init; }
/// <summary>
/// Gets or sets a rectangular region that serves as a hint for focus navigation.
/// This property specifies a rectangle, relative to the coordinate system of the search root,
/// which can be used as a preferred or prioritized target when navigating focus.
/// It can be null if no specific hint region is provided.
/// </summary>
public Rect? FocusHintRectangle { get; init; }
/// <summary>
/// Specifies an optional override for the navigation strategy used in XY focus navigation.
/// This property allows customizing the focus movement behavior when navigating between UI elements.
/// </summary>
public XYFocusNavigationStrategy? NavigationStrategyOverride { get; init; }
/// <summary>
/// Specifies whether occlusivity (overlapping of elements or obstructions)
/// should be ignored during focus navigation. When set to <c>true</c>,
/// the navigation logic disregards obstructions that may block a potential
/// focus target, allowing elements behind such obstructions to be considered.
/// </summary>
public bool IgnoreOcclusivity { get; init; }
}
}

169
src/Avalonia.Base/Input/FocusManager.cs

@ -4,7 +4,6 @@ using System.Linq;
using Avalonia.Input.Navigation;
using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.Reactive;
using Avalonia.VisualTree;
namespace Avalonia.Input
@ -12,7 +11,6 @@ namespace Avalonia.Input
/// <summary>
/// Manages focus for the application.
/// </summary>
[PrivateApi]
public class FocusManager : IFocusManager
{
/// <summary>
@ -42,58 +40,51 @@ namespace Avalonia.Input
RoutingStrategies.Tunnel);
}
[PrivateApi]
public FocusManager()
{
_contentRoot = null;
}
public FocusManager(IInputElement contentRoot)
{
_contentRoot = contentRoot;
}
internal void SetContentRoot(IInputElement? contentRoot)
/// <summary>
/// Gets or sets the content root for the focus management system.
/// </summary>
[PrivateApi]
public IInputElement? ContentRoot
{
_contentRoot = contentRoot;
get => _contentRoot;
set => _contentRoot = value;
}
private IInputElement? Current => KeyboardDevice.Instance?.FocusedElement;
private XYFocus _xyFocus = new();
private XYFocusOptions _xYFocusOptions = new XYFocusOptions();
private readonly XYFocus _xyFocus = new();
private IInputElement? _contentRoot;
private XYFocusOptions? _reusableFocusOptions;
/// <summary>
/// Gets the currently focused <see cref="IInputElement"/>.
/// </summary>
/// <inheritdoc />
public IInputElement? GetFocusedElement() => Current;
/// <summary>
/// Focuses a control.
/// </summary>
/// <param name="control">The control to focus.</param>
/// <param name="method">The method by which focus was changed.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
/// <inheritdoc />
public bool Focus(
IInputElement? control,
IInputElement? element,
NavigationMethod method = NavigationMethod.Unspecified,
KeyModifiers keyModifiers = KeyModifiers.None)
{
if (KeyboardDevice.Instance is not { } keyboardDevice)
return false;
if (control is not null)
if (element is not null)
{
if (!CanFocus(control))
if (!CanFocus(element))
return false;
if (GetFocusScope(control) is StyledElement scope)
if (GetFocusScope(element) is StyledElement scope)
{
scope.SetValue(FocusedElementProperty, control);
scope.SetValue(FocusedElementProperty, element);
_focusRoot = GetFocusRoot(scope);
}
keyboardDevice.SetFocusedElement(control, method, keyModifiers);
keyboardDevice.SetFocusedElement(element, method, keyModifiers);
return true;
}
else if (_focusRoot?.GetValue(FocusedElementProperty) is { } restore &&
@ -110,12 +101,7 @@ namespace Avalonia.Input
}
}
public void ClearFocus()
{
Focus(null);
}
public void ClearFocusOnElementRemoved(IInputElement removedElement, Visual oldParent)
internal void ClearFocusOnElementRemoved(IInputElement removedElement, Visual oldParent)
{
if (oldParent is IInputElement parentElement &&
GetFocusScope(parentElement) is StyledElement scope &&
@ -129,6 +115,7 @@ namespace Avalonia.Input
Focus(null);
}
[PrivateApi]
public IInputElement? GetFocusedElement(IFocusScope scope)
{
return (scope as StyledElement)?.GetValue(FocusedElementProperty);
@ -138,6 +125,7 @@ namespace Avalonia.Input
/// Notifies the focus manager of a change in focus scope.
/// </summary>
/// <param name="scope">The new focus scope.</param>
[PrivateApi]
public void SetFocusScope(IFocusScope scope)
{
if (GetFocusedElement(scope) is { } focused)
@ -153,12 +141,14 @@ namespace Avalonia.Input
}
}
[PrivateApi]
public void RemoveFocusRoot(IFocusScope scope)
{
if (scope == _focusRoot)
ClearFocus();
Focus(null);
}
[PrivateApi]
public static bool GetIsFocusScope(IInputElement e) => e is IFocusScope;
/// <summary>
@ -176,25 +166,15 @@ namespace Avalonia.Input
?? (FocusManager?)AvaloniaLocator.Current.GetService<IFocusManager>();
}
/// <summary>
/// Attempts to change focus from the element with focus to the next focusable element in the specified direction.
/// </summary>
/// <param name="direction">The direction to traverse (in tab order).</param>
/// <returns>true if focus moved; otherwise, false.</returns>
public bool TryMoveFocus(NavigationDirection direction)
/// <inheritdoc />
public bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions? options = null)
{
return FindAndSetNextFocus(direction, _xYFocusOptions);
}
ValidateDirection(direction);
/// <summary>
/// Attempts to change focus from the element with focus to the next focusable element in the specified direction, using the specified navigation options.
/// </summary>
/// <param name="direction">The direction to traverse (in tab order).</param>
/// <param name="options">The options to help identify the next element to receive focus with keyboard/controller/remote navigation.</param>
/// <returns>true if focus moved; otherwise, false.</returns>
public bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions options)
{
return FindAndSetNextFocus(direction, ValidateAndCreateFocusOptions(direction, options));
var focusOptions = ToFocusOptions(options, true);
var result = FindAndSetNextFocus(direction, focusOptions);
_reusableFocusOptions = focusOptions;
return result;
}
/// <summary>
@ -295,10 +275,7 @@ namespace Avalonia.Input
return true;
}
/// <summary>
/// Retrieves the first element that can receive focus.
/// </summary>
/// <returns>The first focusable element.</returns>
/// <inheritdoc />
public IInputElement? FindFirstFocusableElement()
{
var root = (_contentRoot as Visual)?.GetSelfAndVisualDescendants().FirstOrDefault(x => x is IInputElement) as IInputElement;
@ -317,10 +294,7 @@ namespace Avalonia.Input
return GetFirstFocusableElement(searchScope);
}
/// <summary>
/// Retrieves the last element that can receive focus.
/// </summary>
/// <returns>The last focusable element.</returns>
/// <inheritdoc />
public IInputElement? FindLastFocusableElement()
{
var root = (_contentRoot as Visual)?.GetSelfAndVisualDescendants().FirstOrDefault(x => x is IInputElement) as IInputElement;
@ -339,52 +313,59 @@ namespace Avalonia.Input
return GetFocusManager(searchScope)?.GetLastFocusableElement(searchScope);
}
/// <summary>
/// Retrieves the element that should receive focus based on the specified navigation direction.
/// </summary>
/// <param name="direction"></param>
/// <returns></returns>
public IInputElement? FindNextElement(NavigationDirection direction)
/// <inheritdoc />
public IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions? options = null)
{
var xyOption = new XYFocusOptions()
{
UpdateManifold = false
};
ValidateDirection(direction);
return FindNextFocus(direction, xyOption);
var focusOptions = ToFocusOptions(options, false);
var result = FindNextFocus(direction, focusOptions);
_reusableFocusOptions = focusOptions;
return result;
}
/// <summary>
/// Retrieves the element that should receive focus based on the specified navigation direction (cannot be used with tab navigation).
/// </summary>
/// <param name="direction">The direction that focus moves from element to element within the app UI.</param>
/// <param name="options">The options to help identify the next element to receive focus with the provided navigation.</param>
/// <returns>The next element to receive focus.</returns>
public IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions options)
private static void ValidateDirection(NavigationDirection direction)
{
return FindNextFocus(direction, ValidateAndCreateFocusOptions(direction, options));
if (direction is not (
NavigationDirection.Next or
NavigationDirection.Previous or
NavigationDirection.Up or
NavigationDirection.Down or
NavigationDirection.Left or
NavigationDirection.Right))
{
throw new ArgumentOutOfRangeException(
nameof(direction),
direction,
$"Only {nameof(NavigationDirection.Next)}, {nameof(NavigationDirection.Previous)}, " +
$"{nameof(NavigationDirection.Up)}, {nameof(NavigationDirection.Down)}," +
$" {nameof(NavigationDirection.Left)} and {nameof(NavigationDirection.Right)} directions are supported");
}
}
private static XYFocusOptions ValidateAndCreateFocusOptions(NavigationDirection direction, FindNextElementOptions options)
private XYFocusOptions ToFocusOptions(FindNextElementOptions? options, bool updateManifold)
{
if (direction is not NavigationDirection.Up
and not NavigationDirection.Down
and not NavigationDirection.Left
and not NavigationDirection.Right)
// XYFocus only uses the options and never modifies them; we can cache and reset them between calls.
var focusOptions = _reusableFocusOptions;
_reusableFocusOptions = null;
if (focusOptions is null)
focusOptions = new XYFocusOptions();
else
focusOptions.Reset();
if (options is not null)
{
throw new ArgumentOutOfRangeException(nameof(direction),
$"{direction} is not supported with FindNextElementOptions. Only Up, Down, Left and right are supported");
focusOptions.SearchRoot = options.SearchRoot;
focusOptions.ExclusionRect = options.ExclusionRect;
focusOptions.FocusHintRectangle = options.FocusHintRectangle;
focusOptions.NavigationStrategyOverride = options.NavigationStrategyOverride;
focusOptions.IgnoreOcclusivity = options.IgnoreOcclusivity;
}
return new XYFocusOptions
{
UpdateManifold = false,
SearchRoot = options.SearchRoot,
ExclusionRect = options.ExclusionRect,
FocusHintRectangle = options.FocusHintRectangle,
NavigationStrategyOverride = options.NavigationStrategyOverride,
IgnoreOcclusivity = options.IgnoreOcclusivity
};
focusOptions.UpdateManifold = updateManifold;
return focusOptions;
}
internal IInputElement? FindNextFocus(NavigationDirection direction, XYFocusOptions focusOptions, bool updateManifolds = true)

18
src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs

@ -39,6 +39,24 @@ namespace Avalonia.Input.GestureRecognizers
}
}
public bool Remove(GestureRecognizer recognizer)
{
if (_recognizers == null)
return false;
var removed = _recognizers.Remove(recognizer);
if (removed)
{
recognizer.Target = null;
if (recognizer is ISetLogicalParent logical)
logical.SetParent(null);
}
return removed;
}
static readonly List<GestureRecognizer> s_Empty = new List<GestureRecognizer>();
public IEnumerator<GestureRecognizer> GetEnumerator()

265
src/Avalonia.Base/Input/GestureRecognizers/SwipeGestureRecognizer.cs

@ -1,87 +1,102 @@
using System;
using Avalonia.Logging;
using Avalonia.Media;
using System.Diagnostics;
using Avalonia.Platform;
namespace Avalonia.Input.GestureRecognizers
{
/// <summary>
/// A gesture recognizer that detects swipe gestures and raises
/// <see cref="InputElement.SwipeGestureEvent"/> on the target element when a swipe is confirmed.
/// A gesture recognizer that detects swipe gestures for paging interactions.
/// </summary>
/// <remarks>
/// Unlike <see cref="ScrollGestureRecognizer"/>, this recognizer is optimized for discrete
/// paging interactions (e.g., carousel navigation) rather than continuous scrolling.
/// It does not include inertia or friction physics.
/// </remarks>
public class SwipeGestureRecognizer : GestureRecognizer
{
private bool _swiping;
private Point _trackedRootPoint;
private IPointer? _tracking;
private IPointer? _captured;
private Point _initialPosition;
private int _gestureId;
private int _id;
private Vector _velocity;
private long _lastTimestamp;
/// <summary>
/// Defines the <see cref="Threshold"/> property.
/// Defines the <see cref="CanHorizontallySwipe"/> property.
/// </summary>
public static readonly StyledProperty<double> ThresholdProperty =
AvaloniaProperty.Register<SwipeGestureRecognizer, double>(nameof(Threshold), 30d);
public static readonly StyledProperty<bool> CanHorizontallySwipeProperty =
AvaloniaProperty.Register<SwipeGestureRecognizer, bool>(nameof(CanHorizontallySwipe));
/// <summary>
/// Defines the <see cref="CrossAxisCancelThreshold"/> property.
/// Defines the <see cref="CanVerticallySwipe"/> property.
/// </summary>
public static readonly StyledProperty<double> CrossAxisCancelThresholdProperty =
AvaloniaProperty.Register<SwipeGestureRecognizer, double>(
nameof(CrossAxisCancelThreshold), 8d);
public static readonly StyledProperty<bool> CanVerticallySwipeProperty =
AvaloniaProperty.Register<SwipeGestureRecognizer, bool>(nameof(CanVerticallySwipe));
/// <summary>
/// Defines the <see cref="EdgeSize"/> property.
/// Leading-edge start zone in px. 0 (default) = full area.
/// When &gt; 0, only starts tracking if the pointer is within this many px
/// of the leading edge (LTR: left; RTL: right).
/// Defines the <see cref="Threshold"/> property.
/// </summary>
public static readonly StyledProperty<double> EdgeSizeProperty =
AvaloniaProperty.Register<SwipeGestureRecognizer, double>(nameof(EdgeSize), 0d);
/// <remarks>
/// A value of 0 (the default) causes the distance to be read from
/// <see cref="IPlatformSettings"/> at the time of the first gesture.
/// </remarks>
public static readonly StyledProperty<double> ThresholdProperty =
AvaloniaProperty.Register<SwipeGestureRecognizer, double>(nameof(Threshold), defaultValue: 0d);
/// <summary>
/// Defines the <see cref="IsMouseEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsMouseEnabledProperty =
AvaloniaProperty.Register<SwipeGestureRecognizer, bool>(nameof(IsMouseEnabled), defaultValue: false);
/// <summary>
/// Defines the <see cref="IsEnabled"/> property.
/// When false, the recognizer ignores all pointer events.
/// Lets callers toggle the recognizer at runtime without needing to remove it from the
/// collection (GestureRecognizerCollection has Add but no Remove).
/// Default: true.
/// </summary>
public static readonly StyledProperty<bool> IsEnabledProperty =
AvaloniaProperty.Register<SwipeGestureRecognizer, bool>(nameof(IsEnabled), true);
AvaloniaProperty.Register<SwipeGestureRecognizer, bool>(nameof(IsEnabled), defaultValue: true);
/// <summary>
/// Gets or sets the minimum distance in pixels the pointer must travel before a swipe
/// is recognized. Default is 30px.
/// Gets or sets a value indicating whether horizontal swipes are tracked.
/// </summary>
public double Threshold
public bool CanHorizontallySwipe
{
get => GetValue(ThresholdProperty);
set => SetValue(ThresholdProperty, value);
get => GetValue(CanHorizontallySwipeProperty);
set => SetValue(CanHorizontallySwipeProperty, value);
}
/// <summary>
/// Gets or sets the maximum cross-axis drift in pixels allowed before the gesture is
/// cancelled. Default is 8px.
/// Gets or sets a value indicating whether vertical swipes are tracked.
/// </summary>
public double CrossAxisCancelThreshold
public bool CanVerticallySwipe
{
get => GetValue(CrossAxisCancelThresholdProperty);
set => SetValue(CrossAxisCancelThresholdProperty, value);
get => GetValue(CanVerticallySwipeProperty);
set => SetValue(CanVerticallySwipeProperty, value);
}
/// <summary>
/// Gets or sets the leading-edge start zone in pixels. When greater than zero, tracking
/// only begins if the pointer is within this distance of the leading edge. Default is 0
/// (full area).
/// Gets or sets the minimum pointer movement in pixels before a swipe is recognized.
/// A value of 0 reads the threshold from <see cref="IPlatformSettings"/> at gesture time.
/// </summary>
public double EdgeSize
public double Threshold
{
get => GetValue(EdgeSizeProperty);
set => SetValue(EdgeSizeProperty, value);
get => GetValue(ThresholdProperty);
set => SetValue(ThresholdProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether mouse pointer events trigger swipe gestures.
/// Defaults to <see langword="false"/>; touch and pen are always enabled.
/// </summary>
public bool IsMouseEnabled
{
get => GetValue(IsMouseEnabledProperty);
set => SetValue(IsMouseEnabledProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the recognizer responds to pointer events.
/// Setting this to false is a lightweight alternative to removing the recognizer from
/// the collection. Default is true.
/// Gets or sets a value indicating whether this recognizer responds to pointer events.
/// Defaults to <see langword="true"/>.
/// </summary>
public bool IsEnabled
{
@ -89,104 +104,122 @@ namespace Avalonia.Input.GestureRecognizers
set => SetValue(IsEnabledProperty, value);
}
/// <inheritdoc/>
protected override void PointerPressed(PointerPressedEventArgs e)
{
if (!IsEnabled) return;
if (!e.GetCurrentPoint(null).Properties.IsLeftButtonPressed) return;
if (Target is not Visual visual) return;
if (!IsEnabled)
return;
var pos = e.GetPosition(visual);
var edgeSize = EdgeSize;
var point = e.GetCurrentPoint(null);
if (edgeSize > 0)
if ((e.Pointer.Type is PointerType.Touch or PointerType.Pen ||
(IsMouseEnabled && e.Pointer.Type == PointerType.Mouse))
&& point.Properties.IsLeftButtonPressed)
{
bool isRtl = visual.FlowDirection == FlowDirection.RightToLeft;
bool inEdge = isRtl
? pos.X >= visual.Bounds.Width - edgeSize
: pos.X <= edgeSize;
if (!inEdge)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.Control)?.Log(
this, "SwipeGestureRecognizer: press at {Pos} outside edge zone ({EdgeSize}px), ignoring",
pos, edgeSize);
return;
}
EndGesture();
_tracking = e.Pointer;
_id = SwipeGestureEventArgs.GetNextFreeId();
_trackedRootPoint = point.Position;
_velocity = default;
_lastTimestamp = 0;
}
_gestureId = SwipeGestureEventArgs.GetNextFreeId();
_tracking = e.Pointer;
_initialPosition = pos;
Logger.TryGet(LogEventLevel.Verbose, LogArea.Control)?.Log(
this, "SwipeGestureRecognizer: tracking started at {Pos} (pointer={PointerType})",
pos, e.Pointer.Type);
}
/// <inheritdoc/>
protected override void PointerMoved(PointerEventArgs e)
{
if (_tracking != e.Pointer || Target is not Visual visual) return;
var pos = e.GetPosition(visual);
double dx = pos.X - _initialPosition.X;
double dy = pos.Y - _initialPosition.Y;
double absDx = Math.Abs(dx);
double absDy = Math.Abs(dy);
double threshold = Threshold;
if (absDx < threshold && absDy < threshold)
return;
SwipeDirection dir;
Vector delta;
if (absDx >= absDy)
if (e.Pointer == _tracking)
{
dir = dx > 0 ? SwipeDirection.Right : SwipeDirection.Left;
delta = new Vector(dx, 0);
}
else
{
dir = dy > 0 ? SwipeDirection.Down : SwipeDirection.Up;
delta = new Vector(0, dy);
}
var rootPoint = e.GetPosition(null);
var threshold = GetEffectiveThreshold();
Logger.TryGet(LogEventLevel.Verbose, LogArea.Control)?.Log(
this, "SwipeGestureRecognizer: swipe recognized — direction={Direction}, delta={Delta}",
dir, delta);
if (!_swiping)
{
var horizontalTriggered = CanHorizontallySwipe && Math.Abs(_trackedRootPoint.X - rootPoint.X) > threshold;
var verticalTriggered = CanVerticallySwipe && Math.Abs(_trackedRootPoint.Y - rootPoint.Y) > threshold;
if (horizontalTriggered || verticalTriggered)
{
_swiping = true;
_trackedRootPoint = new Point(
horizontalTriggered
? _trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? threshold : -threshold)
: rootPoint.X,
verticalTriggered
? _trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? threshold : -threshold)
: rootPoint.Y);
Capture(e.Pointer);
}
}
_tracking = null;
_captured = e.Pointer;
Capture(e.Pointer);
e.Handled = true;
if (_swiping)
{
var delta = _trackedRootPoint - rootPoint;
var now = Stopwatch.GetTimestamp();
if (_lastTimestamp > 0)
{
var elapsedSeconds = (double)(now - _lastTimestamp) / Stopwatch.Frequency;
if (elapsedSeconds > 0)
{
var instantVelocity = delta / elapsedSeconds;
_velocity = _velocity * 0.5 + instantVelocity * 0.5;
}
}
_lastTimestamp = now;
Target!.RaiseEvent(new SwipeGestureEventArgs(_id, delta, _velocity));
_trackedRootPoint = rootPoint;
e.Handled = true;
}
}
}
var args = new SwipeGestureEventArgs(_gestureId, dir, delta, _initialPosition);
Target?.RaiseEvent(args);
/// <inheritdoc/>
protected override void PointerCaptureLost(IPointer pointer)
{
if (pointer == _tracking)
EndGesture();
}
/// <inheritdoc/>
protected override void PointerReleased(PointerReleasedEventArgs e)
{
if (_tracking == e.Pointer)
if (e.Pointer == _tracking && _swiping)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.Control)?.Log(
this, "SwipeGestureRecognizer: pointer released without crossing threshold — gesture discarded");
_tracking = null;
e.Handled = true;
EndGesture();
}
}
if (_captured == e.Pointer)
private void EndGesture()
{
_tracking = null;
if (_swiping)
{
(e.Pointer as Pointer)?.CaptureGestureRecognizer(null);
_captured = null;
_swiping = false;
var endedArgs = new SwipeGestureEndedEventArgs(_id, _velocity);
_velocity = default;
_lastTimestamp = 0;
_id = 0;
Target!.RaiseEvent(endedArgs);
}
}
protected override void PointerCaptureLost(IPointer pointer)
private const double DefaultTapSize = 10;
private double GetEffectiveThreshold()
{
if (_tracking == pointer)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.Control)?.Log(
this, "SwipeGestureRecognizer: capture lost — gesture cancelled");
_tracking = null;
}
_captured = null;
var configured = Threshold;
if (configured > 0)
return configured;
var tapSize = AvaloniaLocator.Current?.GetService<IPlatformSettings>()
?.GetTapSize(PointerType.Touch).Height ?? DefaultTapSize;
return tapSize / 2;
}
}
}

2
src/Avalonia.Base/Input/Gestures.cs

@ -30,14 +30,12 @@ namespace Avalonia.Input
private static readonly WeakReference<object?> s_lastPress = new WeakReference<object?>(null);
private static Point s_lastPressPoint;
private static CancellationTokenSource? s_holdCancellationToken;
static Gestures()
{
InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed);
InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased);
InputElement.PointerMovedEvent.RouteFinished.Subscribe(PointerMoved);
}
private static object? GetCaptured(RoutedEventArgs? args)
{
if (args is not PointerEventArgs pointerEventArgs)

63
src/Avalonia.Base/Input/IFocusManager.cs

@ -14,9 +14,66 @@ namespace Avalonia.Input
IInputElement? GetFocusedElement();
/// <summary>
/// Clears currently focused element.
/// Focuses a control.
/// </summary>
[Unstable("This API might be removed in 11.x minor updates. Please consider focusing another element instead of removing focus at all for better UX.")]
void ClearFocus();
/// <param name="element">The control to focus.</param>
/// <param name="method">The method by which focus was changed.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
/// <returns><c>true</c> if the focus moved to a control; otherwise, <c>false</c>.</returns>
/// <remarks>
/// If <paramref name="element"/> is null, this method tries to clear the focus. However, it is not advised.
/// For a better user experience, focus should be moved to another element when possible.
///
/// When this method return <c>true</c>, it is not guaranteed that the focus has been moved
/// to <paramref name="element"/>. The focus might have been redirected to another element.
/// </remarks>
bool Focus(
IInputElement? element,
NavigationMethod method = NavigationMethod.Unspecified,
KeyModifiers keyModifiers = KeyModifiers.None);
/// <summary>
/// Attempts to change focus from the element with focus to the next focusable element in the specified direction.
/// </summary>
/// <param name="direction">
/// The direction that focus moves from element to element.
/// Must be one of <see cref="NavigationDirection.Next"/>, <see cref="NavigationDirection.Previous"/>,
/// <see cref="NavigationDirection.Left"/>, <see cref="NavigationDirection.Right"/>,
/// <see cref="NavigationDirection.Up"/> and <see cref="NavigationDirection.Down"/>.
/// </param>
/// <param name="options">
/// The options to help identify the next element to receive focus.
/// They only apply to directional navigation.
/// </param>
/// <returns>true if focus moved; otherwise, false.</returns>
bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions? options = null);
/// <summary>
/// Retrieves the first element that can receive focus.
/// </summary>
/// <returns>The first focusable element.</returns>
IInputElement? FindFirstFocusableElement();
/// <summary>
/// Retrieves the last element that can receive focus.
/// </summary>
/// <returns>The last focusable element.</returns>
IInputElement? FindLastFocusableElement();
/// <summary>
/// Retrieves the element that should receive focus based on the specified navigation direction.
/// </summary>
/// <param name="direction">
/// The direction that focus moves from element to element.
/// Must be one of <see cref="NavigationDirection.Next"/>, <see cref="NavigationDirection.Previous"/>,
/// <see cref="NavigationDirection.Left"/>, <see cref="NavigationDirection.Right"/>,
/// <see cref="NavigationDirection.Up"/> and <see cref="NavigationDirection.Down"/>.
/// </param>
/// <param name="options">
/// The options to help identify the next element to receive focus.
/// They only apply to directional navigation.
/// </param>
/// <returns>The next element to receive focus, if any.</returns>
IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions? options = null);
}
}

16
src/Avalonia.Base/Input/InputElement.Gestures.cs

@ -54,6 +54,13 @@ namespace Avalonia.Input
RoutedEvent.Register<InputElement, SwipeGestureEventArgs>(
nameof(SwipeGesture), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="SwipeGestureEnded"/> event.
/// </summary>
public static readonly RoutedEvent<SwipeGestureEndedEventArgs> SwipeGestureEndedEvent =
RoutedEvent.Register<InputElement, SwipeGestureEndedEventArgs>(
nameof(SwipeGestureEnded), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="ScrollGesture"/> event.
/// </summary>
@ -238,6 +245,15 @@ namespace Avalonia.Input
remove { RemoveHandler(SwipeGestureEvent, value); }
}
/// <summary>
/// Occurs when a swipe gesture ends on the control.
/// </summary>
public event EventHandler<SwipeGestureEndedEventArgs>? SwipeGestureEnded
{
add { AddHandler(SwipeGestureEndedEvent, value); }
remove { RemoveHandler(SwipeGestureEndedEvent, value); }
}
/// <summary>
/// Occurs when the user performs a rapid dragging motion in a single direction on a touchpad.
/// </summary>

4
src/Avalonia.Base/Input/InputElement.cs

@ -523,7 +523,7 @@ namespace Avalonia.Input
if (!IsEffectivelyEnabled && FocusManager.GetFocusManager(this) is { } focusManager
&& Equals(focusManager.GetFocusedElement(), this))
{
focusManager.ClearFocus();
focusManager.Focus(null);
}
}
}
@ -995,7 +995,7 @@ namespace Avalonia.Input
}
else
{
focusManager.ClearFocus();
focusManager.Focus(null);
}
}
}

29
src/Avalonia.Base/Input/Navigation/XYFocusOptions.cs

@ -1,17 +1,38 @@
namespace Avalonia.Input.Navigation;
internal class XYFocusOptions
internal sealed class XYFocusOptions
{
public InputElement? SearchRoot { get; set; }
public Rect ExclusionRect { get; set; }
public Rect? FocusHintRectangle { get; set; }
public Rect? FocusedElementBounds { get; set; }
public XYFocusNavigationStrategy? NavigationStrategyOverride { get; set; }
public bool IgnoreClipping { get; set; } = true;
public bool IgnoreClipping { get; set; }
public bool IgnoreCone { get; set; }
public KeyDeviceType? KeyDeviceType { get; set; }
public bool ConsiderEngagement { get; set; } = true;
public bool UpdateManifold { get; set; } = true;
public bool ConsiderEngagement { get; set; }
public bool UpdateManifold { get; set; }
public bool UpdateManifoldsFromFocusHintRect { get; set; }
public bool IgnoreOcclusivity { get; set; }
public XYFocusOptions()
{
Reset();
}
internal void Reset()
{
SearchRoot = null;
ExclusionRect = default;
FocusHintRectangle = null;
FocusedElementBounds = null;
NavigationStrategyOverride = null;
IgnoreClipping = true;
IgnoreCone = false;
KeyDeviceType = null;
ConsiderEngagement = true;
UpdateManifold = true;
UpdateManifoldsFromFocusHintRect = false;
IgnoreOcclusivity = false;
}
}

28
src/Avalonia.Base/Input/SwipeDirection.cs

@ -0,0 +1,28 @@
namespace Avalonia.Input
{
/// <summary>
/// Specifies the direction of a swipe gesture.
/// </summary>
public enum SwipeDirection
{
/// <summary>
/// The swipe moved to the left.
/// </summary>
Left,
/// <summary>
/// The swipe moved to the right.
/// </summary>
Right,
/// <summary>
/// The swipe moved upward.
/// </summary>
Up,
/// <summary>
/// The swipe moved downward.
/// </summary>
Down
}
}

73
src/Avalonia.Base/Input/SwipeGestureEventArgs.cs

@ -1,50 +1,81 @@
using System;
using System.Threading;
using Avalonia.Interactivity;
namespace Avalonia.Input
{
/// <summary>
/// Specifies the direction of a swipe gesture.
/// </summary>
public enum SwipeDirection { Left, Right, Up, Down }
/// <summary>
/// Provides data for the <see cref="InputElement.SwipeGestureEvent"/> routed event.
/// Provides data for swipe gesture events.
/// </summary>
public class SwipeGestureEventArgs : RoutedEventArgs
{
private static int _nextId = 1;
internal static int GetNextFreeId() => _nextId++;
/// <summary>
/// Initializes a new instance of the <see cref="SwipeGestureEventArgs"/> class.
/// </summary>
/// <param name="id">The unique identifier for this gesture.</param>
/// <param name="delta">The pixel delta since the last event.</param>
/// <param name="velocity">The current swipe velocity in pixels per second.</param>
public SwipeGestureEventArgs(int id, Vector delta, Vector velocity)
: base(InputElement.SwipeGestureEvent)
{
Id = id;
Delta = delta;
Velocity = velocity;
SwipeDirection = Math.Abs(delta.X) >= Math.Abs(delta.Y)
? (delta.X <= 0 ? SwipeDirection.Right : SwipeDirection.Left)
: (delta.Y <= 0 ? SwipeDirection.Down : SwipeDirection.Up);
}
/// <summary>
/// Gets the unique identifier for this swipe gesture instance.
/// Gets the unique identifier for this gesture sequence.
/// </summary>
public int Id { get; }
/// <summary>
/// Gets the direction of the swipe gesture.
/// Gets the pixel delta since the last event.
/// </summary>
public SwipeDirection SwipeDirection { get; }
public Vector Delta { get; }
/// <summary>
/// Gets the total translation vector of the swipe gesture.
/// Gets the current swipe velocity in pixels per second.
/// </summary>
public Vector Delta { get; }
public Vector Velocity { get; }
/// <summary>
/// Gets the position, relative to the target element, where the swipe started.
/// Gets the direction of the dominant swipe axis.
/// </summary>
public Point StartPoint { get; }
public SwipeDirection SwipeDirection { get; }
private static int s_nextId;
internal static int GetNextFreeId() => Interlocked.Increment(ref s_nextId);
}
/// <summary>
/// Provides data for the swipe gesture ended event.
/// </summary>
public class SwipeGestureEndedEventArgs : RoutedEventArgs
{
/// <summary>
/// Initializes a new instance of <see cref="SwipeGestureEventArgs"/>.
/// Initializes a new instance of the <see cref="SwipeGestureEndedEventArgs"/> class.
/// </summary>
public SwipeGestureEventArgs(int id, SwipeDirection direction, Vector delta, Point startPoint)
: base(InputElement.SwipeGestureEvent)
/// <param name="id">The unique identifier for this gesture.</param>
/// <param name="velocity">The swipe velocity at release in pixels per second.</param>
public SwipeGestureEndedEventArgs(int id, Vector velocity)
: base(InputElement.SwipeGestureEndedEvent)
{
Id = id;
SwipeDirection = direction;
Delta = delta;
StartPoint = startPoint;
Velocity = velocity;
}
/// <summary>
/// Gets the unique identifier for this gesture sequence.
/// </summary>
public int Id { get; }
/// <summary>
/// Gets the swipe velocity at release in pixels per second.
/// </summary>
public Vector Velocity { get; }
}
}

4
src/Avalonia.Base/Layout/Layoutable.cs

@ -113,7 +113,7 @@ namespace Avalonia.Layout
/// Defines the <see cref="Margin"/> property.
/// </summary>
public static readonly StyledProperty<Thickness> MarginProperty =
AvaloniaProperty.Register<Layoutable, Thickness>(nameof(Margin));
AvaloniaProperty.Register<Layoutable, Thickness>(nameof(Margin), validate: ValidateThickness);
/// <summary>
/// Defines the <see cref="HorizontalAlignment"/> property.
@ -161,6 +161,8 @@ namespace Avalonia.Layout
private static bool ValidateMinimumDimension(double value) => !double.IsPositiveInfinity(value) && ValidateMaximumDimension(value);
private static bool ValidateMaximumDimension(double value) => value >= 0;
private static bool ValidateThickness(Thickness value) => double.IsFinite(value.Left) && double.IsFinite(value.Top) && double.IsFinite(value.Right) && double.IsFinite(value.Bottom);
/// <summary>
/// Occurs when the element's effective viewport changes.
/// </summary>

4
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs

@ -50,7 +50,7 @@ partial class ServerCompositionVisual
private LtrbRect? _ownClipRect;
private bool _hasExtraDirtyRect;
private bool _needsToAddExtraDirtyRectToDirtyRegion;
private LtrbRect _extraDirtyRect;
public virtual LtrbRect? ComputeOwnContentBounds() => null;
@ -107,7 +107,7 @@ partial class ServerCompositionVisual
_isDirtyForRender |= dirtyForRender;
// If node itself is dirty for render, we don't need to keep track of extra dirty rects
_hasExtraDirtyRect = !dirtyForRender && (_hasExtraDirtyRect || additionalDirtyRegion);
_needsToAddExtraDirtyRectToDirtyRegion = !dirtyForRender && (_needsToAddExtraDirtyRectToDirtyRegion || additionalDirtyRegion);
}
public void RecomputeOwnProperties()

2
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs

@ -166,7 +166,7 @@ partial class ServerCompositionVisual
protected void AddExtraDirtyRect(LtrbRect rect)
{
_extraDirtyRect = _hasExtraDirtyRect ? _extraDirtyRect.Union(rect) : rect;
_extraDirtyRect = _delayPropagateHasExtraDirtyRects ? _extraDirtyRect.Union(rect) : rect;
_delayPropagateHasExtraDirtyRects = true;
EnqueueOwnPropertiesRecompute();
}

6
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs

@ -56,7 +56,7 @@ internal partial class ServerCompositionVisual
private bool NeedToPushBoundsAffectingProperties(ServerCompositionVisual node)
{
return (node._isDirtyForRenderInSubgraph || node._hasExtraDirtyRect || node._contentChanged);
return (node._isDirtyForRenderInSubgraph || node._needsToAddExtraDirtyRectToDirtyRegion || node._contentChanged);
}
public void PreSubgraph(ServerCompositionVisual node, out bool visitChildren)
@ -142,7 +142,7 @@ internal partial class ServerCompositionVisual
// specified before the tranform, i.e. in inner space, hence we have to pick them
// up before we pop the transform from the transform stack.
//
if (node._hasExtraDirtyRect)
if (node._needsToAddExtraDirtyRectToDirtyRegion)
{
AddToDirtyRegion(node._extraDirtyRect);
}
@ -169,7 +169,7 @@ internal partial class ServerCompositionVisual
node._isDirtyForRender = false;
node._isDirtyForRenderInSubgraph = false;
node._needsBoundingBoxUpdate = false;
node._hasExtraDirtyRect = false;
node._needsToAddExtraDirtyRectToDirtyRegion = false;
node._contentChanged = false;
}

2
src/Avalonia.Controls/Border.cs

@ -38,7 +38,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="BorderThickness"/> property.
/// </summary>
public static readonly StyledProperty<Thickness> BorderThicknessProperty =
AvaloniaProperty.Register<Border, Thickness>(nameof(BorderThickness));
AvaloniaProperty.Register<Border, Thickness>(nameof(BorderThickness), validate: MarginProperty.ValidateValue);
/// <summary>
/// Defines the <see cref="CornerRadius"/> property.

178
src/Avalonia.Controls/Carousel.cs

@ -1,11 +1,13 @@
using Avalonia.Animation;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
namespace Avalonia.Controls
{
/// <summary>
/// An items control that displays its items as pages that fill the control.
/// An items control that displays its items as pages and can reveal adjacent pages
/// using <see cref="ViewportFraction"/>.
/// </summary>
public class Carousel : SelectingItemsControl
{
@ -16,13 +18,36 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<Carousel, IPageTransition?>(nameof(PageTransition));
/// <summary>
/// The default value of <see cref="ItemsControl.ItemsPanelProperty"/> for
/// Defines the <see cref="IsSwipeEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsSwipeEnabledProperty =
AvaloniaProperty.Register<Carousel, bool>(nameof(IsSwipeEnabled), defaultValue: false);
/// <summary>
/// Defines the <see cref="ViewportFraction"/> property.
/// </summary>
public static readonly StyledProperty<double> ViewportFractionProperty =
AvaloniaProperty.Register<Carousel, double>(
nameof(ViewportFraction),
defaultValue: 1d,
coerce: (_, value) => double.IsFinite(value) && value > 0 ? value : 1d);
/// <summary>
/// Defines the <see cref="IsSwiping"/> property.
/// </summary>
public static readonly DirectProperty<Carousel, bool> IsSwipingProperty =
AvaloniaProperty.RegisterDirect<Carousel, bool>(nameof(IsSwiping),
o => o.IsSwiping);
/// <summary>
/// The default value of <see cref="ItemsControl.ItemsPanelProperty"/> for
/// <see cref="Carousel"/>.
/// </summary>
private static readonly FuncTemplate<Panel?> DefaultPanel =
new(() => new VirtualizingCarouselPanel());
private IScrollable? _scroller;
private bool _isSwiping;
/// <summary>
/// Initializes static members of the <see cref="Carousel"/> class.
@ -42,15 +67,51 @@ namespace Avalonia.Controls
set => SetValue(PageTransitionProperty, value);
}
/// <summary>
/// Gets or sets whether swipe gestures are enabled for navigating between pages.
/// When enabled, mouse pointer events are also accepted in addition to touch and pen.
/// </summary>
public bool IsSwipeEnabled
{
get => GetValue(IsSwipeEnabledProperty);
set => SetValue(IsSwipeEnabledProperty, value);
}
/// <summary>
/// Gets or sets the fraction of the viewport occupied by each page.
/// A value of 1 shows a single full page; values below 1 reveal adjacent pages.
/// </summary>
public double ViewportFraction
{
get => GetValue(ViewportFractionProperty);
set => SetValue(ViewportFractionProperty, value);
}
/// <summary>
/// Gets a value indicating whether a swipe gesture is currently in progress.
/// </summary>
public bool IsSwiping
{
get => _isSwiping;
internal set => SetAndRaise(IsSwipingProperty, ref _isSwiping, value);
}
/// <summary>
/// Moves to the next item in the carousel.
/// </summary>
public void Next()
{
if (ItemCount == 0)
return;
if (SelectedIndex < ItemCount - 1)
{
++SelectedIndex;
}
else if (WrapSelection)
{
SelectedIndex = 0;
}
}
/// <summary>
@ -58,18 +119,78 @@ namespace Avalonia.Controls
/// </summary>
public void Previous()
{
if (ItemCount == 0)
return;
if (SelectedIndex > 0)
{
--SelectedIndex;
}
else if (WrapSelection)
{
SelectedIndex = ItemCount - 1;
}
}
internal PageSlide.SlideAxis? GetTransitionAxis()
{
var transition = PageTransition;
if (transition is CompositePageTransition composite)
{
foreach (var t in composite.PageTransitions)
{
if (t is PageSlide slide)
return slide.Orientation;
}
return null;
}
return transition is PageSlide ps ? ps.Orientation : null;
}
internal PageSlide.SlideAxis GetLayoutAxis() => GetTransitionAxis() ?? PageSlide.SlideAxis.Horizontal;
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.Handled || ItemCount == 0)
return;
var axis = ViewportFraction != 1d ? GetLayoutAxis() : GetTransitionAxis();
var isVertical = axis == PageSlide.SlideAxis.Vertical;
var isHorizontal = axis == PageSlide.SlideAxis.Horizontal;
switch (e.Key)
{
case Key.Left when !isVertical:
case Key.Up when !isHorizontal:
Previous();
e.Handled = true;
break;
case Key.Right when !isVertical:
case Key.Down when !isHorizontal:
Next();
e.Handled = true;
break;
case Key.Home:
SelectedIndex = 0;
e.Handled = true;
break;
case Key.End:
SelectedIndex = ItemCount - 1;
e.Handled = true;
break;
}
}
protected override Size ArrangeOverride(Size finalSize)
{
var result = base.ArrangeOverride(finalSize);
if (_scroller is not null)
_scroller.Offset = new(SelectedIndex, 0);
SyncScrollOffset();
return result;
}
@ -84,11 +205,54 @@ namespace Avalonia.Controls
{
base.OnPropertyChanged(change);
if (change.Property == SelectedIndexProperty && _scroller is not null)
if (change.Property == SelectedIndexProperty)
{
SyncScrollOffset();
}
if (change.Property == IsSwipeEnabledProperty ||
change.Property == PageTransitionProperty ||
change.Property == ViewportFractionProperty ||
change.Property == WrapSelectionProperty)
{
if (ItemsPanelRoot is VirtualizingCarouselPanel panel)
{
if (change.Property == ViewportFractionProperty && !panel.IsManagingInteractionOffset)
panel.SyncSelectionOffset(SelectedIndex);
panel.RefreshGestureRecognizer();
panel.InvalidateMeasure();
}
SyncScrollOffset();
}
}
private void SyncScrollOffset()
{
if (ItemsPanelRoot is VirtualizingCarouselPanel panel)
{
var value = change.GetNewValue<int>();
_scroller.Offset = new(value, 0);
if (panel.IsManagingInteractionOffset)
return;
panel.SyncSelectionOffset(SelectedIndex);
if (ViewportFraction != 1d)
return;
}
if (_scroller is null)
return;
_scroller.Offset = CreateScrollOffset(SelectedIndex);
}
private Vector CreateScrollOffset(int index)
{
if (ViewportFraction != 1d && GetLayoutAxis() == PageSlide.SlideAxis.Vertical)
return new(0, index);
return new(index, 0);
}
}
}

4
src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs

@ -58,13 +58,13 @@ public class WindowDrawnDecorations : StyledElement
/// Defines the <see cref="DefaultFrameThickness"/> property.
/// </summary>
public static readonly StyledProperty<Thickness> DefaultFrameThicknessProperty =
AvaloniaProperty.Register<WindowDrawnDecorations, Thickness>(nameof(DefaultFrameThickness));
AvaloniaProperty.Register<WindowDrawnDecorations, Thickness>(nameof(DefaultFrameThickness), validate: Border.BorderThicknessProperty.ValidateValue);
/// <summary>
/// Defines the <see cref="DefaultShadowThickness"/> property.
/// </summary>
public static readonly StyledProperty<Thickness> DefaultShadowThicknessProperty =
AvaloniaProperty.Register<WindowDrawnDecorations, Thickness>(nameof(DefaultShadowThickness));
AvaloniaProperty.Register<WindowDrawnDecorations, Thickness>(nameof(DefaultShadowThickness), validate: Border.BorderThicknessProperty.ValidateValue);
/// <summary>
/// Defines the <see cref="TitleBarHeight"/> property.

2
src/Avalonia.Controls/Decorator.cs

@ -20,7 +20,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="Padding"/> property.
/// </summary>
public static readonly StyledProperty<Thickness> PaddingProperty =
AvaloniaProperty.Register<Decorator, Thickness>(nameof(Padding));
AvaloniaProperty.Register<Decorator, Thickness>(nameof(Padding), validate: MarginProperty.ValidateValue);
/// <summary>
/// Initializes static members of the <see cref="Decorator"/> class.

7
src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs

@ -4,6 +4,7 @@ using Avalonia.Automation.Peers;
using Avalonia.Controls.Automation;
using Avalonia.Controls.Automation.Peers;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Platform;
@ -54,6 +55,12 @@ namespace Avalonia.Controls.Embedding
protected override Type StyleKeyOverride => typeof(EmbeddableControlRoot);
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
EnableVisualLayerManagerLayers();
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new EmbeddableControlRootAutomationPeer(this);

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

@ -211,6 +211,7 @@ namespace Avalonia.Controls
private Border? _topBar;
private ToggleButton? _paneButton;
private Border? _backdrop;
private Point _swipeStartPoint;
private IDisposable? _navBarVisibleSub;
private const double EdgeGestureWidth = 20;
@ -292,6 +293,8 @@ namespace Avalonia.Controls
public DrawerPage()
{
GestureRecognizers.Add(_swipeRecognizer);
AddHandler(PointerPressedEvent, OnSwipePointerPressed, handledEventsToo: true);
UpdateSwipeRecognizerAxes();
}
/// <summary>
@ -617,6 +620,7 @@ namespace Avalonia.Controls
}
else if (change.Property == DrawerPlacementProperty)
{
UpdateSwipeRecognizerAxes();
UpdatePanePlacement();
UpdateContentSafeAreaPadding();
}
@ -664,6 +668,12 @@ namespace Avalonia.Controls
nav.SetDrawerPage(null);
}
private void UpdateSwipeRecognizerAxes()
{
_swipeRecognizer.CanVerticallySwipe = IsVerticalPlacement;
_swipeRecognizer.CanHorizontallySwipe = !IsVerticalPlacement;
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
@ -675,6 +685,11 @@ namespace Avalonia.Controls
}
}
private void OnSwipePointerPressed(object? sender, PointerPressedEventArgs e)
{
_swipeStartPoint = e.GetPosition(this);
}
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
@ -714,8 +729,8 @@ namespace Avalonia.Controls
: EdgeGestureWidth;
bool inEdge = DrawerPlacement == DrawerPlacement.Bottom
? e.StartPoint.Y >= Bounds.Height - openGestureEdge
: e.StartPoint.Y <= openGestureEdge;
? _swipeStartPoint.Y >= Bounds.Height - openGestureEdge
: _swipeStartPoint.Y <= openGestureEdge;
if (towardPane && inEdge)
{
@ -746,8 +761,8 @@ namespace Avalonia.Controls
: EdgeGestureWidth;
bool inEdge = IsPaneOnRight
? e.StartPoint.X >= Bounds.Width - openGestureEdge
: e.StartPoint.X <= openGestureEdge;
? _swipeStartPoint.X >= Bounds.Width - openGestureEdge
: _swipeStartPoint.X <= openGestureEdge;
if (towardPane && inEdge)
{

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

@ -68,6 +68,8 @@ namespace Avalonia.Controls
private bool _isBackButtonEffectivelyEnabled;
private DrawerPage? _drawerPage;
private IPageTransition? _overrideTransition;
private Point _swipeStartPoint;
private int _lastSwipeGestureId;
private bool _hasOverrideTransition;
private readonly HashSet<object> _pageSet = new(ReferenceEqualityComparer.Instance);
@ -257,7 +259,12 @@ namespace Avalonia.Controls
public NavigationPage()
{
SetCurrentValue(PagesProperty, new Stack<Page>());
GestureRecognizers.Add(new SwipeGestureRecognizer { EdgeSize = EdgeGestureWidth });
GestureRecognizers.Add(new SwipeGestureRecognizer
{
CanHorizontallySwipe = true,
CanVerticallySwipe = false
});
AddHandler(PointerPressedEvent, OnSwipePointerPressed, handledEventsToo: true);
}
/// <summary>
@ -1871,18 +1878,31 @@ namespace Avalonia.Controls
private void OnSwipeGesture(object? sender, SwipeGestureEventArgs e)
{
if (!IsGestureEnabled || StackDepth <= 1 || _isNavigating || _modalStack.Count > 0)
if (!IsGestureEnabled || StackDepth <= 1 || _isNavigating || _modalStack.Count > 0 || e.Id == _lastSwipeGestureId)
return;
bool inEdge = IsRtl
? _swipeStartPoint.X >= Bounds.Width - EdgeGestureWidth
: _swipeStartPoint.X <= EdgeGestureWidth;
if (!inEdge)
return;
bool shouldPop = IsRtl
? e.SwipeDirection == SwipeDirection.Left
: e.SwipeDirection == SwipeDirection.Right;
if (shouldPop)
{
e.Handled = true;
_lastSwipeGestureId = e.Id;
_ = PopAsync();
}
}
private void OnSwipePointerPressed(object? sender, PointerPressedEventArgs e)
{
_swipeStartPoint = e.GetPosition(this);
}
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);

2
src/Avalonia.Controls/Page/Page.cs

@ -17,7 +17,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="SafeAreaPadding"/> property.
/// </summary>
public static readonly StyledProperty<Thickness> SafeAreaPaddingProperty =
AvaloniaProperty.Register<Page, Thickness>(nameof(SafeAreaPadding));
AvaloniaProperty.Register<Page, Thickness>(nameof(SafeAreaPadding), validate: PaddingProperty.ValidateValue);
/// <summary>
/// Defines the <see cref="Header"/> property.

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

@ -26,6 +26,7 @@ namespace Avalonia.Controls
private TabControl? _tabControl;
private readonly Dictionary<TabItem, Page> _containerPageMap = new();
private readonly Dictionary<Page, TabItem> _pageContainerMap = new();
private int _lastSwipeGestureId;
private readonly SwipeGestureRecognizer _swipeRecognizer = new SwipeGestureRecognizer
{
IsEnabled = false
@ -92,6 +93,7 @@ namespace Avalonia.Controls
Focusable = true;
GestureRecognizers.Add(_swipeRecognizer);
AddHandler(InputElement.SwipeGestureEvent, OnSwipeGesture);
UpdateSwipeRecognizerAxes();
}
/// <summary>
@ -194,7 +196,10 @@ namespace Avalonia.Controls
base.OnPropertyChanged(change);
if (change.Property == TabPlacementProperty)
{
ApplyTabPlacement();
UpdateSwipeRecognizerAxes();
}
else if (change.Property == PageTransitionProperty && _tabControl != null)
_tabControl.PageTransition = change.GetNewValue<IPageTransition?>();
else if (change.Property == IndicatorTemplateProperty)
@ -227,6 +232,14 @@ namespace Avalonia.Controls
};
}
private void UpdateSwipeRecognizerAxes()
{
var placement = ResolveTabPlacement();
var isHorizontal = placement == TabPlacement.Top || placement == TabPlacement.Bottom;
_swipeRecognizer.CanHorizontallySwipe = isHorizontal;
_swipeRecognizer.CanVerticallySwipe = !isHorizontal;
}
private void ApplyIndicatorTemplate()
{
if (_tabControl == null)
@ -500,7 +513,8 @@ namespace Avalonia.Controls
private void OnSwipeGesture(object? sender, SwipeGestureEventArgs e)
{
if (!IsGestureEnabled || _tabControl == null) return;
if (!IsGestureEnabled || _tabControl == null || e.Id == _lastSwipeGestureId)
return;
var placement = ResolveTabPlacement();
bool isHorizontal = placement == TabPlacement.Top || placement == TabPlacement.Bottom;
@ -524,6 +538,7 @@ namespace Avalonia.Controls
{
_tabControl.SelectedIndex = next;
e.Handled = true;
_lastSwipeGestureId = e.Id;
}
}

4
src/Avalonia.Controls/PresentationSource/PresentationSource.cs

@ -61,7 +61,7 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi
field?.SetPresentationSourceForRootVisual(this);
Renderer.CompositionTarget.Root = field?.CompositionVisual;
FocusManager.SetContentRoot(value as IInputElement);
FocusManager.ContentRoot = value;
}
}
@ -152,4 +152,4 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi
}
return null;
}
}
}

2
src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs

@ -19,7 +19,7 @@ namespace Avalonia.Controls.Presenters
public PanelContainerGenerator(ItemsPresenter presenter)
{
Debug.Assert(presenter.ItemsControl is not null);
Debug.Assert(presenter.Panel is not null or VirtualizingPanel);
Debug.Assert(presenter.Panel is not (null or VirtualizingPanel));
_presenter = presenter;
_presenter.ItemsControl.ItemsView.PostCollectionChanged += OnItemsChanged;

28
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Input.TextInput;
using Avalonia.Media;
using Avalonia.Reactive;
@ -71,7 +72,32 @@ namespace Avalonia.Controls.Primitives
public static AdornerLayer? GetAdornerLayer(Visual visual)
{
return visual.FindAncestorOfType<VisualLayerManager>()?.AdornerLayer;
// Check if the visual is inside an OverlayLayer with a dedicated AdornerLayer
foreach (var ancestor in visual.GetVisualAncestors())
{
if (GetDirectAdornerLayer(ancestor) is { } adornerLayer)
return adornerLayer;
}
if (TopLevel.GetTopLevel(visual) is { } topLevel)
{
foreach (var descendant in topLevel.GetVisualDescendants())
{
if (GetDirectAdornerLayer(descendant) is { } adornerLayer)
return adornerLayer;
}
}
return null;
static AdornerLayer? GetDirectAdornerLayer(Visual visual)
{
if (visual is OverlayLayer { AdornerLayer: { } adornerLayer })
return adornerLayer;
if (visual is VisualLayerManager vlm)
return vlm.AdornerLayer;
return null;
}
}
public static bool GetIsClipEnabled(Visual adorner)

5
src/Avalonia.Controls/Primitives/OverlayLayer.cs

@ -13,6 +13,11 @@ namespace Avalonia.Controls.Primitives
public Size AvailableSize { get; private set; }
/// <summary>
/// Gets the dedicated adorner layer for this overlay layer.
/// </summary>
internal AdornerLayer? AdornerLayer { get; set; }
internal OverlayLayer()
{
}

2
src/Avalonia.Controls/Primitives/SelectionHandleType.cs

@ -3,7 +3,7 @@
/// <summary>
/// Represents which part of the selection the TextSelectionHandle controls.
/// </summary>
public enum SelectionHandleType
internal enum SelectionHandleType
{
/// <summary>
/// The Handle controls the caret position.

37
src/Avalonia.Controls/Primitives/TextSearch.cs

@ -1,6 +1,5 @@
using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.Interactivity;
namespace Avalonia.Controls.Primitives
{
@ -15,47 +14,47 @@ namespace Avalonia.Controls.Primitives
/// This property is usually applied to an item container directly.
/// </summary>
public static readonly AttachedProperty<string?> TextProperty
= AvaloniaProperty.RegisterAttached<Interactive, string?>("Text", typeof(TextSearch));
= AvaloniaProperty.RegisterAttached<AvaloniaObject, string?>("Text", typeof(TextSearch));
/// <summary>
/// Defines the TextBinding attached property.
/// The binding will be applied to each item during text search in <see cref="SelectingItemsControl"/> (such as <see cref="ComboBox"/>).
/// </summary>
public static readonly AttachedProperty<BindingBase?> TextBindingProperty
= AvaloniaProperty.RegisterAttached<Interactive, BindingBase?>("TextBinding", typeof(TextSearch));
= AvaloniaProperty.RegisterAttached<AvaloniaObject, BindingBase?>("TextBinding", typeof(TextSearch));
/// <summary>
/// Sets the value of the <see cref="TextProperty"/> attached property to a given <see cref="Control"/>.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="element">The control.</param>
/// <param name="text">The search text to set.</param>
public static void SetText(Interactive control, string? text)
=> control.SetValue(TextProperty, text);
public static void SetText(AvaloniaObject element, string? text)
=> element.SetValue(TextProperty, text);
/// <summary>
/// Gets the value of the <see cref="TextProperty"/> attached property from a given <see cref="Control"/>.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="element">The control.</param>
/// <returns>The search text.</returns>
public static string? GetText(Interactive control)
=> control.GetValue(TextProperty);
public static string? GetText(AvaloniaObject element)
=> element.GetValue(TextProperty);
/// <summary>
/// Sets the value of the <see cref="TextBindingProperty"/> attached property to a given <see cref="Interactive"/>.
/// Sets the value of the <see cref="TextBindingProperty"/> attached property to a given element.
/// </summary>
/// <param name="interactive">The interactive element.</param>
/// <param name="element">The element.</param>
/// <param name="value">The search text binding to set.</param>
public static void SetTextBinding(Interactive interactive, BindingBase? value)
=> interactive.SetValue(TextBindingProperty, value);
public static void SetTextBinding(AvaloniaObject element, BindingBase? value)
=> element.SetValue(TextBindingProperty, value);
/// <summary>
/// Gets the value of the <see cref="TextBindingProperty"/> attached property from a given <see cref="Interactive"/>.
/// Gets the value of the <see cref="TextBindingProperty"/> attached property from a given element.
/// </summary>
/// <param name="interactive">The interactive element.</param>
/// <param name="element">The element.</param>
/// <returns>The search text binding.</returns>
[AssignBinding]
public static BindingBase? GetTextBinding(Interactive interactive)
=> interactive.GetValue(TextBindingProperty);
public static BindingBase? GetTextBinding(AvaloniaObject element)
=> element.GetValue(TextBindingProperty);
/// <summary>
/// <para>Gets the effective text of a given item.</para>
@ -80,9 +79,9 @@ namespace Avalonia.Controls.Primitives
string? text;
if (item is Interactive interactive)
if (item is AvaloniaObject obj)
{
text = interactive.GetValue(TextProperty);
text = obj.GetValue(TextProperty);
if (!string.IsNullOrEmpty(text))
return text;
}

58
src/Avalonia.Controls/Primitives/VisualLayerManager.cs

@ -3,6 +3,9 @@ using Avalonia.LogicalTree;
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// A control that manages multiple layers such as adorners, overlays, text selectors, and popups.
/// </summary>
public sealed class VisualLayerManager : Decorator
{
private const int AdornerZIndex = int.MaxValue - 100;
@ -13,13 +16,37 @@ namespace Avalonia.Controls.Primitives
private ILogicalRoot? _logicalRoot;
private readonly List<Control> _layers = new();
public bool IsPopup { get; set; }
internal AdornerLayer AdornerLayer
private OverlayLayer? _overlayLayer;
/// <summary>
/// Gets or sets a value indicating whether an <see cref="Avalonia.Controls.Primitives.AdornerLayer"/> is
/// created for this <see cref="VisualLayerManager"/>. When enabled, the adorner layer is added to the
/// visual tree, providing a dedicated layer for rendering adorners.
/// </summary>
public bool EnableAdornerLayer { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether an <see cref="Avalonia.Controls.Primitives.OverlayLayer"/> is
/// created for this <see cref="VisualLayerManager"/>. When enabled, the overlay layer is added to the
/// visual tree, providing a dedicated layer for rendering overlay visuals.
/// </summary>
public bool EnableOverlayLayer { get; set; }
internal bool EnablePopupOverlayLayer { get; set; }
/// <summary>
/// Gets or sets a value indicating whether a <see cref="Avalonia.Controls.Primitives.TextSelectorLayer"/> is
/// created for this <see cref="VisualLayerManager"/>. When enabled, the overlay layer is added to the
/// visual tree, providing a dedicated layer for rendering text selection handles.
/// </summary>
public bool EnableTextSelectorLayer { get; set; }
internal AdornerLayer? AdornerLayer
{
get
{
if (!EnableAdornerLayer)
return null;
var rv = FindLayer<AdornerLayer>();
if (rv == null)
AddLayer(rv = new AdornerLayer(), AdornerZIndex);
@ -31,7 +58,7 @@ namespace Avalonia.Controls.Primitives
{
get
{
if (IsPopup)
if (!EnablePopupOverlayLayer)
return null;
var rv = FindLayer<PopupOverlayLayer>();
if (rv == null)
@ -44,12 +71,21 @@ namespace Avalonia.Controls.Primitives
{
get
{
if (IsPopup)
if (!EnableOverlayLayer)
return null;
var rv = FindLayer<OverlayLayer>();
if (rv == null)
AddLayer(rv = new OverlayLayer(), OverlayZIndex);
return rv;
if (_overlayLayer == null)
{
_overlayLayer = new OverlayLayer();
var adorner = new AdornerLayer();
_overlayLayer.AdornerLayer = adorner;
var panel = new Panel();
panel.Children.Add(_overlayLayer);
panel.Children.Add(adorner);
AddLayer(panel, OverlayZIndex);
}
return _overlayLayer;
}
}
@ -57,7 +93,7 @@ namespace Avalonia.Controls.Primitives
{
get
{
if (IsPopup)
if (!EnableTextSelectorLayer)
return null;
var rv = FindLayer<TextSelectorLayer>();
if (rv == null)

16
src/Avalonia.Controls/TopLevel.cs

@ -38,6 +38,7 @@ namespace Avalonia.Controls
/// tracking the widget's <see cref="ClientSize"/>.
/// </remarks>
[TemplatePart("PART_TransparencyFallback", typeof(Border))]
[TemplatePart("PART_VisualLayerManager", typeof(VisualLayerManager))]
public abstract class TopLevel : ContentControl,
ICloseable,
IStyleHost,
@ -125,6 +126,7 @@ namespace Avalonia.Controls
private Size? _frameSize;
private WindowTransparencyLevel _actualTransparencyLevel;
private Border? _transparencyFallbackBorder;
private VisualLayerManager? _visualLayerManager;
private TargetWeakEventSubscriber<TopLevel, ResourcesChangedEventArgs>? _resourcesChangesSubscriber;
private IStorageProvider? _storageProvider;
private Screens? _screens;
@ -133,6 +135,18 @@ namespace Avalonia.Controls
internal TopLevelHost TopLevelHost => _topLevelHost;
internal new PresentationSource PresentationSource => _source;
internal IInputRoot InputRoot => _source;
private protected VisualLayerManager? VisualLayerManager => _visualLayerManager;
private protected void EnableVisualLayerManagerLayers()
{
if (_visualLayerManager is { } vlm)
{
vlm.EnableOverlayLayer = true;
vlm.EnablePopupOverlayLayer = true;
vlm.EnableTextSelectorLayer = true;
}
}
/// <summary>
/// Initializes static members of the <see cref="TopLevel"/> class.
@ -723,6 +737,8 @@ namespace Avalonia.Controls
{
base.OnApplyTemplate(e);
_visualLayerManager = e.NameScope.Find<VisualLayerManager>("PART_VisualLayerManager");
if (PlatformImpl is null)
return;

1282
src/Avalonia.Controls/VirtualizingCarouselPanel.cs

File diff suppressed because it is too large

19
src/Avalonia.Controls/Window.cs

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Chrome;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
@ -248,7 +249,7 @@ namespace Avalonia.Controls
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, WindowResizeReason.Application));
CreatePlatformImplBinding(TitleProperty, title => PlatformImpl!.SetTitle(title));
CreatePlatformImplBinding(IconProperty, icon => PlatformImpl!.SetIcon((icon ?? s_defaultIcon.Value)?.PlatformImpl));
CreatePlatformImplBinding(IconProperty, SetEffectiveIcon);
CreatePlatformImplBinding(CanResizeProperty, canResize => PlatformImpl!.CanResize(canResize));
CreatePlatformImplBinding(CanMinimizeProperty, canMinimize => PlatformImpl!.SetCanMinimize(canMinimize));
CreatePlatformImplBinding(CanMaximizeProperty, canMaximize => PlatformImpl!.SetCanMaximize(canMaximize));
@ -795,6 +796,12 @@ namespace Avalonia.Controls
ShowCore<object>(null, false);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
EnableVisualLayerManagerLayers();
}
protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!IgnoreVisibilityChanges)
@ -892,6 +899,8 @@ namespace Avalonia.Controls
_shown = true;
IsVisible = true;
SetEffectiveIcon(Icon);
// If window position was not set before then platform may provide incorrect scaling at this time,
// but we need it for proper calculation of position and in some cases size (size to content)
SetExpectedScaling(owner);
@ -1378,7 +1387,7 @@ namespace Avalonia.Controls
private static WindowIcon? LoadDefaultIcon()
{
// Use AvaloniaLocator instead of static AssetLoader, so it won't fail on Unit Tests without any asset loader.
// Use AvaloniaLocator instead of static AssetLoader, so it won't fail on Unit Tests without any asset loader.
if (AvaloniaLocator.Current.GetService<IAssetLoader>() is { } assetLoader
&& Assembly.GetEntryAssembly()?.GetName()?.Name is { } assemblyName
&& Uri.TryCreate($"avares://{assemblyName}/!__AvaloniaDefaultWindowIcon", UriKind.Absolute, out var path)
@ -1390,6 +1399,12 @@ namespace Avalonia.Controls
return null;
}
private void SetEffectiveIcon(WindowIcon? icon)
{
icon ??= _shown ? s_defaultIcon.Value : null;
PlatformImpl?.SetIcon(icon?.PlatformImpl);
}
private static bool CoerceCanMaximize(AvaloniaObject target, bool value)
=> value && target is not Window { CanResize: false };
}

2
src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml

@ -12,7 +12,7 @@
<Panel>
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<Border Background="{TemplateBinding Background}">
<VisualLayerManager>
<VisualLayerManager Name="PART_VisualLayerManager">
<ContentPresenter Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"

2
src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml

@ -11,7 +11,7 @@
<Setter Property="Template">
<ControlTemplate>
<LayoutTransformControl LayoutTransform="{TemplateBinding Transform}">
<VisualLayerManager IsPopup="True">
<VisualLayerManager Name="PART_VisualLayerManager">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"

2
src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml

@ -15,7 +15,7 @@
<LayoutTransformControl LayoutTransform="{TemplateBinding Transform}">
<Panel>
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<VisualLayerManager IsPopup="True">
<VisualLayerManager Name="PART_VisualLayerManager">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"

2
src/Avalonia.Themes.Fluent/Controls/Window.xaml

@ -14,7 +14,7 @@
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<Border Background="{TemplateBinding Background}" IsHitTestVisible="False" />
<Panel Background="Transparent" Margin="{TemplateBinding WindowDecorationMargin}" />
<VisualLayerManager>
<VisualLayerManager Name="PART_VisualLayerManager">
<ContentPresenter Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"

2
src/Avalonia.Themes.Simple/Controls/EmbeddableControlRoot.xaml

@ -13,7 +13,7 @@
<Border Name="PART_TransparencyFallback"
IsHitTestVisible="False" />
<Border Background="{TemplateBinding Background}">
<VisualLayerManager>
<VisualLayerManager Name="PART_VisualLayerManager">
<ContentPresenter Name="PART_ContentPresenter"
Margin="{TemplateBinding Padding}"
Content="{TemplateBinding Content}"

2
src/Avalonia.Themes.Simple/Controls/OverlayPopupHost.xaml

@ -13,7 +13,7 @@
<ControlTemplate>
<!-- Do not forget to update Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent test -->
<LayoutTransformControl LayoutTransform="{TemplateBinding Transform}">
<VisualLayerManager IsPopup="True">
<VisualLayerManager Name="PART_VisualLayerManager">
<ContentPresenter Name="PART_ContentPresenter"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"

2
src/Avalonia.Themes.Simple/Controls/PopupRoot.xaml

@ -17,7 +17,7 @@
<Panel>
<Border Name="PART_TransparencyFallback"
IsHitTestVisible="False" />
<VisualLayerManager IsPopup="True">
<VisualLayerManager Name="PART_VisualLayerManager">
<ContentPresenter Name="PART_ContentPresenter"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"

2
src/Avalonia.Themes.Simple/Controls/Window.xaml

@ -17,7 +17,7 @@
IsHitTestVisible="False" />
<Panel Margin="{TemplateBinding WindowDecorationMargin}"
Background="Transparent" />
<VisualLayerManager>
<VisualLayerManager Name="PART_VisualLayerManager">
<ContentPresenter Name="PART_ContentPresenter"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"

4
src/Avalonia.X11/ActivityTrackingHelper.cs

@ -38,7 +38,7 @@ internal class WindowActivationTrackingHelper : IDisposable
if (Mode == X11Globals.WindowActivationTrackingMode._NET_WM_STATE_FOCUSED)
OnNetWmStateChanged(XLib.XGetWindowPropertyAsIntPtrArray(_platform.Display, _window.Handle.Handle,
_platform.Info.Atoms._NET_WM_STATE, _platform.Info.Atoms.XA_ATOM) ?? []);
_platform.Info.Atoms._NET_WM_STATE, _platform.Info.Atoms.ATOM) ?? []);
}
private void OnWindowActivationTrackingModeChanged() =>
@ -70,7 +70,7 @@ internal class WindowActivationTrackingHelper : IDisposable
{
var value = XLib.XGetWindowPropertyAsIntPtrArray(_platform.Display, _platform.Info.RootWindow,
_platform.Info.Atoms._NET_ACTIVE_WINDOW,
(IntPtr)_platform.Info.Atoms.XA_WINDOW);
(IntPtr)_platform.Info.Atoms.WINDOW);
if (value == null || value.Length == 0)
SetActive(false);
else

6
src/Avalonia.X11/Clipboard/ClipboardDataFormatHelper.cs

@ -19,7 +19,7 @@ internal static class ClipboardDataFormatHelper
if (formatAtom == atoms.UTF16_STRING ||
formatAtom == atoms.UTF8_STRING ||
formatAtom == atoms.XA_STRING ||
formatAtom == atoms.STRING ||
formatAtom == atoms.OEMTEXT)
{
return DataFormat.Text;
@ -92,7 +92,7 @@ internal static class ClipboardDataFormatHelper
private static IntPtr GetPreferredStringFormatAtom(IntPtr[] textFormatAtoms, X11Atoms atoms)
{
ReadOnlySpan<IntPtr> preferredFormats = [atoms.UTF16_STRING, atoms.UTF8_STRING, atoms.XA_STRING];
ReadOnlySpan<IntPtr> preferredFormats = [atoms.UTF16_STRING, atoms.UTF8_STRING, atoms.STRING];
foreach (var preferredFormat in preferredFormats)
{
@ -111,7 +111,7 @@ internal static class ClipboardDataFormatHelper
if (formatAtom == atoms.UTF8_STRING)
return Encoding.UTF8;
if (formatAtom == atoms.XA_STRING || formatAtom == atoms.OEMTEXT)
if (formatAtom == atoms.STRING || formatAtom == atoms.OEMTEXT)
return Encoding.ASCII;
return null;

3
src/Avalonia.X11/Clipboard/ClipboardReadSession.cs

@ -127,6 +127,7 @@ class ClipboardReadSession : IDisposable
Append(part);
}
ms.Position = 0L;
return new(null, ms, actualTypeAtom);
}
@ -150,4 +151,4 @@ class ClipboardReadSession : IDisposable
}
}
}
}

6
src/Avalonia.X11/Clipboard/X11Clipboard.cs

@ -32,7 +32,7 @@ namespace Avalonia.X11.Clipboard
_avaloniaSaveTargetsAtom = XInternAtom(_x11.Display, "AVALONIA_SAVE_TARGETS_PROPERTY_ATOM", false);
_textAtoms = new[]
{
_x11.Atoms.XA_STRING,
_x11.Atoms.STRING,
_x11.Atoms.OEMTEXT,
_x11.Atoms.UTF8_STRING,
_x11.Atoms.UTF16_STRING
@ -99,7 +99,7 @@ namespace Avalonia.X11.Clipboard
{
var atoms = ConvertDataTransfer(_storedDataTransfer);
XChangeProperty(_x11.Display, window, property,
_x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms, atoms.Length);
_x11.Atoms.ATOM, 32, PropertyMode.Replace, atoms, atoms.Length);
return property;
}
else if (target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero)
@ -287,7 +287,7 @@ namespace Avalonia.X11.Clipboard
_storeAtomTcs = new TaskCompletionSource<bool>();
var atoms = ConvertDataTransfer(dataTransfer);
XChangeProperty(_x11.Display, _handle, _avaloniaSaveTargetsAtom, _x11.Atoms.XA_ATOM, 32,
XChangeProperty(_x11.Display, _handle, _avaloniaSaveTargetsAtom, _x11.Atoms.ATOM, 32,
PropertyMode.Replace, atoms, atoms.Length);
XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD_MANAGER, _x11.Atoms.SAVE_TARGETS,
_avaloniaSaveTargetsAtom, _handle, IntPtr.Zero);

6
src/Avalonia.X11/Screens/X11Screen.Providers.cs

@ -56,9 +56,9 @@ internal partial class X11Screens
if (!hasEDID)
return null;
XRRGetOutputProperty(x11.Display, rrOutput, x11.Atoms.EDID, 0, EDIDStructureLength, false, false,
x11.Atoms.AnyPropertyType, out IntPtr actualType, out int actualFormat, out int bytesAfter, out _,
AnyPropertyType, out IntPtr actualType, out int actualFormat, out int bytesAfter, out _,
out IntPtr prop);
if (actualType != x11.Atoms.XA_INTEGER)
if (actualType != x11.Atoms.INTEGER)
return null;
if (actualFormat != 8) // Expecting an byte array
return null;
@ -89,7 +89,7 @@ internal partial class X11Screens
IntPtr.Zero,
new IntPtr(128),
false,
x11.Atoms.AnyPropertyType,
AnyPropertyType,
out var type,
out var format,
out var count,

2
src/Avalonia.X11/TransparencyHelper.cs

@ -89,7 +89,7 @@ namespace Avalonia.X11
{
IntPtr value = IntPtr.Zero;
XLib.XChangeProperty(_x11.Display, _window, _x11.Atoms._KDE_NET_WM_BLUR_BEHIND_REGION,
_x11.Atoms.XA_CARDINAL, 32, PropertyMode.Replace, ref value, 1);
_x11.Atoms.CARDINAL, 32, PropertyMode.Replace, ref value, 1);
_blurAtomsAreSet = true;
}
}

147
src/Avalonia.X11/X11Atoms.cs

@ -44,75 +44,74 @@ namespace Avalonia.X11
private readonly IntPtr _display;
// Our atoms
public IntPtr AnyPropertyType = (IntPtr)0;
public IntPtr XA_PRIMARY = (IntPtr)1;
public IntPtr XA_SECONDARY = (IntPtr)2;
public IntPtr XA_ARC = (IntPtr)3;
public IntPtr XA_ATOM = (IntPtr)4;
public IntPtr XA_BITMAP = (IntPtr)5;
public IntPtr XA_CARDINAL = (IntPtr)6;
public IntPtr XA_COLORMAP = (IntPtr)7;
public IntPtr XA_CURSOR = (IntPtr)8;
public IntPtr XA_CUT_BUFFER0 = (IntPtr)9;
public IntPtr XA_CUT_BUFFER1 = (IntPtr)10;
public IntPtr XA_CUT_BUFFER2 = (IntPtr)11;
public IntPtr XA_CUT_BUFFER3 = (IntPtr)12;
public IntPtr XA_CUT_BUFFER4 = (IntPtr)13;
public IntPtr XA_CUT_BUFFER5 = (IntPtr)14;
public IntPtr XA_CUT_BUFFER6 = (IntPtr)15;
public IntPtr XA_CUT_BUFFER7 = (IntPtr)16;
public IntPtr XA_DRAWABLE = (IntPtr)17;
public IntPtr XA_FONT = (IntPtr)18;
public IntPtr XA_INTEGER = (IntPtr)19;
public IntPtr XA_PIXMAP = (IntPtr)20;
public IntPtr XA_POINT = (IntPtr)21;
public IntPtr XA_RECTANGLE = (IntPtr)22;
public IntPtr XA_RESOURCE_MANAGER = (IntPtr)23;
public IntPtr XA_RGB_COLOR_MAP = (IntPtr)24;
public IntPtr XA_RGB_BEST_MAP = (IntPtr)25;
public IntPtr XA_RGB_BLUE_MAP = (IntPtr)26;
public IntPtr XA_RGB_DEFAULT_MAP = (IntPtr)27;
public IntPtr XA_RGB_GRAY_MAP = (IntPtr)28;
public IntPtr XA_RGB_GREEN_MAP = (IntPtr)29;
public IntPtr XA_RGB_RED_MAP = (IntPtr)30;
public IntPtr XA_STRING = (IntPtr)31;
public IntPtr XA_VISUALID = (IntPtr)32;
public IntPtr XA_WINDOW = (IntPtr)33;
public IntPtr XA_WM_COMMAND = (IntPtr)34;
public IntPtr XA_WM_HINTS = (IntPtr)35;
public IntPtr XA_WM_CLIENT_MACHINE = (IntPtr)36;
public IntPtr XA_WM_ICON_NAME = (IntPtr)37;
public IntPtr XA_WM_ICON_SIZE = (IntPtr)38;
public IntPtr XA_WM_NAME = (IntPtr)39;
public IntPtr XA_WM_NORMAL_HINTS = (IntPtr)40;
public IntPtr XA_WM_SIZE_HINTS = (IntPtr)41;
public IntPtr XA_WM_ZOOM_HINTS = (IntPtr)42;
public IntPtr XA_MIN_SPACE = (IntPtr)43;
public IntPtr XA_NORM_SPACE = (IntPtr)44;
public IntPtr XA_MAX_SPACE = (IntPtr)45;
public IntPtr XA_END_SPACE = (IntPtr)46;
public IntPtr XA_SUPERSCRIPT_X = (IntPtr)47;
public IntPtr XA_SUPERSCRIPT_Y = (IntPtr)48;
public IntPtr XA_SUBSCRIPT_X = (IntPtr)49;
public IntPtr XA_SUBSCRIPT_Y = (IntPtr)50;
public IntPtr XA_UNDERLINE_POSITION = (IntPtr)51;
public IntPtr XA_UNDERLINE_THICKNESS = (IntPtr)52;
public IntPtr XA_STRIKEOUT_ASCENT = (IntPtr)53;
public IntPtr XA_STRIKEOUT_DESCENT = (IntPtr)54;
public IntPtr XA_ITALIC_ANGLE = (IntPtr)55;
public IntPtr XA_X_HEIGHT = (IntPtr)56;
public IntPtr XA_QUAD_WIDTH = (IntPtr)57;
public IntPtr XA_WEIGHT = (IntPtr)58;
public IntPtr XA_POINT_SIZE = (IntPtr)59;
public IntPtr XA_RESOLUTION = (IntPtr)60;
public IntPtr XA_COPYRIGHT = (IntPtr)61;
public IntPtr XA_NOTICE = (IntPtr)62;
public IntPtr XA_FONT_NAME = (IntPtr)63;
public IntPtr XA_FAMILY_NAME = (IntPtr)64;
public IntPtr XA_FULL_NAME = (IntPtr)65;
public IntPtr XA_CAP_HEIGHT = (IntPtr)66;
public IntPtr XA_WM_CLASS = (IntPtr)67;
public IntPtr XA_WM_TRANSIENT_FOR = (IntPtr)68;
public readonly IntPtr PRIMARY = 1;
public readonly IntPtr SECONDARY = 2;
public readonly IntPtr ARC = 3;
public readonly IntPtr ATOM = 4;
public readonly IntPtr BITMAP = 5;
public readonly IntPtr CARDINAL = 6;
public readonly IntPtr COLORMAP = 7;
public readonly IntPtr CURSOR = 8;
public readonly IntPtr CUT_BUFFER0 = 9;
public readonly IntPtr CUT_BUFFER1 = 10;
public readonly IntPtr CUT_BUFFER2 = 11;
public readonly IntPtr CUT_BUFFER3 = 12;
public readonly IntPtr CUT_BUFFER4 = 13;
public readonly IntPtr CUT_BUFFER5 = 14;
public readonly IntPtr CUT_BUFFER6 = 15;
public readonly IntPtr CUT_BUFFER7 = 16;
public readonly IntPtr DRAWABLE = 17;
public readonly IntPtr FONT = 18;
public readonly IntPtr INTEGER = 19;
public readonly IntPtr PIXMAP = 20;
public readonly IntPtr POINT = 21;
public readonly IntPtr RECTANGLE = 22;
public readonly IntPtr RESOURCE_MANAGER = 23;
public readonly IntPtr RGB_COLOR_MAP = 24;
public readonly IntPtr RGB_BEST_MAP = 25;
public readonly IntPtr RGB_BLUE_MAP = 26;
public readonly IntPtr RGB_DEFAULT_MAP = 27;
public readonly IntPtr RGB_GRAY_MAP = 28;
public readonly IntPtr RGB_GREEN_MAP = 29;
public readonly IntPtr RGB_RED_MAP = 30;
public readonly IntPtr STRING = 31;
public readonly IntPtr VISUALID = 32;
public readonly IntPtr WINDOW = 33;
public readonly IntPtr WM_COMMAND = 34;
public readonly IntPtr WM_HINTS = 35;
public readonly IntPtr WM_CLIENT_MACHINE = 36;
public readonly IntPtr WM_ICON_NAME = 37;
public readonly IntPtr WM_ICON_SIZE = 38;
public readonly IntPtr WM_NAME = 39;
public readonly IntPtr WM_NORMAL_HINTS = 40;
public readonly IntPtr WM_SIZE_HINTS = 41;
public readonly IntPtr WM_ZOOM_HINTS = 42;
public readonly IntPtr MIN_SPACE = 43;
public readonly IntPtr NORM_SPACE = 44;
public readonly IntPtr MAX_SPACE = 45;
public readonly IntPtr END_SPACE = 46;
public readonly IntPtr SUPERSCRIPT_X = 47;
public readonly IntPtr SUPERSCRIPT_Y = 48;
public readonly IntPtr SUBSCRIPT_X = 49;
public readonly IntPtr SUBSCRIPT_Y = 50;
public readonly IntPtr UNDERLINE_POSITION = 51;
public readonly IntPtr UNDERLINE_THICKNESS = 52;
public readonly IntPtr STRIKEOUT_ASCENT = 53;
public readonly IntPtr STRIKEOUT_DESCENT = 54;
public readonly IntPtr ITALIC_ANGLE = 55;
public readonly IntPtr X_HEIGHT = 56;
public readonly IntPtr QUAD_WIDTH = 57;
public readonly IntPtr WEIGHT = 58;
public readonly IntPtr POINT_SIZE = 59;
public readonly IntPtr RESOLUTION = 60;
public readonly IntPtr COPYRIGHT = 61;
public readonly IntPtr NOTICE = 62;
public readonly IntPtr FONT_NAME = 63;
public readonly IntPtr FAMILY_NAME = 64;
public readonly IntPtr FULL_NAME = 65;
public readonly IntPtr CAP_HEIGHT = 66;
public readonly IntPtr WM_CLASS = 67;
public readonly IntPtr WM_TRANSIENT_FOR = 68;
public IntPtr EDID;
@ -183,7 +182,6 @@ namespace Avalonia.X11
public IntPtr CLIPBOARD_MANAGER;
public IntPtr SAVE_TARGETS;
public IntPtr MULTIPLE;
public IntPtr PRIMARY;
public IntPtr OEMTEXT;
public IntPtr UNICODETEXT;
public IntPtr TARGETS;
@ -208,11 +206,16 @@ namespace Avalonia.X11
if (value != IntPtr.Zero)
{
field = value;
_namesToAtoms[name] = value;
_atomsToNames[value] = name;
SetName(name, value);
}
}
private void SetName(string name, IntPtr value)
{
_namesToAtoms[name] = value;
_atomsToNames[value] = name;
}
public IntPtr GetAtom(string name)
{
if (_namesToAtoms.TryGetValue(name, out var rv))

6
src/Avalonia.X11/X11Globals.cs

@ -109,13 +109,13 @@ namespace Avalonia.X11
{
XGetWindowProperty(_x11.Display, _rootWindow, _x11.Atoms._NET_SUPPORTING_WM_CHECK,
IntPtr.Zero, new IntPtr(IntPtr.Size), false,
_x11.Atoms.XA_WINDOW, out IntPtr actualType, out int actualFormat, out IntPtr nitems,
_x11.Atoms.WINDOW, out IntPtr actualType, out int actualFormat, out IntPtr nitems,
out IntPtr bytesAfter, out IntPtr prop);
if (nitems.ToInt32() != 1)
return IntPtr.Zero;
try
{
if (actualType != _x11.Atoms.XA_WINDOW)
if (actualType != _x11.Atoms.WINDOW)
return IntPtr.Zero;
return *(IntPtr*)prop.ToPointer();
}
@ -197,7 +197,7 @@ namespace Avalonia.X11
if (wm == IntPtr.Zero)
return WindowActivationTrackingMode.FocusEvents;
var supportedFeatures = XGetWindowPropertyAsIntPtrArray(_x11.Display, _x11.RootWindow,
_x11.Atoms._NET_SUPPORTED, _x11.Atoms.XA_ATOM) ?? [];
_x11.Atoms._NET_SUPPORTED, _x11.Atoms.ATOM) ?? [];
if (supportedFeatures.Contains(_x11.Atoms._NET_WM_STATE_FOCUSED))
return WindowActivationTrackingMode._NET_WM_STATE_FOCUSED;

5
src/Avalonia.X11/X11IconLoader.cs

@ -40,14 +40,15 @@ namespace Avalonia.X11
_width = Math.Min(bitmap.PixelSize.Width, 128);
_height = Math.Min(bitmap.PixelSize.Height, 128);
var pixels = new uint[_width * _height];
var size = new PixelSize(_width, _height);
using (var rtb = new RenderTargetBitmap(new PixelSize(128, 128)))
using (var rtb = new RenderTargetBitmap(size))
{
using (var ctx = rtb.CreateDrawingContext(true))
ctx.DrawImage(bitmap, new Rect(rtb.Size));
fixed (void* pPixels = pixels)
rtb.CopyPixels(new LockedFramebuffer((IntPtr)pPixels, new PixelSize(_width, _height), _width * 4,
rtb.CopyPixels(new LockedFramebuffer((IntPtr)pPixels, size, _width * 4,
new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Premul, null));
}

20
src/Avalonia.X11/X11Window.cs

@ -71,6 +71,7 @@ namespace Avalonia.X11
private bool _useCompositorDrivenRenderWindowResize = false;
private bool _usePositioningFlags = false;
private X11WindowMode _mode;
private IWindowIconImpl? _iconImpl;
private enum XSyncState
{
@ -252,14 +253,14 @@ namespace Avalonia.X11
_mode.AppendWmProtocols(data);
XChangeProperty(_x11.Display, _handle, _x11.Atoms.WM_PROTOCOLS, _x11.Atoms.XA_ATOM, 32,
XChangeProperty(_x11.Display, _handle, _x11.Atoms.WM_PROTOCOLS, _x11.Atoms.ATOM, 32,
PropertyMode.Replace, data.ToArray(), data.Count);
if (_x11.HasXSync)
{
_xSyncCounter = XSyncCreateCounter(_x11.Display, _xSyncValue);
XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_SYNC_REQUEST_COUNTER,
_x11.Atoms.XA_CARDINAL, 32, PropertyMode.Replace, ref _xSyncCounter, 1);
_x11.Atoms.CARDINAL, 32, PropertyMode.Replace, ref _xSyncCounter, 1);
}
_storageProvider = new FallbackStorageProvider(new[]
@ -365,7 +366,7 @@ namespace Avalonia.X11
var pid = (uint)s_pid;
// The type of `_NET_WM_PID` is `CARDINAL` which is 32-bit unsigned integer, see https://specifications.freedesktop.org/wm-spec/1.3/ar01s05.html
XChangeProperty(_x11.Display, windowXId,
_x11.Atoms._NET_WM_PID, _x11.Atoms.XA_CARDINAL, 32,
_x11.Atoms._NET_WM_PID, _x11.Atoms.CARDINAL, 32,
PropertyMode.Replace, ref pid, 1);
const int maxLength = 1024;
@ -384,7 +385,7 @@ namespace Avalonia.X11
}
XChangeProperty(_x11.Display, windowXId,
_x11.Atoms.XA_WM_CLIENT_MACHINE, _x11.Atoms.XA_STRING, 8,
_x11.Atoms.WM_CLIENT_MACHINE, _x11.Atoms.STRING, 8,
PropertyMode.Replace, name, length);
}
@ -1149,7 +1150,7 @@ namespace Avalonia.X11
public void SetParent(IWindowImpl? parent)
{
if (parent == null || parent.Handle == null || parent.Handle.Handle == IntPtr.Zero)
XDeleteProperty(_x11.Display, _handle, _x11.Atoms.XA_WM_TRANSIENT_FOR);
XDeleteProperty(_x11.Display, _handle, _x11.Atoms.WM_TRANSIENT_FOR);
else
XSetTransientForHint(_x11.Display, _handle, parent.Handle.Handle);
}
@ -1393,7 +1394,7 @@ namespace Avalonia.X11
if (string.IsNullOrEmpty(title))
{
XDeleteProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_NAME);
XDeleteProperty(_x11.Display, _handle, _x11.Atoms.XA_WM_NAME);
XDeleteProperty(_x11.Display, _handle, _x11.Atoms.WM_NAME);
}
else
{
@ -1530,6 +1531,11 @@ namespace Avalonia.X11
public void SetIcon(IWindowIconImpl? icon)
{
if (ReferenceEquals(_iconImpl, icon))
return;
_iconImpl = icon;
if (icon != null)
{
var data = ((X11IconData)icon).Data;
@ -1642,7 +1648,7 @@ namespace Avalonia.X11
_ => _x11.Atoms._NET_WM_WINDOW_TYPE_NORMAL
};
XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_WINDOW_TYPE, _x11.Atoms.XA_ATOM,
XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_WINDOW_TYPE, _x11.Atoms.ATOM,
32, PropertyMode.Replace, new[] { atom }, 1);
}

2
src/Avalonia.X11/XLib.cs

@ -22,6 +22,8 @@ namespace Avalonia.X11
private const string libXInput = "libXi.so.6";
private const string libXCursor = "libXcursor.so.1";
public const IntPtr AnyPropertyType = 0;
[DllImport(libX11)]
public static extern IntPtr XOpenDisplay(IntPtr display);

6
src/Avalonia.X11/XResources.cs

@ -51,9 +51,9 @@ internal class XResources
string? ReadResourcesString()
{
XGetWindowProperty(_x11.Display, _x11.RootWindow, _x11.Atoms.XA_RESOURCE_MANAGER,
XGetWindowProperty(_x11.Display, _x11.RootWindow, _x11.Atoms.RESOURCE_MANAGER,
IntPtr.Zero, new IntPtr(0x7fffffff),
false, _x11.Atoms.XA_STRING, out _, out var actualFormat,
false, _x11.Atoms.STRING, out _, out var actualFormat,
out var nitems, out _, out var prop);
try
{
@ -69,7 +69,7 @@ internal class XResources
private void OnRootPropertyChanged(IntPtr atom)
{
if (atom == _x11.Atoms.XA_RESOURCE_MANAGER)
if (atom == _x11.Atoms.RESOURCE_MANAGER)
UpdateResources();
}
}

15
src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs

@ -20,9 +20,9 @@ public static class HeadlessWindowExtensions
/// <returns>Bitmap with last rendered frame. Null, if nothing was rendered.</returns>
public static WriteableBitmap? CaptureRenderedFrame(this TopLevel topLevel)
{
Dispatcher.UIThread.RunJobs();
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
return topLevel.GetLastRenderedFrame();
WriteableBitmap? bitmap = null;
topLevel.RunJobsOnImpl(w => bitmap = w.GetLastRenderedFrame());
return bitmap;
}
/// <summary>
@ -114,6 +114,15 @@ public static class HeadlessWindowExtensions
DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsOnImpl(topLevel, w => w.DragDrop(point, type, data, effects, modifiers));
/// <summary>
/// Changes the render scaling (DPI) of the headless window/toplevel.
/// This simulates a DPI change, triggering scaling changed notifications and a layout pass.
/// </summary>
/// <param name="topLevel">The target headless top level.</param>
/// <param name="scaling">The new render scaling factor. Must be greater than zero.</param>
public static void SetRenderScaling(this TopLevel topLevel, double scaling) =>
RunJobsOnImpl(topLevel, w => w.SetRenderScaling(scaling));
private static void RunJobsOnImpl(this TopLevel topLevel, Action<IHeadlessWindow> action)
{
RunJobsAndRender();

16
src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs

@ -49,7 +49,7 @@ namespace Avalonia.Headless
public Size ClientSize { get; set; }
public Size? FrameSize => null;
public double RenderScaling { get; } = 1;
public double RenderScaling { get; private set; } = 1;
public double DesktopScaling => RenderScaling;
public IPlatformRenderSurface[] Surfaces { get; }
public Action<RawInputEventArgs>? Input { get; set; }
@ -358,6 +358,20 @@ namespace Avalonia.Headless
Input?.Invoke(new RawDragEvent(device, type, InputRoot!, point, data, effects, modifiers));
}
void IHeadlessWindow.SetRenderScaling(double scaling)
{
if (scaling <= 0)
throw new ArgumentOutOfRangeException(nameof(scaling), "Scaling must be greater than zero.");
if (RenderScaling == scaling)
return;
var oldScaledSize = ClientSize;
RenderScaling = scaling;
ScalingChanged?.Invoke(scaling);
Resize(oldScaledSize, WindowResizeReason.DpiChange);
}
void IWindowImpl.Move(PixelPoint point)
{
Position = point;

1
src/Headless/Avalonia.Headless/IHeadlessWindow.cs

@ -16,5 +16,6 @@ namespace Avalonia.Headless
void MouseUp(Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None);
void MouseWheel(Point point, Vector delta, RawInputModifiers modifiers = RawInputModifiers.None);
void DragDrop(Point point, RawDragEventType type, IDataTransfer data, DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None);
void SetRenderScaling(double scaling);
}
}

4
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs

@ -209,8 +209,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
throw new XamlSelectorsTransformException("Unable to parse selector: " + e.Message, node, e);
}
// Selectors should resolve control types only.
// isMarkupExtension = false to prevent resolving selector types to XExtension.
var selector = Create(parsed, (p, n)
=> TypeReferenceResolver.ResolveType(context, $"{p}:{n}", true, node, true));
=> TypeReferenceResolver.ResolveType(context, $"{p}:{n}", false, node, true));
pn.Values[0] = selector;
var templateType = GetLastTemplateTypeFromSelector(selector);

48
src/Windows/Avalonia.Win32.Interoperability/WinForms/WinFormsAvaloniaMessageFilter.cs

@ -0,0 +1,48 @@
using System;
using System.Windows.Forms;
using static Avalonia.Win32.Interop.UnmanagedMethods;
namespace Avalonia.Win32.Interoperability;
/// <summary>
/// Provides a message filter for integrating Avalonia within a WinForms application.
/// </summary>
/// <remarks>
/// This filter ensures that key messages, which are typically handled specially by WinForms,
/// are intercepted and routed to Avalonia's windows. This is necessary to preserve proper input handling
/// in mixed WinForms and Avalonia application scenarios.
/// </remarks>
public class WinFormsAvaloniaMessageFilter : IMessageFilter
{
/// <inheritdoc />
public bool PreFilterMessage(ref Message m)
{
// WinForms handles key messages specially, preventing them from reaching Avalonia's windows.
// Handle them first.
if (m.Msg >= (int)WindowsMessage.WM_KEYFIRST &&
m.Msg <= (int)WindowsMessage.WM_KEYLAST &&
WindowImpl.IsOurWindowGlobal(m.HWnd) &&
!IsInsideWinForms(m.HWnd))
{
var msg = new MSG
{
hwnd = m.HWnd,
message = (uint)m.Msg,
wParam = m.WParam,
lParam = m.LParam
};
TranslateMessage(ref msg);
DispatchMessage(ref msg);
return true;
}
return false;
}
private static bool IsInsideWinForms(IntPtr hwnd)
{
var parentHwnd = GetParent(hwnd);
return parentHwnd != IntPtr.Zero && Control.FromHandle(parentHwnd) is WinFormsAvaloniaControlHost;
}
}

8
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@ -1004,6 +1004,14 @@ namespace Avalonia.Win32
if (hwnd == _hwnd)
return true;
return IsOurWindowGlobal(hwnd);
}
internal static bool IsOurWindowGlobal(IntPtr hwnd)
{
if (hwnd == IntPtr.Zero)
return false;
lock (s_instances)
for (int i = 0; i < s_instances.Count; i++)
if (s_instances[i]._hwnd == hwnd)

3
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -809,6 +809,9 @@ namespace Avalonia.Win32
public void SetIcon(IWindowIconImpl? icon)
{
if (ReferenceEquals(_iconImpl, icon))
return;
_iconImpl = (IconImpl?)icon;
ClearIconCache();
RefreshIcon();

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

Loading…
Cancel
Save