From bccb425f7489c1803f2b7a4c7d6b72e79c45946c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 31 Jul 2026 18:04:12 +1000 Subject: [PATCH 1/7] Add Avalonia Catalog sample and backend --- .gitignore | 2 - ImageSharp.Drawing.All.sln | 740 ++++++- samples/AvaloniaControlCatalog/App.xaml | 97 + samples/AvaloniaControlCatalog/App.xaml.cs | 175 ++ .../Assets/CurvedHeader/avatar.jpg | 3 + .../Assets/CurvedHeader/featured.jpg | 3 + .../Assets/CurvedHeader/product1.jpg | 3 + .../Assets/CurvedHeader/product2.jpg | 3 + .../Assets/CurvedHeader/product3.jpg | 3 + .../Assets/CurvedHeader/update1.jpg | 3 + .../Assets/CurvedHeader/update2.jpg | 3 + .../Assets/CurvedHeader/update3.jpg | 3 + .../Assets/Fonts/SourceSansPro-Bold.ttf | 3 + .../Assets/Fonts/SourceSansPro-BoldItalic.ttf | 3 + .../Assets/Fonts/SourceSansPro-Italic.ttf | 3 + .../Assets/Fonts/SourceSansPro-Regular.ttf | 3 + .../Assets/Fonts/WenQuanYiMicroHei-01.ttf | 3 + .../AvaloniaControlCatalog/Assets/Icon.png | 3 + .../Assets/ModernApp/avatar.jpg | 3 + .../Assets/ModernApp/dest_alps.jpg | 3 + .../Assets/ModernApp/dest_forest.jpg | 3 + .../Assets/ModernApp/dest_norway.jpg | 3 + .../Assets/ModernApp/exp_angkor.jpg | 3 + .../Assets/ModernApp/exp_tokyo.jpg | 3 + .../Assets/ModernApp/gallery_alpine.jpg | 3 + .../Assets/ModernApp/gallery_bay.jpg | 3 + .../Assets/ModernApp/gallery_city.jpg | 3 + .../Assets/ModernApp/gallery_paris.jpg | 3 + .../Assets/ModernApp/gallery_tropical.jpg | 3 + .../Assets/ModernApp/gallery_venice.jpg | 3 + .../Assets/ModernApp/story1.jpg | 3 + .../Assets/ModernApp/story2.jpg | 3 + .../Assets/ModernApp/story3.jpg | 3 + .../Assets/Movies/cast1.jpg | 3 + .../Assets/Movies/cast2.jpg | 3 + .../Assets/Movies/continue1.jpg | 3 + .../Assets/Movies/hero.jpg | 3 + .../Assets/Movies/morelike1.jpg | 3 + .../Assets/Movies/search1.jpg | 3 + .../Assets/Movies/toprated1.jpg | 3 + .../Assets/Movies/toprated2.jpg | 3 + .../Assets/Movies/toprated3.jpg | 3 + .../Assets/Movies/toprated4.jpg | 3 + .../Assets/Movies/trending1.jpg | 3 + .../Assets/Movies/trending2.jpg | 3 + .../Assets/Pulse/cat_hiit.jpg | 3 + .../Assets/Pulse/cat_strength.jpg | 3 + .../Assets/Pulse/cat_yoga.jpg | 3 + .../Assets/Pulse/ex_bench.jpg | 3 + .../Assets/Pulse/ex_deadlifts.jpg | 3 + .../Assets/Pulse/ex_overhead.jpg | 3 + .../Assets/Pulse/ex_pullups.jpg | 3 + .../Assets/Pulse/ex_squats.jpg | 3 + .../Assets/Pulse/profile_avatar.jpg | 3 + .../Assets/Pulse/rec_fullbody.jpg | 3 + .../Assets/Pulse/rec_mobility.jpg | 3 + .../Assets/Pulse/rec_powercore.jpg | 3 + .../Assets/Pulse/workout_hero.jpg | 3 + .../Assets/Restaurant/dish1.jpg | 3 + .../Assets/Restaurant/dish2.jpg | 3 + .../Assets/Restaurant/dish3.jpg | 3 + .../Assets/Restaurant/dish4.jpg | 3 + .../Assets/Restaurant/featured_dish.jpg | 3 + .../Assets/Restaurant/user_avatar.jpg | 3 + .../Assets/RetroGaming/cyber_city.jpg | 3 + .../Assets/RetroGaming/dungeon_bit.jpg | 3 + .../Assets/RetroGaming/forest_spirit.jpg | 3 + .../Assets/RetroGaming/hero.jpg | 3 + .../Assets/RetroGaming/neon_ninja.jpg | 3 + .../Assets/RetroGaming/neon_racer.jpg | 3 + .../Assets/RetroGaming/pixel_quest.jpg | 3 + .../Assets/RetroGaming/space_voids.jpg | 3 + .../Assets/Sanctuary/city_bg.jpg | 3 + .../Assets/Sanctuary/forest_bg.jpg | 3 + .../Assets/Sanctuary/main_arctic_silence.jpg | 3 + .../Assets/Sanctuary/main_deep_forest.jpg | 3 + .../Assets/Sanctuary/main_desert_sands.jpg | 3 + .../Assets/Sanctuary/main_hero.jpg | 3 + .../Assets/Sanctuary/mountain_bg.jpg | 3 + .../Assets/avalonia-32.png | 3 + .../Assets/delicate-arch-896885_640.jpg | 3 + .../Assets/github_icon.png | 3 + .../Assets/hirsch-899118_640.jpg | 3 + .../AvaloniaControlCatalog/Assets/image1.jpg | 3 + .../AvaloniaControlCatalog/Assets/image2.jpg | 3 + .../AvaloniaControlCatalog/Assets/image3.jpg | 3 + .../AvaloniaControlCatalog/Assets/image4.jpg | 3 + .../AvaloniaControlCatalog/Assets/image5.jpg | 3 + .../AvaloniaControlCatalog/Assets/image6.jpg | 3 + .../AvaloniaControlCatalog/Assets/image7.jpg | 3 + .../Assets/maple-leaf-888807_640.jpg | 3 + .../Assets/test_icon.ico | 3 + .../AvaloniaControlCatalog.csproj | 99 + .../AnonymousObserverNonGenericHelper.cs | 13 + .../AvaloniaPropertyBindingObservable.cs | 126 ++ .../AvaloniaPropertyChangedObservable.cs | 46 + .../AvaloniaPropertyObservable.cs | 117 ++ .../AvaloniaReactive/CombinedSubject.cs | 23 + .../AvaloniaReactive/CompositeDisposable.cs | 427 ++++ .../AvaloniaReactive/Disposable.cs | 98 + .../AvaloniaReactive/DisposableMixin.cs | 37 + .../AvaloniaReactive/IAvaloniaSubject.cs | 8 + .../LightweightObservableBase.cs | 237 +++ .../AvaloniaReactive/LightweightSubject.cs | 30 + .../AvaloniaReactive/Observable.cs | 259 +++ .../Operators/CombineLatest.cs | 374 ++++ .../AvaloniaReactive/Operators/Sink.cs | 111 + .../AvaloniaReactive/Operators/Switch.cs | 145 ++ .../SingleSubscriberObservableBase.cs | 78 + .../CombinedGeometryImpl.cs | 49 + .../Converter/DegToRadConverter.cs | 29 + .../Converter/GdpToBrushConverter.cs | 31 + .../Converter/HexConverter.cs | 34 + .../Converter/MathSubtractConverter.cs | 22 + .../TableViewColumnWidthConverter.cs | 27 + .../DecoratedWindow.xaml | 66 + .../DecoratedWindow.xaml.cs | 45 + .../DrawingBackendMode.cs | 25 + .../DrawingContextImpl.cs | 1810 +++++++++++++++++ .../EllipseGeometryImpl.cs | 32 + .../AvaloniaControlCatalog/FontManagerImpl.cs | 297 +++ .../GeometryGroupImpl.cs | 51 + .../AvaloniaControlCatalog/GeometryImpl.cs | 280 +++ .../AvaloniaControlCatalog/GlobalUsings.cs | 35 + .../AvaloniaControlCatalog/GlyphRunImpl.cs | 243 +++ .../IDrawableBitmapImpl.cs | 32 + samples/AvaloniaControlCatalog/Icons.cs | 194 ++ .../ImageSharpDrawingAppBuilderExtensions.cs | 26 + .../ImageSharpExtensions.cs | 328 +++ .../ImmutableBitmapImpl.cs | 274 +++ .../LineGeometryImpl.cs | 23 + samples/AvaloniaControlCatalog/MainView.xaml | 161 ++ .../AvaloniaControlCatalog/MainView.xaml.cs | 375 ++++ .../AvaloniaControlCatalog/MainWindow.xaml | 79 + .../AvaloniaControlCatalog/MainWindow.xaml.cs | 74 + .../MiniMvvm/MiniCommand.cs | 67 + .../MiniMvvm/PropertyChangedExtensions.cs | 110 + .../MiniMvvm/ViewModelBase.cs | 25 + .../Models/CatalogTheme.cs | 8 + .../AvaloniaControlCatalog/Models/Person.cs | 125 ++ .../Models/StateData.cs | 20 + .../AvaloniaControlCatalog/NavHeaderItem.xaml | 101 + samples/AvaloniaControlCatalog/NuGet.Config | 27 + .../Pages/AcceleratorPage.xaml | 116 ++ .../Pages/AcceleratorPage.xaml.cs | 12 + .../Pages/AcrylicPage.xaml | 165 ++ .../Pages/AcrylicPage.xaml.cs | 12 + .../Pages/AdornerLayerPage.xaml | 76 + .../Pages/AdornerLayerPage.xaml.cs | 34 + .../Pages/AutoCompleteBoxPage.xaml | 83 + .../Pages/AutoCompleteBoxPage.xaml.cs | 178 ++ .../Pages/BitmapCachePage.axaml | 49 + .../Pages/BitmapCachePage.axaml.cs | 13 + .../Pages/BorderPage.xaml | 65 + .../Pages/BorderPage.xaml.cs | 12 + .../Pages/ButtonSpinnerPage.xaml | 34 + .../Pages/ButtonSpinnerPage.xaml.cs | 49 + .../Pages/ButtonsPage.xaml | 292 +++ .../Pages/ButtonsPage.xaml.cs | 21 + .../Pages/CalendarDatePickerPage.xaml | 56 + .../Pages/CalendarDatePickerPage.xaml.cs | 23 + .../Pages/CalendarPage.xaml | 53 + .../Pages/CalendarPage.xaml.cs | 20 + .../Pages/CanvasPage.xaml | 38 + .../Pages/CanvasPage.xaml.cs | 12 + .../Pages/CarouselDemoPage.xaml | 14 + .../Pages/CarouselDemoPage.xaml.cs | 91 + .../Pages/CarouselPage.xaml | 119 ++ .../Pages/CarouselPage.xaml.cs | 149 ++ .../CarouselPage/CareCompanionAppPage.xaml | 98 + .../CarouselPage/CareCompanionAppPage.xaml.cs | 1069 ++++++++++ .../CarouselCustomizationPage.xaml | 119 ++ .../CarouselCustomizationPage.xaml.cs | 48 + .../CarouselPage/CarouselDataBindingPage.xaml | 60 + .../CarouselDataBindingPage.xaml.cs | 95 + .../CarouselPage/CarouselGalleryAppPage.xaml | 557 +++++ .../CarouselGalleryAppPage.xaml.cs | 101 + .../CarouselPage/CarouselGesturesPage.xaml | 93 + .../CarouselPage/CarouselGesturesPage.xaml.cs | 59 + .../CarouselGettingStartedPage.xaml | 74 + .../CarouselGettingStartedPage.xaml.cs | 40 + .../CarouselPage/CarouselMultiItemPage.xaml | 140 ++ .../CarouselMultiItemPage.xaml.cs | 47 + .../CarouselPageCustomizationPage.xaml | 115 ++ .../CarouselPageCustomizationPage.xaml.cs | 79 + .../CarouselPageDataTemplatePage.xaml | 44 + .../CarouselPageDataTemplatePage.xaml.cs | 209 ++ .../CarouselPage/CarouselPageEventsPage.xaml | 37 + .../CarouselPageEventsPage.xaml.cs | 92 + .../CarouselPageFirstLookPage.xaml | 61 + .../CarouselPageFirstLookPage.xaml.cs | 35 + .../CarouselPage/CarouselPageGesturePage.xaml | 64 + .../CarouselPageGesturePage.xaml.cs | 44 + .../CarouselPagePerformancePage.xaml | 45 + .../CarouselPagePerformancePage.xaml.cs | 103 + .../CarouselPageSelectionPage.xaml | 90 + .../CarouselPageSelectionPage.xaml.cs | 56 + .../CarouselPageTransitionsPage.xaml | 65 + .../CarouselPageTransitionsPage.xaml.cs | 69 + .../CarouselPage/CarouselTransitionsPage.xaml | 97 + .../CarouselTransitionsPage.xaml.cs | 66 + .../CarouselPage/CarouselVerticalPage.xaml | 132 ++ .../CarouselPage/CarouselVerticalPage.xaml.cs | 39 + .../Pages/CarouselPage/SanctuaryMainPage.xaml | 298 +++ .../CarouselPage/SanctuaryMainPage.xaml.cs | 11 + .../CarouselPage/SanctuaryShowcasePage.xaml | 335 +++ .../SanctuaryShowcasePage.xaml.cs | 70 + .../Pages/CheckBoxPage.xaml | 29 + .../Pages/CheckBoxPage.xaml.cs | 12 + .../Pages/ClipboardPage.xaml | 31 + .../Pages/ClipboardPage.xaml.cs | 213 ++ .../Pages/ColorPickerPage.xaml | 129 ++ .../Pages/ColorPickerPage.xaml.cs | 28 + .../Pages/ComboBoxPage.xaml | 152 ++ .../Pages/ComboBoxPage.xaml.cs | 17 + .../CommandBarCustomizationPage.xaml | 136 ++ .../CommandBarCustomizationPage.xaml.cs | 85 + .../CommandBarDynamicOverflowPage.xaml | 93 + .../CommandBarDynamicOverflowPage.xaml.cs | 101 + .../CommandBar/CommandBarEventsPage.xaml | 95 + .../CommandBar/CommandBarEventsPage.xaml.cs | 220 ++ .../CommandBar/CommandBarFirstLookPage.xaml | 94 + .../CommandBarFirstLookPage.xaml.cs | 27 + .../CommandBar/CommandBarKeyboardPage.xaml | 121 ++ .../CommandBar/CommandBarKeyboardPage.xaml.cs | 93 + .../CommandBarLabelPositionPage.xaml | 61 + .../CommandBarLabelPositionPage.xaml.cs | 12 + .../CommandBar/CommandBarOverflowPage.xaml | 74 + .../CommandBar/CommandBarOverflowPage.xaml.cs | 54 + .../CommandBar/CommandBarTogglePage.xaml | 77 + .../CommandBar/CommandBarTogglePage.xaml.cs | 50 + .../Pages/CommandBarPage.xaml | 11 + .../Pages/CommandBarPage.xaml.cs | 160 ++ .../Pages/CompositionPage.axaml | 61 + .../Pages/CompositionPage.axaml.cs | 357 ++++ .../Pages/ConnectedAnimationDemoPage.xaml | 14 + .../Pages/ConnectedAnimationDemoPage.xaml.cs | 144 ++ .../Pages/ContainerQueryPage.xaml | 111 + .../Pages/ContainerQueryPage.xaml.cs | 12 + .../Pages/ContentDemoPage.xaml | 14 + .../Pages/ContentDemoPage.xaml.cs | 160 ++ .../ContentPageCommandBarPage.xaml | 83 + .../ContentPageCommandBarPage.xaml.cs | 212 ++ .../ContentPageCustomizationPage.xaml | 106 + .../ContentPageCustomizationPage.xaml.cs | 64 + .../ContentPage/ContentPageEventsPage.xaml | 66 + .../ContentPage/ContentPageEventsPage.xaml.cs | 103 + .../ContentPage/ContentPageFirstLookPage.xaml | 48 + .../ContentPageFirstLookPage.xaml.cs | 93 + .../ContentPagePerformancePage.xaml | 104 + .../ContentPagePerformancePage.xaml.cs | 125 ++ .../ContentPage/ContentPageSafeAreaPage.xaml | 171 ++ .../ContentPageSafeAreaPage.xaml.cs | 70 + .../Pages/ContextFlyoutPage.xaml | 170 ++ .../Pages/ContextFlyoutPage.xaml.cs | 59 + .../Pages/ContextMenuPage.xaml | 100 + .../Pages/ContextMenuPage.xaml.cs | 51 + .../Pages/CursorPage.xaml | 31 + .../Pages/CursorPage.xaml.cs | 14 + .../Pages/CustomDrawing.xaml | 108 + .../Pages/CustomDrawing.xaml.cs | 40 + .../Pages/CustomDrawingExampleControl.cs | 216 ++ .../Pages/DataValidationPage.axaml | 43 + .../Pages/DataValidationPage.axaml.cs | 14 + .../Pages/DateTimePickerPage.xaml | 211 ++ .../Pages/DateTimePickerPage.xaml.cs | 23 + .../Pages/DialogsPage.xaml | 88 + .../Pages/DialogsPage.xaml.cs | 519 +++++ .../Pages/DragAndDropPage.xaml | 61 + .../Pages/DragAndDropPage.xaml.cs | 174 ++ .../Pages/DrawerDemoPage.xaml | 14 + .../Pages/DrawerDemoPage.xaml.cs | 76 + .../DrawerPage/ControlsGalleryAppPage.xaml | 400 ++++ .../DrawerPage/ControlsGalleryAppPage.xaml.cs | 464 +++++ .../DrawerPage/DrawerPageBreakpointPage.xaml | 113 + .../DrawerPageBreakpointPage.xaml.cs | 84 + .../DrawerPage/DrawerPageCompactPage.xaml | 103 + .../DrawerPage/DrawerPageCompactPage.xaml.cs | 78 + .../DrawerPageCustomFlyoutPage.xaml | 268 +++ .../DrawerPageCustomFlyoutPage.xaml.cs | 146 ++ .../DrawerPageCustomizationPage.xaml | 196 ++ .../DrawerPageCustomizationPage.xaml.cs | 326 +++ .../DrawerPage/DrawerPageEventsPage.xaml | 96 + .../DrawerPage/DrawerPageEventsPage.xaml.cs | 129 ++ .../DrawerPage/DrawerPageFirstLookPage.xaml | 66 + .../DrawerPageFirstLookPage.xaml.cs | 78 + .../DrawerPage/DrawerPageNavigationPage.xaml | 112 + .../DrawerPageNavigationPage.xaml.cs | 52 + .../DrawerPage/DrawerPagePerformancePage.xaml | 148 ++ .../DrawerPagePerformancePage.xaml.cs | 150 ++ .../Pages/DrawerPage/DrawerPageRtlPage.xaml | 108 + .../DrawerPage/DrawerPageRtlPage.xaml.cs | 57 + .../DrawerPage/DrawerPageTransitionsPage.xaml | 118 ++ .../DrawerPageTransitionsPage.xaml.cs | 94 + .../Pages/DrawerPage/EcoTrackerAppPage.xaml | 302 +++ .../DrawerPage/EcoTrackerAppPage.xaml.cs | 319 +++ .../DrawerPage/EcoTrackerCommunityView.xaml | 239 +++ .../EcoTrackerCommunityView.xaml.cs | 8 + .../DrawerPage/EcoTrackerHabitsView.xaml | 189 ++ .../DrawerPage/EcoTrackerHabitsView.xaml.cs | 8 + .../Pages/DrawerPage/EcoTrackerHomeView.xaml | 255 +++ .../DrawerPage/EcoTrackerHomeView.xaml.cs | 14 + .../Pages/DrawerPage/EcoTrackerStatsView.xaml | 224 ++ .../DrawerPage/EcoTrackerStatsView.xaml.cs | 8 + .../Pages/DrawerPage/ModernAppPage.xaml | 176 ++ .../Pages/DrawerPage/ModernAppPage.xaml.cs | 118 ++ .../Pages/DrawerPage/ModernDiscoverView.xaml | 360 ++++ .../DrawerPage/ModernDiscoverView.xaml.cs | 8 + .../Pages/DrawerPage/ModernMyTripsView.xaml | 124 ++ .../DrawerPage/ModernMyTripsView.xaml.cs | 8 + .../Pages/DrawerPage/ModernProfileView.xaml | 160 ++ .../DrawerPage/ModernProfileView.xaml.cs | 8 + .../Pages/DrawerPage/ModernSettingsView.xaml | 177 ++ .../DrawerPage/ModernSettingsView.xaml.cs | 8 + .../Pages/ExpanderPage.xaml | 76 + .../Pages/ExpanderPage.xaml.cs | 17 + .../Pages/FlyoutsPage.axaml | 286 +++ .../Pages/FlyoutsPage.axaml.cs | 94 + .../Pages/FocusPage.xaml | 51 + .../Pages/FocusPage.xaml.cs | 14 + .../Pages/GesturePage.xaml | 14 + .../Pages/GesturePage.xaml.cs | 39 + .../Gestures/GesturePinchRotationPage.xaml | 59 + .../Gestures/GesturePinchRotationPage.xaml.cs | 33 + .../Pages/Gestures/GesturePinchZoomPage.xaml | 51 + .../Gestures/GesturePinchZoomPage.xaml.cs | 134 ++ .../Pages/Gestures/GesturePullPage.xaml | 113 + .../Pages/Gestures/GesturePullPage.xaml.cs | 100 + .../Pages/Gestures/GestureSwipePage.xaml | 123 ++ .../Pages/Gestures/GestureSwipePage.xaml.cs | 125 ++ .../Pages/HeaderedContentPage.axaml | 31 + .../Pages/HeaderedContentPage.axaml.cs | 14 + .../Pages/ImagePage.xaml | 69 + .../Pages/ImagePage.xaml.cs | 63 + .../Pages/LabelsPage.axaml | 45 + .../Pages/LabelsPage.axaml.cs | 35 + .../Pages/LayoutTransformControlPage.xaml | 28 + .../Pages/LayoutTransformControlPage.xaml.cs | 12 + .../Pages/ListBoxPage.xaml | 51 + .../Pages/ListBoxPage.xaml.cs | 14 + .../Pages/MenuPage.xaml | 99 + .../Pages/MenuPage.xaml.cs | 28 + .../Pages/NativeEmbedPage.xaml | 69 + .../Pages/NativeEmbedPage.xaml.cs | 77 + .../Pages/NavigationDemoHelper.cs | 242 +++ .../Pages/NavigationDemoPage.xaml | 14 + .../Pages/NavigationDemoPage.xaml.cs | 94 + .../NavigationPage/AvaloniaFlixAppPage.xaml | 258 +++ .../AvaloniaFlixAppPage.xaml.cs | 191 ++ .../AvaloniaFlixDetailView.xaml | 332 +++ .../AvaloniaFlixDetailView.xaml.cs | 60 + .../NavigationPage/AvaloniaFlixHomeView.xaml | 630 ++++++ .../AvaloniaFlixHomeView.xaml.cs | 21 + .../AvaloniaFlixSearchView.xaml | 302 +++ .../AvaloniaFlixSearchView.xaml.cs | 36 + .../CurvedHeaderHomeScrollView.xaml | 205 ++ .../CurvedHeaderHomeScrollView.xaml.cs | 16 + .../CurvedHeaderProfileScrollView.xaml | 104 + .../CurvedHeaderProfileScrollView.xaml.cs | 8 + .../Pages/NavigationPage/LAvenirAppPage.xaml | 191 ++ .../NavigationPage/LAvenirAppPage.xaml.cs | 243 +++ .../NavigationPage/LAvenirDishDetailView.xaml | 119 ++ .../LAvenirDishDetailView.xaml.cs | 34 + .../Pages/NavigationPage/LAvenirMenuView.xaml | 186 ++ .../NavigationPage/LAvenirMenuView.xaml.cs | 28 + .../NavigationPage/LAvenirProfileView.xaml | 32 + .../NavigationPage/LAvenirProfileView.xaml.cs | 8 + .../LAvenirReservationsView.xaml | 189 ++ .../LAvenirReservationsView.xaml.cs | 8 + .../NavigationPageAppearancePage.xaml | 149 ++ .../NavigationPageAppearancePage.xaml.cs | 172 ++ .../NavigationPageAttachedMethodsPage.xaml | 90 + .../NavigationPageAttachedMethodsPage.xaml.cs | 175 ++ .../NavigationPageBackButtonPage.xaml | 104 + .../NavigationPageBackButtonPage.xaml.cs | 176 ++ .../NavigationPageCurvedHeaderPage.xaml | 48 + .../NavigationPageCurvedHeaderPage.xaml.cs | 298 +++ .../NavigationPageEventsPage.xaml | 77 + .../NavigationPageEventsPage.xaml.cs | 105 + .../NavigationPageFirstLookPage.xaml | 57 + .../NavigationPageFirstLookPage.xaml.cs | 66 + .../NavigationPageGesturePage.xaml | 57 + .../NavigationPageGesturePage.xaml.cs | 66 + .../NavigationPageInteractiveHeaderPage.xaml | 26 + ...avigationPageInteractiveHeaderPage.xaml.cs | 154 ++ .../NavigationPageModalPage.xaml | 63 + .../NavigationPageModalPage.xaml.cs | 66 + .../NavigationPageModalTransitionsPage.xaml | 61 + ...NavigationPageModalTransitionsPage.xaml.cs | 135 ++ .../NavigationPageMvvmNavigation.cs | 95 + .../NavigationPageMvvmPage.xaml | 86 + .../NavigationPageMvvmPage.xaml.cs | 32 + .../NavigationPageMvvmPageFactory.cs | 252 +++ .../NavigationPageMvvmViewModels.cs | 238 +++ .../NavigationPagePassDataPage.xaml | 56 + .../NavigationPagePassDataPage.xaml.cs | 255 +++ .../NavigationPagePerformancePage.xaml | 81 + .../NavigationPagePerformancePage.xaml.cs | 119 ++ .../NavigationPageScrollAwarePage.xaml | 56 + .../NavigationPageScrollAwarePage.xaml.cs | 167 ++ .../NavigationPageStackPage.xaml | 46 + .../NavigationPageStackPage.xaml.cs | 176 ++ .../NavigationPageTitlePage.xaml | 62 + .../NavigationPageTitlePage.xaml.cs | 177 ++ .../NavigationPageToolbarPage.xaml | 80 + .../NavigationPageToolbarPage.xaml.cs | 138 ++ .../NavigationPageTransitionsPage.xaml | 67 + .../NavigationPageTransitionsPage.xaml.cs | 83 + .../Pages/NavigationPage/PulseAppPage.xaml | 46 + .../Pages/NavigationPage/PulseAppPage.xaml.cs | 140 ++ .../Pages/NavigationPage/PulseHomeView.xaml | 270 +++ .../NavigationPage/PulseHomeView.xaml.cs | 25 + .../Pages/NavigationPage/PulseLoginView.xaml | 145 ++ .../NavigationPage/PulseLoginView.xaml.cs | 15 + .../NavigationPage/PulseProfileView.xaml | 202 ++ .../NavigationPage/PulseProfileView.xaml.cs | 8 + .../PulseWorkoutDetailView.xaml | 248 +++ .../PulseWorkoutDetailView.xaml.cs | 15 + .../NavigationPage/PulseWorkoutsView.xaml | 290 +++ .../NavigationPage/PulseWorkoutsView.xaml.cs | 8 + .../NavigationPage/RetroGamingAppPage.xaml | 179 ++ .../NavigationPage/RetroGamingAppPage.xaml.cs | 306 +++ .../NavigationPage/RetroGamingDetailView.xaml | 229 +++ .../RetroGamingDetailView.xaml.cs | 69 + .../RetroGamingFavoritesView.xaml | 26 + .../RetroGamingFavoritesView.xaml.cs | 8 + .../NavigationPage/RetroGamingGamesView.xaml | 214 ++ .../RetroGamingGamesView.xaml.cs | 39 + .../NavigationPage/RetroGamingHomeView.xaml | 346 ++++ .../RetroGamingHomeView.xaml.cs | 22 + .../RetroGamingProfileView.xaml | 70 + .../RetroGamingProfileView.xaml.cs | 8 + .../NavigationPage/RetroGamingSearchView.xaml | 239 +++ .../RetroGamingSearchView.xaml.cs | 24 + .../Transitions/CompositeTransition.cs | 120 ++ .../Transitions/FadeThroughTransition.cs | 137 ++ .../Transitions/PageSlideTransition.cs | 97 + .../Transitions/ParallaxSlideTransition.cs | 121 ++ .../NavigationPerformanceMonitorHelper.cs | 275 +++ .../Pages/NotificationsPage.xaml | 25 + .../Pages/NotificationsPage.xaml.cs | 39 + .../Pages/NumericUpDownPage.xaml | 143 ++ .../Pages/NumericUpDownPage.xaml.cs | 100 + .../Pages/OpenGl/GlPageKnobs.xaml | 30 + .../Pages/OpenGl/GlPageKnobs.xaml.cs | 70 + .../Pages/OpenGl/OpenGlContent.cs | 311 +++ .../Pages/OpenGl/OpenGlFbo.cs | 118 ++ .../Pages/OpenGl/OpenGlLeasePage.xaml | 9 + .../Pages/OpenGl/OpenGlLeasePage.xaml.cs | 207 ++ .../Pages/OpenGlPage.xaml | 14 + .../Pages/OpenGlPage.xaml.cs | 82 + .../PipsPager/PipsPagerCarouselPage.xaml | 50 + .../PipsPager/PipsPagerCarouselPage.xaml.cs | 11 + .../PipsPagerCustomButtonThemesPage.xaml | 78 + .../PipsPagerCustomButtonThemesPage.xaml.cs | 11 + .../PipsPager/PipsPagerCustomColorsPage.xaml | 59 + .../PipsPagerCustomColorsPage.xaml.cs | 11 + .../PipsPagerCustomTemplatesPage.xaml | 197 ++ .../PipsPagerCustomTemplatesPage.xaml.cs | 11 + .../Pages/PipsPager/PipsPagerEventsPage.xaml | 35 + .../PipsPager/PipsPagerEventsPage.xaml.cs | 29 + .../PipsPagerGettingStartedPage.xaml | 46 + .../PipsPagerGettingStartedPage.xaml.cs | 11 + .../PipsPagerLargeCollectionPage.xaml | 52 + .../PipsPagerLargeCollectionPage.xaml.cs | 11 + .../Pages/PipsPagerPage.xaml | 14 + .../Pages/PipsPagerPage.xaml.cs | 54 + .../Pages/PlatformInfoPage.xaml | 57 + .../Pages/PlatformInfoPage.xaml.cs | 14 + .../Pages/PointerCanvas.cs | 236 +++ .../Pages/PointerContactsTab.cs | 109 + .../Pages/PointersPage.xaml | 69 + .../Pages/PointersPage.xaml.cs | 59 + .../Pages/ProgressBarPage.xaml | 43 + .../Pages/ProgressBarPage.xaml.cs | 12 + .../Pages/RadioButtonPage.xaml | 41 + .../Pages/RadioButtonPage.xaml.cs | 12 + .../Pages/RefreshContainerPage.axaml | 44 + .../Pages/RefreshContainerPage.axaml.cs | 28 + .../Pages/RelativePanelPage.axaml | 64 + .../Pages/RelativePanelPage.axaml.cs | 12 + .../Pages/ScreenPage.cs | 174 ++ .../Pages/ScrollViewerPage.xaml | 271 +++ .../Pages/ScrollViewerPage.xaml.cs | 104 + .../Pages/SliderPage.xaml | 84 + .../Pages/SliderPage.xaml.cs | 12 + .../Pages/SplitViewPage.xaml | 113 + .../Pages/SplitViewPage.xaml.cs | 14 + .../Pages/TabControlPage.xaml | 112 + .../Pages/TabControlPage.xaml.cs | 47 + .../Pages/TabStripPage.xaml | 35 + .../Pages/TabStripPage.xaml.cs | 33 + .../Pages/TabbedDemoPage.xaml | 14 + .../Pages/TabbedDemoPage.xaml.cs | 96 + .../Pages/TabbedPage/CenteredTabPanel.cs | 63 + .../TabbedPage/FluidNavBar/FluidNavBar.cs | 620 ++++++ .../TabbedPage/FluidNavBar/FluidNavItem.cs | 14 + .../TabbedPage/TabbedPageCollectionPage.xaml | 55 + .../TabbedPageCollectionPage.xaml.cs | 82 + .../TabbedPageCustomTabBarPage.xaml | 187 ++ .../TabbedPageCustomTabBarPage.xaml.cs | 34 + .../TabbedPageCustomizationPage.xaml | 117 ++ .../TabbedPageCustomizationPage.xaml.cs | 126 ++ .../TabbedPageDataTemplatePage.xaml | 43 + .../TabbedPageDataTemplatePage.xaml.cs | 207 ++ .../TabbedPageDisabledTabsPage.xaml | 70 + .../TabbedPageDisabledTabsPage.xaml.cs | 40 + .../TabbedPage/TabbedPageEventsPage.xaml | 51 + .../TabbedPage/TabbedPageEventsPage.xaml.cs | 82 + .../Pages/TabbedPage/TabbedPageFabPage.xaml | 367 ++++ .../TabbedPage/TabbedPageFabPage.xaml.cs | 45 + .../TabbedPage/TabbedPageFirstLookPage.xaml | 74 + .../TabbedPageFirstLookPage.xaml.cs | 83 + .../TabbedPage/TabbedPageFluidNavPage.xaml | 305 +++ .../TabbedPage/TabbedPageFluidNavPage.xaml.cs | 78 + .../TabbedPage/TabbedPageGesturePage.xaml | 72 + .../TabbedPage/TabbedPageGesturePage.xaml.cs | 43 + .../TabbedPage/TabbedPageKeyboardPage.xaml | 108 + .../TabbedPage/TabbedPageKeyboardPage.xaml.cs | 46 + .../TabbedPage/TabbedPagePerformancePage.xaml | 47 + .../TabbedPagePerformancePage.xaml.cs | 93 + .../TabbedPage/TabbedPagePlacementPage.xaml | 58 + .../TabbedPagePlacementPage.xaml.cs | 26 + .../TabbedPageProgrammaticPage.xaml | 71 + .../TabbedPageProgrammaticPage.xaml.cs | 52 + .../TabbedPage/TabbedPageTransitionsPage.xaml | 77 + .../TabbedPageTransitionsPage.xaml.cs | 48 + .../TabbedPage/TabbedPageWithDrawerPage.xaml | 56 + .../TabbedPageWithDrawerPage.xaml.cs | 114 ++ .../TabbedPageWithNavigationPage.xaml | 50 + .../TabbedPageWithNavigationPage.xaml.cs | 100 + .../Pages/TableViewPage.axaml | 71 + .../Pages/TableViewPage.axaml.cs | 13 + .../Pages/TextBlockPage.xaml | 141 ++ .../Pages/TextBlockPage.xaml.cs | 12 + .../Pages/TextBoxPage.xaml | 105 + .../Pages/TextBoxPage.xaml.cs | 12 + .../Pages/ThemePage.axaml | 80 + .../Pages/ThemePage.axaml.cs | 33 + .../Pages/ToggleSwitchPage.xaml | 81 + .../Pages/ToggleSwitchPage.xaml.cs | 12 + .../Pages/ToolTipPage.xaml | 95 + .../Pages/ToolTipPage.xaml.cs | 42 + .../TransitioningContentControlPage.axaml | 85 + .../TransitioningContentControlPage.axaml.cs | 12 + .../Transitions/CardStackPageTransition.cs | 447 ++++ .../Transitions/WaveRevealPageTransition.cs | 380 ++++ .../Pages/TreeViewPage.xaml | 36 + .../Pages/TreeViewPage.xaml.cs | 14 + .../Pages/ViewboxPage.xaml | 54 + .../Pages/ViewboxPage.xaml.cs | 12 + .../Pages/WindowCustomizationsPage.xaml | 68 + .../Pages/WindowCustomizationsPage.xaml.cs | 12 + .../AvaloniaControlCatalog/Pages/teapot.bin | Bin 0 -> 15464 bytes .../PlatformRenderInterface.cs | 315 +++ samples/AvaloniaControlCatalog/Program.cs | 56 + .../RectangleGeometryImpl.cs | 27 + samples/AvaloniaControlCatalog/RegionImpl.cs | 106 + .../RenderTargetBitmapImpl.cs | 106 + .../RenderTargetImpl.cs | 58 + .../AvaloniaControlCatalog/ScrollPage.xaml | 34 + .../StreamGeometryImpl.cs | 216 ++ .../AvaloniaControlCatalog/TextShaperImpl.cs | 185 ++ .../TransformedGeometryImpl.cs | 47 + .../TransparentStyles.axaml | 25 + .../TransparentStyles.axaml.cs | 5 + .../ViewModels/ApplicationViewModel.cs | 26 + .../ViewModels/ComboBoxPageViewModel.cs | 50 + .../ViewModels/ContextPageViewModel.cs | 74 + .../ViewModels/CursorPageViewModel.cs | 43 + .../ViewModels/DataValidationViewModel.cs | 45 + .../ViewModels/ExpanderPageViewModel.cs | 27 + .../ViewModels/ListBoxPageViewModel.cs | 132 ++ .../ViewModels/MainWindowViewModel.cs | 150 ++ .../MainWindowViewModel_PageList.cs | 248 +++ .../ViewModels/MenuItemViewModel.cs | 13 + .../ViewModels/MenuPageViewModel.cs | 96 + .../ViewModels/NotificationViewModel.cs | 44 + .../PlatformInformationViewModel.cs | 58 + .../ViewModels/RefreshContainerViewModel.cs | 25 + .../ViewModels/SplitViewPageViewModel.cs | 54 + .../ViewModels/TabControlPageViewModel.cs | 26 + .../ViewModels/TableViewPageViewModel.cs | 246 +++ ...ransitioningContentControlPageViewModel.cs | 317 +++ .../ViewModels/TreeViewPageViewModel.cs | 122 ++ .../Views/CustomNotificationView.xaml | 21 + .../Views/CustomNotificationView.xaml.cs | 12 + .../AvaloniaControlCatalog/WebGPULayerImpl.cs | 153 ++ .../WebGPURenderTargetImpl.cs | 142 ++ .../WriteableBitmapImpl.cs | 202 ++ samples/AvaloniaControlCatalog/app.manifest | 11 + 591 files changed, 54327 insertions(+), 92 deletions(-) create mode 100644 samples/AvaloniaControlCatalog/App.xaml create mode 100644 samples/AvaloniaControlCatalog/App.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Assets/CurvedHeader/avatar.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/CurvedHeader/featured.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/CurvedHeader/product1.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/CurvedHeader/product2.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/CurvedHeader/product3.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/CurvedHeader/update1.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/CurvedHeader/update2.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/CurvedHeader/update3.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Bold.ttf create mode 100644 samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-BoldItalic.ttf create mode 100644 samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Italic.ttf create mode 100644 samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Regular.ttf create mode 100644 samples/AvaloniaControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf create mode 100644 samples/AvaloniaControlCatalog/Assets/Icon.png create mode 100644 samples/AvaloniaControlCatalog/Assets/ModernApp/avatar.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/ModernApp/dest_alps.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/ModernApp/dest_forest.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/ModernApp/dest_norway.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/ModernApp/exp_angkor.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/ModernApp/exp_tokyo.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_alpine.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_bay.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_city.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_paris.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_tropical.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_venice.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/ModernApp/story1.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/ModernApp/story2.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/ModernApp/story3.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Movies/cast1.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Movies/cast2.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Movies/continue1.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Movies/hero.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Movies/morelike1.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Movies/search1.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Movies/toprated1.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Movies/toprated2.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Movies/toprated3.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Movies/toprated4.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Movies/trending1.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Movies/trending2.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Pulse/cat_hiit.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Pulse/cat_strength.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Pulse/cat_yoga.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Pulse/ex_bench.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Pulse/ex_deadlifts.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Pulse/ex_overhead.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Pulse/ex_pullups.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Pulse/ex_squats.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Pulse/profile_avatar.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Pulse/rec_fullbody.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Pulse/rec_mobility.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Pulse/rec_powercore.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Pulse/workout_hero.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Restaurant/dish1.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Restaurant/dish2.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Restaurant/dish3.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Restaurant/dish4.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Restaurant/featured_dish.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Restaurant/user_avatar.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/RetroGaming/cyber_city.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/RetroGaming/dungeon_bit.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/RetroGaming/forest_spirit.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/RetroGaming/hero.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/RetroGaming/neon_ninja.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/RetroGaming/neon_racer.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/RetroGaming/pixel_quest.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/RetroGaming/space_voids.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Sanctuary/city_bg.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Sanctuary/forest_bg.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Sanctuary/main_arctic_silence.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Sanctuary/main_deep_forest.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Sanctuary/main_desert_sands.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Sanctuary/main_hero.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/Sanctuary/mountain_bg.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/avalonia-32.png create mode 100644 samples/AvaloniaControlCatalog/Assets/delicate-arch-896885_640.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/github_icon.png create mode 100644 samples/AvaloniaControlCatalog/Assets/hirsch-899118_640.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/image1.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/image2.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/image3.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/image4.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/image5.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/image6.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/image7.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/maple-leaf-888807_640.jpg create mode 100644 samples/AvaloniaControlCatalog/Assets/test_icon.ico create mode 100644 samples/AvaloniaControlCatalog/AvaloniaControlCatalog.csproj create mode 100644 samples/AvaloniaControlCatalog/AvaloniaReactive/AnonymousObserverNonGenericHelper.cs create mode 100644 samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyBindingObservable.cs create mode 100644 samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyChangedObservable.cs create mode 100644 samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyObservable.cs create mode 100644 samples/AvaloniaControlCatalog/AvaloniaReactive/CombinedSubject.cs create mode 100644 samples/AvaloniaControlCatalog/AvaloniaReactive/CompositeDisposable.cs create mode 100644 samples/AvaloniaControlCatalog/AvaloniaReactive/Disposable.cs create mode 100644 samples/AvaloniaControlCatalog/AvaloniaReactive/DisposableMixin.cs create mode 100644 samples/AvaloniaControlCatalog/AvaloniaReactive/IAvaloniaSubject.cs create mode 100644 samples/AvaloniaControlCatalog/AvaloniaReactive/LightweightObservableBase.cs create mode 100644 samples/AvaloniaControlCatalog/AvaloniaReactive/LightweightSubject.cs create mode 100644 samples/AvaloniaControlCatalog/AvaloniaReactive/Observable.cs create mode 100644 samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/CombineLatest.cs create mode 100644 samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/Sink.cs create mode 100644 samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/Switch.cs create mode 100644 samples/AvaloniaControlCatalog/AvaloniaReactive/SingleSubscriberObservableBase.cs create mode 100644 samples/AvaloniaControlCatalog/CombinedGeometryImpl.cs create mode 100644 samples/AvaloniaControlCatalog/Converter/DegToRadConverter.cs create mode 100644 samples/AvaloniaControlCatalog/Converter/GdpToBrushConverter.cs create mode 100644 samples/AvaloniaControlCatalog/Converter/HexConverter.cs create mode 100644 samples/AvaloniaControlCatalog/Converter/MathSubtractConverter.cs create mode 100644 samples/AvaloniaControlCatalog/Converter/TableViewColumnWidthConverter.cs create mode 100644 samples/AvaloniaControlCatalog/DecoratedWindow.xaml create mode 100644 samples/AvaloniaControlCatalog/DecoratedWindow.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/DrawingBackendMode.cs create mode 100644 samples/AvaloniaControlCatalog/DrawingContextImpl.cs create mode 100644 samples/AvaloniaControlCatalog/EllipseGeometryImpl.cs create mode 100644 samples/AvaloniaControlCatalog/FontManagerImpl.cs create mode 100644 samples/AvaloniaControlCatalog/GeometryGroupImpl.cs create mode 100644 samples/AvaloniaControlCatalog/GeometryImpl.cs create mode 100644 samples/AvaloniaControlCatalog/GlobalUsings.cs create mode 100644 samples/AvaloniaControlCatalog/GlyphRunImpl.cs create mode 100644 samples/AvaloniaControlCatalog/IDrawableBitmapImpl.cs create mode 100644 samples/AvaloniaControlCatalog/Icons.cs create mode 100644 samples/AvaloniaControlCatalog/ImageSharpDrawingAppBuilderExtensions.cs create mode 100644 samples/AvaloniaControlCatalog/ImageSharpExtensions.cs create mode 100644 samples/AvaloniaControlCatalog/ImmutableBitmapImpl.cs create mode 100644 samples/AvaloniaControlCatalog/LineGeometryImpl.cs create mode 100644 samples/AvaloniaControlCatalog/MainView.xaml create mode 100644 samples/AvaloniaControlCatalog/MainView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/MainWindow.xaml create mode 100644 samples/AvaloniaControlCatalog/MainWindow.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/MiniMvvm/MiniCommand.cs create mode 100644 samples/AvaloniaControlCatalog/MiniMvvm/PropertyChangedExtensions.cs create mode 100644 samples/AvaloniaControlCatalog/MiniMvvm/ViewModelBase.cs create mode 100644 samples/AvaloniaControlCatalog/Models/CatalogTheme.cs create mode 100644 samples/AvaloniaControlCatalog/Models/Person.cs create mode 100644 samples/AvaloniaControlCatalog/Models/StateData.cs create mode 100644 samples/AvaloniaControlCatalog/NavHeaderItem.xaml create mode 100644 samples/AvaloniaControlCatalog/NuGet.Config create mode 100644 samples/AvaloniaControlCatalog/Pages/AcceleratorPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/AcceleratorPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/AcrylicPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/AcrylicPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/AdornerLayerPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/AdornerLayerPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/AutoCompleteBoxPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/BitmapCachePage.axaml create mode 100644 samples/AvaloniaControlCatalog/Pages/BitmapCachePage.axaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/BorderPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/BorderPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ButtonSpinnerPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ButtonSpinnerPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ButtonsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ButtonsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CanvasPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CanvasPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselDemoPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselDemoPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselCustomizationPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselCustomizationPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselDataBindingPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselDataBindingPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselGalleryAppPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselGalleryAppPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselGesturesPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselGesturesPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselGettingStartedPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselGettingStartedPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselMultiItemPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselMultiItemPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselPageCustomizationPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselPageCustomizationPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselPageDataTemplatePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselPageDataTemplatePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselPageEventsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselPageEventsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselPageFirstLookPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselPageFirstLookPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselPageGesturePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselPageGesturePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselPagePerformancePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselPagePerformancePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselPageSelectionPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselPageSelectionPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselPageTransitionsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselPageTransitionsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselTransitionsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselTransitionsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselVerticalPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/CarouselVerticalPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CheckBoxPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CheckBoxPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ClipboardPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ClipboardPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ColorPickerPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ColorPickerPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ComboBoxPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ComboBoxPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBar/CommandBarEventsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBar/CommandBarEventsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBar/CommandBarFirstLookPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBar/CommandBarFirstLookPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBar/CommandBarKeyboardPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBar/CommandBarKeyboardPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBar/CommandBarLabelPositionPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBar/CommandBarLabelPositionPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBar/CommandBarOverflowPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBar/CommandBarOverflowPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBar/CommandBarTogglePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBar/CommandBarTogglePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBarPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CommandBarPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CompositionPage.axaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CompositionPage.axaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ConnectedAnimationDemoPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ConnectedAnimationDemoPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ContainerQueryPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ContainerQueryPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ContentDemoPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ContentDemoPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ContentPage/ContentPageCommandBarPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ContentPage/ContentPageCommandBarPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ContentPage/ContentPageCustomizationPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ContentPage/ContentPageCustomizationPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ContentPage/ContentPageEventsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ContentPage/ContentPageEventsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ContentPage/ContentPageFirstLookPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ContentPage/ContentPageFirstLookPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ContentPage/ContentPagePerformancePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ContentPage/ContentPagePerformancePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ContentPage/ContentPageSafeAreaPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ContentPage/ContentPageSafeAreaPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ContextFlyoutPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ContextFlyoutPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ContextMenuPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ContextMenuPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CursorPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CursorPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CustomDrawing.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/CustomDrawing.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/CustomDrawingExampleControl.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DataValidationPage.axaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DataValidationPage.axaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DateTimePickerPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DateTimePickerPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DialogsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DialogsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DragAndDropPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DragAndDropPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerDemoPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerDemoPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageFirstLookPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageFirstLookPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageNavigationPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageNavigationPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPagePerformancePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPagePerformancePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageRtlPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageRtlPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerCommunityView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerCommunityView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerHabitsView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerHabitsView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerHomeView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerHomeView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernAppPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernAppPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernDiscoverView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernDiscoverView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernMyTripsView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernMyTripsView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernProfileView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernProfileView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ExpanderPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ExpanderPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/FlyoutsPage.axaml create mode 100644 samples/AvaloniaControlCatalog/Pages/FlyoutsPage.axaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/FocusPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/FocusPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/GesturePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/GesturePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchZoomPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchZoomPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/Gestures/GesturePullPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/Gestures/GesturePullPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/Gestures/GestureSwipePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/Gestures/GestureSwipePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/HeaderedContentPage.axaml create mode 100644 samples/AvaloniaControlCatalog/Pages/HeaderedContentPage.axaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ImagePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ImagePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/LabelsPage.axaml create mode 100644 samples/AvaloniaControlCatalog/Pages/LabelsPage.axaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/LayoutTransformControlPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/LayoutTransformControlPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ListBoxPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ListBoxPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/MenuPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/MenuPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NativeEmbedPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NativeEmbedPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationDemoHelper.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationDemoPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationDemoPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderProfileScrollView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderProfileScrollView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirDishDetailView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirDishDetailView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirMenuView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirMenuView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirProfileView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirProfileView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirReservationsView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirReservationsView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageAttachedMethodsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageAttachedMethodsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageBackButtonPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageBackButtonPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageCurvedHeaderPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageCurvedHeaderPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageEventsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageEventsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageFirstLookPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageFirstLookPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageGesturePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageGesturePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageInteractiveHeaderPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageInteractiveHeaderPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageModalPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageModalPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageModalTransitionsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageModalTransitionsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageMvvmNavigation.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageMvvmPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageMvvmPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageMvvmPageFactory.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageMvvmViewModels.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPagePassDataPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPagePassDataPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPagePerformancePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPagePerformancePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageScrollAwarePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageScrollAwarePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageStackPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageStackPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageTitlePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageTitlePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageToolbarPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageToolbarPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageTransitionsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageTransitionsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseAppPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseAppPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseHomeView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseHomeView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseLoginView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseLoginView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseProfileView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseProfileView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutDetailView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutDetailView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/CompositeTransition.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/FadeThroughTransition.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/PageSlideTransition.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/ParallaxSlideTransition.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NavigationPerformanceMonitorHelper.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NotificationsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NotificationsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/NumericUpDownPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/NumericUpDownPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/OpenGl/GlPageKnobs.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/OpenGl/GlPageKnobs.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/OpenGl/OpenGlContent.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/OpenGl/OpenGlFbo.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/OpenGl/OpenGlLeasePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/OpenGl/OpenGlLeasePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/OpenGlPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/OpenGlPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/PipsPagerPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/PipsPagerPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/PlatformInfoPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/PlatformInfoPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/PointerCanvas.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/PointerContactsTab.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/PointersPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/PointersPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ProgressBarPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ProgressBarPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/RadioButtonPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/RadioButtonPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/RefreshContainerPage.axaml create mode 100644 samples/AvaloniaControlCatalog/Pages/RefreshContainerPage.axaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/RelativePanelPage.axaml create mode 100644 samples/AvaloniaControlCatalog/Pages/RelativePanelPage.axaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ScreenPage.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ScrollViewerPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ScrollViewerPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/SliderPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/SliderPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/SplitViewPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/SplitViewPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabControlPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabControlPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabStripPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabStripPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedDemoPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedDemoPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/CenteredTabPanel.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavBar.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavItem.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageCollectionPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageCollectionPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageCustomTabBarPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageCustomTabBarPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageCustomizationPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageCustomizationPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageDataTemplatePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageDataTemplatePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageDisabledTabsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageDisabledTabsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageEventsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageEventsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFirstLookPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFirstLookPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFluidNavPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFluidNavPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageGesturePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageGesturePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageKeyboardPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageKeyboardPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPagePerformancePage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPagePerformancePage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPagePlacementPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPagePlacementPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageProgrammaticPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageProgrammaticPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageTransitionsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageTransitionsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithDrawerPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithDrawerPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TableViewPage.axaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TableViewPage.axaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TextBlockPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TextBlockPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TextBoxPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TextBoxPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ThemePage.axaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ThemePage.axaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ToggleSwitchPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ToggleSwitchPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ToolTipPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ToolTipPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TransitioningContentControlPage.axaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TransitioningContentControlPage.axaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/Transitions/CardStackPageTransition.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/Transitions/WaveRevealPageTransition.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/TreeViewPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/TreeViewPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/ViewboxPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/ViewboxPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/WindowCustomizationsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/WindowCustomizationsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/teapot.bin create mode 100644 samples/AvaloniaControlCatalog/PlatformRenderInterface.cs create mode 100644 samples/AvaloniaControlCatalog/Program.cs create mode 100644 samples/AvaloniaControlCatalog/RectangleGeometryImpl.cs create mode 100644 samples/AvaloniaControlCatalog/RegionImpl.cs create mode 100644 samples/AvaloniaControlCatalog/RenderTargetBitmapImpl.cs create mode 100644 samples/AvaloniaControlCatalog/RenderTargetImpl.cs create mode 100644 samples/AvaloniaControlCatalog/ScrollPage.xaml create mode 100644 samples/AvaloniaControlCatalog/StreamGeometryImpl.cs create mode 100644 samples/AvaloniaControlCatalog/TextShaperImpl.cs create mode 100644 samples/AvaloniaControlCatalog/TransformedGeometryImpl.cs create mode 100644 samples/AvaloniaControlCatalog/TransparentStyles.axaml create mode 100644 samples/AvaloniaControlCatalog/TransparentStyles.axaml.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/ApplicationViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/ComboBoxPageViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/ContextPageViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/CursorPageViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/DataValidationViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/ExpanderPageViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/ListBoxPageViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/MainWindowViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/MainWindowViewModel_PageList.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/MenuItemViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/MenuPageViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/NotificationViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/PlatformInformationViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/RefreshContainerViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/SplitViewPageViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/TabControlPageViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/TableViewPageViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/TreeViewPageViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/Views/CustomNotificationView.xaml create mode 100644 samples/AvaloniaControlCatalog/Views/CustomNotificationView.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/WebGPULayerImpl.cs create mode 100644 samples/AvaloniaControlCatalog/WebGPURenderTargetImpl.cs create mode 100644 samples/AvaloniaControlCatalog/WriteableBitmapImpl.cs create mode 100644 samples/AvaloniaControlCatalog/app.manifest diff --git a/.gitignore b/.gitignore index 4e61c3c8..b3124ebd 100644 --- a/.gitignore +++ b/.gitignore @@ -232,5 +232,3 @@ SixLabors.Shapes.Coverage.xml .codex-* .claude /plans/ - -samples/Avalonia** diff --git a/ImageSharp.Drawing.All.sln b/ImageSharp.Drawing.All.sln index f82b7951..8c2b44c3 100644 --- a/ImageSharp.Drawing.All.sln +++ b/ImageSharp.Drawing.All.sln @@ -318,10 +318,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Png", "Png", "{E1C42A6F-913 tests\Images\Input\Png\zlib-overflow.png = tests\Images\Input\Png\zlib-overflow.png EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Drawing.Tests", "tests\ImageSharp.Drawing.Tests\ImageSharp.Drawing.Tests.csproj", "{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Drawing.Benchmarks", "tests\ImageSharp.Drawing.Benchmarks\ImageSharp.Drawing.Benchmarks.csproj", "{59804113-1DD4-4F80-8D06-35FF71652508}" -EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SharedInfrastructure", "shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.shproj", "{68A8CC40-6AED-4E96-B524-31B1158FDEEA}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{23859314-5693-4E6C-BE5C-80A433439D2A}" @@ -329,22 +325,102 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.WebGPU", "src\ImageSharp.Drawing.WebGPU\ImageSharp.Drawing.WebGPU.csproj", "{061582C2-658F-40AE-A978-7D74A4EB2C0A}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.WebGPU.ShaderGen", "src\ImageSharp.Drawing.WebGPU.ShaderGen\ImageSharp.Drawing.WebGPU.ShaderGen.csproj", "{C7606104-5D58-4670-912C-3F336606B02D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.ManualBenchmarks", "tests\ImageSharp.Drawing.ManualBenchmarks\ImageSharp.Drawing.ManualBenchmarks.csproj", "{70193989-E587-451A-AF34-4C74FEE5DA5A}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebGPUWindowDemo", "samples\WebGPUWindowDemo\WebGPUWindowDemo.csproj", "{999D90DE-6940-4693-9F80-701F2D5DB77A}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawShapesWithImageSharp", "samples\DrawShapesWithImageSharp\DrawShapesWithImageSharp.csproj", "{43A3CA46-2675-4AD4-88FC-DDC2E657F0B7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebGPUExternalSurfaceDemo", "samples\WebGPUExternalSurfaceDemo\WebGPUExternalSurfaceDemo.csproj", "{F0AAE591-A44E-48A5-8C74-395BDF98F646}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AvaloniaControlCatalog", "samples\AvaloniaControlCatalog\AvaloniaControlCatalog.csproj", "{333F3D4F-D251-5601-4851-307FF0F13D23}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SixLabors.Fonts", "..\Fonts\src\SixLabors.Fonts\SixLabors.Fonts.csproj", "{4A922B77-34EC-EA6A-8E96-8353C8FA0640}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolygonClipper", "..\PolygonClipper\src\PolygonClipper\PolygonClipper.csproj", "{5EC089DC-69F7-643B-93BB-293D199F5F74}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "temp", "temp", "{29F3AA1B-6D75-C75D-9183-749A65CD24A7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.ColorPicker", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj", "{38CA36F4-AD3A-4B37-AB75-9DEF57219A42}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Base", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.Base\Avalonia.Base.csproj", "{9AFBFA31-F636-4685-A8B9-4F623139C01B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevAnalyzers", "..\..\JimBobSquarePants\Avalonia\src\tools\DevAnalyzers\DevAnalyzers.csproj", "{AA365C58-287B-47CE-8EDA-6FA3BF10574E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Analyzers.CSharp", "..\..\JimBobSquarePants\Avalonia\src\tools\Avalonia.Analyzers.CSharp\Avalonia.Analyzers.CSharp.csproj", "{80A082A3-4FC7-4AD4-AFB6-503B3033B32D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Analyzers.CodeFixes.CSharp", "..\..\JimBobSquarePants\Avalonia\src\tools\Avalonia.Analyzers.CodeFixes.CSharp\Avalonia.Analyzers.CodeFixes.CSharp.csproj", "{7237DC3A-C8C5-49C1-A9CB-00D93E8F6CED}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Analyzers.VisualBasic", "..\..\JimBobSquarePants\Avalonia\src\tools\Avalonia.Analyzers.VisualBasic\Avalonia.Analyzers.VisualBasic.csproj", "{E78916D9-1240-4C13-BEC3-242AFF8BFB84}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevGenerators", "..\..\JimBobSquarePants\Avalonia\src\tools\DevGenerators\DevGenerators.csproj", "{F253A0A3-3202-45AE-A2F5-30098F8B3BB8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Build.Tasks", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj", "{7BD98865-305F-428F-BDCE-10311B96174A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Remote.Protocol", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj", "{3CCFA7A8-ED2D-4E85-9024-9FCF5EC63C86}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Markup.Xaml", "..\..\JimBobSquarePants\Avalonia\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj", "{2956B3D9-23A0-4818-A12D-178C15F5F3D9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.Controls\Avalonia.Controls.csproj", "{505BA299-AAE0-4258-934F-5CF9B6DC4EAC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Markup", "..\..\JimBobSquarePants\Avalonia\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj", "{D3730B8F-1568-4CAD-8617-0BA565E435D7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Desktop", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.Desktop\Avalonia.Desktop.csproj", "{3EADAED3-1360-4944-9434-DE3359DE2233}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Win32", "..\..\JimBobSquarePants\Avalonia\src\Windows\Avalonia.Win32\Avalonia.Win32.csproj", "{B8D27D9E-1D57-4F89-9CCE-B1B5E947947D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia", "..\..\JimBobSquarePants\Avalonia\packages\Avalonia\Avalonia.csproj", "{7A7E13F8-68AE-4EE3-AA58-410EB33125E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators", "..\..\JimBobSquarePants\Avalonia\src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{5DA79C9E-5625-4D88-9D42-674D27538B74}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DesignerSupport", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj", "{B7840472-147D-49DA-9AC4-0EEB5E9F27FC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.OpenGL", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.OpenGL\Avalonia.OpenGL.csproj", "{0CFDCBC5-32FD-458B-A487-D5E9551BADF9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Metal", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.Metal\Avalonia.Metal.csproj", "{45053362-1DBC-4EAD-8B4C-51B6E56F60F6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Vulkan", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.Vulkan\Avalonia.Vulkan.csproj", "{ABD471D5-4CDF-48E8-BD19-0CC2AA4868EB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Dialogs", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{1C3FDE70-832F-41FB-A9A4-4DD8DBBBF171}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.MicroCom", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.MicroCom\Avalonia.MicroCom.csproj", "{93A496DB-F661-49E0-97C5-4680D92163ED}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Win32.Automation", "..\..\JimBobSquarePants\Avalonia\src\Windows\Avalonia.Win32.Automation\Avalonia.Win32.Automation.csproj", "{F7F47D90-331B-4D44-BB01-5CE31A2D2532}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Skia", "..\..\JimBobSquarePants\Avalonia\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj", "{F0AA05AC-508E-4D13-B416-7D080A6ED5B0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Native", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.Native\Avalonia.Native.csproj", "{14E80B26-EDD6-468F-BC51-350DD9C83100}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.X11", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.X11\Avalonia.X11.csproj", "{1AC23054-F30F-48A9-B750-1D60BDCFE3FD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.FreeDesktop", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{F1FD356A-0712-4379-AF87-11BB6F4D554B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.FreeDesktop.AtSpi", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.FreeDesktop.AtSpi\Avalonia.FreeDesktop.AtSpi.csproj", "{E241F16B-FE81-4107-8C9F-6BFA029C7934}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DBus.Generators", "..\..\JimBobSquarePants\Avalonia\src\tools\Avalonia.DBus.Generators\Avalonia.DBus.Generators.csproj", "{B1749E6E-EF88-4C46-89BC-CF1BF7A46851}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.HarfBuzz", "..\..\JimBobSquarePants\Avalonia\src\HarfBuzz\Avalonia.HarfBuzz\Avalonia.HarfBuzz.csproj", "{A45F0090-68EA-4FC5-AAEB-6D71CBB1483E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{099273DB-F00E-43C2-BCAE-DA7A1C3AB49D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Themes.Fluent", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{F28E672C-DBA2-496E-8784-E6F17CE261D5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Themes.Simple", "..\..\JimBobSquarePants\Avalonia\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj", "{5483EE45-0838-46C4-B59B-5B52F7C7D975}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp", "..\ImageSharp\src\ImageSharp\ImageSharp.csproj", "{FE71764E-E7BD-B378-8A4C-86929A28AD25}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawingBackendBenchmark", "samples\DrawingBackendBenchmark\DrawingBackendBenchmark.csproj", "{B5CF5531-624F-9F49-46BD-937FF4062F54}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebGPUExternalSurfaceDemo", "samples\WebGPUExternalSurfaceDemo\WebGPUExternalSurfaceDemo.csproj", "{97619DEB-226C-EB67-140D-0C7E67B2F7D0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebGPUWindowDemo", "samples\WebGPUWindowDemo\WebGPUWindowDemo.csproj", "{4570F31D-D00F-6A80-DB7C-74826CBD15AE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.WebGPU", "src\ImageSharp.Drawing.WebGPU\ImageSharp.Drawing.WebGPU.csproj", "{638FC2AE-7FC1-6527-E051-C6B34803F08D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.Tests", "tests\ImageSharp.Drawing.Tests\ImageSharp.Drawing.Tests.csproj", "{D73D1783-E5F5-5094-6D3E-D77B45A51B4A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.Benchmarks", "tests\ImageSharp.Drawing.Benchmarks\ImageSharp.Drawing.Benchmarks.csproj", "{78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.ManualBenchmarks", "tests\ImageSharp.Drawing.ManualBenchmarks\ImageSharp.Drawing.ManualBenchmarks.csproj", "{62B1D30A-935F-819E-A227-A7FFF2E12CC1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -367,42 +443,6 @@ Global {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|x64.Build.0 = Release|Any CPU {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|x86.ActiveCfg = Release|Any CPU {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|x86.Build.0 = Release|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x64.ActiveCfg = Debug|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x64.Build.0 = Debug|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x86.ActiveCfg = Debug|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x86.Build.0 = Debug|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.Build.0 = Release|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x64.ActiveCfg = Release|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x64.Build.0 = Release|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x86.ActiveCfg = Release|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x86.Build.0 = Release|Any CPU - {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|Any CPU.Build.0 = Debug|Any CPU - {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|x64.ActiveCfg = Debug|Any CPU - {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|x64.Build.0 = Debug|Any CPU - {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|x86.ActiveCfg = Debug|Any CPU - {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|x86.Build.0 = Debug|Any CPU - {59804113-1DD4-4F80-8D06-35FF71652508}.Release|Any CPU.ActiveCfg = Release|Any CPU - {59804113-1DD4-4F80-8D06-35FF71652508}.Release|Any CPU.Build.0 = Release|Any CPU - {59804113-1DD4-4F80-8D06-35FF71652508}.Release|x64.ActiveCfg = Release|Any CPU - {59804113-1DD4-4F80-8D06-35FF71652508}.Release|x64.Build.0 = Release|Any CPU - {59804113-1DD4-4F80-8D06-35FF71652508}.Release|x86.ActiveCfg = Release|Any CPU - {59804113-1DD4-4F80-8D06-35FF71652508}.Release|x86.Build.0 = Release|Any CPU - {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|x64.ActiveCfg = Debug|Any CPU - {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|x64.Build.0 = Debug|Any CPU - {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|x86.ActiveCfg = Debug|Any CPU - {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|x86.Build.0 = Debug|Any CPU - {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|Any CPU.Build.0 = Release|Any CPU - {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|x64.ActiveCfg = Release|Any CPU - {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|x64.Build.0 = Release|Any CPU - {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|x86.ActiveCfg = Release|Any CPU - {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|x86.Build.0 = Release|Any CPU {C7606104-5D58-4670-912C-3F336606B02D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C7606104-5D58-4670-912C-3F336606B02D}.Debug|Any CPU.Build.0 = Debug|Any CPU {C7606104-5D58-4670-912C-3F336606B02D}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -415,30 +455,6 @@ Global {C7606104-5D58-4670-912C-3F336606B02D}.Release|x64.Build.0 = Release|Any CPU {C7606104-5D58-4670-912C-3F336606B02D}.Release|x86.ActiveCfg = Release|Any CPU {C7606104-5D58-4670-912C-3F336606B02D}.Release|x86.Build.0 = Release|Any CPU - {70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|x64.ActiveCfg = Debug|Any CPU - {70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|x64.Build.0 = Debug|Any CPU - {70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|x86.ActiveCfg = Debug|Any CPU - {70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|x86.Build.0 = Debug|Any CPU - {70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|Any CPU.Build.0 = Release|Any CPU - {70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|x64.ActiveCfg = Release|Any CPU - {70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|x64.Build.0 = Release|Any CPU - {70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|x86.ActiveCfg = Release|Any CPU - {70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|x86.Build.0 = Release|Any CPU - {999D90DE-6940-4693-9F80-701F2D5DB77A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {999D90DE-6940-4693-9F80-701F2D5DB77A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {999D90DE-6940-4693-9F80-701F2D5DB77A}.Debug|x64.ActiveCfg = Debug|Any CPU - {999D90DE-6940-4693-9F80-701F2D5DB77A}.Debug|x64.Build.0 = Debug|Any CPU - {999D90DE-6940-4693-9F80-701F2D5DB77A}.Debug|x86.ActiveCfg = Debug|Any CPU - {999D90DE-6940-4693-9F80-701F2D5DB77A}.Debug|x86.Build.0 = Debug|Any CPU - {999D90DE-6940-4693-9F80-701F2D5DB77A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {999D90DE-6940-4693-9F80-701F2D5DB77A}.Release|Any CPU.Build.0 = Release|Any CPU - {999D90DE-6940-4693-9F80-701F2D5DB77A}.Release|x64.ActiveCfg = Release|Any CPU - {999D90DE-6940-4693-9F80-701F2D5DB77A}.Release|x64.Build.0 = Release|Any CPU - {999D90DE-6940-4693-9F80-701F2D5DB77A}.Release|x86.ActiveCfg = Release|Any CPU - {999D90DE-6940-4693-9F80-701F2D5DB77A}.Release|x86.Build.0 = Release|Any CPU {43A3CA46-2675-4AD4-88FC-DDC2E657F0B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {43A3CA46-2675-4AD4-88FC-DDC2E657F0B7}.Debug|Any CPU.Build.0 = Debug|Any CPU {43A3CA46-2675-4AD4-88FC-DDC2E657F0B7}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -451,18 +467,450 @@ Global {43A3CA46-2675-4AD4-88FC-DDC2E657F0B7}.Release|x64.Build.0 = Release|Any CPU {43A3CA46-2675-4AD4-88FC-DDC2E657F0B7}.Release|x86.ActiveCfg = Release|Any CPU {43A3CA46-2675-4AD4-88FC-DDC2E657F0B7}.Release|x86.Build.0 = Release|Any CPU - {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Debug|x64.ActiveCfg = Debug|Any CPU - {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Debug|x64.Build.0 = Debug|Any CPU - {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Debug|x86.ActiveCfg = Debug|Any CPU - {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Debug|x86.Build.0 = Debug|Any CPU - {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Release|Any CPU.Build.0 = Release|Any CPU - {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Release|x64.ActiveCfg = Release|Any CPU - {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Release|x64.Build.0 = Release|Any CPU - {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Release|x86.ActiveCfg = Release|Any CPU - {F0AAE591-A44E-48A5-8C74-395BDF98F646}.Release|x86.Build.0 = Release|Any CPU + {333F3D4F-D251-5601-4851-307FF0F13D23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {333F3D4F-D251-5601-4851-307FF0F13D23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {333F3D4F-D251-5601-4851-307FF0F13D23}.Debug|x64.ActiveCfg = Debug|Any CPU + {333F3D4F-D251-5601-4851-307FF0F13D23}.Debug|x64.Build.0 = Debug|Any CPU + {333F3D4F-D251-5601-4851-307FF0F13D23}.Debug|x86.ActiveCfg = Debug|Any CPU + {333F3D4F-D251-5601-4851-307FF0F13D23}.Debug|x86.Build.0 = Debug|Any CPU + {333F3D4F-D251-5601-4851-307FF0F13D23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {333F3D4F-D251-5601-4851-307FF0F13D23}.Release|Any CPU.Build.0 = Release|Any CPU + {333F3D4F-D251-5601-4851-307FF0F13D23}.Release|x64.ActiveCfg = Release|Any CPU + {333F3D4F-D251-5601-4851-307FF0F13D23}.Release|x64.Build.0 = Release|Any CPU + {333F3D4F-D251-5601-4851-307FF0F13D23}.Release|x86.ActiveCfg = Release|Any CPU + {333F3D4F-D251-5601-4851-307FF0F13D23}.Release|x86.Build.0 = Release|Any CPU + {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Debug|x64.Build.0 = Debug|Any CPU + {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Debug|x86.ActiveCfg = Debug|Any CPU + {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Debug|x86.Build.0 = Debug|Any CPU + {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Release|Any CPU.Build.0 = Release|Any CPU + {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Release|x64.ActiveCfg = Release|Any CPU + {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Release|x64.Build.0 = Release|Any CPU + {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Release|x86.ActiveCfg = Release|Any CPU + {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Release|x86.Build.0 = Release|Any CPU + {5EC089DC-69F7-643B-93BB-293D199F5F74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5EC089DC-69F7-643B-93BB-293D199F5F74}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5EC089DC-69F7-643B-93BB-293D199F5F74}.Debug|x64.ActiveCfg = Debug|Any CPU + {5EC089DC-69F7-643B-93BB-293D199F5F74}.Debug|x64.Build.0 = Debug|Any CPU + {5EC089DC-69F7-643B-93BB-293D199F5F74}.Debug|x86.ActiveCfg = Debug|Any CPU + {5EC089DC-69F7-643B-93BB-293D199F5F74}.Debug|x86.Build.0 = Debug|Any CPU + {5EC089DC-69F7-643B-93BB-293D199F5F74}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5EC089DC-69F7-643B-93BB-293D199F5F74}.Release|Any CPU.Build.0 = Release|Any CPU + {5EC089DC-69F7-643B-93BB-293D199F5F74}.Release|x64.ActiveCfg = Release|Any CPU + {5EC089DC-69F7-643B-93BB-293D199F5F74}.Release|x64.Build.0 = Release|Any CPU + {5EC089DC-69F7-643B-93BB-293D199F5F74}.Release|x86.ActiveCfg = Release|Any CPU + {5EC089DC-69F7-643B-93BB-293D199F5F74}.Release|x86.Build.0 = Release|Any CPU + {38CA36F4-AD3A-4B37-AB75-9DEF57219A42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38CA36F4-AD3A-4B37-AB75-9DEF57219A42}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38CA36F4-AD3A-4B37-AB75-9DEF57219A42}.Debug|x64.ActiveCfg = Debug|Any CPU + {38CA36F4-AD3A-4B37-AB75-9DEF57219A42}.Debug|x64.Build.0 = Debug|Any CPU + {38CA36F4-AD3A-4B37-AB75-9DEF57219A42}.Debug|x86.ActiveCfg = Debug|Any CPU + {38CA36F4-AD3A-4B37-AB75-9DEF57219A42}.Debug|x86.Build.0 = Debug|Any CPU + {38CA36F4-AD3A-4B37-AB75-9DEF57219A42}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38CA36F4-AD3A-4B37-AB75-9DEF57219A42}.Release|Any CPU.Build.0 = Release|Any CPU + {38CA36F4-AD3A-4B37-AB75-9DEF57219A42}.Release|x64.ActiveCfg = Release|Any CPU + {38CA36F4-AD3A-4B37-AB75-9DEF57219A42}.Release|x64.Build.0 = Release|Any CPU + {38CA36F4-AD3A-4B37-AB75-9DEF57219A42}.Release|x86.ActiveCfg = Release|Any CPU + {38CA36F4-AD3A-4B37-AB75-9DEF57219A42}.Release|x86.Build.0 = Release|Any CPU + {9AFBFA31-F636-4685-A8B9-4F623139C01B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AFBFA31-F636-4685-A8B9-4F623139C01B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AFBFA31-F636-4685-A8B9-4F623139C01B}.Debug|x64.ActiveCfg = Debug|Any CPU + {9AFBFA31-F636-4685-A8B9-4F623139C01B}.Debug|x64.Build.0 = Debug|Any CPU + {9AFBFA31-F636-4685-A8B9-4F623139C01B}.Debug|x86.ActiveCfg = Debug|Any CPU + {9AFBFA31-F636-4685-A8B9-4F623139C01B}.Debug|x86.Build.0 = Debug|Any CPU + {9AFBFA31-F636-4685-A8B9-4F623139C01B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AFBFA31-F636-4685-A8B9-4F623139C01B}.Release|Any CPU.Build.0 = Release|Any CPU + {9AFBFA31-F636-4685-A8B9-4F623139C01B}.Release|x64.ActiveCfg = Release|Any CPU + {9AFBFA31-F636-4685-A8B9-4F623139C01B}.Release|x64.Build.0 = Release|Any CPU + {9AFBFA31-F636-4685-A8B9-4F623139C01B}.Release|x86.ActiveCfg = Release|Any CPU + {9AFBFA31-F636-4685-A8B9-4F623139C01B}.Release|x86.Build.0 = Release|Any CPU + {AA365C58-287B-47CE-8EDA-6FA3BF10574E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA365C58-287B-47CE-8EDA-6FA3BF10574E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA365C58-287B-47CE-8EDA-6FA3BF10574E}.Debug|x64.ActiveCfg = Debug|Any CPU + {AA365C58-287B-47CE-8EDA-6FA3BF10574E}.Debug|x64.Build.0 = Debug|Any CPU + {AA365C58-287B-47CE-8EDA-6FA3BF10574E}.Debug|x86.ActiveCfg = Debug|Any CPU + {AA365C58-287B-47CE-8EDA-6FA3BF10574E}.Debug|x86.Build.0 = Debug|Any CPU + {AA365C58-287B-47CE-8EDA-6FA3BF10574E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA365C58-287B-47CE-8EDA-6FA3BF10574E}.Release|Any CPU.Build.0 = Release|Any CPU + {AA365C58-287B-47CE-8EDA-6FA3BF10574E}.Release|x64.ActiveCfg = Release|Any CPU + {AA365C58-287B-47CE-8EDA-6FA3BF10574E}.Release|x64.Build.0 = Release|Any CPU + {AA365C58-287B-47CE-8EDA-6FA3BF10574E}.Release|x86.ActiveCfg = Release|Any CPU + {AA365C58-287B-47CE-8EDA-6FA3BF10574E}.Release|x86.Build.0 = Release|Any CPU + {80A082A3-4FC7-4AD4-AFB6-503B3033B32D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80A082A3-4FC7-4AD4-AFB6-503B3033B32D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80A082A3-4FC7-4AD4-AFB6-503B3033B32D}.Debug|x64.ActiveCfg = Debug|Any CPU + {80A082A3-4FC7-4AD4-AFB6-503B3033B32D}.Debug|x64.Build.0 = Debug|Any CPU + {80A082A3-4FC7-4AD4-AFB6-503B3033B32D}.Debug|x86.ActiveCfg = Debug|Any CPU + {80A082A3-4FC7-4AD4-AFB6-503B3033B32D}.Debug|x86.Build.0 = Debug|Any CPU + {80A082A3-4FC7-4AD4-AFB6-503B3033B32D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80A082A3-4FC7-4AD4-AFB6-503B3033B32D}.Release|Any CPU.Build.0 = Release|Any CPU + {80A082A3-4FC7-4AD4-AFB6-503B3033B32D}.Release|x64.ActiveCfg = Release|Any CPU + {80A082A3-4FC7-4AD4-AFB6-503B3033B32D}.Release|x64.Build.0 = Release|Any CPU + {80A082A3-4FC7-4AD4-AFB6-503B3033B32D}.Release|x86.ActiveCfg = Release|Any CPU + {80A082A3-4FC7-4AD4-AFB6-503B3033B32D}.Release|x86.Build.0 = Release|Any CPU + {7237DC3A-C8C5-49C1-A9CB-00D93E8F6CED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7237DC3A-C8C5-49C1-A9CB-00D93E8F6CED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7237DC3A-C8C5-49C1-A9CB-00D93E8F6CED}.Debug|x64.ActiveCfg = Debug|Any CPU + {7237DC3A-C8C5-49C1-A9CB-00D93E8F6CED}.Debug|x64.Build.0 = Debug|Any CPU + {7237DC3A-C8C5-49C1-A9CB-00D93E8F6CED}.Debug|x86.ActiveCfg = Debug|Any CPU + {7237DC3A-C8C5-49C1-A9CB-00D93E8F6CED}.Debug|x86.Build.0 = Debug|Any CPU + {7237DC3A-C8C5-49C1-A9CB-00D93E8F6CED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7237DC3A-C8C5-49C1-A9CB-00D93E8F6CED}.Release|Any CPU.Build.0 = Release|Any CPU + {7237DC3A-C8C5-49C1-A9CB-00D93E8F6CED}.Release|x64.ActiveCfg = Release|Any CPU + {7237DC3A-C8C5-49C1-A9CB-00D93E8F6CED}.Release|x64.Build.0 = Release|Any CPU + {7237DC3A-C8C5-49C1-A9CB-00D93E8F6CED}.Release|x86.ActiveCfg = Release|Any CPU + {7237DC3A-C8C5-49C1-A9CB-00D93E8F6CED}.Release|x86.Build.0 = Release|Any CPU + {E78916D9-1240-4C13-BEC3-242AFF8BFB84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E78916D9-1240-4C13-BEC3-242AFF8BFB84}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E78916D9-1240-4C13-BEC3-242AFF8BFB84}.Debug|x64.ActiveCfg = Debug|Any CPU + {E78916D9-1240-4C13-BEC3-242AFF8BFB84}.Debug|x64.Build.0 = Debug|Any CPU + {E78916D9-1240-4C13-BEC3-242AFF8BFB84}.Debug|x86.ActiveCfg = Debug|Any CPU + {E78916D9-1240-4C13-BEC3-242AFF8BFB84}.Debug|x86.Build.0 = Debug|Any CPU + {E78916D9-1240-4C13-BEC3-242AFF8BFB84}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E78916D9-1240-4C13-BEC3-242AFF8BFB84}.Release|Any CPU.Build.0 = Release|Any CPU + {E78916D9-1240-4C13-BEC3-242AFF8BFB84}.Release|x64.ActiveCfg = Release|Any CPU + {E78916D9-1240-4C13-BEC3-242AFF8BFB84}.Release|x64.Build.0 = Release|Any CPU + {E78916D9-1240-4C13-BEC3-242AFF8BFB84}.Release|x86.ActiveCfg = Release|Any CPU + {E78916D9-1240-4C13-BEC3-242AFF8BFB84}.Release|x86.Build.0 = Release|Any CPU + {F253A0A3-3202-45AE-A2F5-30098F8B3BB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F253A0A3-3202-45AE-A2F5-30098F8B3BB8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F253A0A3-3202-45AE-A2F5-30098F8B3BB8}.Debug|x64.ActiveCfg = Debug|Any CPU + {F253A0A3-3202-45AE-A2F5-30098F8B3BB8}.Debug|x64.Build.0 = Debug|Any CPU + {F253A0A3-3202-45AE-A2F5-30098F8B3BB8}.Debug|x86.ActiveCfg = Debug|Any CPU + {F253A0A3-3202-45AE-A2F5-30098F8B3BB8}.Debug|x86.Build.0 = Debug|Any CPU + {F253A0A3-3202-45AE-A2F5-30098F8B3BB8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F253A0A3-3202-45AE-A2F5-30098F8B3BB8}.Release|Any CPU.Build.0 = Release|Any CPU + {F253A0A3-3202-45AE-A2F5-30098F8B3BB8}.Release|x64.ActiveCfg = Release|Any CPU + {F253A0A3-3202-45AE-A2F5-30098F8B3BB8}.Release|x64.Build.0 = Release|Any CPU + {F253A0A3-3202-45AE-A2F5-30098F8B3BB8}.Release|x86.ActiveCfg = Release|Any CPU + {F253A0A3-3202-45AE-A2F5-30098F8B3BB8}.Release|x86.Build.0 = Release|Any CPU + {7BD98865-305F-428F-BDCE-10311B96174A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7BD98865-305F-428F-BDCE-10311B96174A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7BD98865-305F-428F-BDCE-10311B96174A}.Debug|x64.ActiveCfg = Debug|Any CPU + {7BD98865-305F-428F-BDCE-10311B96174A}.Debug|x64.Build.0 = Debug|Any CPU + {7BD98865-305F-428F-BDCE-10311B96174A}.Debug|x86.ActiveCfg = Debug|Any CPU + {7BD98865-305F-428F-BDCE-10311B96174A}.Debug|x86.Build.0 = Debug|Any CPU + {7BD98865-305F-428F-BDCE-10311B96174A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7BD98865-305F-428F-BDCE-10311B96174A}.Release|Any CPU.Build.0 = Release|Any CPU + {7BD98865-305F-428F-BDCE-10311B96174A}.Release|x64.ActiveCfg = Release|Any CPU + {7BD98865-305F-428F-BDCE-10311B96174A}.Release|x64.Build.0 = Release|Any CPU + {7BD98865-305F-428F-BDCE-10311B96174A}.Release|x86.ActiveCfg = Release|Any CPU + {7BD98865-305F-428F-BDCE-10311B96174A}.Release|x86.Build.0 = Release|Any CPU + {3CCFA7A8-ED2D-4E85-9024-9FCF5EC63C86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3CCFA7A8-ED2D-4E85-9024-9FCF5EC63C86}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3CCFA7A8-ED2D-4E85-9024-9FCF5EC63C86}.Debug|x64.ActiveCfg = Debug|Any CPU + {3CCFA7A8-ED2D-4E85-9024-9FCF5EC63C86}.Debug|x64.Build.0 = Debug|Any CPU + {3CCFA7A8-ED2D-4E85-9024-9FCF5EC63C86}.Debug|x86.ActiveCfg = Debug|Any CPU + {3CCFA7A8-ED2D-4E85-9024-9FCF5EC63C86}.Debug|x86.Build.0 = Debug|Any CPU + {3CCFA7A8-ED2D-4E85-9024-9FCF5EC63C86}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3CCFA7A8-ED2D-4E85-9024-9FCF5EC63C86}.Release|Any CPU.Build.0 = Release|Any CPU + {3CCFA7A8-ED2D-4E85-9024-9FCF5EC63C86}.Release|x64.ActiveCfg = Release|Any CPU + {3CCFA7A8-ED2D-4E85-9024-9FCF5EC63C86}.Release|x64.Build.0 = Release|Any CPU + {3CCFA7A8-ED2D-4E85-9024-9FCF5EC63C86}.Release|x86.ActiveCfg = Release|Any CPU + {3CCFA7A8-ED2D-4E85-9024-9FCF5EC63C86}.Release|x86.Build.0 = Release|Any CPU + {2956B3D9-23A0-4818-A12D-178C15F5F3D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2956B3D9-23A0-4818-A12D-178C15F5F3D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2956B3D9-23A0-4818-A12D-178C15F5F3D9}.Debug|x64.ActiveCfg = Debug|Any CPU + {2956B3D9-23A0-4818-A12D-178C15F5F3D9}.Debug|x64.Build.0 = Debug|Any CPU + {2956B3D9-23A0-4818-A12D-178C15F5F3D9}.Debug|x86.ActiveCfg = Debug|Any CPU + {2956B3D9-23A0-4818-A12D-178C15F5F3D9}.Debug|x86.Build.0 = Debug|Any CPU + {2956B3D9-23A0-4818-A12D-178C15F5F3D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2956B3D9-23A0-4818-A12D-178C15F5F3D9}.Release|Any CPU.Build.0 = Release|Any CPU + {2956B3D9-23A0-4818-A12D-178C15F5F3D9}.Release|x64.ActiveCfg = Release|Any CPU + {2956B3D9-23A0-4818-A12D-178C15F5F3D9}.Release|x64.Build.0 = Release|Any CPU + {2956B3D9-23A0-4818-A12D-178C15F5F3D9}.Release|x86.ActiveCfg = Release|Any CPU + {2956B3D9-23A0-4818-A12D-178C15F5F3D9}.Release|x86.Build.0 = Release|Any CPU + {505BA299-AAE0-4258-934F-5CF9B6DC4EAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {505BA299-AAE0-4258-934F-5CF9B6DC4EAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {505BA299-AAE0-4258-934F-5CF9B6DC4EAC}.Debug|x64.ActiveCfg = Debug|Any CPU + {505BA299-AAE0-4258-934F-5CF9B6DC4EAC}.Debug|x64.Build.0 = Debug|Any CPU + {505BA299-AAE0-4258-934F-5CF9B6DC4EAC}.Debug|x86.ActiveCfg = Debug|Any CPU + {505BA299-AAE0-4258-934F-5CF9B6DC4EAC}.Debug|x86.Build.0 = Debug|Any CPU + {505BA299-AAE0-4258-934F-5CF9B6DC4EAC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {505BA299-AAE0-4258-934F-5CF9B6DC4EAC}.Release|Any CPU.Build.0 = Release|Any CPU + {505BA299-AAE0-4258-934F-5CF9B6DC4EAC}.Release|x64.ActiveCfg = Release|Any CPU + {505BA299-AAE0-4258-934F-5CF9B6DC4EAC}.Release|x64.Build.0 = Release|Any CPU + {505BA299-AAE0-4258-934F-5CF9B6DC4EAC}.Release|x86.ActiveCfg = Release|Any CPU + {505BA299-AAE0-4258-934F-5CF9B6DC4EAC}.Release|x86.Build.0 = Release|Any CPU + {D3730B8F-1568-4CAD-8617-0BA565E435D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3730B8F-1568-4CAD-8617-0BA565E435D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3730B8F-1568-4CAD-8617-0BA565E435D7}.Debug|x64.ActiveCfg = Debug|Any CPU + {D3730B8F-1568-4CAD-8617-0BA565E435D7}.Debug|x64.Build.0 = Debug|Any CPU + {D3730B8F-1568-4CAD-8617-0BA565E435D7}.Debug|x86.ActiveCfg = Debug|Any CPU + {D3730B8F-1568-4CAD-8617-0BA565E435D7}.Debug|x86.Build.0 = Debug|Any CPU + {D3730B8F-1568-4CAD-8617-0BA565E435D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3730B8F-1568-4CAD-8617-0BA565E435D7}.Release|Any CPU.Build.0 = Release|Any CPU + {D3730B8F-1568-4CAD-8617-0BA565E435D7}.Release|x64.ActiveCfg = Release|Any CPU + {D3730B8F-1568-4CAD-8617-0BA565E435D7}.Release|x64.Build.0 = Release|Any CPU + {D3730B8F-1568-4CAD-8617-0BA565E435D7}.Release|x86.ActiveCfg = Release|Any CPU + {D3730B8F-1568-4CAD-8617-0BA565E435D7}.Release|x86.Build.0 = Release|Any CPU + {3EADAED3-1360-4944-9434-DE3359DE2233}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3EADAED3-1360-4944-9434-DE3359DE2233}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3EADAED3-1360-4944-9434-DE3359DE2233}.Debug|x64.ActiveCfg = Debug|Any CPU + {3EADAED3-1360-4944-9434-DE3359DE2233}.Debug|x64.Build.0 = Debug|Any CPU + {3EADAED3-1360-4944-9434-DE3359DE2233}.Debug|x86.ActiveCfg = Debug|Any CPU + {3EADAED3-1360-4944-9434-DE3359DE2233}.Debug|x86.Build.0 = Debug|Any CPU + {3EADAED3-1360-4944-9434-DE3359DE2233}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3EADAED3-1360-4944-9434-DE3359DE2233}.Release|Any CPU.Build.0 = Release|Any CPU + {3EADAED3-1360-4944-9434-DE3359DE2233}.Release|x64.ActiveCfg = Release|Any CPU + {3EADAED3-1360-4944-9434-DE3359DE2233}.Release|x64.Build.0 = Release|Any CPU + {3EADAED3-1360-4944-9434-DE3359DE2233}.Release|x86.ActiveCfg = Release|Any CPU + {3EADAED3-1360-4944-9434-DE3359DE2233}.Release|x86.Build.0 = Release|Any CPU + {B8D27D9E-1D57-4F89-9CCE-B1B5E947947D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8D27D9E-1D57-4F89-9CCE-B1B5E947947D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8D27D9E-1D57-4F89-9CCE-B1B5E947947D}.Debug|x64.ActiveCfg = Debug|Any CPU + {B8D27D9E-1D57-4F89-9CCE-B1B5E947947D}.Debug|x64.Build.0 = Debug|Any CPU + {B8D27D9E-1D57-4F89-9CCE-B1B5E947947D}.Debug|x86.ActiveCfg = Debug|Any CPU + {B8D27D9E-1D57-4F89-9CCE-B1B5E947947D}.Debug|x86.Build.0 = Debug|Any CPU + {B8D27D9E-1D57-4F89-9CCE-B1B5E947947D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8D27D9E-1D57-4F89-9CCE-B1B5E947947D}.Release|Any CPU.Build.0 = Release|Any CPU + {B8D27D9E-1D57-4F89-9CCE-B1B5E947947D}.Release|x64.ActiveCfg = Release|Any CPU + {B8D27D9E-1D57-4F89-9CCE-B1B5E947947D}.Release|x64.Build.0 = Release|Any CPU + {B8D27D9E-1D57-4F89-9CCE-B1B5E947947D}.Release|x86.ActiveCfg = Release|Any CPU + {B8D27D9E-1D57-4F89-9CCE-B1B5E947947D}.Release|x86.Build.0 = Release|Any CPU + {7A7E13F8-68AE-4EE3-AA58-410EB33125E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A7E13F8-68AE-4EE3-AA58-410EB33125E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A7E13F8-68AE-4EE3-AA58-410EB33125E9}.Debug|x64.ActiveCfg = Debug|Any CPU + {7A7E13F8-68AE-4EE3-AA58-410EB33125E9}.Debug|x64.Build.0 = Debug|Any CPU + {7A7E13F8-68AE-4EE3-AA58-410EB33125E9}.Debug|x86.ActiveCfg = Debug|Any CPU + {7A7E13F8-68AE-4EE3-AA58-410EB33125E9}.Debug|x86.Build.0 = Debug|Any CPU + {7A7E13F8-68AE-4EE3-AA58-410EB33125E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A7E13F8-68AE-4EE3-AA58-410EB33125E9}.Release|Any CPU.Build.0 = Release|Any CPU + {7A7E13F8-68AE-4EE3-AA58-410EB33125E9}.Release|x64.ActiveCfg = Release|Any CPU + {7A7E13F8-68AE-4EE3-AA58-410EB33125E9}.Release|x64.Build.0 = Release|Any CPU + {7A7E13F8-68AE-4EE3-AA58-410EB33125E9}.Release|x86.ActiveCfg = Release|Any CPU + {7A7E13F8-68AE-4EE3-AA58-410EB33125E9}.Release|x86.Build.0 = Release|Any CPU + {5DA79C9E-5625-4D88-9D42-674D27538B74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DA79C9E-5625-4D88-9D42-674D27538B74}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DA79C9E-5625-4D88-9D42-674D27538B74}.Debug|x64.ActiveCfg = Debug|Any CPU + {5DA79C9E-5625-4D88-9D42-674D27538B74}.Debug|x64.Build.0 = Debug|Any CPU + {5DA79C9E-5625-4D88-9D42-674D27538B74}.Debug|x86.ActiveCfg = Debug|Any CPU + {5DA79C9E-5625-4D88-9D42-674D27538B74}.Debug|x86.Build.0 = Debug|Any CPU + {5DA79C9E-5625-4D88-9D42-674D27538B74}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DA79C9E-5625-4D88-9D42-674D27538B74}.Release|Any CPU.Build.0 = Release|Any CPU + {5DA79C9E-5625-4D88-9D42-674D27538B74}.Release|x64.ActiveCfg = Release|Any CPU + {5DA79C9E-5625-4D88-9D42-674D27538B74}.Release|x64.Build.0 = Release|Any CPU + {5DA79C9E-5625-4D88-9D42-674D27538B74}.Release|x86.ActiveCfg = Release|Any CPU + {5DA79C9E-5625-4D88-9D42-674D27538B74}.Release|x86.Build.0 = Release|Any CPU + {B7840472-147D-49DA-9AC4-0EEB5E9F27FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7840472-147D-49DA-9AC4-0EEB5E9F27FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7840472-147D-49DA-9AC4-0EEB5E9F27FC}.Debug|x64.ActiveCfg = Debug|Any CPU + {B7840472-147D-49DA-9AC4-0EEB5E9F27FC}.Debug|x64.Build.0 = Debug|Any CPU + {B7840472-147D-49DA-9AC4-0EEB5E9F27FC}.Debug|x86.ActiveCfg = Debug|Any CPU + {B7840472-147D-49DA-9AC4-0EEB5E9F27FC}.Debug|x86.Build.0 = Debug|Any CPU + {B7840472-147D-49DA-9AC4-0EEB5E9F27FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7840472-147D-49DA-9AC4-0EEB5E9F27FC}.Release|Any CPU.Build.0 = Release|Any CPU + {B7840472-147D-49DA-9AC4-0EEB5E9F27FC}.Release|x64.ActiveCfg = Release|Any CPU + {B7840472-147D-49DA-9AC4-0EEB5E9F27FC}.Release|x64.Build.0 = Release|Any CPU + {B7840472-147D-49DA-9AC4-0EEB5E9F27FC}.Release|x86.ActiveCfg = Release|Any CPU + {B7840472-147D-49DA-9AC4-0EEB5E9F27FC}.Release|x86.Build.0 = Release|Any CPU + {0CFDCBC5-32FD-458B-A487-D5E9551BADF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CFDCBC5-32FD-458B-A487-D5E9551BADF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CFDCBC5-32FD-458B-A487-D5E9551BADF9}.Debug|x64.ActiveCfg = Debug|Any CPU + {0CFDCBC5-32FD-458B-A487-D5E9551BADF9}.Debug|x64.Build.0 = Debug|Any CPU + {0CFDCBC5-32FD-458B-A487-D5E9551BADF9}.Debug|x86.ActiveCfg = Debug|Any CPU + {0CFDCBC5-32FD-458B-A487-D5E9551BADF9}.Debug|x86.Build.0 = Debug|Any CPU + {0CFDCBC5-32FD-458B-A487-D5E9551BADF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CFDCBC5-32FD-458B-A487-D5E9551BADF9}.Release|Any CPU.Build.0 = Release|Any CPU + {0CFDCBC5-32FD-458B-A487-D5E9551BADF9}.Release|x64.ActiveCfg = Release|Any CPU + {0CFDCBC5-32FD-458B-A487-D5E9551BADF9}.Release|x64.Build.0 = Release|Any CPU + {0CFDCBC5-32FD-458B-A487-D5E9551BADF9}.Release|x86.ActiveCfg = Release|Any CPU + {0CFDCBC5-32FD-458B-A487-D5E9551BADF9}.Release|x86.Build.0 = Release|Any CPU + {45053362-1DBC-4EAD-8B4C-51B6E56F60F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {45053362-1DBC-4EAD-8B4C-51B6E56F60F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {45053362-1DBC-4EAD-8B4C-51B6E56F60F6}.Debug|x64.ActiveCfg = Debug|Any CPU + {45053362-1DBC-4EAD-8B4C-51B6E56F60F6}.Debug|x64.Build.0 = Debug|Any CPU + {45053362-1DBC-4EAD-8B4C-51B6E56F60F6}.Debug|x86.ActiveCfg = Debug|Any CPU + {45053362-1DBC-4EAD-8B4C-51B6E56F60F6}.Debug|x86.Build.0 = Debug|Any CPU + {45053362-1DBC-4EAD-8B4C-51B6E56F60F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {45053362-1DBC-4EAD-8B4C-51B6E56F60F6}.Release|Any CPU.Build.0 = Release|Any CPU + {45053362-1DBC-4EAD-8B4C-51B6E56F60F6}.Release|x64.ActiveCfg = Release|Any CPU + {45053362-1DBC-4EAD-8B4C-51B6E56F60F6}.Release|x64.Build.0 = Release|Any CPU + {45053362-1DBC-4EAD-8B4C-51B6E56F60F6}.Release|x86.ActiveCfg = Release|Any CPU + {45053362-1DBC-4EAD-8B4C-51B6E56F60F6}.Release|x86.Build.0 = Release|Any CPU + {ABD471D5-4CDF-48E8-BD19-0CC2AA4868EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ABD471D5-4CDF-48E8-BD19-0CC2AA4868EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ABD471D5-4CDF-48E8-BD19-0CC2AA4868EB}.Debug|x64.ActiveCfg = Debug|Any CPU + {ABD471D5-4CDF-48E8-BD19-0CC2AA4868EB}.Debug|x64.Build.0 = Debug|Any CPU + {ABD471D5-4CDF-48E8-BD19-0CC2AA4868EB}.Debug|x86.ActiveCfg = Debug|Any CPU + {ABD471D5-4CDF-48E8-BD19-0CC2AA4868EB}.Debug|x86.Build.0 = Debug|Any CPU + {ABD471D5-4CDF-48E8-BD19-0CC2AA4868EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ABD471D5-4CDF-48E8-BD19-0CC2AA4868EB}.Release|Any CPU.Build.0 = Release|Any CPU + {ABD471D5-4CDF-48E8-BD19-0CC2AA4868EB}.Release|x64.ActiveCfg = Release|Any CPU + {ABD471D5-4CDF-48E8-BD19-0CC2AA4868EB}.Release|x64.Build.0 = Release|Any CPU + {ABD471D5-4CDF-48E8-BD19-0CC2AA4868EB}.Release|x86.ActiveCfg = Release|Any CPU + {ABD471D5-4CDF-48E8-BD19-0CC2AA4868EB}.Release|x86.Build.0 = Release|Any CPU + {1C3FDE70-832F-41FB-A9A4-4DD8DBBBF171}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C3FDE70-832F-41FB-A9A4-4DD8DBBBF171}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C3FDE70-832F-41FB-A9A4-4DD8DBBBF171}.Debug|x64.ActiveCfg = Debug|Any CPU + {1C3FDE70-832F-41FB-A9A4-4DD8DBBBF171}.Debug|x64.Build.0 = Debug|Any CPU + {1C3FDE70-832F-41FB-A9A4-4DD8DBBBF171}.Debug|x86.ActiveCfg = Debug|Any CPU + {1C3FDE70-832F-41FB-A9A4-4DD8DBBBF171}.Debug|x86.Build.0 = Debug|Any CPU + {1C3FDE70-832F-41FB-A9A4-4DD8DBBBF171}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C3FDE70-832F-41FB-A9A4-4DD8DBBBF171}.Release|Any CPU.Build.0 = Release|Any CPU + {1C3FDE70-832F-41FB-A9A4-4DD8DBBBF171}.Release|x64.ActiveCfg = Release|Any CPU + {1C3FDE70-832F-41FB-A9A4-4DD8DBBBF171}.Release|x64.Build.0 = Release|Any CPU + {1C3FDE70-832F-41FB-A9A4-4DD8DBBBF171}.Release|x86.ActiveCfg = Release|Any CPU + {1C3FDE70-832F-41FB-A9A4-4DD8DBBBF171}.Release|x86.Build.0 = Release|Any CPU + {93A496DB-F661-49E0-97C5-4680D92163ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93A496DB-F661-49E0-97C5-4680D92163ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93A496DB-F661-49E0-97C5-4680D92163ED}.Debug|x64.ActiveCfg = Debug|Any CPU + {93A496DB-F661-49E0-97C5-4680D92163ED}.Debug|x64.Build.0 = Debug|Any CPU + {93A496DB-F661-49E0-97C5-4680D92163ED}.Debug|x86.ActiveCfg = Debug|Any CPU + {93A496DB-F661-49E0-97C5-4680D92163ED}.Debug|x86.Build.0 = Debug|Any CPU + {93A496DB-F661-49E0-97C5-4680D92163ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93A496DB-F661-49E0-97C5-4680D92163ED}.Release|Any CPU.Build.0 = Release|Any CPU + {93A496DB-F661-49E0-97C5-4680D92163ED}.Release|x64.ActiveCfg = Release|Any CPU + {93A496DB-F661-49E0-97C5-4680D92163ED}.Release|x64.Build.0 = Release|Any CPU + {93A496DB-F661-49E0-97C5-4680D92163ED}.Release|x86.ActiveCfg = Release|Any CPU + {93A496DB-F661-49E0-97C5-4680D92163ED}.Release|x86.Build.0 = Release|Any CPU + {F7F47D90-331B-4D44-BB01-5CE31A2D2532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7F47D90-331B-4D44-BB01-5CE31A2D2532}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7F47D90-331B-4D44-BB01-5CE31A2D2532}.Debug|x64.ActiveCfg = Debug|Any CPU + {F7F47D90-331B-4D44-BB01-5CE31A2D2532}.Debug|x64.Build.0 = Debug|Any CPU + {F7F47D90-331B-4D44-BB01-5CE31A2D2532}.Debug|x86.ActiveCfg = Debug|Any CPU + {F7F47D90-331B-4D44-BB01-5CE31A2D2532}.Debug|x86.Build.0 = Debug|Any CPU + {F7F47D90-331B-4D44-BB01-5CE31A2D2532}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7F47D90-331B-4D44-BB01-5CE31A2D2532}.Release|Any CPU.Build.0 = Release|Any CPU + {F7F47D90-331B-4D44-BB01-5CE31A2D2532}.Release|x64.ActiveCfg = Release|Any CPU + {F7F47D90-331B-4D44-BB01-5CE31A2D2532}.Release|x64.Build.0 = Release|Any CPU + {F7F47D90-331B-4D44-BB01-5CE31A2D2532}.Release|x86.ActiveCfg = Release|Any CPU + {F7F47D90-331B-4D44-BB01-5CE31A2D2532}.Release|x86.Build.0 = Release|Any CPU + {F0AA05AC-508E-4D13-B416-7D080A6ED5B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0AA05AC-508E-4D13-B416-7D080A6ED5B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0AA05AC-508E-4D13-B416-7D080A6ED5B0}.Debug|x64.ActiveCfg = Debug|Any CPU + {F0AA05AC-508E-4D13-B416-7D080A6ED5B0}.Debug|x64.Build.0 = Debug|Any CPU + {F0AA05AC-508E-4D13-B416-7D080A6ED5B0}.Debug|x86.ActiveCfg = Debug|Any CPU + {F0AA05AC-508E-4D13-B416-7D080A6ED5B0}.Debug|x86.Build.0 = Debug|Any CPU + {F0AA05AC-508E-4D13-B416-7D080A6ED5B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0AA05AC-508E-4D13-B416-7D080A6ED5B0}.Release|Any CPU.Build.0 = Release|Any CPU + {F0AA05AC-508E-4D13-B416-7D080A6ED5B0}.Release|x64.ActiveCfg = Release|Any CPU + {F0AA05AC-508E-4D13-B416-7D080A6ED5B0}.Release|x64.Build.0 = Release|Any CPU + {F0AA05AC-508E-4D13-B416-7D080A6ED5B0}.Release|x86.ActiveCfg = Release|Any CPU + {F0AA05AC-508E-4D13-B416-7D080A6ED5B0}.Release|x86.Build.0 = Release|Any CPU + {14E80B26-EDD6-468F-BC51-350DD9C83100}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14E80B26-EDD6-468F-BC51-350DD9C83100}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14E80B26-EDD6-468F-BC51-350DD9C83100}.Debug|x64.ActiveCfg = Debug|Any CPU + {14E80B26-EDD6-468F-BC51-350DD9C83100}.Debug|x64.Build.0 = Debug|Any CPU + {14E80B26-EDD6-468F-BC51-350DD9C83100}.Debug|x86.ActiveCfg = Debug|Any CPU + {14E80B26-EDD6-468F-BC51-350DD9C83100}.Debug|x86.Build.0 = Debug|Any CPU + {14E80B26-EDD6-468F-BC51-350DD9C83100}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14E80B26-EDD6-468F-BC51-350DD9C83100}.Release|Any CPU.Build.0 = Release|Any CPU + {14E80B26-EDD6-468F-BC51-350DD9C83100}.Release|x64.ActiveCfg = Release|Any CPU + {14E80B26-EDD6-468F-BC51-350DD9C83100}.Release|x64.Build.0 = Release|Any CPU + {14E80B26-EDD6-468F-BC51-350DD9C83100}.Release|x86.ActiveCfg = Release|Any CPU + {14E80B26-EDD6-468F-BC51-350DD9C83100}.Release|x86.Build.0 = Release|Any CPU + {1AC23054-F30F-48A9-B750-1D60BDCFE3FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1AC23054-F30F-48A9-B750-1D60BDCFE3FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1AC23054-F30F-48A9-B750-1D60BDCFE3FD}.Debug|x64.ActiveCfg = Debug|Any CPU + {1AC23054-F30F-48A9-B750-1D60BDCFE3FD}.Debug|x64.Build.0 = Debug|Any CPU + {1AC23054-F30F-48A9-B750-1D60BDCFE3FD}.Debug|x86.ActiveCfg = Debug|Any CPU + {1AC23054-F30F-48A9-B750-1D60BDCFE3FD}.Debug|x86.Build.0 = Debug|Any CPU + {1AC23054-F30F-48A9-B750-1D60BDCFE3FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1AC23054-F30F-48A9-B750-1D60BDCFE3FD}.Release|Any CPU.Build.0 = Release|Any CPU + {1AC23054-F30F-48A9-B750-1D60BDCFE3FD}.Release|x64.ActiveCfg = Release|Any CPU + {1AC23054-F30F-48A9-B750-1D60BDCFE3FD}.Release|x64.Build.0 = Release|Any CPU + {1AC23054-F30F-48A9-B750-1D60BDCFE3FD}.Release|x86.ActiveCfg = Release|Any CPU + {1AC23054-F30F-48A9-B750-1D60BDCFE3FD}.Release|x86.Build.0 = Release|Any CPU + {F1FD356A-0712-4379-AF87-11BB6F4D554B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F1FD356A-0712-4379-AF87-11BB6F4D554B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1FD356A-0712-4379-AF87-11BB6F4D554B}.Debug|x64.ActiveCfg = Debug|Any CPU + {F1FD356A-0712-4379-AF87-11BB6F4D554B}.Debug|x64.Build.0 = Debug|Any CPU + {F1FD356A-0712-4379-AF87-11BB6F4D554B}.Debug|x86.ActiveCfg = Debug|Any CPU + {F1FD356A-0712-4379-AF87-11BB6F4D554B}.Debug|x86.Build.0 = Debug|Any CPU + {F1FD356A-0712-4379-AF87-11BB6F4D554B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F1FD356A-0712-4379-AF87-11BB6F4D554B}.Release|Any CPU.Build.0 = Release|Any CPU + {F1FD356A-0712-4379-AF87-11BB6F4D554B}.Release|x64.ActiveCfg = Release|Any CPU + {F1FD356A-0712-4379-AF87-11BB6F4D554B}.Release|x64.Build.0 = Release|Any CPU + {F1FD356A-0712-4379-AF87-11BB6F4D554B}.Release|x86.ActiveCfg = Release|Any CPU + {F1FD356A-0712-4379-AF87-11BB6F4D554B}.Release|x86.Build.0 = Release|Any CPU + {E241F16B-FE81-4107-8C9F-6BFA029C7934}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E241F16B-FE81-4107-8C9F-6BFA029C7934}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E241F16B-FE81-4107-8C9F-6BFA029C7934}.Debug|x64.ActiveCfg = Debug|Any CPU + {E241F16B-FE81-4107-8C9F-6BFA029C7934}.Debug|x64.Build.0 = Debug|Any CPU + {E241F16B-FE81-4107-8C9F-6BFA029C7934}.Debug|x86.ActiveCfg = Debug|Any CPU + {E241F16B-FE81-4107-8C9F-6BFA029C7934}.Debug|x86.Build.0 = Debug|Any CPU + {E241F16B-FE81-4107-8C9F-6BFA029C7934}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E241F16B-FE81-4107-8C9F-6BFA029C7934}.Release|Any CPU.Build.0 = Release|Any CPU + {E241F16B-FE81-4107-8C9F-6BFA029C7934}.Release|x64.ActiveCfg = Release|Any CPU + {E241F16B-FE81-4107-8C9F-6BFA029C7934}.Release|x64.Build.0 = Release|Any CPU + {E241F16B-FE81-4107-8C9F-6BFA029C7934}.Release|x86.ActiveCfg = Release|Any CPU + {E241F16B-FE81-4107-8C9F-6BFA029C7934}.Release|x86.Build.0 = Release|Any CPU + {B1749E6E-EF88-4C46-89BC-CF1BF7A46851}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1749E6E-EF88-4C46-89BC-CF1BF7A46851}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1749E6E-EF88-4C46-89BC-CF1BF7A46851}.Debug|x64.ActiveCfg = Debug|Any CPU + {B1749E6E-EF88-4C46-89BC-CF1BF7A46851}.Debug|x64.Build.0 = Debug|Any CPU + {B1749E6E-EF88-4C46-89BC-CF1BF7A46851}.Debug|x86.ActiveCfg = Debug|Any CPU + {B1749E6E-EF88-4C46-89BC-CF1BF7A46851}.Debug|x86.Build.0 = Debug|Any CPU + {B1749E6E-EF88-4C46-89BC-CF1BF7A46851}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1749E6E-EF88-4C46-89BC-CF1BF7A46851}.Release|Any CPU.Build.0 = Release|Any CPU + {B1749E6E-EF88-4C46-89BC-CF1BF7A46851}.Release|x64.ActiveCfg = Release|Any CPU + {B1749E6E-EF88-4C46-89BC-CF1BF7A46851}.Release|x64.Build.0 = Release|Any CPU + {B1749E6E-EF88-4C46-89BC-CF1BF7A46851}.Release|x86.ActiveCfg = Release|Any CPU + {B1749E6E-EF88-4C46-89BC-CF1BF7A46851}.Release|x86.Build.0 = Release|Any CPU + {A45F0090-68EA-4FC5-AAEB-6D71CBB1483E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A45F0090-68EA-4FC5-AAEB-6D71CBB1483E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A45F0090-68EA-4FC5-AAEB-6D71CBB1483E}.Debug|x64.ActiveCfg = Debug|Any CPU + {A45F0090-68EA-4FC5-AAEB-6D71CBB1483E}.Debug|x64.Build.0 = Debug|Any CPU + {A45F0090-68EA-4FC5-AAEB-6D71CBB1483E}.Debug|x86.ActiveCfg = Debug|Any CPU + {A45F0090-68EA-4FC5-AAEB-6D71CBB1483E}.Debug|x86.Build.0 = Debug|Any CPU + {A45F0090-68EA-4FC5-AAEB-6D71CBB1483E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A45F0090-68EA-4FC5-AAEB-6D71CBB1483E}.Release|Any CPU.Build.0 = Release|Any CPU + {A45F0090-68EA-4FC5-AAEB-6D71CBB1483E}.Release|x64.ActiveCfg = Release|Any CPU + {A45F0090-68EA-4FC5-AAEB-6D71CBB1483E}.Release|x64.Build.0 = Release|Any CPU + {A45F0090-68EA-4FC5-AAEB-6D71CBB1483E}.Release|x86.ActiveCfg = Release|Any CPU + {A45F0090-68EA-4FC5-AAEB-6D71CBB1483E}.Release|x86.Build.0 = Release|Any CPU + {099273DB-F00E-43C2-BCAE-DA7A1C3AB49D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {099273DB-F00E-43C2-BCAE-DA7A1C3AB49D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {099273DB-F00E-43C2-BCAE-DA7A1C3AB49D}.Debug|x64.ActiveCfg = Debug|Any CPU + {099273DB-F00E-43C2-BCAE-DA7A1C3AB49D}.Debug|x64.Build.0 = Debug|Any CPU + {099273DB-F00E-43C2-BCAE-DA7A1C3AB49D}.Debug|x86.ActiveCfg = Debug|Any CPU + {099273DB-F00E-43C2-BCAE-DA7A1C3AB49D}.Debug|x86.Build.0 = Debug|Any CPU + {099273DB-F00E-43C2-BCAE-DA7A1C3AB49D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {099273DB-F00E-43C2-BCAE-DA7A1C3AB49D}.Release|Any CPU.Build.0 = Release|Any CPU + {099273DB-F00E-43C2-BCAE-DA7A1C3AB49D}.Release|x64.ActiveCfg = Release|Any CPU + {099273DB-F00E-43C2-BCAE-DA7A1C3AB49D}.Release|x64.Build.0 = Release|Any CPU + {099273DB-F00E-43C2-BCAE-DA7A1C3AB49D}.Release|x86.ActiveCfg = Release|Any CPU + {099273DB-F00E-43C2-BCAE-DA7A1C3AB49D}.Release|x86.Build.0 = Release|Any CPU + {F28E672C-DBA2-496E-8784-E6F17CE261D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F28E672C-DBA2-496E-8784-E6F17CE261D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F28E672C-DBA2-496E-8784-E6F17CE261D5}.Debug|x64.ActiveCfg = Debug|Any CPU + {F28E672C-DBA2-496E-8784-E6F17CE261D5}.Debug|x64.Build.0 = Debug|Any CPU + {F28E672C-DBA2-496E-8784-E6F17CE261D5}.Debug|x86.ActiveCfg = Debug|Any CPU + {F28E672C-DBA2-496E-8784-E6F17CE261D5}.Debug|x86.Build.0 = Debug|Any CPU + {F28E672C-DBA2-496E-8784-E6F17CE261D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F28E672C-DBA2-496E-8784-E6F17CE261D5}.Release|Any CPU.Build.0 = Release|Any CPU + {F28E672C-DBA2-496E-8784-E6F17CE261D5}.Release|x64.ActiveCfg = Release|Any CPU + {F28E672C-DBA2-496E-8784-E6F17CE261D5}.Release|x64.Build.0 = Release|Any CPU + {F28E672C-DBA2-496E-8784-E6F17CE261D5}.Release|x86.ActiveCfg = Release|Any CPU + {F28E672C-DBA2-496E-8784-E6F17CE261D5}.Release|x86.Build.0 = Release|Any CPU + {5483EE45-0838-46C4-B59B-5B52F7C7D975}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5483EE45-0838-46C4-B59B-5B52F7C7D975}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5483EE45-0838-46C4-B59B-5B52F7C7D975}.Debug|x64.ActiveCfg = Debug|Any CPU + {5483EE45-0838-46C4-B59B-5B52F7C7D975}.Debug|x64.Build.0 = Debug|Any CPU + {5483EE45-0838-46C4-B59B-5B52F7C7D975}.Debug|x86.ActiveCfg = Debug|Any CPU + {5483EE45-0838-46C4-B59B-5B52F7C7D975}.Debug|x86.Build.0 = Debug|Any CPU + {5483EE45-0838-46C4-B59B-5B52F7C7D975}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5483EE45-0838-46C4-B59B-5B52F7C7D975}.Release|Any CPU.Build.0 = Release|Any CPU + {5483EE45-0838-46C4-B59B-5B52F7C7D975}.Release|x64.ActiveCfg = Release|Any CPU + {5483EE45-0838-46C4-B59B-5B52F7C7D975}.Release|x64.Build.0 = Release|Any CPU + {5483EE45-0838-46C4-B59B-5B52F7C7D975}.Release|x86.ActiveCfg = Release|Any CPU + {5483EE45-0838-46C4-B59B-5B52F7C7D975}.Release|x86.Build.0 = Release|Any CPU + {FE71764E-E7BD-B378-8A4C-86929A28AD25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE71764E-E7BD-B378-8A4C-86929A28AD25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE71764E-E7BD-B378-8A4C-86929A28AD25}.Debug|x64.ActiveCfg = Debug|Any CPU + {FE71764E-E7BD-B378-8A4C-86929A28AD25}.Debug|x64.Build.0 = Debug|Any CPU + {FE71764E-E7BD-B378-8A4C-86929A28AD25}.Debug|x86.ActiveCfg = Debug|Any CPU + {FE71764E-E7BD-B378-8A4C-86929A28AD25}.Debug|x86.Build.0 = Debug|Any CPU + {FE71764E-E7BD-B378-8A4C-86929A28AD25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE71764E-E7BD-B378-8A4C-86929A28AD25}.Release|Any CPU.Build.0 = Release|Any CPU + {FE71764E-E7BD-B378-8A4C-86929A28AD25}.Release|x64.ActiveCfg = Release|Any CPU + {FE71764E-E7BD-B378-8A4C-86929A28AD25}.Release|x64.Build.0 = Release|Any CPU + {FE71764E-E7BD-B378-8A4C-86929A28AD25}.Release|x86.ActiveCfg = Release|Any CPU + {FE71764E-E7BD-B378-8A4C-86929A28AD25}.Release|x86.Build.0 = Release|Any CPU {B5CF5531-624F-9F49-46BD-937FF4062F54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B5CF5531-624F-9F49-46BD-937FF4062F54}.Debug|Any CPU.Build.0 = Debug|Any CPU {B5CF5531-624F-9F49-46BD-937FF4062F54}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -475,6 +923,78 @@ Global {B5CF5531-624F-9F49-46BD-937FF4062F54}.Release|x64.Build.0 = Release|Any CPU {B5CF5531-624F-9F49-46BD-937FF4062F54}.Release|x86.ActiveCfg = Release|Any CPU {B5CF5531-624F-9F49-46BD-937FF4062F54}.Release|x86.Build.0 = Release|Any CPU + {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Debug|x64.ActiveCfg = Debug|Any CPU + {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Debug|x64.Build.0 = Debug|Any CPU + {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Debug|x86.ActiveCfg = Debug|Any CPU + {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Debug|x86.Build.0 = Debug|Any CPU + {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Release|Any CPU.Build.0 = Release|Any CPU + {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Release|x64.ActiveCfg = Release|Any CPU + {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Release|x64.Build.0 = Release|Any CPU + {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Release|x86.ActiveCfg = Release|Any CPU + {97619DEB-226C-EB67-140D-0C7E67B2F7D0}.Release|x86.Build.0 = Release|Any CPU + {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Debug|x64.ActiveCfg = Debug|Any CPU + {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Debug|x64.Build.0 = Debug|Any CPU + {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Debug|x86.ActiveCfg = Debug|Any CPU + {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Debug|x86.Build.0 = Debug|Any CPU + {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Release|Any CPU.Build.0 = Release|Any CPU + {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Release|x64.ActiveCfg = Release|Any CPU + {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Release|x64.Build.0 = Release|Any CPU + {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Release|x86.ActiveCfg = Release|Any CPU + {4570F31D-D00F-6A80-DB7C-74826CBD15AE}.Release|x86.Build.0 = Release|Any CPU + {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Debug|x64.ActiveCfg = Debug|Any CPU + {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Debug|x64.Build.0 = Debug|Any CPU + {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Debug|x86.ActiveCfg = Debug|Any CPU + {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Debug|x86.Build.0 = Debug|Any CPU + {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Release|Any CPU.Build.0 = Release|Any CPU + {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Release|x64.ActiveCfg = Release|Any CPU + {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Release|x64.Build.0 = Release|Any CPU + {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Release|x86.ActiveCfg = Release|Any CPU + {638FC2AE-7FC1-6527-E051-C6B34803F08D}.Release|x86.Build.0 = Release|Any CPU + {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Debug|x64.ActiveCfg = Debug|x64 + {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Debug|x64.Build.0 = Debug|x64 + {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Debug|x86.ActiveCfg = Debug|x86 + {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Debug|x86.Build.0 = Debug|x86 + {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Release|Any CPU.Build.0 = Release|Any CPU + {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Release|x64.ActiveCfg = Release|x64 + {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Release|x64.Build.0 = Release|x64 + {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Release|x86.ActiveCfg = Release|x86 + {D73D1783-E5F5-5094-6D3E-D77B45A51B4A}.Release|x86.Build.0 = Release|x86 + {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Debug|x64.ActiveCfg = Debug|Any CPU + {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Debug|x64.Build.0 = Debug|Any CPU + {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Debug|x86.ActiveCfg = Debug|Any CPU + {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Debug|x86.Build.0 = Debug|Any CPU + {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Release|Any CPU.Build.0 = Release|Any CPU + {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Release|x64.ActiveCfg = Release|Any CPU + {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Release|x64.Build.0 = Release|Any CPU + {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Release|x86.ActiveCfg = Release|Any CPU + {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3}.Release|x86.Build.0 = Release|Any CPU + {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Debug|x64.ActiveCfg = Debug|Any CPU + {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Debug|x64.Build.0 = Debug|Any CPU + {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Debug|x86.ActiveCfg = Debug|Any CPU + {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Debug|x86.Build.0 = Debug|Any CPU + {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Release|Any CPU.Build.0 = Release|Any CPU + {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Release|x64.ActiveCfg = Release|Any CPU + {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Release|x64.Build.0 = Release|Any CPU + {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Release|x86.ActiveCfg = Release|Any CPU + {62B1D30A-935F-819E-A227-A7FFF2E12CC1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -497,25 +1017,65 @@ Global {6458AFCB-A159-47D5-8F2B-50C95C0915E0} = {DB21FED7-E8CB-4B00-9EB2-9144D32A590A} {39F5197B-CF6C-41A5-9739-7F97E78BB104} = {6458AFCB-A159-47D5-8F2B-50C95C0915E0} {E1C42A6F-913B-4A7B-B1A8-2BB62843B254} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66} - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC} - {59804113-1DD4-4F80-8D06-35FF71652508} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC} {68A8CC40-6AED-4E96-B524-31B1158FDEEA} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} {23859314-5693-4E6C-BE5C-80A433439D2A} = {1799C43E-5C54-4A8F-8D64-B1475241DB0D} - {061582C2-658F-40AE-A978-7D74A4EB2C0A} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} {C7606104-5D58-4670-912C-3F336606B02D} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} - {70193989-E587-451A-AF34-4C74FEE5DA5A} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC} - {999D90DE-6940-4693-9F80-701F2D5DB77A} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070} {43A3CA46-2675-4AD4-88FC-DDC2E657F0B7} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070} - {F0AAE591-A44E-48A5-8C74-395BDF98F646} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070} + {333F3D4F-D251-5601-4851-307FF0F13D23} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070} + {4A922B77-34EC-EA6A-8E96-8353C8FA0640} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} + {5EC089DC-69F7-643B-93BB-293D199F5F74} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} + {38CA36F4-AD3A-4B37-AB75-9DEF57219A42} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {9AFBFA31-F636-4685-A8B9-4F623139C01B} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {AA365C58-287B-47CE-8EDA-6FA3BF10574E} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {80A082A3-4FC7-4AD4-AFB6-503B3033B32D} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {7237DC3A-C8C5-49C1-A9CB-00D93E8F6CED} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {E78916D9-1240-4C13-BEC3-242AFF8BFB84} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {F253A0A3-3202-45AE-A2F5-30098F8B3BB8} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {7BD98865-305F-428F-BDCE-10311B96174A} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {3CCFA7A8-ED2D-4E85-9024-9FCF5EC63C86} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {2956B3D9-23A0-4818-A12D-178C15F5F3D9} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {505BA299-AAE0-4258-934F-5CF9B6DC4EAC} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {D3730B8F-1568-4CAD-8617-0BA565E435D7} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {3EADAED3-1360-4944-9434-DE3359DE2233} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {B8D27D9E-1D57-4F89-9CCE-B1B5E947947D} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {7A7E13F8-68AE-4EE3-AA58-410EB33125E9} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {5DA79C9E-5625-4D88-9D42-674D27538B74} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {B7840472-147D-49DA-9AC4-0EEB5E9F27FC} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {0CFDCBC5-32FD-458B-A487-D5E9551BADF9} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {45053362-1DBC-4EAD-8B4C-51B6E56F60F6} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {ABD471D5-4CDF-48E8-BD19-0CC2AA4868EB} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {1C3FDE70-832F-41FB-A9A4-4DD8DBBBF171} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {93A496DB-F661-49E0-97C5-4680D92163ED} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {F7F47D90-331B-4D44-BB01-5CE31A2D2532} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {F0AA05AC-508E-4D13-B416-7D080A6ED5B0} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {14E80B26-EDD6-468F-BC51-350DD9C83100} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {1AC23054-F30F-48A9-B750-1D60BDCFE3FD} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {F1FD356A-0712-4379-AF87-11BB6F4D554B} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {E241F16B-FE81-4107-8C9F-6BFA029C7934} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {B1749E6E-EF88-4C46-89BC-CF1BF7A46851} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {A45F0090-68EA-4FC5-AAEB-6D71CBB1483E} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {099273DB-F00E-43C2-BCAE-DA7A1C3AB49D} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {F28E672C-DBA2-496E-8784-E6F17CE261D5} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {5483EE45-0838-46C4-B59B-5B52F7C7D975} = {29F3AA1B-6D75-C75D-9183-749A65CD24A7} + {FE71764E-E7BD-B378-8A4C-86929A28AD25} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} {B5CF5531-624F-9F49-46BD-937FF4062F54} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070} + {97619DEB-226C-EB67-140D-0C7E67B2F7D0} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070} + {4570F31D-D00F-6A80-DB7C-74826CBD15AE} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070} + {638FC2AE-7FC1-6527-E051-C6B34803F08D} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} + {D73D1783-E5F5-5094-6D3E-D77B45A51B4A} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC} + {78BC7FFA-DFB0-B9ED-03C7-3A59E499B4B3} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC} + {62B1D30A-935F-819E-A227-A7FFF2E12CC1} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution - shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{061582c2-658f-40ae-a978-7d74a4eb2c0a}*SharedItemsImports = 5 shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{2e33181e-6e28-4662-a801-e2e7dc206029}*SharedItemsImports = 5 + ..\Fonts\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{4a922b77-34ec-ea6a-8e96-8353c8fa0640}*SharedItemsImports = 5 + ..\PolygonClipper\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{5ec089dc-69f7-643b-93bb-293d199f5f74}*SharedItemsImports = 5 + shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{638fc2ae-7fc1-6527-e051-c6b34803f08d}*SharedItemsImports = 5 shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13 + ..\ImageSharp\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{fe71764e-e7bd-b378-8a4c-86929a28ad25}*SharedItemsImports = 5 EndGlobalSection GlobalSection(Performance) = preSolution HasPerformanceSessions = true diff --git a/samples/AvaloniaControlCatalog/App.xaml b/samples/AvaloniaControlCatalog/App.xaml new file mode 100644 index 00000000..42f35c72 --- /dev/null +++ b/samples/AvaloniaControlCatalog/App.xaml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + #33000000 + #99000000 + #FFE6E6E6 + #FF000000 + + + #33FFFFFF + #99FFFFFF + #FF1F1F1F + #FFFFFFFF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/App.xaml.cs b/samples/AvaloniaControlCatalog/App.xaml.cs new file mode 100644 index 00000000..159e4ebf --- /dev/null +++ b/samples/AvaloniaControlCatalog/App.xaml.cs @@ -0,0 +1,175 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Avalonia.Platform; +using Avalonia.Styling; +using Avalonia.Themes.Simple; +using Avalonia.Themes.Fluent; +using ControlCatalog.Models; +using ControlCatalog.ViewModels; + +namespace ControlCatalog +{ + public class App : Application + { + private readonly Styles _themeStylesContainer = new(); + private FluentTheme? _fluentTheme; + private SimpleTheme? _simpleTheme; + private IStyle? _colorPickerFluent, _colorPickerSimple; + + public App() + { + DataContext = new ApplicationViewModel(); + } + + public override void Initialize() + { + Styles.Add(_themeStylesContainer); + + AvaloniaXamlLoader.Load(this); + + _fluentTheme = (FluentTheme)Resources["FluentTheme"]!; + _simpleTheme = (SimpleTheme)Resources["SimpleTheme"]!; + _colorPickerFluent = (IStyle)Resources["ColorPickerFluent"]!; + _colorPickerSimple = (IStyle)Resources["ColorPickerSimple"]!; + + SetCatalogThemes(CatalogTheme.Fluent); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + { + desktopLifetime.MainWindow = new MainWindow { DataContext = new MainWindowViewModel() }; + + if (int.TryParse(Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_EXIT_AFTER_MS"), out int exitDelay) && exitDelay > 0) + { + // Test-only hook: let the real desktop lifetime render normally, then shut it down + // without a human closing the window. + desktopLifetime.MainWindow.Opened += async (_, _) => + { + await Task.Delay(exitDelay); + desktopLifetime.Shutdown(); + }; + } + } + else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime) + { + singleViewFactoryApplicationLifetime.MainViewFactory = () => new PageNavigationHost() + { + Page = new MainView { DataContext = new MainWindowViewModel() } + }; + } + else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) + { + singleViewLifetime.MainView = new PageNavigationHost() + { + Page = new MainView { DataContext = new MainWindowViewModel() } + }; + } + + if (this.TryGetFeature() is { } activatableApplicationLifetime) + { + activatableApplicationLifetime.Activated += (sender, args) => + Console.WriteLine($"App activated: {args.Kind}"); + activatableApplicationLifetime.Deactivated += (sender, args) => + Console.WriteLine($"App deactivated: {args.Kind}"); + } + + base.OnFrameworkInitializationCompleted(); + } + + public void OnDockNewWindowClicked(object? sender, EventArgs e) + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime) + { + var window = new MainWindow(); + window.Show(); + } + } + + public void OnDockShowMainWindowClicked(object? sender, EventArgs e) + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + { + desktopLifetime.MainWindow?.Activate(); + } + } + + private int _dockMenuItemCount; + + public void OnDockAddItemClicked(object? sender, EventArgs e) + { + var dockMenu = NativeDock.GetMenu(this); + if (dockMenu is not null) + { + _dockMenuItemCount++; + var item = new NativeMenuItem($"New item {_dockMenuItemCount}"); + item.Click += (_, _) => + { + dockMenu.Items.Remove(item); + }; + dockMenu.Items.Insert(0, item); + } + } + + private CatalogTheme _prevTheme; + public static CatalogTheme CurrentTheme => ((App)Current!)._prevTheme; + public static void SetCatalogThemes(CatalogTheme theme) + { + var app = (App)Current!; + var prevTheme = app._prevTheme; + app._prevTheme = theme; + var shouldReopenWindow = prevTheme != theme; + + if (app._themeStylesContainer.Count == 0) + { + app._themeStylesContainer.Add(new Style()); + app._themeStylesContainer.Add(new Style()); + app._themeStylesContainer.Add(new Style()); + } + + if (theme == CatalogTheme.Fluent) + { + app._themeStylesContainer[0] = app._fluentTheme!; + app._themeStylesContainer[1] = app._colorPickerFluent!; + } + else if (theme == CatalogTheme.Simple) + { + app._themeStylesContainer[0] = app._simpleTheme!; + app._themeStylesContainer[1] = app._colorPickerSimple!; + } + + if (shouldReopenWindow) + { + if (app.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + { + var oldWindow = desktopLifetime.MainWindow; + var newWindow = new MainWindow() + { + DataContext = new MainWindowViewModel() + }; + desktopLifetime.MainWindow = newWindow; + newWindow.Show(); + oldWindow?.Close(); + } + else if (app.ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime) + { + singleViewFactoryApplicationLifetime.MainViewFactory = () => new PageNavigationHost() + { + Page = new MainView { DataContext = new MainWindowViewModel() } + }; + } + else if (app.ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) + { + singleViewLifetime.MainView = new PageNavigationHost() + { + Page = new MainView { DataContext = new MainWindowViewModel() } + }; + } + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/Assets/CurvedHeader/avatar.jpg b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/avatar.jpg new file mode 100644 index 00000000..ddcf4395 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/avatar.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72bc7dc39313ed42adb7ccc430b4083e2d4c91b4433a7eca0c620fdb64892316 +size 270846 diff --git a/samples/AvaloniaControlCatalog/Assets/CurvedHeader/featured.jpg b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/featured.jpg new file mode 100644 index 00000000..bfc31e4b --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/featured.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b86c986b58e751da873fb022a9c3fbc8608c5884db65dca711866f3b63b59a3 +size 342297 diff --git a/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product1.jpg b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product1.jpg new file mode 100644 index 00000000..4842769e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3b4ab4d1fe41bc08f45170c393f9c7bf0dab08e3f9334ec781a48a25f6926e0 +size 392467 diff --git a/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product2.jpg b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product2.jpg new file mode 100644 index 00000000..0fab3111 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product2.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:328cf4e51f1aa7c85263d7f643f949fbdf026a02267d09a9549a9b724ce260cb +size 311991 diff --git a/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product3.jpg b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product3.jpg new file mode 100644 index 00000000..ef7100e6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/product3.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:041d41538d29d7b8dc60c4f004eb6c2214a50367bfe00ef65255ce8209d61dd2 +size 314528 diff --git a/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update1.jpg b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update1.jpg new file mode 100644 index 00000000..fbb14a94 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b91f15899decfc3ee76c1a81a3046690fd6e311d99967ec8a4cc3f3de71eda22 +size 201420 diff --git a/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update2.jpg b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update2.jpg new file mode 100644 index 00000000..e66023e5 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update2.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1705f35eb2edd8b7a8e244a44488670f54288910277e8b48671837aa02e4a18 +size 202337 diff --git a/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update3.jpg b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update3.jpg new file mode 100644 index 00000000..cf1c0277 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/CurvedHeader/update3.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0540dc8d5d03e5043ff4c976a4946efbcb01a4f7c3e0c4f6005dbf87630be478 +size 219094 diff --git a/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Bold.ttf b/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Bold.ttf new file mode 100644 index 00000000..0a35698d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Bold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da4f442e66843990825ed4757e27ad3442cad83f9844cc503e8ece85e00f77f2 +size 290916 diff --git a/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-BoldItalic.ttf b/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-BoldItalic.ttf new file mode 100644 index 00000000..1dcbaab5 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-BoldItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4413cbf92a508e6a6eb0a089ee9bcf1b94f0b366ea755c5c64b6d4755f160c40 +size 103200 diff --git a/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Italic.ttf b/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Italic.ttf new file mode 100644 index 00000000..5f1badfb --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Italic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dac7846e443ec58c0802cd6fa55836060a62d8a7448c0bccff5242bac9e71d0e +size 103828 diff --git a/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Regular.ttf b/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Regular.ttf new file mode 100644 index 00000000..68410ff1 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Fonts/SourceSansPro-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71d10a86b4c54a5a9c0c8b467e53ac67d79edb96c956e4e9f65a7074dfb9992a +size 293516 diff --git a/samples/AvaloniaControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf b/samples/AvaloniaControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf new file mode 100644 index 00000000..d70ce051 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e2297609126d20a5c3f35eb0ed8ac8bc7e4a2290a820f4055c0703d2c598912 +size 4505016 diff --git a/samples/AvaloniaControlCatalog/Assets/Icon.png b/samples/AvaloniaControlCatalog/Assets/Icon.png new file mode 100644 index 00000000..26522b1f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1821e7bd48a6f407bddcbc1fd789132a3d5b6a5b787951114e3360b4bd650c96 +size 14349 diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/avatar.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/avatar.jpg new file mode 100644 index 00000000..baeae257 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/avatar.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c48b24b7af0a316393b9492b04f26c3716efb385f222cb1f3098a3225e5fd3d4 +size 22590 diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_alps.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_alps.jpg new file mode 100644 index 00000000..db24b89b --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_alps.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:942940764a67ff522ab558510d0fc28d8c6d8139d36ad12f0150ca80061e5911 +size 40061 diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_forest.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_forest.jpg new file mode 100644 index 00000000..3034a1fa --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_forest.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13210c53e74791192cd5f0a08e525acbb84a4fe01634af2a37c26d4440c13335 +size 57483 diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_norway.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_norway.jpg new file mode 100644 index 00000000..d6e6bde5 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/dest_norway.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d35bf25fec4fe7e043daf0a2db45fd60022d6ac30f822de04476e9b75a1613b6 +size 26764 diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/exp_angkor.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/exp_angkor.jpg new file mode 100644 index 00000000..33d774d8 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/exp_angkor.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67d40972a68c877c5caa83ede7e9f9f740262b2f66f643c61f37bff5455e7ffd +size 78618 diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/exp_tokyo.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/exp_tokyo.jpg new file mode 100644 index 00000000..271b47c2 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/exp_tokyo.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:386207dd4f978585f3eab30b99e26edd7d809c739a65d6e6b21c6044d36c1899 +size 104781 diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_alpine.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_alpine.jpg new file mode 100644 index 00000000..6a5363ec --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_alpine.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a952107d9fcdc461dd40dd185e661813766c6f03dee7dfa1727e85973e15b0c +size 39021 diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_bay.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_bay.jpg new file mode 100644 index 00000000..e96394b4 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_bay.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae12c0a8c2580d98ed4ed4fea5b156f7849081199e1b2716f3c8abb4e986de0f +size 35928 diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_city.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_city.jpg new file mode 100644 index 00000000..a7b42af8 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_city.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9d399a312edfc37e9b485c02b11cb3cd5c8eae45c6638966a7a65fe7f8c5218 +size 38189 diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_paris.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_paris.jpg new file mode 100644 index 00000000..a0a71973 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_paris.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea917b0df0e1dfb0125f782dd86942eec48f81da011cf0c69719efbf050dac75 +size 16808 diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_tropical.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_tropical.jpg new file mode 100644 index 00000000..c1af3469 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_tropical.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2885affa5a5400330e5ddbf2a88b4a5ffedc38e7cd7a5d3c4988c84869d8b481 +size 23530 diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_venice.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_venice.jpg new file mode 100644 index 00000000..8331f302 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/gallery_venice.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d80c97e65cf610c562ebda65357356aa6d4d47c32e36e7a77212449aeb9a457c +size 32633 diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/story1.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/story1.jpg new file mode 100644 index 00000000..024fa15b --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/story1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04fe12f3211d8413bcf576839355460357081c5c29b3ee3c8767d9008a00aff9 +size 13068 diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/story2.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/story2.jpg new file mode 100644 index 00000000..ce3d2d40 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/story2.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c81284b88d9cc65af2e35d286dbff0c72da487672358e332be8499c2a174ffe +size 10385 diff --git a/samples/AvaloniaControlCatalog/Assets/ModernApp/story3.jpg b/samples/AvaloniaControlCatalog/Assets/ModernApp/story3.jpg new file mode 100644 index 00000000..a491aa12 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/ModernApp/story3.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5fec94c831245620f3bfc82a34d62482668359418dd4c72ba8b0f16671ae521 +size 9262 diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/cast1.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/cast1.jpg new file mode 100644 index 00000000..a82fd4de --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Movies/cast1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f085e47346df78b2d8d7fc3202c94f0a05f51c94669842f1deaca61edf3513a0 +size 383729 diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/cast2.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/cast2.jpg new file mode 100644 index 00000000..52efa377 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Movies/cast2.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97f9c47d312a7a20d6f816c1f1aa12553d889cdcc812895c6de133e5c4163d3f +size 384405 diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/continue1.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/continue1.jpg new file mode 100644 index 00000000..28e76e59 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Movies/continue1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8864dd8af0a41fecdcd10c9f3dd7f46fd40835eaceec84d9c3bdb469b908c1de +size 243330 diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/hero.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/hero.jpg new file mode 100644 index 00000000..d19b19d2 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Movies/hero.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1d19998f473a10f475fb09dbc9333846e33926df134cae53fbb26ef12dfa065 +size 365476 diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/morelike1.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/morelike1.jpg new file mode 100644 index 00000000..5a74f00e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Movies/morelike1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:232f551a9920a0df27e3f63e6c1bb0284b32654873bbdb6e0fd01d2b03fa289e +size 386942 diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/search1.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/search1.jpg new file mode 100644 index 00000000..fe0ac989 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Movies/search1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e49187f3a3d58250c0340654ef62986c5bcf71cc2bc7ccf8e9f464d06d29843 +size 365280 diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/toprated1.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/toprated1.jpg new file mode 100644 index 00000000..9c0594bf --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Movies/toprated1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:379bed75f5e9fdcf81e0be3b8ce244097b3f941945c64bd4f793501062b92ab8 +size 323000 diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/toprated2.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/toprated2.jpg new file mode 100644 index 00000000..bc2ac63a --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Movies/toprated2.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6027435c1abaf0a84f7badf3b496589c2eb12211b86506273c374a3da6fc4328 +size 193691 diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/toprated3.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/toprated3.jpg new file mode 100644 index 00000000..fc909227 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Movies/toprated3.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81bd4874b15875b3297f59958b67e76fe0cc36c84a3c3e55f1ec72466ef0de01 +size 276190 diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/toprated4.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/toprated4.jpg new file mode 100644 index 00000000..428d1837 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Movies/toprated4.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b64cedeb1b6e382eb48c4ac46667fccf8fcdca4de7a754b6b6e9afed5bac0c17 +size 338325 diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/trending1.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/trending1.jpg new file mode 100644 index 00000000..275d2095 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Movies/trending1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f48ab966356c739a134dfdf518572722d655a1c8ff19d16349f93dd4c5da574 +size 354912 diff --git a/samples/AvaloniaControlCatalog/Assets/Movies/trending2.jpg b/samples/AvaloniaControlCatalog/Assets/Movies/trending2.jpg new file mode 100644 index 00000000..0714cc90 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Movies/trending2.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c4ae07bf3716d1131c59766518d74e50e0a6150f9a018c74fde92b39b786470 +size 360307 diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/cat_hiit.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/cat_hiit.jpg new file mode 100644 index 00000000..6cb7b1dc --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Pulse/cat_hiit.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d2255faa3104c9dbe66b1d5a0a27b9eecac836b322fdf30f43d84d798bc0904 +size 363012 diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/cat_strength.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/cat_strength.jpg new file mode 100644 index 00000000..7619e560 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Pulse/cat_strength.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98eff7032787cf75e5c01968070ca3a18b4167d8124e7dde37ab5ee883e9206f +size 328458 diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/cat_yoga.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/cat_yoga.jpg new file mode 100644 index 00000000..99d8a442 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Pulse/cat_yoga.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:273226cb81451cfed6405f3f337dfd5a1a90f806052c1c5b849fcc31c9ed2bb9 +size 220062 diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/ex_bench.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_bench.jpg new file mode 100644 index 00000000..dff9c638 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_bench.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b6c1e389d2e8a9b3bf8638629ccc6be5dda989fa5aeeb7fbac3b15c347aaf3e +size 348183 diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/ex_deadlifts.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_deadlifts.jpg new file mode 100644 index 00000000..1bf715e1 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_deadlifts.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:729e1e7ab3cad488f8c15055bd2bc46dbf4df378235d09b17b6faf345535dcae +size 350168 diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/ex_overhead.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_overhead.jpg new file mode 100644 index 00000000..ba6c56f2 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_overhead.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d05add74876aa9913d0668ba14814def56385cc6d5a71ef32e2947aa8cf35e4 +size 318869 diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/ex_pullups.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_pullups.jpg new file mode 100644 index 00000000..d73d2714 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_pullups.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c61f8680ec76d0a6dce17194f90960bd071f3e0eb9f4e5c1fa44ad42e08d5fee +size 259525 diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/ex_squats.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_squats.jpg new file mode 100644 index 00000000..db4673a6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Pulse/ex_squats.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4c0ba6f7dfecb0f725199f27771fa143626fd4af82c2267a3e6a4e033d4c2bc +size 349045 diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/profile_avatar.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/profile_avatar.jpg new file mode 100644 index 00000000..e878e284 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Pulse/profile_avatar.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:099c45bf78acddb5e3706c08c78570b2216bc287bee4c9551331ef7755985a87 +size 234819 diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/rec_fullbody.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/rec_fullbody.jpg new file mode 100644 index 00000000..1edfdab7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Pulse/rec_fullbody.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f74e1316bc51221749ebb06bdfbd6e7a291a3cd2bf27482ff8927a1fe7722ff +size 299718 diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/rec_mobility.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/rec_mobility.jpg new file mode 100644 index 00000000..8a197c6f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Pulse/rec_mobility.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80ea995c7241abeda718538e4d39e99c905c561c6b646a536c75cac3968b942a +size 378365 diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/rec_powercore.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/rec_powercore.jpg new file mode 100644 index 00000000..35621156 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Pulse/rec_powercore.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4c4fbf95b31719a0c1aa149635e17ae1165e93a4bae19774c0dd628025bdc5f +size 333866 diff --git a/samples/AvaloniaControlCatalog/Assets/Pulse/workout_hero.jpg b/samples/AvaloniaControlCatalog/Assets/Pulse/workout_hero.jpg new file mode 100644 index 00000000..6736801e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Pulse/workout_hero.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9cce5e1a6dba98d1517ecba88a22b14c9974a775f83190ecfd700b1a09e2996 +size 357270 diff --git a/samples/AvaloniaControlCatalog/Assets/Restaurant/dish1.jpg b/samples/AvaloniaControlCatalog/Assets/Restaurant/dish1.jpg new file mode 100644 index 00000000..47c7c3e4 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Restaurant/dish1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e53d7c95d20decc8fd69432d737f49955d9aad6c4b5e97dbfa8f7d14034e3b2 +size 63977 diff --git a/samples/AvaloniaControlCatalog/Assets/Restaurant/dish2.jpg b/samples/AvaloniaControlCatalog/Assets/Restaurant/dish2.jpg new file mode 100644 index 00000000..7da1fd38 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Restaurant/dish2.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0e26e64efacd609c7c47b5f46f2238f5eeb69a94374c1d57b2bc773962f9596 +size 19720 diff --git a/samples/AvaloniaControlCatalog/Assets/Restaurant/dish3.jpg b/samples/AvaloniaControlCatalog/Assets/Restaurant/dish3.jpg new file mode 100644 index 00000000..0e33613e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Restaurant/dish3.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5fdc8a579038c204a04c44e6828f89e3c18026537258283d46a416fcadc4256a +size 56364 diff --git a/samples/AvaloniaControlCatalog/Assets/Restaurant/dish4.jpg b/samples/AvaloniaControlCatalog/Assets/Restaurant/dish4.jpg new file mode 100644 index 00000000..42b17c21 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Restaurant/dish4.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9854911823e06db37e4ab09de2946c46ffadd035735a9e4ac907b0ed1a6ec92c +size 46057 diff --git a/samples/AvaloniaControlCatalog/Assets/Restaurant/featured_dish.jpg b/samples/AvaloniaControlCatalog/Assets/Restaurant/featured_dish.jpg new file mode 100644 index 00000000..3756a344 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Restaurant/featured_dish.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c73b185ddc65fef06e932526f145e2d94ce3b2421b3e75ecca24d54568274e9f +size 68998 diff --git a/samples/AvaloniaControlCatalog/Assets/Restaurant/user_avatar.jpg b/samples/AvaloniaControlCatalog/Assets/Restaurant/user_avatar.jpg new file mode 100644 index 00000000..ce3d2d40 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Restaurant/user_avatar.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c81284b88d9cc65af2e35d286dbff0c72da487672358e332be8499c2a174ffe +size 10385 diff --git a/samples/AvaloniaControlCatalog/Assets/RetroGaming/cyber_city.jpg b/samples/AvaloniaControlCatalog/Assets/RetroGaming/cyber_city.jpg new file mode 100644 index 00000000..e931dc47 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/RetroGaming/cyber_city.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87d6f01e3c0216f1b87b165fd30a45bd58ca01efd38e779eacf09e9acfd771f6 +size 354653 diff --git a/samples/AvaloniaControlCatalog/Assets/RetroGaming/dungeon_bit.jpg b/samples/AvaloniaControlCatalog/Assets/RetroGaming/dungeon_bit.jpg new file mode 100644 index 00000000..4c2c3016 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/RetroGaming/dungeon_bit.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1879f78ca91253fd12e66e9f10b1fe5943471f1c84561086dd4ebb930e26ef20 +size 468907 diff --git a/samples/AvaloniaControlCatalog/Assets/RetroGaming/forest_spirit.jpg b/samples/AvaloniaControlCatalog/Assets/RetroGaming/forest_spirit.jpg new file mode 100644 index 00000000..36c5957d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/RetroGaming/forest_spirit.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e03ea60a9425aa35d8daea7336eb8ba6eb5a6388bc7c4b918480a1e8153f66ad +size 426054 diff --git a/samples/AvaloniaControlCatalog/Assets/RetroGaming/hero.jpg b/samples/AvaloniaControlCatalog/Assets/RetroGaming/hero.jpg new file mode 100644 index 00000000..deeb1959 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/RetroGaming/hero.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a12f0998ac595661b6eb9b0db10d2c8b0dc94036a9ac8979aabef0907bde8517 +size 220826 diff --git a/samples/AvaloniaControlCatalog/Assets/RetroGaming/neon_ninja.jpg b/samples/AvaloniaControlCatalog/Assets/RetroGaming/neon_ninja.jpg new file mode 100644 index 00000000..240ae5ea --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/RetroGaming/neon_ninja.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbd118a09032ee0e8e095199f65a8230ffadb301f02799308d8dc9221f617f38 +size 303206 diff --git a/samples/AvaloniaControlCatalog/Assets/RetroGaming/neon_racer.jpg b/samples/AvaloniaControlCatalog/Assets/RetroGaming/neon_racer.jpg new file mode 100644 index 00000000..78723686 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/RetroGaming/neon_racer.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eae91ec8b75d3910700ec06ba95da8dbef9e6390414db02466e1e8c8c12bf359 +size 398687 diff --git a/samples/AvaloniaControlCatalog/Assets/RetroGaming/pixel_quest.jpg b/samples/AvaloniaControlCatalog/Assets/RetroGaming/pixel_quest.jpg new file mode 100644 index 00000000..ea0352cb --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/RetroGaming/pixel_quest.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c14d6c36f708f5182d97ef13f5f93ab38d976ab69700d89936eede18f5041ea4 +size 318451 diff --git a/samples/AvaloniaControlCatalog/Assets/RetroGaming/space_voids.jpg b/samples/AvaloniaControlCatalog/Assets/RetroGaming/space_voids.jpg new file mode 100644 index 00000000..9eaa32bd --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/RetroGaming/space_voids.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04404c6cb7b46ac40b5e5d939328c00d81952ccd29d8f766f038f7bb5b15e47b +size 346663 diff --git a/samples/AvaloniaControlCatalog/Assets/Sanctuary/city_bg.jpg b/samples/AvaloniaControlCatalog/Assets/Sanctuary/city_bg.jpg new file mode 100644 index 00000000..84094687 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Sanctuary/city_bg.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7289d08dfd44e1f9b1b9165492924d34600e14330086c3bf311c0d3ccaff429 +size 416179 diff --git a/samples/AvaloniaControlCatalog/Assets/Sanctuary/forest_bg.jpg b/samples/AvaloniaControlCatalog/Assets/Sanctuary/forest_bg.jpg new file mode 100644 index 00000000..5cc52e30 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Sanctuary/forest_bg.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84201ad5c3993927ff95093312f194401f85359298bb8dcc0061d9a6d9576c2b +size 460289 diff --git a/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_arctic_silence.jpg b/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_arctic_silence.jpg new file mode 100644 index 00000000..11ced44e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_arctic_silence.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5507dcbeb97f469087eb4b66f4e9de9dff988628cab77c9f74922ea39d2d14fd +size 468065 diff --git a/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_deep_forest.jpg b/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_deep_forest.jpg new file mode 100644 index 00000000..944c463a --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_deep_forest.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2d5c7b9f823005cd5245ede7f3587cc00d6cf1ccb113f5aece0aebf4b017711 +size 537150 diff --git a/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_desert_sands.jpg b/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_desert_sands.jpg new file mode 100644 index 00000000..eea7a888 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_desert_sands.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb636651ef45fc72bb98c324f556cc89589527d0f277e405b192f09c7b833c1b +size 331737 diff --git a/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_hero.jpg b/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_hero.jpg new file mode 100644 index 00000000..72e22e63 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Sanctuary/main_hero.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49646d05d73283e485c1cff68bab10090815e65b30798d51b376c57c8042dd76 +size 466219 diff --git a/samples/AvaloniaControlCatalog/Assets/Sanctuary/mountain_bg.jpg b/samples/AvaloniaControlCatalog/Assets/Sanctuary/mountain_bg.jpg new file mode 100644 index 00000000..d61797ad --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/Sanctuary/mountain_bg.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:605e9595791622ee51d12f84411c8d6ccae372d134b6fe8d9d3276bbde5c0ae0 +size 416532 diff --git a/samples/AvaloniaControlCatalog/Assets/avalonia-32.png b/samples/AvaloniaControlCatalog/Assets/avalonia-32.png new file mode 100644 index 00000000..f896a9f2 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/avalonia-32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6d9f3f015b19fd537e192fdfc0e8069ea5dcd983ddd2cdf7213f3638bff82d0 +size 1322 diff --git a/samples/AvaloniaControlCatalog/Assets/delicate-arch-896885_640.jpg b/samples/AvaloniaControlCatalog/Assets/delicate-arch-896885_640.jpg new file mode 100644 index 00000000..e1f825cf --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/delicate-arch-896885_640.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f015d396bc046aab2849a62d5c30842be3423ddc4ff58d0b2b07d7f9898b0dc +size 95812 diff --git a/samples/AvaloniaControlCatalog/Assets/github_icon.png b/samples/AvaloniaControlCatalog/Assets/github_icon.png new file mode 100644 index 00000000..41e45765 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/github_icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3c7853d2d976abc126cb5f533d0080ea03436bd05c6f0bfcd57c1efdff69683 +size 17223 diff --git a/samples/AvaloniaControlCatalog/Assets/hirsch-899118_640.jpg b/samples/AvaloniaControlCatalog/Assets/hirsch-899118_640.jpg new file mode 100644 index 00000000..c3254eb4 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/hirsch-899118_640.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37545dd5cf7e85f58cc55dc0e271cabaf15cb4a8ccdadb352122095615b74c63 +size 216662 diff --git a/samples/AvaloniaControlCatalog/Assets/image1.jpg b/samples/AvaloniaControlCatalog/Assets/image1.jpg new file mode 100644 index 00000000..29ab7de1 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/image1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89852956751255d12625b89737fbc97faa9b4cc3c8cf40f28983491ad0ef8e17 +size 71581 diff --git a/samples/AvaloniaControlCatalog/Assets/image2.jpg b/samples/AvaloniaControlCatalog/Assets/image2.jpg new file mode 100644 index 00000000..75c97f00 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/image2.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0f13d195f3daf0d313cb408b3895bf6fd95324a4fdc4523c718bbe15b4bfa50 +size 53850 diff --git a/samples/AvaloniaControlCatalog/Assets/image3.jpg b/samples/AvaloniaControlCatalog/Assets/image3.jpg new file mode 100644 index 00000000..6b70c394 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/image3.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64f5c61afcaca7e4a5d2b32967acce91f8a6976c2eeb1848efb5bd6556412b51 +size 54199 diff --git a/samples/AvaloniaControlCatalog/Assets/image4.jpg b/samples/AvaloniaControlCatalog/Assets/image4.jpg new file mode 100644 index 00000000..d7bd96d1 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/image4.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2c4c6245efa1bcade1d6819be4fc932fd5a78076e009faa54008d8f91a5071a +size 97298 diff --git a/samples/AvaloniaControlCatalog/Assets/image5.jpg b/samples/AvaloniaControlCatalog/Assets/image5.jpg new file mode 100644 index 00000000..3bfd6f62 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/image5.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fcaecb19b5907e37e3728f935390f8744c739df7d0233d7d5688db2d33b5809c +size 69100 diff --git a/samples/AvaloniaControlCatalog/Assets/image6.jpg b/samples/AvaloniaControlCatalog/Assets/image6.jpg new file mode 100644 index 00000000..3d7239a9 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/image6.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c81db4378d3cf4d5f871c59824763014cc8dc88e9adedbd734ae35cecc8e03da +size 109665 diff --git a/samples/AvaloniaControlCatalog/Assets/image7.jpg b/samples/AvaloniaControlCatalog/Assets/image7.jpg new file mode 100644 index 00000000..818793ee --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/image7.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:137a9d41c4e59c420711cfd49090621cf3c0f300e7c3429046a5c783083a7ebc +size 61519 diff --git a/samples/AvaloniaControlCatalog/Assets/maple-leaf-888807_640.jpg b/samples/AvaloniaControlCatalog/Assets/maple-leaf-888807_640.jpg new file mode 100644 index 00000000..b892ddfc --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/maple-leaf-888807_640.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b9f04697c2afc982323df5fcc3804e9a832da24bc213057f7258b0f8915c2f9 +size 113622 diff --git a/samples/AvaloniaControlCatalog/Assets/test_icon.ico b/samples/AvaloniaControlCatalog/Assets/test_icon.ico new file mode 100644 index 00000000..b023aa24 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Assets/test_icon.ico @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df9262e2176d830bfbef5b5e1dfa12f91c89e8ecb764eee366c3352df371d665 +size 176111 diff --git a/samples/AvaloniaControlCatalog/AvaloniaControlCatalog.csproj b/samples/AvaloniaControlCatalog/AvaloniaControlCatalog.csproj new file mode 100644 index 00000000..62e50786 --- /dev/null +++ b/samples/AvaloniaControlCatalog/AvaloniaControlCatalog.csproj @@ -0,0 +1,99 @@ + + + net10.0-windows + WinExe + true + + default + enable + ControlCatalog + AvaloniaControlCatalog + true + true + false + $(NoWarn);AVA3001 + ..\..\..\..\JimBobSquarePants\Avalonia + $(MSBuildProjectDirectory)\$(LocalAvaloniaRoot)\build\avalonia.snk + $(MSBuildProjectDirectory)\NuGet.Config + $(MSBuildProjectDirectory)\$(LocalAvaloniaRoot)\src\Avalonia.Build.Tasks\bin\Release\netstandard2.0\Avalonia.Build.Tasks.dll + true + false + true + true + app.manifest + + + + + + + + + + %(Filename) + + + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/AnonymousObserverNonGenericHelper.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/AnonymousObserverNonGenericHelper.cs new file mode 100644 index 00000000..134c67b2 --- /dev/null +++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/AnonymousObserverNonGenericHelper.cs @@ -0,0 +1,13 @@ +using System; + +namespace Avalonia.Reactive; + +/// +/// Contains fields for that aren't using generic arguments. +/// Separated to avoid unnecessary generic instantiations. +/// +internal static class AnonymousObserverNonGenericHelper +{ + public static readonly Action ThrowsOnError = ex => throw ex; + public static readonly Action NoOpCompleted = () => { }; +} diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyBindingObservable.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyBindingObservable.cs new file mode 100644 index 00000000..fd68381d --- /dev/null +++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyBindingObservable.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using Avalonia.Data; + +namespace Avalonia.Reactive +{ + internal class AvaloniaPropertyBindingObservable : LightweightObservableBase>, IDescription + { + private readonly WeakReference _target; + private readonly AvaloniaProperty _property; + private readonly Func? _converter; + private BindingValue _value = BindingValue.Unset; + + public AvaloniaPropertyBindingObservable( + AvaloniaObject target, + AvaloniaProperty property, + Func? converter = null) + { + _target = new WeakReference(target); + _property = property; + _converter = converter; + } + + public string Description => $"{_target.GetType().Name}.{_property.Name}"; + + protected override void Initialize() + { + if (_target.TryGetTarget(out var target)) + { + if (_converter is { } converter) + { + var unconvertedValue = (TSource)target.GetValue(_property)!; + _value = converter(unconvertedValue); + target.PropertyChanged += PropertyChanged_WithConversion; + } + else + { + _value = (TResult)target.GetValue(_property)!; + target.PropertyChanged += PropertyChanged; + } + } + } + + protected override void Deinitialize() + { + if (_target.TryGetTarget(out var target)) + { + if (_converter is not null) + { + target.PropertyChanged -= PropertyChanged_WithConversion; + } + else + { + target.PropertyChanged -= PropertyChanged; + } + } + } + + protected override void Subscribed(IObserver> observer, bool first) + { + if (_value.Type != BindingValueType.UnsetValue) + { + observer.OnNext(_value); + } + } + + private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + if (e is AvaloniaPropertyChangedEventArgs typedArgs) + { + PublishValue(e.Sender.GetValue(typedArgs.Property)); + } + else + { + PublishUntypedValue(e.Sender.GetValue(e.Property)); + } + } + } + + private void PropertyChanged_WithConversion(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + if (e is AvaloniaPropertyChangedEventArgs typedArgs) + { + var newValueRaw = e.Sender.GetValue(typedArgs.Property); + + var newValue = _converter!(newValueRaw); + + PublishValue(newValue); + } + else + { + var newValue = e.Sender.GetValue(e.Property); + + if (newValue is TSource source) + { + newValue = _converter!(source); + } + + PublishUntypedValue(newValue); + } + } + } + + private void PublishValue(TResult newValue) + { + if (!_value.HasValue || !EqualityComparer.Default.Equals(newValue, _value.Value)) + { + _value = newValue; + PublishNext(_value); + } + } + + private void PublishUntypedValue(object? newValue) + { + if (!Equals(newValue, _value)) + { + _value = (TResult)newValue!; + PublishNext(_value); + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyChangedObservable.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyChangedObservable.cs new file mode 100644 index 00000000..fa876a89 --- /dev/null +++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyChangedObservable.cs @@ -0,0 +1,46 @@ +using System; + +namespace Avalonia.Reactive +{ + internal class AvaloniaPropertyChangedObservable : + LightweightObservableBase, + IDescription + { + private readonly WeakReference _target; + private readonly AvaloniaProperty _property; + + public AvaloniaPropertyChangedObservable( + AvaloniaObject target, + AvaloniaProperty property) + { + _target = new WeakReference(target); + _property = property; + } + + public string Description => $"{_target.GetType().Name}.{_property.Name}"; + + protected override void Initialize() + { + if (_target.TryGetTarget(out var target)) + { + target.PropertyChanged += PropertyChanged; + } + } + + protected override void Deinitialize() + { + if (_target.TryGetTarget(out var target)) + { + target.PropertyChanged -= PropertyChanged; + } + } + + private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + PublishNext(e); + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyObservable.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyObservable.cs new file mode 100644 index 00000000..0d40fa96 --- /dev/null +++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/AvaloniaPropertyObservable.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using Avalonia.Data; + +namespace Avalonia.Reactive +{ + internal class AvaloniaPropertyObservable : LightweightObservableBase, IDescription + { + private readonly WeakReference _target; + private readonly AvaloniaProperty _property; + private readonly Func? _converter; + private Optional _value; + + public AvaloniaPropertyObservable( + AvaloniaObject target, + AvaloniaProperty property, + Func? converter = null) + { + _target = new WeakReference(target); + _property = property; + _converter = converter; + } + + public string Description => $"{_target.GetType().Name}.{_property.Name}"; + + protected override void Initialize() + { + if (_target.TryGetTarget(out var target)) + { + if (_converter is { } converter) + { + var unconvertedValue = (TSource)target.GetValue(_property)!; + _value = converter(unconvertedValue); + target.PropertyChanged += PropertyChanged_WithConversion; + } + else + { + _value = (TResult)target.GetValue(_property)!; + target.PropertyChanged += PropertyChanged; + } + } + } + + protected override void Deinitialize() + { + if (_target.TryGetTarget(out var target)) + { + if (_converter is not null) + { + target.PropertyChanged -= PropertyChanged_WithConversion; + } + else + { + target.PropertyChanged -= PropertyChanged; + } + } + + _value = default; + } + + protected override void Subscribed(IObserver observer, bool first) + { + if (_value.HasValue) + observer.OnNext(_value.Value); + } + + private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + TResult newValue; + + if (e is AvaloniaPropertyChangedEventArgs typed) + { + newValue = AvaloniaObjectExtensions.GetValue(e.Sender, typed.Property); + } + else + { + newValue = (TResult)e.Sender.GetValue(e.Property)!; + } + + PublishNewValue(newValue); + } + } + + private void PropertyChanged_WithConversion(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + TSource newValueRaw; + + if (e is AvaloniaPropertyChangedEventArgs typed) + { + newValueRaw = AvaloniaObjectExtensions.GetValue(e.Sender, typed.Property); + } + else + { + newValueRaw = (TSource)e.Sender.GetValue(e.Property)!; + } + + var newValue = _converter!(newValueRaw); + + PublishNewValue(newValue); + } + } + + private void PublishNewValue(TResult newValue) + { + if (!_value.HasValue || + !EqualityComparer.Default.Equals(newValue, _value.Value)) + { + _value = newValue; + PublishNext(_value.Value!); + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/CombinedSubject.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/CombinedSubject.cs new file mode 100644 index 00000000..dc615313 --- /dev/null +++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/CombinedSubject.cs @@ -0,0 +1,23 @@ +using System; + +namespace Avalonia.Reactive; + +internal class CombinedSubject : IAvaloniaSubject +{ + private readonly IObserver _observer; + private readonly IObservable _observable; + + public CombinedSubject(IObserver observer, IObservable observable) + { + _observer = observer; + _observable = observable; + } + + public void OnCompleted() => _observer.OnCompleted(); + + public void OnError(Exception error) => _observer.OnError(error); + + public void OnNext(T value) => _observer.OnNext(value); + + public IDisposable Subscribe(IObserver observer) => _observable.Subscribe(observer); +} diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/CompositeDisposable.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/CompositeDisposable.cs new file mode 100644 index 00000000..4952686a --- /dev/null +++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/CompositeDisposable.cs @@ -0,0 +1,427 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; + +namespace Avalonia.Reactive; + +internal sealed class CompositeDisposable : ICollection, IDisposable +{ + private readonly object _gate = new object(); + private bool _disposed; + private List _disposables; + private int _count; + private const int ShrinkThreshold = 64; + + /// + /// Initializes a new instance of the class with the specified number of disposables. + /// + /// The number of disposables that the new CompositeDisposable can initially store. + /// is less than zero. + public CompositeDisposable(int capacity) + { + if (capacity < 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity)); + } + + _disposables = new List(capacity); + } + + /// + /// Initializes a new instance of the class from a group of disposables. + /// + /// Disposables that will be disposed together. + /// is null. + /// Any of the disposables in the collection is null. + public CompositeDisposable(params IDisposable[] disposables) + { + if (disposables == null) + { + throw new ArgumentNullException(nameof(disposables)); + } + + _disposables = ToList(disposables); + + // _count can be read by other threads and thus should be properly visible + // also releases the _disposables contents so it becomes thread-safe + Volatile.Write(ref _count, _disposables.Count); + } + + /// + /// Initializes a new instance of the class from a group of disposables. + /// + /// Disposables that will be disposed together. + /// is null. + /// Any of the disposables in the collection is null. + public CompositeDisposable(IList disposables) + { + if (disposables == null) + { + throw new ArgumentNullException(nameof(disposables)); + } + + _disposables = ToList(disposables); + + // _count can be read by other threads and thus should be properly visible + // also releases the _disposables contents so it becomes thread-safe + Volatile.Write(ref _count, _disposables.Count); + } + + private static List ToList(IEnumerable disposables) + { + var capacity = disposables switch + { + IDisposable[] a => a.Length, + ICollection c => c.Count, + _ => 12 + }; + + var list = new List(capacity); + + // do the copy and null-check in one step to avoid a + // second loop for just checking for null items + foreach (var d in disposables) + { + if (d == null) + { + throw new ArgumentException("Disposables can't contain null", nameof(disposables)); + } + + list.Add(d); + } + + return list; + } + + /// + /// Gets the number of disposables contained in the . + /// + public int Count => Volatile.Read(ref _count); + + /// + /// Adds a disposable to the or disposes the disposable if the is disposed. + /// + /// Disposable to add. + /// is null. + public void Add(IDisposable item) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + lock (_gate) + { + if (!_disposed) + { + _disposables.Add(item); + + // If read atomically outside the lock, it should be written atomically inside + // the plain read on _count is fine here because manipulation always happens + // from inside a lock. + Volatile.Write(ref _count, _count + 1); + return; + } + } + + item.Dispose(); + } + + /// + /// Removes and disposes the first occurrence of a disposable from the . + /// + /// Disposable to remove. + /// true if found; false otherwise. + /// is null. + public bool Remove(IDisposable item) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + lock (_gate) + { + // this composite was already disposed and if the item was in there + // it has been already removed/disposed + if (_disposed) + { + return false; + } + + // + // List doesn't shrink the size of the underlying array but does collapse the array + // by copying the tail one position to the left of the removal index. We don't need + // index-based lookup but only ordering for sequential disposal. So, instead of spending + // cycles on the Array.Copy imposed by Remove, we use a null sentinel value. We also + // do manual Swiss cheese detection to shrink the list if there's a lot of holes in it. + // + + // read fields as infrequently as possible + var current = _disposables; + + var i = current.IndexOf(item); + if (i < 0) + { + // not found, just return + return false; + } + + current[i] = null; + + if (current.Capacity > ShrinkThreshold && _count < current.Capacity / 2) + { + var fresh = new List(current.Capacity / 2); + + foreach (var d in current) + { + if (d != null) + { + fresh.Add(d); + } + } + + _disposables = fresh; + } + + // make sure the Count property sees an atomic update + Volatile.Write(ref _count, _count - 1); + } + + // if we get here, the item was found and removed from the list + // just dispose it and report success + + item.Dispose(); + + return true; + } + + /// + /// Disposes all disposables in the group and removes them from the group. + /// + public void Dispose() + { + List? currentDisposables = null; + + lock (_gate) + { + if (!_disposed) + { + currentDisposables = _disposables; + + // nulling out the reference is faster no risk to + // future Add/Remove because _disposed will be true + // and thus _disposables won't be touched again. + _disposables = null!; // NB: All accesses are guarded by _disposed checks. + + Volatile.Write(ref _count, 0); + Volatile.Write(ref _disposed, true); + } + } + + if (currentDisposables != null) + { + foreach (var d in currentDisposables) + { + d?.Dispose(); + } + } + } + + /// + /// Removes and disposes all disposables from the , but does not dispose the . + /// + public void Clear() + { + IDisposable?[] previousDisposables; + + lock (_gate) + { + // disposed composites are always clear + if (_disposed) + { + return; + } + + var current = _disposables; + + previousDisposables = current.ToArray(); + current.Clear(); + + Volatile.Write(ref _count, 0); + } + + foreach (var d in previousDisposables) + { + d?.Dispose(); + } + } + + /// + /// Determines whether the contains a specific disposable. + /// + /// Disposable to search for. + /// true if the disposable was found; otherwise, false. + /// is null. + public bool Contains(IDisposable item) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + lock (_gate) + { + if (_disposed) + { + return false; + } + + return _disposables.Contains(item); + } + } + + /// + /// Copies the disposables contained in the to an array, starting at a particular array index. + /// + /// Array to copy the contained disposables to. + /// Target index at which to copy the first disposable of the group. + /// is null. + /// is less than zero. -or - is larger than or equal to the array length. + public void CopyTo(IDisposable[] array, int arrayIndex) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (arrayIndex < 0 || arrayIndex >= array.Length) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + + lock (_gate) + { + // disposed composites are always empty + if (_disposed) + { + return; + } + + if (arrayIndex + _count > array.Length) + { + // there is not enough space beyond arrayIndex + // to accommodate all _count disposables in this composite + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + + var i = arrayIndex; + + foreach (var d in _disposables) + { + if (d != null) + { + array[i++] = d; + } + } + } + } + + /// + /// Always returns false. + /// + public bool IsReadOnly => false; + + /// + /// Returns an enumerator that iterates through the . + /// + /// An enumerator to iterate over the disposables. + public IEnumerator GetEnumerator() + { + lock (_gate) + { + if (_disposed || _count == 0) + { + return EmptyEnumerator; + } + + // the copy is unavoidable but the creation + // of an outer IEnumerable is avoidable + return new CompositeEnumerator(_disposables.ToArray()); + } + } + + /// + /// Returns an enumerator that iterates through the . + /// + /// An enumerator to iterate over the disposables. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Gets a value that indicates whether the object is disposed. + /// + public bool IsDisposed => Volatile.Read(ref _disposed); + + /// + /// An empty enumerator for the + /// method to avoid allocation on disposed or empty composites. + /// + private static readonly CompositeEnumerator EmptyEnumerator = + new CompositeEnumerator(Array.Empty()); + + /// + /// An enumerator for an array of disposables. + /// + private sealed class CompositeEnumerator : IEnumerator + { + private readonly IDisposable?[] _disposables; + private int _index; + + public CompositeEnumerator(IDisposable?[] disposables) + { + _disposables = disposables; + _index = -1; + } + + public IDisposable Current => _disposables[_index]!; // NB: _index is only advanced to non-null positions. + + object IEnumerator.Current => _disposables[_index]!; + + public void Dispose() + { + // Avoid retention of the referenced disposables + // beyond the lifecycle of the enumerator. + // Not sure if this happens by default to + // generic array enumerators though. + var disposables = _disposables; + Array.Clear(disposables, 0, disposables.Length); + } + + public bool MoveNext() + { + var disposables = _disposables; + + for (;;) + { + var idx = ++_index; + + if (idx >= disposables.Length) + { + return false; + } + + // inlined that filter for null elements + if (disposables[idx] != null) + { + return true; + } + } + } + + public void Reset() + { + _index = -1; + } + } +} diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/Disposable.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/Disposable.cs new file mode 100644 index 00000000..17b0d01d --- /dev/null +++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/Disposable.cs @@ -0,0 +1,98 @@ +using System; +using System.Threading; + +namespace Avalonia.Reactive; + +/// +/// Provides a set of static methods for creating objects. +/// +internal static class Disposable +{ + /// + /// Represents a disposable that does nothing on disposal. + /// + private sealed class EmptyDisposable : IDisposable + { + public static readonly EmptyDisposable Instance = new(); + + private EmptyDisposable() + { + } + + public void Dispose() + { + // no op + } + } + + internal sealed class AnonymousDisposable : IDisposable + { + private volatile Action? _dispose; + public AnonymousDisposable(Action dispose) + { + _dispose = dispose; + } + public bool IsDisposed => _dispose == null; + public void Dispose() + { + Interlocked.Exchange(ref _dispose, null)?.Invoke(); + } + } + + internal sealed class AnonymousDisposable : IDisposable + { + private TState _state; + private volatile Action? _dispose; + + public AnonymousDisposable(TState state, Action dispose) + { + _state = state; + _dispose = dispose; + } + + public bool IsDisposed => _dispose == null; + public void Dispose() + { + Interlocked.Exchange(ref _dispose, null)?.Invoke(_state); + _state = default!; + } + } + + /// + /// Gets the disposable that does nothing when disposed. + /// + public static IDisposable Empty => EmptyDisposable.Instance; + + /// + /// Creates a disposable object that invokes the specified action when disposed. + /// + /// Action to run during the first call to . The action is guaranteed to be run at most once. + /// The disposable object that runs the given action upon disposal. + /// is null. + public static IDisposable Create(Action dispose) + { + if (dispose == null) + { + throw new ArgumentNullException(nameof(dispose)); + } + + return new AnonymousDisposable(dispose); + } + + /// + /// Creates a disposable object that invokes the specified action when disposed. + /// + /// The state to be passed to the action. + /// Action to run during the first call to . The action is guaranteed to be run at most once. + /// The disposable object that runs the given action upon disposal. + /// is null. + public static IDisposable Create(TState state, Action dispose) + { + if (dispose == null) + { + throw new ArgumentNullException(nameof(dispose)); + } + + return new AnonymousDisposable(state, dispose); + } +} diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/DisposableMixin.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/DisposableMixin.cs new file mode 100644 index 00000000..478312fa --- /dev/null +++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/DisposableMixin.cs @@ -0,0 +1,37 @@ +using System; +using Avalonia.Reactive; + +namespace Avalonia.Reactive; + +/// +/// Extension methods associated with the IDisposable interface. +/// +internal static class DisposableMixin +{ + /// + /// Ensures the provided disposable is disposed with the specified . + /// + /// + /// The type of the disposable. + /// + /// + /// The disposable we are going to want to be disposed by the CompositeDisposable. + /// + /// + /// The to which will be added. + /// + /// + /// The disposable. + /// + public static T DisposeWith(this T item, CompositeDisposable compositeDisposable) + where T : IDisposable + { + if (compositeDisposable is null) + { + throw new ArgumentNullException(nameof(compositeDisposable)); + } + + compositeDisposable.Add(item); + return item; + } +} diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/IAvaloniaSubject.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/IAvaloniaSubject.cs new file mode 100644 index 00000000..272e2190 --- /dev/null +++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/IAvaloniaSubject.cs @@ -0,0 +1,8 @@ +using System; + +namespace Avalonia.Reactive; + +internal interface IAvaloniaSubject : IObserver, IObservable /*, ISubject */ +{ + +} diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/LightweightObservableBase.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/LightweightObservableBase.cs new file mode 100644 index 00000000..0688a4ed --- /dev/null +++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/LightweightObservableBase.cs @@ -0,0 +1,237 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Threading; + +namespace Avalonia.Reactive +{ + /// + /// Lightweight base class for observable implementations. + /// + /// The observable type. + /// + /// ObservableBase{T} is rather heavyweight in terms of allocations and memory + /// usage. This class provides a more lightweight base for some internal observable types + /// in the Avalonia framework. + /// + internal abstract class LightweightObservableBase : IObservable + { + private Exception? _error; + private List>? _observers = new List>(); + + public bool HasObservers => _observers?.Count > 0; + + public IDisposable Subscribe(IObserver observer) + { + _ = observer ?? throw new ArgumentNullException(nameof(observer)); + + //Dispatcher.UIThread.VerifyAccess(); + + var first = false; + + for (; ; ) + { + if (Volatile.Read(ref _observers) == null) + { + if (_error != null) + { + observer.OnError(_error); + } + else + { + observer.OnCompleted(); + } + + return Disposable.Empty; + } + + lock (this) + { + if (_observers == null) + { + continue; + } + + first = _observers.Count == 0; + _observers.Add(observer); + break; + } + } + + if (first) + { + Initialize(); + } + + Subscribed(observer, first); + + return new RemoveObserver(this, observer); + } + + void Remove(IObserver observer) + { + if (Volatile.Read(ref _observers) != null) + { + lock (this) + { + var observers = _observers; + + if (observers != null) + { + observers.Remove(observer); + + if (observers.Count == 0) + { + observers.TrimExcess(); + Deinitialize(); + } + } + } + } + } + + sealed class RemoveObserver : IDisposable + { + LightweightObservableBase? _parent; + + IObserver? _observer; + + public RemoveObserver(LightweightObservableBase parent, IObserver observer) + { + _parent = parent; + Volatile.Write(ref _observer, observer); + } + + public void Dispose() + { + var observer = _observer; + Interlocked.Exchange(ref _parent, null)?.Remove(observer!); + _observer = null; + } + } + + protected abstract void Initialize(); + protected abstract void Deinitialize(); + + protected void PublishNext(T value) + { + if (Volatile.Read(ref _observers) != null) + { + IObserver[]? observers = null; + int count = 0; + + // Optimize for the common case of 1/2/3 observers. + IObserver? observer0 = null; + IObserver? observer1 = null; + IObserver? observer2 = null; + lock (this) + { + if (_observers == null) + { + return; + } + + count = _observers.Count; + switch (count) + { + case 3: + observer0 = _observers[0]; + observer1 = _observers[1]; + observer2 = _observers[2]; + break; + case 2: + observer0 = _observers[0]; + observer1 = _observers[1]; + break; + case 1: + observer0 = _observers[0]; + break; + case 0: + return; + default: + { + observers = ArrayPool>.Shared.Rent(count); + _observers.CopyTo(observers); + break; + } + } + } + + if (observer0 != null) + { + observer0.OnNext(value); + observer1?.OnNext(value); + observer2?.OnNext(value); + } + else if (observers != null) + { + for(int i = 0; i < count; i++) + { + observers[i].OnNext(value); + // Avoid memory leak by clearing the reference. + observers[i] = null!; + } + + ArrayPool>.Shared.Return(observers); + } + } + } + + protected void PublishCompleted() + { + if (Volatile.Read(ref _observers) != null) + { + IObserver[] observers; + + lock (this) + { + if (_observers == null) + { + return; + } + observers = _observers.ToArray(); + Volatile.Write(ref _observers, null); + } + + foreach (var observer in observers) + { + observer.OnCompleted(); + } + + Deinitialize(); + } + } + + protected void PublishError(Exception error) + { + if (Volatile.Read(ref _observers) != null) + { + + IObserver[] observers; + + lock (this) + { + if (_observers == null) + { + return; + } + + _error = error; + observers = _observers.ToArray(); + Volatile.Write(ref _observers, null); + } + + foreach (var observer in observers) + { + observer.OnError(error); + } + + Deinitialize(); + } + } + + protected virtual void Subscribed(IObserver observer, bool first) + { + } + } +} diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/LightweightSubject.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/LightweightSubject.cs new file mode 100644 index 00000000..5663fd66 --- /dev/null +++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/LightweightSubject.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using Avalonia.Threading; + +namespace Avalonia.Reactive; + +internal class LightweightSubject : LightweightObservableBase, IAvaloniaSubject +{ + public void OnCompleted() + { + PublishCompleted(); + } + + public void OnError(Exception error) + { + PublishError(error); + } + + public void OnNext(T value) + { + PublishNext(value); + } + + protected override void Initialize() { } + + protected override void Deinitialize() { } +} diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/Observable.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/Observable.cs new file mode 100644 index 00000000..4cffdaa6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/Observable.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Reactive.Operators; +using Avalonia.Threading; + +namespace Avalonia.Reactive; + +/// +/// Provides common observable methods as a replacement for the Rx framework. +/// +internal static class Observable +{ + public static IObservable Create(Func, IDisposable> subscribe) + { + return new CreateWithDisposableObservable(subscribe); + } + + public static IDisposable Subscribe(this IObservable source, Action action) + { + return source.Subscribe(new AnonymousObserver(action)); + } + + public static IObservable Select(this IObservable source, Func selector) + { + return Create(obs => + { + return source.Subscribe(new AnonymousObserver( + input => + { + TResult value; + try + { + value = selector(input); + } + catch (Exception ex) + { + obs.OnError(ex); + return; + } + + obs.OnNext(value); + }, obs.OnError, obs.OnCompleted)); + }); + } + + public static IObservable StartWith(this IObservable source, TSource value) + { + return Create(obs => + { + obs.OnNext(value); + return source.Subscribe(obs); + }); + } + + public static IObservable Where(this IObservable source, Func predicate) + { + return Create(obs => + { + return source.Subscribe(new AnonymousObserver( + input => + { + bool shouldRun; + try + { + shouldRun = predicate(input); + } + catch (Exception ex) + { + obs.OnError(ex); + return; + } + if (shouldRun) + { + obs.OnNext(input); + } + }, obs.OnError, obs.OnCompleted)); + }); + } + + public static IObservable Switch( + this IObservable> sources) + { + return new Switch(sources); + } + + public static IObservable CombineLatest( + this IObservable first, IObservable second, + Func resultSelector) + { + return new CombineLatest(first, second, resultSelector); + } + + public static IObservable CombineLatest( + this IEnumerable> inputs) + { + return new CombineLatest(inputs, items => items); + } + + public static IObservable Skip(this IObservable source, int skipCount) + { + if (skipCount <= 0) + { + throw new ArgumentException("Skip count must be bigger than zero", nameof(skipCount)); + } + + return Create(obs => + { + var remaining = skipCount; + return source.Subscribe(new AnonymousObserver( + input => + { + if (remaining <= 0) + { + obs.OnNext(input); + } + else + { + remaining--; + } + }, obs.OnError, obs.OnCompleted)); + }); + } + + public static IObservable Take(this IObservable source, int takeCount) + { + if (takeCount <= 0) + { + return Empty(); + } + + return Create(obs => + { + var remaining = takeCount; + IDisposable? sub = null; + sub = source.Subscribe(new AnonymousObserver( + input => + { + if (remaining > 0) + { + --remaining; + obs.OnNext(input); + + if (remaining == 0) + { + sub?.Dispose(); + obs.OnCompleted(); + } + } + }, obs.OnError, obs.OnCompleted)); + return sub; + }); + } + + public static IObservable FromEventPattern(Action addHandler, Action removeHandler) + { + return Create(observer => + { + var handler = new Action(observer.OnNext); + var converted = new EventHandler((_, args) => handler(args)); + addHandler(converted); + + return Disposable.Create(() => removeHandler(converted)); + }); + } + + public static IObservable FromEventPattern(Action> addHandler, Action> removeHandler) where T : EventArgs + { + return Create(observer => + { + var handler = new Action(observer.OnNext); + var converted = new EventHandler((_, args) => handler(args)); + addHandler(converted); + + return Disposable.Create(() => removeHandler(converted)); + }); + } + + public static IObservable Return(T value) + { + return new ReturnImpl(value); + } + + public static IObservable Empty() + { + return EmptyImpl.Instance; + } + + /// + /// Returns an observable that fires once with the specified value and never completes. + /// + /// The type of the value. + /// The value. + /// The observable. + public static IObservable SingleValue(T value) + { + return new SingleValueImpl(value); + } + + private sealed class SingleValueImpl : IObservable + { + private readonly T _value; + + public SingleValueImpl(T value) + { + _value = value; + } + public IDisposable Subscribe(IObserver observer) + { + observer.OnNext(_value); + return Disposable.Empty; + } + } + + private sealed class ReturnImpl : IObservable + { + private readonly T _value; + + public ReturnImpl(T value) + { + _value = value; + } + public IDisposable Subscribe(IObserver observer) + { + observer.OnNext(_value); + observer.OnCompleted(); + return Disposable.Empty; + } + } + + internal sealed class EmptyImpl : IObservable + { + internal static readonly IObservable Instance = new EmptyImpl(); + + private EmptyImpl() { } + + public IDisposable Subscribe(IObserver observer) + { + observer.OnCompleted(); + return Disposable.Empty; + } + } + + private sealed class CreateWithDisposableObservable : IObservable + { + private readonly Func, IDisposable> _subscribe; + + public CreateWithDisposableObservable(Func, IDisposable> subscribe) + { + _subscribe = subscribe; + } + + public IDisposable Subscribe(IObserver observer) + { + return _subscribe(observer); + } + } +} diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/CombineLatest.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/CombineLatest.cs new file mode 100644 index 00000000..aa95ceec --- /dev/null +++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/CombineLatest.cs @@ -0,0 +1,374 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Avalonia.Reactive.Operators; + +// Code based on https://github.com/dotnet/reactive/blob/main/Rx.NET/Source/src/System.Reactive/Linq/Observable/CombineLatest.cs + +internal sealed class CombineLatest : IObservable +{ + private readonly IObservable _first; + private readonly IObservable _second; + private readonly Func _resultSelector; + + public CombineLatest(IObservable first, IObservable second, + Func resultSelector) + { + _first = first; + _second = second; + _resultSelector = resultSelector; + } + + public IDisposable Subscribe(IObserver observer) + { + var sink = new _(_resultSelector, observer); + sink.Run(_first, _second); + return sink; + } + + internal sealed class _ : IdentitySink + { + private readonly Func _resultSelector; + private readonly object _gate = new object(); + + public _(Func resultSelector, IObserver observer) + : base(observer) + { + _resultSelector = resultSelector; + _firstDisposable = null!; + _secondDisposable = null!; + } + + private IDisposable _firstDisposable; + private IDisposable _secondDisposable; + + public void Run(IObservable first, IObservable second) + { + var fstO = new FirstObserver(this); + var sndO = new SecondObserver(this); + + fstO.SetOther(sndO); + sndO.SetOther(fstO); + + _firstDisposable = first.Subscribe(fstO); + _secondDisposable = second.Subscribe(sndO); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _firstDisposable.Dispose(); + _secondDisposable.Dispose(); + } + + base.Dispose(disposing); + } + + private sealed class FirstObserver : IObserver + { + private readonly _ _parent; + private SecondObserver _other; + + public FirstObserver(_ parent) + { + _parent = parent; + _other = default!; // NB: Will be set by SetOther. + } + + public void SetOther(SecondObserver other) { _other = other; } + + public bool HasValue { get; private set; } + public TFirst? Value { get; private set; } + public bool Done { get; private set; } + + public void OnNext(TFirst value) + { + lock (_parent._gate) + { + HasValue = true; + Value = value; + + if (_other.HasValue) + { + TResult res; + try + { + res = _parent._resultSelector(value, _other.Value!); + } + catch (Exception ex) + { + _parent.ForwardOnError(ex); + return; + } + + _parent.ForwardOnNext(res); + } + else if (_other.Done) + { + _parent.ForwardOnCompleted(); + } + } + } + + public void OnError(Exception error) + { + lock (_parent._gate) + { + _parent.ForwardOnError(error); + } + } + + public void OnCompleted() + { + lock (_parent._gate) + { + Done = true; + + if (_other.Done) + { + _parent.ForwardOnCompleted(); + } + else + { + _parent._firstDisposable.Dispose(); + } + } + } + } + + private sealed class SecondObserver : IObserver + { + private readonly _ _parent; + private FirstObserver _other; + + public SecondObserver(_ parent) + { + _parent = parent; + _other = default!; // NB: Will be set by SetOther. + } + + public void SetOther(FirstObserver other) { _other = other; } + + public bool HasValue { get; private set; } + public TSecond? Value { get; private set; } + public bool Done { get; private set; } + + public void OnNext(TSecond value) + { + lock (_parent._gate) + { + HasValue = true; + Value = value; + + if (_other.HasValue) + { + TResult res; + try + { + res = _parent._resultSelector(_other.Value!, value); + } + catch (Exception ex) + { + _parent.ForwardOnError(ex); + return; + } + + _parent.ForwardOnNext(res); + } + else if (_other.Done) + { + _parent.ForwardOnCompleted(); + } + } + } + + public void OnError(Exception error) + { + lock (_parent._gate) + { + _parent.ForwardOnError(error); + } + } + + public void OnCompleted() + { + lock (_parent._gate) + { + Done = true; + + if (_other.Done) + { + _parent.ForwardOnCompleted(); + } + else + { + _parent._secondDisposable.Dispose(); + } + } + } + } + } +} + +internal sealed class CombineLatest : IObservable +{ + private readonly IEnumerable> _sources; + private readonly Func _resultSelector; + + public CombineLatest(IEnumerable> sources, Func resultSelector) + { + _sources = sources; + _resultSelector = resultSelector; + } + + public IDisposable Subscribe(IObserver observer) + { + var sink = new _(_resultSelector, observer); + sink.Run(_sources); + return sink; + } + + internal sealed class _ : IdentitySink + { + private readonly object _gate = new object(); + private readonly Func _resultSelector; + + public _(Func resultSelector, IObserver observer) + : base(observer) + { + _resultSelector = resultSelector; + + // NB: These will be set in Run before getting used. + _hasValue = null!; + _values = null!; + _isDone = null!; + _subscriptions = null!; + } + + private bool[] _hasValue; + private bool _hasValueAll; + private TSource[] _values; + private bool[] _isDone; + private IDisposable[] _subscriptions; + + public void Run(IEnumerable> sources) + { + var srcs = sources.ToArray(); + + var N = srcs.Length; + + _hasValue = new bool[N]; + _hasValueAll = false; + + _values = new TSource[N]; + + _isDone = new bool[N]; + + _subscriptions = new IDisposable[N]; + + for (var i = 0; i < N; i++) + { + var j = i; + + var o = new SourceObserver(this, j); + _subscriptions[j] = o; + + o.Disposable = srcs[j].Subscribe(o); + } + + SetUpstream(new CompositeDisposable(_subscriptions)); + } + + private void OnNext(int index, TSource value) + { + lock (_gate) + { + _values[index] = value; + + _hasValue[index] = true; + + if (_hasValueAll || (_hasValueAll = _hasValue.All(v => v))) + { + TResult res; + try + { + res = _resultSelector(_values); + } + catch (Exception ex) + { + ForwardOnError(ex); + return; + } + + ForwardOnNext(res); + } + else if (_isDone.Where((_, i) => i != index).All(d => d)) + { + ForwardOnCompleted(); + } + } + } + + private new void OnError(Exception error) + { + lock (_gate) + { + ForwardOnError(error); + } + } + + private void OnCompleted(int index) + { + lock (_gate) + { + _isDone[index] = true; + + if (_isDone.All(d => d)) + { + ForwardOnCompleted(); + } + else + { + _subscriptions[index].Dispose(); + } + } + } + + private sealed class SourceObserver : IObserver, IDisposable + { + private readonly _ _parent; + private readonly int _index; + + public SourceObserver(_ parent, int index) + { + _parent = parent; + _index = index; + } + + public IDisposable? Disposable { get; set; } + + public void OnNext(TSource value) + { + _parent.OnNext(_index, value); + } + + public void OnError(Exception error) + { + _parent.OnError(error); + } + + public void OnCompleted() + { + _parent.OnCompleted(_index); + } + + public void Dispose() + { + Disposable?.Dispose(); + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/Sink.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/Sink.cs new file mode 100644 index 00000000..7acb5833 --- /dev/null +++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/Sink.cs @@ -0,0 +1,111 @@ +using System; +using System.Threading; + +namespace Avalonia.Reactive.Operators; + +// Code based on https://github.com/dotnet/reactive/blob/main/Rx.NET/Source/src/System.Reactive/Internal/Sink.cs + +internal abstract class Sink : IDisposable +{ + private IDisposable? _upstream; + private readonly IObserver _observer; + + protected Sink(IObserver observer) + { + _observer = observer; + } + + public void Dispose() + { + Dispose(true); + } + + /// + /// Override this method to dispose additional resources. + /// The method is guaranteed to be called at most once. + /// + /// If true, the method was called from . + protected virtual void Dispose(bool disposing) + { + //Calling base.Dispose(true) is not a proper disposal, so we can omit the assignment here. + //Sink is internal so this can pretty much be enforced. + //_observer = NopObserver.Instance; + + _upstream?.Dispose(); + } + + public void ForwardOnNext(TTarget value) + { + _observer.OnNext(value); + } + + public void ForwardOnCompleted() + { + _observer.OnCompleted(); + Dispose(); + } + + public void ForwardOnError(Exception error) + { + _observer.OnError(error); + Dispose(); + } + + protected void SetUpstream(IDisposable upstream) + { + _upstream = upstream; + } + + protected void DisposeUpstream() + { + _upstream?.Dispose(); + } +} + +internal abstract class Sink : Sink, IObserver +{ + protected Sink(IObserver observer) : base(observer) + { + } + + public virtual void Run(IObservable source) + { + SetUpstream(source.Subscribe(this)); + } + + public abstract void OnNext(TSource value); + + public virtual void OnError(Exception error) => ForwardOnError(error); + + public virtual void OnCompleted() => ForwardOnCompleted(); + + public IObserver GetForwarder() => new _(this); + + private sealed class _ : IObserver + { + private readonly Sink _forward; + + public _(Sink forward) + { + _forward = forward; + } + + public void OnNext(TTarget value) => _forward.ForwardOnNext(value); + + public void OnError(Exception error) => _forward.ForwardOnError(error); + + public void OnCompleted() => _forward.ForwardOnCompleted(); + } +} + +internal abstract class IdentitySink : Sink +{ + protected IdentitySink(IObserver observer) : base(observer) + { + } + + public override void OnNext(T value) + { + ForwardOnNext(value); + } +} diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/Switch.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/Switch.cs new file mode 100644 index 00000000..f0ca9e3b --- /dev/null +++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/Operators/Switch.cs @@ -0,0 +1,145 @@ +using System; + +namespace Avalonia.Reactive.Operators; + +// Code based on https://github.com/dotnet/reactive/blob/main/Rx.NET/Source/src/System.Reactive/Linq/Observable/Switch.cs + +internal sealed class Switch : IObservable +{ + private readonly IObservable> _sources; + + public Switch(IObservable> sources) + { + _sources = sources; + } + + public IDisposable Subscribe(IObserver observer) + { + return _sources.Subscribe(new _(observer)); + } + + internal sealed class _ : Sink, TSource> + { + private readonly object _gate = new object(); + + public _(IObserver observer) + : base(observer) + { + } + + private IDisposable? _innerSerialDisposable; + private bool _isStopped; + private ulong _latest; + private bool _hasLatest; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _innerSerialDisposable?.Dispose(); + } + + base.Dispose(disposing); + } + + public override void OnNext(IObservable value) + { + ulong id; + + lock (_gate) + { + id = unchecked(++_latest); + _hasLatest = true; + } + + var innerObserver = new InnerObserver(this, id); + + _innerSerialDisposable?.Dispose(); + _innerSerialDisposable = innerObserver; + innerObserver.Disposable = value.Subscribe(innerObserver); + } + + public override void OnError(Exception error) + { + lock (_gate) + { + ForwardOnError(error); + } + } + + public override void OnCompleted() + { + lock (_gate) + { + DisposeUpstream(); + + _isStopped = true; + if (!_hasLatest) + { + ForwardOnCompleted(); + } + } + } + + private sealed class InnerObserver : IObserver, IDisposable + { + private readonly _ _parent; + private readonly ulong _id; + + public InnerObserver(_ parent, ulong id) + { + _parent = parent; + _id = id; + } + + public IDisposable? Disposable { get; set; } + + public void OnNext(TSource value) + { + lock (_parent._gate) + { + if (_parent._latest == _id) + { + _parent.ForwardOnNext(value); + } + } + } + + public void OnError(Exception error) + { + lock (_parent._gate) + { + Dispose(); + + if (_parent._latest == _id) + { + _parent.ForwardOnError(error); + } + } + } + + public void OnCompleted() + { + lock (_parent._gate) + { + Dispose(); + + if (_parent._latest == _id) + { + _parent._hasLatest = false; + + if (_parent._isStopped) + { + _parent.ForwardOnCompleted(); + } + } + } + } + + public void Dispose() + { + Disposable?.Dispose(); + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/AvaloniaReactive/SingleSubscriberObservableBase.cs b/samples/AvaloniaControlCatalog/AvaloniaReactive/SingleSubscriberObservableBase.cs new file mode 100644 index 00000000..10d42214 --- /dev/null +++ b/samples/AvaloniaControlCatalog/AvaloniaReactive/SingleSubscriberObservableBase.cs @@ -0,0 +1,78 @@ +using System; +using Avalonia.Threading; + +namespace Avalonia.Reactive +{ + internal abstract class SingleSubscriberObservableBase : IObservable, IDisposable + { + private Exception? _error; + private IObserver? _observer; + private bool _completed; + + public IDisposable Subscribe(IObserver observer) + { + _ = observer ?? throw new ArgumentNullException(nameof(observer)); + Dispatcher.UIThread.VerifyAccess(); + + if (_observer != null) + { + throw new InvalidOperationException("The observable can only be subscribed once."); + } + + if (_error != null) + { + observer.OnError(_error); + } + else if (_completed) + { + observer.OnCompleted(); + } + else + { + _observer = observer; + Subscribed(); + } + + return this; + } + + public virtual void Dispose() + { + Unsubscribed(); + _observer = null; + } + + protected abstract void Unsubscribed(); + + protected void PublishNext(T value) + { + _observer?.OnNext(value); + } + + protected void PublishCompleted() + { + _completed = true; + + if (_observer != null) + { + _observer.OnCompleted(); + Unsubscribed(); + _observer = null; + } + } + + protected void PublishError(Exception error) + { + _error = error; + + if (_observer != null) + { + _observer.OnError(error); + Unsubscribed(); + _observer = null; + } + } + + protected abstract void Subscribed(); + } +} diff --git a/samples/AvaloniaControlCatalog/CombinedGeometryImpl.cs b/samples/AvaloniaControlCatalog/CombinedGeometryImpl.cs new file mode 100644 index 00000000..1148cc19 --- /dev/null +++ b/samples/AvaloniaControlCatalog/CombinedGeometryImpl.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Avalonia.Media; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; + +namespace AvaloniaControlCatalog; + +/// +/// Avalonia combined geometry implementation backed by ImageSharp path clipping. +/// +internal sealed class CombinedGeometryImpl : GeometryImpl +{ + /// + /// Creates a combined geometry from two ImageSharp-backed geometries. + /// + /// The geometry combine mode. + /// The first geometry. + /// The second geometry. + /// The combined geometry. + public static CombinedGeometryImpl Create(GeometryCombineMode combineMode, GeometryImpl first, GeometryImpl second) + => new(CreatePaths(combineMode, first, second)); + + private CombinedGeometryImpl((IPath StrokePath, IPath? FillPath, IntersectionRule FillRule) paths) + : base(paths.StrokePath, paths.FillPath, paths.FillRule) + { + } + + private static (IPath StrokePath, IPath? FillPath, IntersectionRule FillRule) CreatePaths( + GeometryCombineMode combineMode, + GeometryImpl first, + GeometryImpl second) + { + BooleanOperation operation = combineMode.ToBooleanOperation(); + + IPath strokePath = first.StrokePath.Clip(operation, first.FillRule, second.StrokePath); + IPath? fillPath = null; + + if (first.FillPath is not null && second.FillPath is not null) + { + fillPath = ReferenceEquals(first.FillPath, first.StrokePath) && ReferenceEquals(second.FillPath, second.StrokePath) + ? strokePath + : first.FillPath.Clip(operation, first.FillRule, second.FillPath); + } + + return (strokePath, fillPath, first.FillRule); + } +} diff --git a/samples/AvaloniaControlCatalog/Converter/DegToRadConverter.cs b/samples/AvaloniaControlCatalog/Converter/DegToRadConverter.cs new file mode 100644 index 00000000..b062bcb6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Converter/DegToRadConverter.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using Avalonia.Data.Converters; + +namespace ControlCatalog.Converter +{ + public class DegToRadConverter : IValueConverter + { + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is double rad) + { + return rad * 180.0d / Math.PI; + } + return 0.0d; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is double deg) + { + return deg / 180.0d * Math.PI; + } + return 0.0d; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Converter/GdpToBrushConverter.cs b/samples/AvaloniaControlCatalog/Converter/GdpToBrushConverter.cs new file mode 100644 index 00000000..d5d6f5cd --- /dev/null +++ b/samples/AvaloniaControlCatalog/Converter/GdpToBrushConverter.cs @@ -0,0 +1,31 @@ +using System; +using System.Globalization; +using Avalonia; +using Avalonia.Data.Converters; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace ControlCatalog.Converter; + +public sealed class GdpToBrushConverter : IValueConverter +{ + private readonly ImmutableSolidColorBrush _orangeBrush = new(Colors.Orange, 0.6); + private readonly ImmutableSolidColorBrush _yellowBrush = new(Colors.Yellow, 0.6); + private readonly ImmutableSolidColorBrush _greenBrush = new(Colors.LightGreen, 0.6); + + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not int gdp) + return AvaloniaProperty.UnsetValue; + + return gdp switch + { + <= 5000 => _orangeBrush, + <= 10000 => _yellowBrush, + _ => _greenBrush + }; + } + + object IValueConverter.ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => throw new NotSupportedException(); +} diff --git a/samples/AvaloniaControlCatalog/Converter/HexConverter.cs b/samples/AvaloniaControlCatalog/Converter/HexConverter.cs new file mode 100644 index 00000000..31cce5ba --- /dev/null +++ b/samples/AvaloniaControlCatalog/Converter/HexConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Globalization; +using Avalonia; +using Avalonia.Data.Converters; + +namespace ControlCatalog.Converter; + +public class HexConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var str = value?.ToString(); + if (str == null) + return AvaloniaProperty.UnsetValue; + if (int.TryParse(str, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int x)) + return (decimal)x; + return AvaloniaProperty.UnsetValue; + + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + try + { + if (value is decimal d) + return ((int)d).ToString("X8"); + return AvaloniaProperty.UnsetValue; + } + catch + { + return AvaloniaProperty.UnsetValue; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Converter/MathSubtractConverter.cs b/samples/AvaloniaControlCatalog/Converter/MathSubtractConverter.cs new file mode 100644 index 00000000..6f3c99ab --- /dev/null +++ b/samples/AvaloniaControlCatalog/Converter/MathSubtractConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace ControlCatalog.Converter; + +public class MathSubtractConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is double dv && parameter is double dp) + { + return dv - dp; + } + return double.NaN; + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/samples/AvaloniaControlCatalog/Converter/TableViewColumnWidthConverter.cs b/samples/AvaloniaControlCatalog/Converter/TableViewColumnWidthConverter.cs new file mode 100644 index 00000000..e9f976f6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Converter/TableViewColumnWidthConverter.cs @@ -0,0 +1,27 @@ +using System; +using System.Globalization; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data.Converters; + +namespace ControlCatalog.Converter; + +public sealed class TableViewColumnWidthConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is bool useStarSize && + parameter is string stringParameter && + double.TryParse(stringParameter, NumberStyles.Number, CultureInfo.InvariantCulture, out var baseWidth)) + { + return useStarSize ? + new GridLength(baseWidth, GridUnitType.Star) : + new GridLength(baseWidth * 100, GridUnitType.Pixel); + } + + return AvaloniaProperty.UnsetValue; + } + + object? IValueConverter.ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => throw new NotSupportedException(); +} diff --git a/samples/AvaloniaControlCatalog/DecoratedWindow.xaml b/samples/AvaloniaControlCatalog/DecoratedWindow.xaml new file mode 100644 index 00000000..804eebfb --- /dev/null +++ b/samples/AvaloniaControlCatalog/DecoratedWindow.xaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + Title + + + + + + + + + + + + Hello world! + + + + None + BorderOnly + Full + + + CanResize + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/DecoratedWindow.xaml.cs b/samples/AvaloniaControlCatalog/DecoratedWindow.xaml.cs new file mode 100644 index 00000000..bb87d982 --- /dev/null +++ b/samples/AvaloniaControlCatalog/DecoratedWindow.xaml.cs @@ -0,0 +1,45 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace ControlCatalog +{ + public partial class DecoratedWindow : Window + { + public DecoratedWindow() + { + InitializeComponent(); + TitleBar.PointerPressed += (i, e) => + { + BeginMoveDrag(e); + }; + SetupSide(Left, StandardCursorType.LeftSide, WindowEdge.West); + SetupSide(Right, StandardCursorType.RightSide, WindowEdge.East); + SetupSide(Top, StandardCursorType.TopSide, WindowEdge.North); + SetupSide(Bottom, StandardCursorType.BottomSide, WindowEdge.South); + SetupSide(TopLeft, StandardCursorType.TopLeftCorner, WindowEdge.NorthWest); + SetupSide(TopRight, StandardCursorType.TopRightCorner, WindowEdge.NorthEast); + SetupSide(BottomLeft, StandardCursorType.BottomLeftCorner, WindowEdge.SouthWest); + SetupSide(BottomRight, StandardCursorType.BottomRightCorner, WindowEdge.SouthEast); + MinimizeButton.Click += delegate + { this.WindowState = WindowState.Minimized; }; + MaximizeButton.Click += delegate + { + WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; + }; + CloseButton.Click += delegate + { + Close(); + }; + } + + private void SetupSide(Control ctl, StandardCursorType cursor, WindowEdge edge) + { + ctl.Cursor = new Cursor(cursor); + ctl.PointerPressed += (i, e) => + { + if (WindowState == WindowState.Normal) + BeginResizeDrag(edge, e); + }; + } + } +} diff --git a/samples/AvaloniaControlCatalog/DrawingBackendMode.cs b/samples/AvaloniaControlCatalog/DrawingBackendMode.cs new file mode 100644 index 00000000..366f40b2 --- /dev/null +++ b/samples/AvaloniaControlCatalog/DrawingBackendMode.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace AvaloniaControlCatalog; + +/// +/// Selects the rendering backend used by the ImageSharp.Drawing Avalonia sample. +/// +internal enum DrawingBackendMode +{ + /// + /// Uses WebGPU when Avalonia exposes a compatible native surface; otherwise uses the CPU framebuffer. + /// + Auto, + + /// + /// Uses the CPU framebuffer renderer. + /// + Cpu, + + /// + /// Requires a WebGPU-compatible native surface. + /// + WebGpu +} diff --git a/samples/AvaloniaControlCatalog/DrawingContextImpl.cs b/samples/AvaloniaControlCatalog/DrawingContextImpl.cs new file mode 100644 index 00000000..a1d2187a --- /dev/null +++ b/samples/AvaloniaControlCatalog/DrawingContextImpl.cs @@ -0,0 +1,1810 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Numerics; +using System.Text; +using Avalonia; +using Avalonia.Platform; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Transforms; +using AvaloniaEdgeMode = Avalonia.Media.EdgeMode; +using AvaloniaRenderOptions = Avalonia.Media.RenderOptions; +using AvaloniaTextOptions = Avalonia.Media.TextOptions; +using AvaloniaTextRenderingMode = Avalonia.Media.TextRenderingMode; +using BoxShadow = Avalonia.Media.BoxShadow; + +namespace AvaloniaControlCatalog; + +/// +/// Avalonia drawing context implementation backed by an ImageSharp.Drawing canvas. +/// +internal sealed class DrawingContextImpl : IDrawingContextImpl, IDrawingContextImplWithEffects, IDrawingContextWithAcrylicLikeSupport +{ + private const float AcrylicBlurSigma = 10F; + + private readonly ILockedFramebuffer? framebuffer; + private readonly Image? image; + private readonly WebGPURenderTarget? renderTarget; + private readonly WebGPUSurfaceFrame? surfaceFrame; + private readonly DrawingCanvas canvas; + private readonly bool useWebGpuBackend; + private readonly bool ownsCanvas; + private readonly int traceId; + private readonly StringBuilder? trace; + private readonly Stack transforms = new(); + private readonly Stack renderOptions = new(); + private readonly Stack textOptions = new(); + private readonly Stack opacities = new(); + private readonly Stack opacityLayers = new(); + private readonly Stack<(Brush? Brush, RectangleF Bounds, Rectangle LayerBounds, float Opacity, bool HasLayer)> opacityMasks = new(); + private readonly Stack effects = new(); + private readonly List brushImages = []; + private readonly AvaloniaVector dpi; + private readonly bool ownsImage; + private static int nextTraceId; + private static bool environmentTraceConsumed; + private int traceOperationIndex; + private int canvasStateDepth; + private AvaloniaRenderOptions currentRenderOptions; + private AvaloniaTextOptions currentTextOptions; + private double opacity = 1; + private bool transientResourcesDisposed; + + /// + /// Selects the drawing backend used by top-level render targets. + /// + public static DrawingBackendMode BackendMode { get; set; } = DrawingBackendMode.Auto; + + /// + /// When set, the next framebuffer render is saved to a PNG in the temp folder for debugging, then the + /// flag resets so exactly one frame is captured on demand (no per-frame disk spam). Flip it from the + /// debugger or wire it to an input to grab the rendered framebuffer and inspect rendering vs present. + /// + public static bool DebugSaveNextFrame { get; set; } = false; + + /// + /// When set, the next drawing context writes a text trace of clips, clears, layers, and geometry + /// draws to the temp folder, then resets. Enable it when a frame needs operation-level diagnostics. + /// + public static bool DebugTraceNextFrame { get; set; } = false; + + private static readonly DrawingTextCache TextCache = new(); + + /// + /// Initializes a drawing context over an Avalonia framebuffer lock. + /// + /// The locked framebuffer to draw into. + public DrawingContextImpl(ILockedFramebuffer framebuffer) + : this(WrapFramebuffer(framebuffer), framebuffer.Dpi, true) + { + this.framebuffer = framebuffer; + + this.Trace(() => $"Framebuffer size={framebuffer.Size.Width}x{framebuffer.Size.Height} rowBytes={framebuffer.RowBytes} format={framebuffer.Format} alpha={framebuffer.AlphaFormat}"); + } + + /// + /// Initializes a drawing context over an existing ImageSharp image. + /// + /// The ImageSharp image to draw into. + /// The image DPI. + public DrawingContextImpl(Image image, AvaloniaVector dpi) + : this(image, dpi, false) + { + } + + /// + /// Initializes a drawing context over an acquired WebGPU surface frame. + /// + /// The acquired surface frame that will present on disposal. + /// The frame DPI. + /// The frame size in pixels. + public DrawingContextImpl(WebGPUSurfaceFrame surfaceFrame, AvaloniaVector dpi, PixelSize pixelSize) + { + this.surfaceFrame = surfaceFrame; + this.canvas = surfaceFrame.Canvas; + this.dpi = dpi; + this.useWebGpuBackend = true; + this.ownsCanvas = false; + + if (ShouldTraceContext(ownsImage: false, pixelSize.Width, pixelSize.Height)) + { + this.traceId = Interlocked.Increment(ref nextTraceId); + this.trace = new StringBuilder(); + this.Trace(() => $"Begin size={pixelSize.Width}x{pixelSize.Height} dpi={dpi.X},{dpi.Y} backend=WebGPU ownsImage=false"); + } + } + + /// + /// Initializes a drawing context over a WebGPU offscreen render target. + /// + /// The WebGPU render target that receives layer drawing commands. + /// The render target DPI. + /// The render target size in pixels. + public DrawingContextImpl(WebGPURenderTarget renderTarget, AvaloniaVector dpi, PixelSize pixelSize) + : this(renderTarget, dpi, pixelSize, ownsCanvas: true) + { + } + + /// + /// Initializes a drawing context over a WebGPU offscreen render target. + /// + /// The WebGPU render target that receives layer drawing commands. + /// The render target DPI. + /// The render target size in pixels. + /// A value indicating whether this context owns the canvas lifetime. + internal DrawingContextImpl(WebGPURenderTarget renderTarget, AvaloniaVector dpi, PixelSize pixelSize, bool ownsCanvas) + { + this.renderTarget = renderTarget; + this.canvas = renderTarget.CreateCanvas(new DrawingOptions(), TextCache); + this.dpi = dpi; + this.useWebGpuBackend = true; + this.ownsCanvas = ownsCanvas; + + if (ShouldTraceContext(ownsImage: false, pixelSize.Width, pixelSize.Height)) + { + this.traceId = Interlocked.Increment(ref nextTraceId); + this.trace = new StringBuilder(); + this.Trace(() => $"Begin size={pixelSize.Width}x{pixelSize.Height} dpi={dpi.X},{dpi.Y} backend=WebGPU-layer ownsImage=false"); + } + } + + private DrawingContextImpl(Image image, AvaloniaVector dpi, bool ownsImage) + { + this.image = image; + this.dpi = dpi; + this.ownsImage = ownsImage; + this.useWebGpuBackend = false; + this.ownsCanvas = true; + + if (ShouldTraceContext(ownsImage, image.Width, image.Height)) + { + this.traceId = Interlocked.Increment(ref nextTraceId); + this.trace = new StringBuilder(); + this.Trace(() => $"Begin size={image.Width}x{image.Height} dpi={dpi.X},{dpi.Y} backend=CPU ownsImage={ownsImage}"); + } + + this.canvas = image.Frames.RootFrame.CreateCanvas(Configuration.Default, new DrawingOptions(), TextCache); + } + + /// + public Matrix Transform { get; set; } = Matrix.Identity; + + /// + /// Gets the drawing canvas used by this context. + /// + internal DrawingCanvas Canvas => this.canvas; + + /// + public void Clear(Avalonia.Media.Color color) + { + this.Trace(() => $"Clear color={Describe(color)}"); + this.canvas.Clear(Brushes.Solid(color.ToColor())); + } + + /// + public void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) + { + IDrawableBitmapImpl bitmap = (IDrawableBitmapImpl)source; + double imageOpacity = opacity * this.opacity; + Rectangle imageSourceRect = sourceRect.ToRectangle(); + RectangleF imageDestinationRect = destRect.ToRectangleF(); + bool isUpscaling = imageDestinationRect.Width > imageSourceRect.Width || imageDestinationRect.Height > imageSourceRect.Height; + var sampler = this.currentRenderOptions.BitmapInterpolationMode.ToResampler(isUpscaling); + PixelAlphaCompositionMode alphaCompositionMode = this.currentRenderOptions.BitmapBlendingMode.ToAlphaCompositionMode(); + + this.Trace(() => $"DrawBitmap image={bitmap.PixelSize.Width}x{bitmap.PixelSize.Height} opacity={imageOpacity:0.###} source={Describe(sourceRect)} dest={Describe(destRect)} transform={Describe(this.Transform)}"); + bitmap.Draw(this, imageSourceRect, imageDestinationRect, sampler, (float)imageOpacity, alphaCompositionMode); + } + + /// + /// Draws an ImageSharp image into this context using the supplied bitmap draw state. + /// + /// The image to draw. + /// The source rectangle in image pixels. + /// The destination rectangle in drawing coordinates. + /// The resampler used when scaling the image. + /// The opacity applied to the image draw. + /// The alpha composition mode used to blend the image with the destination. + public void DrawImage( + Image image, + Rectangle sourceRect, + RectangleF destinationRect, + IResampler sampler, + float opacity, + PixelAlphaCompositionMode alphaCompositionMode) + { + DrawingOptions options = this.CreateDrawingOptions(IntersectionRule.EvenOdd); + options.GraphicsOptions.AlphaCompositionMode = alphaCompositionMode; + this.SaveCanvasState(options, $"image alpha={alphaCompositionMode}"); + + if (opacity >= 1) + { + this.canvas.DrawImage(image, sourceRect, destinationRect, sampler); + this.RestoreCanvasState("DrawImage state"); + return; + } + + Rectangle layerBounds = ToEnclosingRectangle(destinationRect); + if (layerBounds.Width <= 0 || layerBounds.Height <= 0) + { + this.RestoreCanvasState("DrawImage state"); + return; + } + + this.SaveCanvasLayer( + new GraphicsOptions + { + BlendPercentage = opacity, + AlphaCompositionMode = alphaCompositionMode + }, + layerBounds, + "DrawImage opacity"); + + this.canvas.DrawImage(image, sourceRect, destinationRect, sampler); + this.RestoreCanvasState("DrawImage opacity layer"); + this.RestoreCanvasState("DrawImage state"); + } + + /// + public void DrawBitmap(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + { + this.Trace(() => $"DrawBitmap opacityMask={DescribeBrush(opacityMask)} maskRect={Describe(opacityMaskRect)} dest={Describe(destRect)} transform={Describe(this.Transform)}"); + this.PushOpacityMask(opacityMask, opacityMaskRect); + this.DrawBitmap(source, 1, new Rect(((IDrawableBitmapImpl)source).PixelSize.ToSize(1)), destRect); + this.PopOpacityMask(); + } + + /// + public void DrawLine(IPen? pen, Avalonia.Point p1, Avalonia.Point p2) + { + Rect targetRect = new Rect(p1, p2).Normalize(); + Pen? strokePen = this.CreatePen(pen, targetRect); + + if (strokePen is not null) + { + this.PushDrawingState(IntersectionRule.EvenOdd, this.GetBrushOpacity(pen?.Brush)); + this.canvas.DrawLine(strokePen, p1.ToPointF(), p2.ToPointF()); + this.RestoreCanvasState("DrawLine state"); + } + } + + /// + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + { + GeometryImpl geometryImpl = (GeometryImpl)geometry; + Rect fillBounds = geometryImpl.GetRenderBounds(null); + Rect strokeBounds = geometryImpl.GetRenderBounds(pen); + + this.Trace(() => $"DrawGeometry fillRule={geometryImpl.FillRule} fillBounds={Describe(fillBounds)} strokeBounds={Describe(strokeBounds)} brush={DescribeBrush(brush)} pen={DescribePen(pen)} transform={Describe(this.Transform)}"); + + Brush? fillBrush = this.CreateBrush(brush, fillBounds); + + if (fillBrush is not null && geometryImpl.FillPath is not null) + { + this.FillPath(fillBrush, geometryImpl.FillPath, geometryImpl.FillRule, this.GetBrushOpacity(brush)); + } + + Pen? strokePen = this.CreatePen(pen, strokeBounds); + + if (strokePen is not null) + { + IntersectionRule strokeRule = geometryImpl.FillRule; + this.Trace(() => $"StrokePath fillRule={strokeRule} pathBounds={Describe(geometryImpl.StrokePath.Bounds)} strokeBounds={Describe(strokeBounds)} transform={Describe(this.Transform)}"); + + this.PushDrawingState(strokeRule, this.GetBrushOpacity(pen?.Brush)); + this.canvas.Draw(strokePen, geometryImpl.StrokePath); + this.RestoreCanvasState("DrawGeometry stroke state"); + } + } + + /// + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) + { + this.Trace(() => $"DrawRectangle rect={Describe(rect.Rect)} rounded={rect.IsRounded} brush={DescribeBrush(brush)} pen={DescribePen(pen)} shadows={boxShadows.Count} transform={Describe(this.Transform)}"); + + IPath path = rect.IsRounded + ? rect.ToRoundedRectanglePath() + : new RectanglePolygon(rect.Rect.ToRectangleF()); + + this.DrawOuterBoxShadows(rect, path, boxShadows); + + // Inset shadows paint above the fill and below the stroke, matching the Skia backend, + // so the fill and stroke split around them when any are present. + if (boxShadows.HasInsetShadows) + { + this.DrawGeometry(brush, null, new StreamGeometryImpl(path, path, IntersectionRule.EvenOdd)); + this.DrawInnerBoxShadows(rect, path, boxShadows); + this.DrawGeometry(null, pen, new StreamGeometryImpl(path, path, IntersectionRule.EvenOdd)); + return; + } + + this.DrawGeometry(brush, pen, new StreamGeometryImpl(path, path, IntersectionRule.EvenOdd)); + } + + /// + public void DrawRectangle(Avalonia.Media.IExperimentalAcrylicMaterial material, RoundedRect rect) + { + if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0) + { + return; + } + + this.Trace(() => $"DrawAcrylicRectangle rect={Describe(rect.Rect)} rounded={rect.IsRounded} background={material.BackgroundSource} material={Describe(material.MaterialColor)} tint={Describe(material.TintColor)} transform={Describe(this.Transform)}"); + + IPath path = rect.IsRounded + ? rect.ToRoundedRectanglePath() + : new RectanglePolygon(rect.Rect.ToRectangleF()); + + // The acrylic effect frosts and tints whatever lies beneath the shape and replaces the + // region with the result: the backdrop write-back carries Src semantics, which is the one + // thing Skia's Digger mode changes on its paint. Under a transparent window the backdrop + // is empty and the region becomes the bare tint, exactly Skia's material; over in-app + // content the material is genuinely frosted, which Skia fakes with a noise tile. + Color materialTint = FlattenAcrylicTint(material.MaterialColor, material.TintColor); + this.SaveCanvasLayer( + new GraphicsOptions(), + path, + new WebGPUBackdropAcrylicLayerEffect(AcrylicBlurSigma, materialTint), + this.CreateDrawingOptions(IntersectionRule.EvenOdd), + "Acrylic rectangle"); + + this.RestoreCanvasState("Acrylic rectangle layer"); + } + + /// + /// Flattens the acrylic material and tint into the single colour whose source-over composite + /// equals tint over material. + /// + /// + /// Skia's acrylic paint composes two constant colour shaders, tint source-over material, per + /// pixel. Because source-over is associative, tint over (material over backdrop) equals + /// (tint over material) over backdrop, so the pair collapses to one Porter-Duff over of the two + /// constants: the weighted sum below is the premultiplied blend and the division by the result + /// alpha converts back to the straight colour the acrylic effect's matrix expects. + /// + private static Color FlattenAcrylicTint(AvaloniaColor material, AvaloniaColor tint) + { + float materialAlpha = material.A / 255F; + float tintAlpha = tint.A / 255F; + float alpha = tintAlpha + (materialAlpha * (1F - tintAlpha)); + if (alpha <= 0F) + { + return Color.Transparent; + } + + byte Flatten(byte tintChannel, byte materialChannel) + => (byte)Math.Round(((tintChannel * tintAlpha) + (materialChannel * materialAlpha * (1F - tintAlpha))) / alpha); + + return new AvaloniaColor( + (byte)Math.Round(alpha * 255F), + Flatten(tint.R, material.R), + Flatten(tint.G, material.G), + Flatten(tint.B, material.B)).ToColor(); + } + + /// + public void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion region) + { + RegionImpl regionImpl = (RegionImpl)region; + + this.Trace(() => $"DrawRegion empty={regionImpl.IsEmpty} bounds={Describe(regionImpl.Bounds)} rects={Describe(regionImpl.Rects)} brush={DescribeBrush(brush)} pen={DescribePen(pen)} transform={Describe(this.Transform)}"); + + if (regionImpl.IsEmpty) + { + return; + } + + IPath path = regionImpl.ToPath(); + + this.DrawGeometry(brush, pen, new StreamGeometryImpl(path, path, IntersectionRule.NonZero)); + } + + /// + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + { + IPath path = new EllipsePolygon(rect.Center.ToPointF(), rect.Size.ToSizeF()); + this.DrawGeometry(brush, pen, new StreamGeometryImpl(path, path, IntersectionRule.EvenOdd)); + } + + /// + public void DrawGlyphRun(IBrush? foreground, IGlyphRunImpl glyphRun) + { + if (glyphRun is not GlyphRunImpl glyphRunImpl) + { + this.Trace(() => $"DrawGlyphRun unsupported={glyphRun.GetType().Name}"); + return; + } + + ReadOnlyMemory glyphIds = glyphRunImpl.GlyphIds; + if (glyphIds.IsEmpty) + { + this.Trace(() => $"DrawGlyphRun empty bounds={Describe(glyphRunImpl.Bounds)} transform={Describe(this.Transform)}"); + return; + } + + Brush? brush = this.CreateBrush(foreground, glyphRunImpl.Bounds); + + this.Trace(() => $"DrawGlyphRun glyphs={glyphIds.Length} bounds={Describe(glyphRunImpl.Bounds)} foreground={DescribeBrush(foreground)} transform={Describe(this.Transform)}"); + + if (brush is null) + { + return; + } + + // Start with the currently pushed TextOptions, then apply the same RenderOptions + // fallback shape as the Skia backend before creating the glyph rendering options. + AvaloniaTextOptions effectiveTextOptions = this.currentTextOptions; + AvaloniaRenderOptions effectiveRenderOptions = this.currentRenderOptions; + +#pragma warning disable CS0618 + if (effectiveTextOptions.TextRenderingMode == AvaloniaTextRenderingMode.Unspecified && + effectiveRenderOptions.TextRenderingMode != AvaloniaTextRenderingMode.Unspecified) + { + effectiveTextOptions = effectiveTextOptions with + { + TextRenderingMode = effectiveRenderOptions.TextRenderingMode + }; + } +#pragma warning restore CS0618 + + // If TextRenderingMode is still unspecified, derive it from EdgeMode as Skia does. + if (effectiveTextOptions.TextRenderingMode == AvaloniaTextRenderingMode.Unspecified) + { + effectiveTextOptions = effectiveTextOptions with + { + TextRenderingMode = effectiveRenderOptions.EdgeMode == AvaloniaEdgeMode.Aliased + ? AvaloniaTextRenderingMode.Alias + : AvaloniaTextRenderingMode.SubpixelAntialias + }; + } + + bool antialias = effectiveTextOptions.TextRenderingMode != AvaloniaTextRenderingMode.Alias; + + // Keep glyph-level font option mapping on the glyph run, matching Skia's + // GlyphRunImpl.CreateFont ownership. + RichGlyphOptions options = glyphRunImpl.CreateGlyphOptions(effectiveTextOptions); + + this.PushDrawingState(IntersectionRule.NonZero, antialias, this.GetBrushOpacity(foreground)); + this.canvas.DrawText(glyphIds.Span, glyphRunImpl.GlyphOrigins.Span, options, brush, pen: null); + this.RestoreCanvasState("DrawGlyphRun state"); + } + + /// + public IDrawingContextLayerImpl CreateLayer(PixelSize size) + { + if (this.useWebGpuBackend) + { + if (this.surfaceFrame is not null) + { + WebGPURenderTarget layerTarget = this.surfaceFrame.CreateRenderTarget(size.Width, size.Height); + + return new WebGPULayerImpl(layerTarget, size, this.dpi); + } + + if (this.renderTarget is not null) + { + WebGPURenderTarget layerTarget = this.renderTarget.CreateRenderTarget(size.Width, size.Height); + + return new WebGPULayerImpl(layerTarget, size, this.dpi); + } + + throw new InvalidOperationException("The WebGPU drawing context has no target for layer allocation."); + } + + return new RenderTargetBitmapImpl(size, this.dpi); + } + + /// + /// Copies a CPU-backed layer into this context. + /// + /// The source image. + /// The layer size in pixels. + internal void Blit(Image source, PixelSize size) + { + using DrawingCanvas sourceCanvas = source.Frames.RootFrame.CreateCanvas(Configuration.Default, new DrawingOptions(), TextCache); + this.canvas.CopyPixelsFrom( + sourceCanvas, + new Rectangle(0, 0, size.Width, size.Height), + new SixLabors.ImageSharp.Point(0, 0)); + } + + /// + /// Copies a layer canvas into this context. + /// + /// The source canvas. + /// The layer size in pixels. + internal void Blit(DrawingCanvas source, PixelSize size) + { + this.canvas.CopyPixelsFrom( + source, + new Rectangle(0, 0, size.Width, size.Height), + new SixLabors.ImageSharp.Point(0, 0)); + } + + /// + public void PushClip(Rect clip) + { + this.Trace(() => $"PushClip rect={Describe(clip)} transform={Describe(this.Transform)}"); + this.SaveTransform(); + this.PushDrawingState(IntersectionRule.EvenOdd, antialias: false); + this.canvas.Clip(new RectanglePolygon(clip.ToRectangleF())); + } + + /// + public void PushClip(RoundedRect clip) + { + this.Trace(() => $"PushClip roundedRect={Describe(clip.Rect)} rounded={clip.IsRounded} transform={Describe(this.Transform)}"); + this.SaveTransform(); + this.PushDrawingState(IntersectionRule.EvenOdd); + this.canvas.Clip(clip.ToRoundedRectanglePath()); + } + + /// + public void PushClip(IPlatformRenderInterfaceRegion region) + { + RegionImpl regionImpl = (RegionImpl)region; + + this.Trace(() => $"PushClip region empty={regionImpl.IsEmpty} bounds={Describe(regionImpl.Bounds)} rects={Describe(regionImpl.Rects)} transform={Describe(this.Transform)}"); + this.SaveTransform(); + Matrix transform = this.Transform; + this.Transform = Matrix.Identity; + + // Avalonia's Skia backend uses SkCanvas.ClipRegion here. Skia documents region clips as + // device-space and aliased, so keep the dirty-region edge hard instead of blending stale + // pixels at the redraw boundary. + this.PushDrawingState(IntersectionRule.NonZero, antialias: false); + this.canvas.Clip(regionImpl.ToPath()); + this.Transform = transform; + } + + /// + public void PopClip() + { + this.Trace(() => "PopClip"); + this.RestoreCanvasState("Clip state"); + this.RestoreTransform(); + } + + /// + public void PushLayer(Rect bounds) + { + this.Trace(() => $"PushLayer bounds={Describe(bounds)} transform={Describe(this.Transform)}"); + this.SaveTransform(); + this.SaveCanvasLayer(new GraphicsOptions(), ToEnclosingRectangle(bounds), this.CreateDrawingOptions(IntersectionRule.EvenOdd), "PushLayer"); + } + + /// + public void PopLayer() + { + this.Trace(() => "PopLayer"); + this.RestoreCanvasState("Layer"); + this.RestoreTransform(); + } + + /// + public void PushOpacity(double opacity, Rect? bounds) + { + this.Trace(() => $"PushOpacity opacity={opacity:0.###} bounds={(bounds.HasValue ? Describe(bounds.Value) : "")} current={this.opacity:0.###} transform={Describe(this.Transform)}"); + this.opacities.Push(this.opacity); + + bool useOpacityLayer = this.currentRenderOptions.RequiresFullOpacityHandling == true; + this.opacityLayers.Push(useOpacityLayer); + + if (useOpacityLayer) + { + float layerOpacity = (float)(this.opacity * opacity); + Rectangle layerBounds = bounds is not null ? ToEnclosingRectangle(bounds.Value) : this.canvas.Bounds; + + // Some controls require opacity to be applied to the composed result rather than to each + // child brush. Match Skia by isolating those controls in a layer and fading the layer when + // it is restored. + this.opacity = 1; + this.SaveCanvasLayer( + new GraphicsOptions { BlendPercentage = layerOpacity }, + layerBounds, + this.CreateDrawingOptions(IntersectionRule.EvenOdd), + "PushOpacity full opacity layer"); + + return; + } + + this.opacity *= opacity; + } + + /// + public void PopOpacity() + { + this.Trace(() => $"PopOpacity current={this.opacity:0.###}"); + if (this.opacityLayers.Pop()) + { + this.RestoreCanvasState("Opacity layer"); + } + + this.opacity = this.opacities.Pop(); + } + + /// + public void PushOpacityMask(IBrush mask, Rect bounds) + { + this.SaveTransform(); + + Brush? maskBrush = this.CreateBrush(mask, bounds); + Rectangle layerBounds = ToEnclosingRectangle(bounds); + bool hasLayer = maskBrush is not null && layerBounds.Width > 0 && layerBounds.Height > 0; + + this.opacityMasks.Push((maskBrush, bounds.ToRectangleF(), layerBounds, this.GetBrushOpacity(mask), hasLayer)); + + if (hasLayer) + { + // Establish the active transform on the canvas state so the layer (and the content drawn + // into it) is positioned correctly, then isolate the masked content in its own layer. + this.PushDrawingState(IntersectionRule.EvenOdd); + this.SaveCanvasLayer(new GraphicsOptions(), layerBounds, "OpacityMask content"); + } + } + + /// + public void PopOpacityMask() + { + (Brush? brush, RectangleF maskBounds, Rectangle layerBounds, float maskOpacity, bool hasLayer) = this.opacityMasks.Pop(); + + if (hasLayer) + { + // Draw the mask into a DstIn layer so, on composite, it keeps the content layer's colour + // but scales its alpha by the mask's alpha; the content then fades wherever the mask is + // transparent. Both layers and the saved state are unwound here. + this.SaveCanvasLayer( + new GraphicsOptions { AlphaCompositionMode = PixelAlphaCompositionMode.DestIn }, + layerBounds, + this.CreateDrawingOptions(IntersectionRule.EvenOdd, maskOpacity), + "OpacityMask mask"); + + this.canvas.Fill(brush!, new RectanglePolygon(maskBounds)); + this.RestoreCanvasState("OpacityMask mask layer"); + this.RestoreCanvasState("OpacityMask content layer"); + this.RestoreCanvasState("OpacityMask state"); + } + + this.RestoreTransform(); + } + + /// + public void PushEffect(AvaloniaRect? effectClipRect, IEffect effect) + { + this.Trace(() => $"PushEffect clip={(effectClipRect.HasValue ? Describe(effectClipRect.Value) : "")} effect={effect.GetType().Name} transform={Describe(this.Transform)}"); + + LayerEffect? layerEffect = this.CreateLayerEffect(effect); + this.effects.Push(layerEffect is not null); + + if (layerEffect is not null) + { + // Content drawn between push and pop renders into an isolated effect layer; the canvas + // applies the effect to the layer's content when the layer is restored. The ambient + // opacity rides the layer composite so the effect output fades with the content, and is + // reset inside the layer to avoid applying it twice, matching PushOpacity. + Rectangle bounds = effectClipRect is not null + ? ToEnclosingRectangle(effectClipRect.Value) + : this.canvas.Bounds; + + float layerOpacity = (float)this.opacity; + this.opacities.Push(this.opacity); + this.opacity = 1; + + this.SaveCanvasLayer( + new GraphicsOptions { BlendPercentage = layerOpacity }, + bounds, + layerEffect, + this.CreateDrawingOptions(IntersectionRule.EvenOdd), + "Effect content"); + } + } + + /// + public void PopEffect() + { + this.Trace(() => "PopEffect"); + + if (this.effects.Pop()) + { + this.RestoreCanvasState("Effect layer"); + this.opacity = this.opacities.Pop(); + } + } + + /// + /// Resolves an Avalonia effect into a canvas with device-space + /// parameters, mirroring the Skia backend's effect-to-image-filter mapping. Unsupported effect + /// types and degenerate blurs resolve to and draw unaffected. + /// + /// The effect to resolve. + /// The resolved effect, or . + private LayerEffect? CreateLayerEffect(IEffect effect) + { + // Blur sigma and shadow offsets act on the layer's pixels after the transform has been + // applied to the geometry, so both are mapped through the current transform the same way + // Skia's image filters follow the canvas matrix. + AvaloniaMatrix transform = this.Transform; + float scale = (float)Math.Sqrt(Math.Abs((transform.M11 * transform.M22) - (transform.M12 * transform.M21))); + + if (effect is IBlurEffect blur) + { + if (blur.Radius <= 0) + { + return null; + } + + return new WebGPUGaussianBlurLayerEffect(BlurRadiusToSigma(blur.Radius) * scale); + } + + if (effect is IDropShadowEffect drop) + { + float sigma = drop.BlurRadius > 0 ? BlurRadiusToSigma(drop.BlurRadius) * scale : 0F; + + // The offset is a direction vector, so it maps through the transform without translation. + SixLabors.ImageSharp.Point offset = new( + (int)Math.Round((drop.OffsetX * transform.M11) + (drop.OffsetY * transform.M21)), + (int)Math.Round((drop.OffsetX * transform.M12) + (drop.OffsetY * transform.M22))); + + // The colour's own alpha and the effect opacity scale the silhouette's alpha; the + // ambient opacity rides the effect layer's composite instead, matching the Skia + // backend's opacity-save-layer path. + byte alpha = (byte)Math.Clamp(drop.Color.A * drop.Opacity, 0D, 255D); + Color color = new AvaloniaColor(alpha, drop.Color.R, drop.Color.G, drop.Color.B).ToColor(); + + return new WebGPUDropShadowLayerEffect(offset, sigma, color); + } + + return null; + } + + /// + public void PushGeometryClip(IGeometryImpl clip) + { + this.SaveTransform(); + + GeometryImpl geometry = (GeometryImpl)clip; + this.Trace(() => $"PushGeometryClip fillRule={geometry.FillRule} pathBounds={Describe(geometry.FillPath?.Bounds ?? RectangleF.Empty)} transform={Describe(this.Transform)}"); + this.PushDrawingState(geometry.FillRule); + this.canvas.Clip(geometry.FillPath ?? EmptyPath.ClosedPath); + } + + /// + public void PopGeometryClip() => this.PopClip(); + + /// + public void PushRenderOptions(RenderOptions renderOptions) + { + this.renderOptions.Push(this.currentRenderOptions); + this.currentRenderOptions = this.currentRenderOptions.MergeWith(renderOptions); + this.TraceState($"PushRenderOptions requiresFullOpacity={this.currentRenderOptions.RequiresFullOpacityHandling}"); + } + + /// + public void PopRenderOptions() + { + this.currentRenderOptions = this.renderOptions.Pop(); + this.TraceState($"PopRenderOptions requiresFullOpacity={this.currentRenderOptions.RequiresFullOpacityHandling}"); + } + + /// + public void PushTextOptions(AvaloniaTextOptions textOptions) + { + this.textOptions.Push(this.currentTextOptions); + this.currentTextOptions = this.currentTextOptions.MergeWith(textOptions); + this.TraceState($"PushTextOptions rendering={this.currentTextOptions.TextRenderingMode}"); + } + + /// + public void PopTextOptions() + { + this.currentTextOptions = this.textOptions.Pop(); + this.TraceState($"PopTextOptions rendering={this.currentTextOptions.TextRenderingMode}"); + } + + /// + public object? GetFeature(Type t) => null; + + /// + public void Dispose() + { + if (this.surfaceFrame is not null) + { + // Surface frames own presentation. Disposing the frame flushes its canvas and presents the + // acquired swapchain texture, matching Skia's direct platform-drawable session shape. + this.surfaceFrame.Dispose(); + } + else + { + // Layer contexts leave their canvas alive for the owning layer's blit. Other contexts dispose the + // canvas here so queued commands render into their target. + if (this.ownsCanvas) + { + this.canvas.Dispose(); + } + else + { + this.canvas.Flush(); + } + } + + // On-demand capture of the rendered framebuffer (the actual window surface, not layers). Splits + // "holes are in the rendered image" from "holes appear only at present/display". + if (DebugSaveNextFrame && this.framebuffer is not null && this.image is not null) + { + DebugSaveNextFrame = false; + string path = System.IO.Path.Combine( + GetCaptureDirectory(), + $"imagesharp-frame-{DateTime.Now:yyyyMMdd-HHmmss-fff}.png"); + this.image.SaveAsPng(path); + } + + if (ShouldSaveContextImage() && this.image is not null) + { + string path = System.IO.Path.Combine( + GetCaptureDirectory(), + $"imagesharp-context-{DateTime.Now:yyyyMMdd-HHmmss-fff}-{this.traceId}-{(this.useWebGpuBackend ? "webgpu" : "cpu")}-{(this.ownsImage ? "owned" : "borrowed")}.png"); + this.image.SaveAsPng(path); + } + + if (this.trace is not null) + { + string path = System.IO.Path.Combine( + GetCaptureDirectory(), + $"imagesharp-trace-{DateTime.Now:yyyyMMdd-HHmmss-fff}-{this.traceId}.log"); + File.WriteAllText(path, this.trace.ToString()); + Debug.WriteLine($"ImageSharp drawing trace saved to {path}"); + } + + if (this.ownsCanvas || this.surfaceFrame is not null) + { + this.DisposeTransientResources(); + } + + this.framebuffer?.Dispose(); + + if (this.ownsImage && this.image is not null) + { + this.image.Dispose(); + } + } + + /// + /// Disposes temporary images referenced by queued draw commands after those commands have rendered. + /// + internal void DisposeTransientResources() + { + if (this.transientResourcesDisposed) + { + return; + } + + for (int i = 0; i < this.brushImages.Count; i++) + { + this.brushImages[i].Dispose(); + } + + this.transientResourcesDisposed = true; + } + + /// + /// Converts an Avalonia brush into an ImageSharp brush for the supplied target bounds. + /// + private Brush? CreateBrush(IBrush? brush, Rect targetRect) + { + if (brush is null) + { + return null; + } + + if (brush is ISolidColorBrush solid) + { + return Brushes.Solid(solid.Color.ToColor()); + } + + if (brush is IGradientBrush gradient) + { + // Brushes are built in local/context space; the DrawingCanvas applies the active + // transform to the brush, so the backend must not pre-transform it here. + return CreateGradientBrush(gradient, targetRect); + } + + if (brush is IImageBrush imageBrush) + { + return this.CreateImageBrush(imageBrush, targetRect); + } + + if (brush is Avalonia.Media.ISceneBrush sceneBrush) + { + using Avalonia.Media.ISceneBrushContent? content = sceneBrush.CreateContent(); + + return content is null + ? null + : this.CreateSceneBrushContent(content, targetRect); + } + + if (brush is Avalonia.Media.ISceneBrushContent sceneBrushContent) + { + return this.CreateSceneBrushContent(sceneBrushContent, targetRect); + } + + return null; + } + + /// + /// Gets the blend amount for a brush draw from Avalonia's brush opacity and active context opacity. + /// + private float GetBrushOpacity(IBrush? brush) + => (float)((brush?.Opacity ?? 1) * this.opacity); + + /// + /// Wraps the Avalonia framebuffer memory so the CPU canvas can render directly into it. + /// + private static unsafe Image WrapFramebuffer(ILockedFramebuffer framebuffer) + { + int rowByteCount = framebuffer.Size.Width * 4; + int bufferByteCount = (framebuffer.RowBytes * (framebuffer.Size.Height - 1)) + rowByteCount; + + return Image.WrapMemory( + framebuffer.Address.ToPointer(), + bufferByteCount, + framebuffer.Size.Width, + framebuffer.Size.Height, + framebuffer.RowBytes); + } + + /// + /// Saves the canvas state with the current transform, clip, and fill-rule options. + /// + /// + /// Save snapshots the current state and inherits the active clip; the canvas applies the active + /// transform to drawn paths and clips itself, so nothing is pre-transformed here. + /// + private void PushDrawingState(IntersectionRule fillRule) + => this.SaveCanvasState(this.CreateDrawingOptions(fillRule), $"fillRule={fillRule}"); + + /// + /// Saves a canvas state and records diagnostic stack depth when tracing is enabled. + /// + private void SaveCanvasState(DrawingOptions options, string reason) + { + this.canvas.Save(options); + this.canvasStateDepth++; + this.TraceState($"Save {reason}"); + } + + /// + /// Saves a compositing layer and records diagnostic stack depth when tracing is enabled. + /// + private void SaveCanvasLayer(GraphicsOptions layerOptions, Rectangle bounds, string reason) + { + this.canvas.SaveLayer(layerOptions, bounds); + this.canvasStateDepth++; + this.TraceState( + $"SaveLayer {reason} bounds={Describe(bounds)} blend={layerOptions.BlendPercentage:0.###} alpha={layerOptions.AlphaCompositionMode}"); + } + + /// + /// Saves a compositing layer with explicit drawing options and records diagnostic stack depth. + /// + private void SaveCanvasLayer(GraphicsOptions layerOptions, Rectangle bounds, DrawingOptions options, string reason) + { + this.canvas.SaveLayer(layerOptions, bounds, options); + this.canvasStateDepth++; + this.TraceState( + $"SaveLayer {reason} bounds={Describe(bounds)} blend={layerOptions.BlendPercentage:0.###} alpha={layerOptions.AlphaCompositionMode}"); + } + + /// + /// Saves an effect compositing layer and records diagnostic stack depth when tracing is enabled. + /// + private void SaveCanvasLayer(GraphicsOptions layerOptions, Rectangle bounds, LayerEffect effect, DrawingOptions options, string reason) + { + this.canvas.SaveLayer(layerOptions, bounds, effect, options); + this.canvasStateDepth++; + this.TraceState( + $"SaveLayer {reason} effect={effect.GetType().Name} bounds={Describe(bounds)} blend={layerOptions.BlendPercentage:0.###} alpha={layerOptions.AlphaCompositionMode}"); + } + + /// + /// Saves an effect compositing layer over a path region and records diagnostic stack depth + /// when tracing is enabled. + /// + private void SaveCanvasLayer(GraphicsOptions layerOptions, IPath region, LayerEffect effect, DrawingOptions options, string reason) + { + this.canvas.SaveLayer(layerOptions, region, effect, options); + this.canvasStateDepth++; + this.TraceState( + $"SaveLayer {reason} effect={effect.GetType().Name} bounds={Describe(region.Bounds)} blend={layerOptions.BlendPercentage:0.###} alpha={layerOptions.AlphaCompositionMode}"); + } + + /// + /// Restores a canvas state and records diagnostic stack depth when tracing is enabled. + /// + private void RestoreCanvasState(string reason) + { + this.canvas.Restore(); + this.canvasStateDepth--; + this.TraceState($"Restore {reason}"); + } + + /// + /// Saves the canvas state with the current transform, clip, fill-rule options, and draw opacity. + /// + private void PushDrawingState(IntersectionRule fillRule, float blendPercentage) + => this.SaveCanvasState(this.CreateDrawingOptions(fillRule, blendPercentage), $"fillRule={fillRule} blend={blendPercentage:0.###}"); + + /// + /// Saves the canvas state with an explicit antialiasing value. + /// + private void PushDrawingState(IntersectionRule fillRule, bool antialias, float blendPercentage = 1) + { + DrawingOptions options = this.CreateDrawingOptions(fillRule, blendPercentage); + options.GraphicsOptions.Antialias = antialias; + + this.SaveCanvasState(options, $"fillRule={fillRule} antialias={antialias} blend={blendPercentage:0.###}"); + } + + /// + /// Creates ImageSharp drawing options from the active Avalonia drawing state. + /// + private DrawingOptions CreateDrawingOptions(IntersectionRule fillRule) + => new() + { + Transform = this.Transform.ToMatrix4x4(), + IntersectionRule = fillRule + }; + + /// + /// Creates ImageSharp drawing options with Avalonia opacity mapped to brush blend amount. + /// + private DrawingOptions CreateDrawingOptions(IntersectionRule fillRule, float blendPercentage) + { + DrawingOptions options = this.CreateDrawingOptions(fillRule); + options.GraphicsOptions.BlendPercentage = blendPercentage; + + return options; + } + + /// + /// Saves the current transform before Avalonia pushes a nested drawing state. + /// + private void SaveTransform() + { + this.transforms.Push(this.Transform); + this.TraceState("SaveTransform"); + } + + /// + /// Restores the transform saved by the matching push operation. + /// + private void RestoreTransform() + { + this.Transform = this.transforms.Pop(); + this.TraceState("RestoreTransform"); + } + + /// + /// Converts an Avalonia pen into an ImageSharp pen for the supplied target bounds. + /// + private Pen? CreatePen(IPen? pen, Rect targetRect) + { + if (pen is null || pen.Thickness <= 0) + { + return null; + } + + Brush? brush = this.CreateBrush(pen.Brush, targetRect); + + return brush is null ? null : pen.ToPen(brush); + } + + /// + /// Draws non-inset rectangle shadows before the rectangle fill/stroke. + /// + private void DrawOuterBoxShadows(RoundedRect rect, IPath contentPath, BoxShadows boxShadows) + { + if (boxShadows.Count == 0) + { + return; + } + + foreach (BoxShadow shadow in boxShadows) + { + if (shadow == default || shadow.Color.A == 0 || shadow.IsInset) + { + continue; + } + + this.DrawOuterBoxShadow(rect, contentPath, shadow); + } + } + + /// + /// Draws inset rectangle shadows after the rectangle fill and before its stroke. + /// + private void DrawInnerBoxShadows(RoundedRect rect, IPath contentPath, BoxShadows boxShadows) + { + foreach (BoxShadow shadow in boxShadows) + { + if (shadow == default || shadow.Color.A == 0 || !shadow.IsInset) + { + continue; + } + + this.DrawInnerBoxShadow(rect, contentPath, shadow); + } + } + + /// + /// Draws one inset shadow. Gaussian blur is linear and preserves constants, so the + /// blurred complement of the hole equals one minus the blurred hole mask: the content is + /// filled with the shadow color and the blurred hole mask is punched out of it with a + /// DestOut layer composite. No complement geometry padded by the blur reach is needed, + /// because one minus the mask saturates on its own away from the hole, and the technique + /// works for any content geometry the clip can express. + /// + private void DrawInnerBoxShadow(RoundedRect rect, IPath contentPath, BoxShadow shadow) + { + if (rect.Rect.Width <= 0 || rect.Rect.Height <= 0) + { + return; + } + + // The hole the light shines through: the content deflated by the spread and shifted + // by the offset. Everything outside it, inside the content, is in shadow. + RoundedRect holeRect = TranslateRoundedRect( + rect.Deflate(shadow.Spread, shadow.Spread), + shadow.OffsetX, + shadow.OffsetY); + + this.PushDrawingState(IntersectionRule.EvenOdd); + this.canvas.Clip(ClipOperation.Intersection, contentPath); + this.SaveCanvasLayer(new GraphicsOptions { BlendPercentage = (float)this.opacity }, ToEnclosingRectangle(rect.Rect), "InnerBoxShadow"); + this.canvas.Fill(Brushes.Solid(shadow.Color.ToColor()), contentPath); + + // A spread large enough to consume the content leaves no hole; the whole interior + // stays in shadow. + if (holeRect.Rect.Width > 0 && holeRect.Rect.Height > 0) + { + IPath holePath = holeRect.IsRounded + ? holeRect.ToRoundedRectanglePath() + : new RectanglePolygon(holeRect.Rect.ToRectangleF()); + + // The hole mask must be opaque so the composite removes the shadow entirely + // inside the hole; the shadow color's own alpha applies to the shadow, not here. + // The hole is drawn through a blur effect layer so the mask feathers before the + // DestOut composite; a zero blur passes through unchanged. + this.SaveCanvasLayer( + new GraphicsOptions { AlphaCompositionMode = PixelAlphaCompositionMode.DestOut }, + ToEnclosingRectangle(holeRect.Rect), + new WebGPUGaussianBlurLayerEffect(BlurRadiusToSigma(shadow.Blur)), + this.CreateDrawingOptions(IntersectionRule.EvenOdd), + "InnerBoxShadow hole"); + + this.canvas.Fill(Brushes.Solid(Color.Black), holePath); + this.RestoreCanvasState("InnerBoxShadow hole layer"); + } + + this.RestoreCanvasState("InnerBoxShadow layer"); + this.RestoreCanvasState("InnerBoxShadow intersection clip"); + } + + /// + /// Draws one non-inset shadow. + /// + private void DrawOuterBoxShadow(RoundedRect rect, IPath contentPath, BoxShadow shadow) + { + RoundedRect shadowRect = TranslateRoundedRect( + rect.Inflate(shadow.Spread, shadow.Spread), + shadow.OffsetX, + shadow.OffsetY); + + IPath shadowPath = shadowRect.IsRounded + ? shadowRect.ToRoundedRectanglePath() + : new RectanglePolygon(shadowRect.Rect.ToRectangleF()); + + if (shadow.Blur > 0) + { + // The shadow geometry fills a blur effect layer; the canvas blurs it when the layer is + // restored, and the layer's blend carries the ambient opacity. + this.PushDrawingState(IntersectionRule.EvenOdd); + this.canvas.Clip(ClipOperation.Difference, contentPath); + this.SaveCanvasLayer( + new GraphicsOptions { BlendPercentage = (float)this.opacity }, + ToEnclosingRectangle(shadowRect.Rect), + new WebGPUGaussianBlurLayerEffect(BlurRadiusToSigma(shadow.Blur)), + this.CreateDrawingOptions(IntersectionRule.EvenOdd), + "OuterBoxShadow blur"); + + this.canvas.Fill(Brushes.Solid(shadow.Color.ToColor()), shadowPath); + this.RestoreCanvasState("OuterBoxShadow blur layer"); + this.RestoreCanvasState("OuterBoxShadow difference clip"); + + return; + } + + this.FillPath(Brushes.Solid(shadow.Color.ToColor()), new ComplexPolygon(shadowPath, contentPath), IntersectionRule.EvenOdd, (float)this.opacity); + } + + /// + /// Converts an Avalonia blur radius to the sigma used by ImageSharp's Gaussian blur. + /// + private static float BlurRadiusToSigma(double radius) + => radius <= 0 ? 0 : (float)radius / 3F; + + /// + /// Converts a floating-point Avalonia rectangle to a pixel rectangle that fully contains it. + /// + private static Rectangle ToEnclosingRectangle(Rect rect) + { + int left = (int)Math.Floor(rect.Left); + int top = (int)Math.Floor(rect.Top); + int right = (int)Math.Ceiling(rect.Right); + int bottom = (int)Math.Ceiling(rect.Bottom); + + return new Rectangle(left, top, right - left, bottom - top); + } + + /// + /// Converts a floating-point ImageSharp rectangle to a pixel rectangle that fully contains it. + /// + private static Rectangle ToEnclosingRectangle(RectangleF rect) + { + int left = (int)Math.Floor(rect.Left); + int top = (int)Math.Floor(rect.Top); + int right = (int)Math.Ceiling(rect.Right); + int bottom = (int)Math.Ceiling(rect.Bottom); + + return new Rectangle(left, top, right - left, bottom - top); + } + + /// + /// Translates a rounded rectangle without changing its corner radii. + /// + private static RoundedRect TranslateRoundedRect(RoundedRect rect, double offsetX, double offsetY) + => new( + rect.Rect.Translate(new AvaloniaVector(offsetX, offsetY)), + rect.RadiiTopLeft, + rect.RadiiTopRight, + rect.RadiiBottomRight, + rect.RadiiBottomLeft); + + /// + /// Fills a path using the caller-supplied fill rule. + /// + private void FillPath(Brush brush, IPath path, IntersectionRule fillRule, float blendPercentage = 1) + { + this.Trace(() => $"FillPath fillRule={fillRule} pathBounds={Describe(path.Bounds)} transform={Describe(this.Transform)}"); + this.PushDrawingState(fillRule, blendPercentage); + this.canvas.Fill(brush, path); + this.RestoreCanvasState("FillPath state"); + } + + /// + /// Converts an Avalonia gradient brush into the matching ImageSharp gradient brush. + /// + private static Brush? CreateGradientBrush(IGradientBrush gradient, Rect targetRect) + { + ColorStop[] stops = ToColorStops(gradient.GradientStops); + GradientRepetitionMode repetitionMode = gradient.SpreadMethod.ToGradientRepetitionMode(); + ILinearGradientBrush? linear = gradient as ILinearGradientBrush; + + if (linear is not null) + { + PointF start = linear.StartPoint.ToPixels(targetRect).ToPointF(); + PointF end = linear.EndPoint.ToPixels(targetRect).ToPointF(); + + return new LinearGradientBrush(start, end, repetitionMode, stops); + } + + IRadialGradientBrush? radial = gradient as IRadialGradientBrush; + + if (radial is not null) + { + PointF center = radial.Center.ToPixels(targetRect).ToPointF(); + PointF origin = radial.GradientOrigin.ToPixels(targetRect).ToPointF(); + float radiusX = (float)radial.RadiusX.ToValue(targetRect.Width); + float radiusY = (float)radial.RadiusY.ToValue(targetRect.Height); + + if (origin == center && radiusX != radiusY) + { + return new EllipticGradientBrush( + center, + new PointF(center.X + radiusX, center.Y), + radiusY / radiusX, + repetitionMode, + stops); + } + + return origin == center + ? new RadialGradientBrush(center, radiusX, repetitionMode, stops) + : new RadialGradientBrush(origin, 0, center, radiusX, repetitionMode, stops); + } + + IConicGradientBrush? conic = gradient as IConicGradientBrush; + + if (conic is not null) + { + PointF center = conic.Center.ToPixels(targetRect).ToPointF(); + float startAngle = (float)(90 - conic.Angle); + + return new SweepGradientBrush(center, startAngle, startAngle + 360, repetitionMode, stops); + } + + return null; + } + + /// + /// Converts an Avalonia image brush into an ImageSharp tiled image brush. + /// + private ImageBrush? CreateImageBrush(IImageBrush brush, Rect targetRect) + { + IDrawableBitmapImpl? bitmap = brush.Source?.GetBitmap() as IDrawableBitmapImpl; + + if (bitmap is null) + { + return null; + } + + // Skia feeds TileBrushCalculator the bitmap size in the current drawing DPI, then draws + // from source pixels into that logical size. Keep the same split here: layout in Avalonia + // DIPs, source sampling through the bitmap implementation. + Avalonia.Size sourceSize = bitmap.PixelSize.ToSizeWithDpi(this.dpi); + return this.CreateTileBrush(bitmap, sourceSize, brush, targetRect); + } + + /// + /// Converts an Avalonia scene brush into an ImageSharp tiled image brush. + /// + private ImageBrush? CreateSceneBrushContent(Avalonia.Media.ISceneBrushContent content, Rect targetRect) + { + Rect contentRect = content.Rect; + int width = (int)Math.Ceiling(contentRect.Width); + int height = (int)Math.Ceiling(contentRect.Height); + + if (width < 1 || height < 1) + { + return null; + } + + Image sourceImage = new(WriteableBitmapImpl.ImageConfiguration, width, height); + + using (DrawingContextImpl context = new(sourceImage, this.dpi)) + { + // Scene brushes render from their own content bounds, not from the bounds of the + // visual being painted: + // + // +----------------------+ target visual + // | | + // | +-----+----+ | content bounds + // | | | | | + // | +-----+ | | + // | +--+ | | + // | +--+ | | + // +----------------------+ + // + // Translate non-zero content bounds back to the intermediate origin, matching Skia's + // VisualBrush path before the generated image is tiled over the target box. + Matrix? transform = contentRect.TopLeft == default + ? null + : Matrix.CreateTranslation(-contentRect.X, -contentRect.Y); + + context.PushRenderOptions(this.currentRenderOptions); + context.Clear(Avalonia.Media.Colors.Transparent); + content.Render(context, transform); + context.PopRenderOptions(); + } + + using WriteableBitmapImpl sourceBitmap = new(sourceImage, this.dpi); + + return this.CreateTileBrush(sourceBitmap, contentRect.Size, content.Brush, targetRect); + } + + /// + /// Converts a source bitmap and Avalonia tile brush settings into an ImageSharp tiled image brush. + /// + private ImageBrush? CreateTileBrush( + IDrawableBitmapImpl sourceBitmap, + Avalonia.Size sourceSize, + ITileBrush brush, + Rect targetRect) + { + TileBrushLayout layout = CalculateTileBrushLayout(brush, sourceSize, targetRect); + + if (!layout.IsValid) + { + return null; + } + + Image tileImage = new(WriteableBitmapImpl.ImageConfiguration, layout.IntermediateWidth, layout.IntermediateHeight); + + using (DrawingContextImpl context = new(tileImage, this.dpi)) + { + context.Clear(Avalonia.Media.Colors.Transparent); + context.PushClip(layout.IntermediateClip); + context.PushRenderOptions(this.currentRenderOptions); + context.Transform = layout.IntermediateTransform; + context.DrawBitmap(sourceBitmap, 1, new Rect(sourceBitmap.PixelSize.ToSize(1)), new Rect(sourceSize)); + context.PopRenderOptions(); + context.PopClip(); + } + + this.brushImages.Add(tileImage); + + (WrapMode wrapX, WrapMode wrapY) = ToWrapModes(brush.TileMode); + return new ImageBrush(tileImage, tileImage.Bounds, layout.Offset, wrapX, wrapY); + } + + /// + /// Maps Avalonia tile modes to ImageSharp wrap modes. + /// + private static (WrapMode X, WrapMode Y) ToWrapModes(TileMode tileMode) + => tileMode switch + { + TileMode.Tile => (WrapMode.Repeat, WrapMode.Repeat), + TileMode.FlipX => (WrapMode.Mirror, WrapMode.Repeat), + TileMode.FlipY => (WrapMode.Repeat, WrapMode.Mirror), + TileMode.FlipXY => (WrapMode.Mirror, WrapMode.Mirror), + _ => (WrapMode.None, WrapMode.None), + }; + + /// + /// Converts Avalonia gradient stops to ImageSharp color stops. + /// + private static ColorStop[] ToColorStops(IReadOnlyList gradientStops) + { + ColorStop[] stops = new ColorStop[gradientStops.Count]; + + for (int i = 0; i < gradientStops.Count; i++) + { + IGradientStop stop = gradientStops[i]; + stops[i] = new ColorStop((float)stop.Offset, stop.Color.ToColor()); + } + + return stops; + } + + /// + /// Calculates the ImageSharp tile image that represents Avalonia's tile brush viewbox/viewport rules. + /// + private static TileBrushLayout CalculateTileBrushLayout(ITileBrush brush, Avalonia.Size sourceSize, Rect targetRect) + { + Rect sourceRect = brush.SourceRect.ToPixels(sourceSize); + Avalonia.Size targetSize = targetRect.Size; + Rect destinationRect = brush.DestinationRect.ToPixels(targetSize); + + if (sourceRect.Width <= 0 || sourceRect.Height <= 0 || destinationRect.Width <= 0 || destinationRect.Height <= 0) + { + return default; + } + + AvaloniaVector scale = Avalonia.Media.MediaExtensions.CalculateScaling( + brush.Stretch, + destinationRect.Size, + sourceRect.Size); + + Avalonia.Size scaledSize = new(sourceRect.Width * scale.X, sourceRect.Height * scale.Y); + AvaloniaVector translate = CalculateTileTranslate( + brush.AlignmentX, + brush.AlignmentY, + scaledSize, + destinationRect.Size); + + bool isTiled = brush.TileMode != TileMode.None; + Avalonia.Size intermediateSize = isTiled ? destinationRect.Size : targetSize; + Matrix intermediateTransform = + Matrix.CreateTranslation(-sourceRect.Position) * + Matrix.CreateScale(scale) * + Matrix.CreateTranslation(translate); + + Rect intermediateClip; + + if (isTiled) + { + intermediateClip = new Rect(destinationRect.Size); + } + else + { + intermediateClip = destinationRect; + intermediateTransform *= Matrix.CreateTranslation(destinationRect.Position); + } + + int intermediateWidth = Math.Max(1, (int)Math.Ceiling(intermediateSize.Width)); + int intermediateHeight = Math.Max(1, (int)Math.Ceiling(intermediateSize.Height)); + + // Relative viewports are anchored to the painted target. Absolute viewports are anchored + // in drawing coordinates, so subtract the target origin before ImageSharp adds it back + // when creating the brush renderer for the painted shape. + double offsetX = isTiled ? destinationRect.X : 0; + double offsetY = isTiled ? destinationRect.Y : 0; + + if (brush.DestinationRect.Unit == RelativeUnit.Absolute) + { + offsetX -= targetRect.X; + offsetY -= targetRect.Y; + } + + SixLabors.ImageSharp.Point offset = new((int)Math.Floor(offsetX), (int)Math.Floor(offsetY)); + return new TileBrushLayout(intermediateClip, intermediateTransform, intermediateWidth, intermediateHeight, offset); + } + + /// + /// Calculates Avalonia tile alignment translation for the scaled source size. + /// + private static AvaloniaVector CalculateTileTranslate(AlignmentX alignmentX, AlignmentY alignmentY, Avalonia.Size sourceSize, Avalonia.Size destinationSize) + { + double x = alignmentX switch + { + AlignmentX.Center => (destinationSize.Width - sourceSize.Width) / 2, + AlignmentX.Right => destinationSize.Width - sourceSize.Width, + _ => 0 + }; + + double y = alignmentY switch + { + AlignmentY.Center => (destinationSize.Height - sourceSize.Height) / 2, + AlignmentY.Bottom => destinationSize.Height - sourceSize.Height, + _ => 0 + }; + + return new AvaloniaVector(x, y); + } + + /// + /// Holds the baked tile image placement required by an Avalonia tile brush. + /// + private readonly struct TileBrushLayout + { + /// + /// Initializes a new instance of the struct. + /// + /// The tile-space clip applied while drawing the source image. + /// The tile-space transform applied while drawing the source image. + /// The baked tile image width. + /// The baked tile image height. + /// The offset that anchors the baked tile in drawing coordinates. + public TileBrushLayout( + Rect intermediateClip, + Matrix intermediateTransform, + int intermediateWidth, + int intermediateHeight, + SixLabors.ImageSharp.Point offset) + { + this.IntermediateClip = intermediateClip; + this.IntermediateTransform = intermediateTransform; + this.IntermediateWidth = intermediateWidth; + this.IntermediateHeight = intermediateHeight; + this.Offset = offset; + this.IsValid = true; + } + + /// + /// Gets a value indicating whether the layout contains drawable tile geometry. + /// + public bool IsValid { get; } + + /// + /// Gets the tile-space clip applied while drawing the source image. + /// + public Rect IntermediateClip { get; } + + /// + /// Gets the tile-space transform applied while drawing the source image. + /// + public Matrix IntermediateTransform { get; } + + /// + /// Gets the baked tile image width. + /// + public int IntermediateWidth { get; } + + /// + /// Gets the baked tile image height. + /// + public int IntermediateHeight { get; } + + /// + /// Gets the offset that anchors the baked tile in drawing coordinates. + /// + public SixLabors.ImageSharp.Point Offset { get; } + } + + /// + /// Writes a trace entry when tracing is enabled for this context. + /// + private void Trace(Func message) + { + if (this.trace is null) + { + return; + } + + this.trace.Append(this.traceOperationIndex++).Append(": ").AppendLine(message()); + } + + /// + /// Writes the current sample and canvas state stack depths to the active diagnostic trace. + /// + private void TraceState(string action) + { + if (this.trace is null) + { + return; + } + + this.trace + .Append(this.traceOperationIndex++) + .Append(": ") + .Append(action) + .Append(" canvasDepth=") + .Append(this.canvasStateDepth) + .Append(" transformDepth=") + .Append(this.transforms.Count) + .Append(" opacityDepth=") + .Append(this.opacities.Count) + .Append(" opacityLayerDepth=") + .Append(this.opacityLayers.Count) + .Append(" opacityMaskDepth=") + .Append(this.opacityMasks.Count) + .Append(" renderOptionsDepth=") + .Append(this.renderOptions.Count) + .Append(" textOptionsDepth=") + .Append(this.textOptions.Count) + .Append(" opacity=") + .Append(this.opacity.ToString("0.###")) + .Append(" transform=") + .AppendLine(Describe(this.Transform)); + } + + /// + /// Determines whether the next drawing context should capture operation-level diagnostics. + /// + private static bool ShouldTraceContext(bool ownsImage, int width, int height) + { + if (DebugTraceNextFrame || DebugSaveNextFrame) + { + DebugTraceNextFrame = false; + return true; + } + + if (ownsImage && Environment.GetEnvironmentVariable("IMAGESHARP_DRAWING_TRACE_FRAMEBUFFER_CONTEXTS") == "1") + { + return true; + } + + if (width >= 256 && height >= 256 && Environment.GetEnvironmentVariable("IMAGESHARP_DRAWING_TRACE_LARGE_CONTEXTS") == "1") + { + return true; + } + + if (Environment.GetEnvironmentVariable("IMAGESHARP_DRAWING_TRACE_ALL_CONTEXTS") == "1") + { + return true; + } + + if (!environmentTraceConsumed && Environment.GetEnvironmentVariable("IMAGESHARP_DRAWING_TRACE_NEXT_FRAME") == "1") + { + environmentTraceConsumed = true; + return true; + } + + return false; + } + + /// + /// Determines whether every drawing context should save its rendered image. + /// + private static bool ShouldSaveContextImage() + => Environment.GetEnvironmentVariable("IMAGESHARP_DRAWING_SAVE_ALL_CONTEXTS") == "1"; + + /// + /// Gets the directory used for on-demand image and trace captures. + /// + private static string GetCaptureDirectory() + { + string? directory = Environment.GetEnvironmentVariable("IMAGESHARP_DRAWING_CAPTURE_DIRECTORY"); + + if (string.IsNullOrWhiteSpace(directory)) + { + return System.IO.Path.GetTempPath(); + } + + Directory.CreateDirectory(directory); + + return directory; + } + + /// + /// Formats an Avalonia color for diagnostic traces. + /// + private static string Describe(Avalonia.Media.Color color) + => $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}"; + + /// + /// Formats an Avalonia rectangle for diagnostic traces. + /// + private static string Describe(Rect rect) + => $"({rect.X:0.###},{rect.Y:0.###},{rect.Width:0.###},{rect.Height:0.###})"; + + /// + /// Formats an ImageSharp rectangle for diagnostic traces. + /// + private static string Describe(RectangleF rect) + => $"({rect.X:0.###},{rect.Y:0.###},{rect.Width:0.###},{rect.Height:0.###})"; + + /// + /// Formats an ImageSharp pixel rectangle for diagnostic traces. + /// + private static string Describe(Rectangle rect) + => $"({rect.X},{rect.Y},{rect.Width},{rect.Height})"; + + /// + /// Formats a pixel rectangle for diagnostic traces. + /// + private static string Describe(LtrbPixelRect rect) + => $"({rect.Left},{rect.Top},{rect.Right},{rect.Bottom})"; + + /// + /// Formats a list of pixel rectangles for diagnostic traces. + /// + private static string Describe(IList rects) + { + StringBuilder builder = new(); + builder.Append(rects.Count).Append('['); + + for (int i = 0; i < rects.Count; i++) + { + if (i > 0) + { + builder.Append(','); + } + + builder.Append(Describe(rects[i])); + } + + return builder.Append(']').ToString(); + } + + /// + /// Formats an Avalonia transform matrix for diagnostic traces. + /// + private static string Describe(Matrix matrix) + => $"[{matrix.M11:0.###},{matrix.M12:0.###},{matrix.M21:0.###},{matrix.M22:0.###},{matrix.M31:0.###},{matrix.M32:0.###}]"; + + /// + /// Formats an Avalonia brush for diagnostic traces. + /// + private static string DescribeBrush(IBrush? brush) + { + if (brush is null) + { + return ""; + } + + if (brush is ISolidColorBrush solid) + { + return $"Solid({Describe(solid.Color)}, opacity={brush.Opacity:0.###})"; + } + + return $"{brush.GetType().Name}(opacity={brush.Opacity:0.###})"; + } + + /// + /// Formats an Avalonia pen for diagnostic traces. + /// + private static string DescribePen(IPen? pen) + { + if (pen is null) + { + return ""; + } + + return $"Pen(thickness={pen.Thickness:0.###}, brush={DescribeBrush(pen.Brush)})"; + } +} diff --git a/samples/AvaloniaControlCatalog/EllipseGeometryImpl.cs b/samples/AvaloniaControlCatalog/EllipseGeometryImpl.cs new file mode 100644 index 00000000..fd459953 --- /dev/null +++ b/samples/AvaloniaControlCatalog/EllipseGeometryImpl.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Avalonia; +using SixLabors.ImageSharp.Drawing; + +namespace AvaloniaControlCatalog; + +/// +/// Avalonia ellipse geometry implementation backed by an ImageSharp ellipse path. +/// +internal sealed class EllipseGeometryImpl : GeometryImpl +{ + /// + /// Initializes a new ellipse geometry. + /// + /// The ellipse bounds. + public EllipseGeometryImpl(Rect rect) + : this(CreatePath(rect)) + { + } + + private EllipseGeometryImpl(IPath path) + : base(path, path, IntersectionRule.EvenOdd) + { + } + + private static IPath CreatePath(Rect rect) + => rect.Width <= 0 || rect.Height <= 0 + ? EmptyPath.ClosedPath + : new EllipsePolygon(rect.Center.ToPointF(), rect.Size.ToSizeF()); +} diff --git a/samples/AvaloniaControlCatalog/FontManagerImpl.cs b/samples/AvaloniaControlCatalog/FontManagerImpl.cs new file mode 100644 index 00000000..72fcb9ff --- /dev/null +++ b/samples/AvaloniaControlCatalog/FontManagerImpl.cs @@ -0,0 +1,297 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using Avalonia.Platform; +using SixLabors.Fonts.Unicode; +using SixLaborsTag = SixLabors.Fonts.Tables.AdvancedTypographic.Tag; +using SixLaborsColorFontSupport = SixLabors.Fonts.ColorFontSupport; +using SixLaborsFontCollection = SixLabors.Fonts.FontCollection; +using SixLaborsFontDescription = SixLabors.Fonts.FontDescription; +using SixLaborsFontFamily = SixLabors.Fonts.FontFamily; +using SixLaborsFontMatch = SixLabors.Fonts.FontMatch; +using SixLaborsFontMetrics = SixLabors.Fonts.FontMetrics; +using SixLaborsFontStyle = SixLabors.Fonts.FontStyle; +using SixLaborsSystemFonts = SixLabors.Fonts.SystemFonts; + +namespace AvaloniaControlCatalog; + +/// +/// Avalonia font manager implementation backed by SixLabors.Fonts. +/// +internal sealed class FontManagerImpl : IFontManagerImpl +{ + private const SixLaborsColorFontSupport FallbackColorFontSupport = + SixLaborsColorFontSupport.ColrV0 | SixLaborsColorFontSupport.ColrV1 | SixLaborsColorFontSupport.Svg; + + /// + /// Initializes a new instance of the class. + /// + public FontManagerImpl() + { + } + + /// + public string GetDefaultFontFamilyName() + => SixLaborsSystemFonts.GetDefaultFamilyName(); + + /// + public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) + => SixLaborsSystemFonts.GetFamilyNames(checkForUpdates); + + /// + public bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, string? familyName, System.Globalization.CultureInfo? culture, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) + { + CodePoint codePoint = new(codepoint); + SixLaborsFontStyle imageSharpStyle = ToImageSharpStyle(style, weight); + + if (SixLaborsSystemFonts.TryMatchCharacter(codePoint, imageSharpStyle, familyName, culture, out SixLaborsFontMatch match) + && this.TryCreatePlatformTypeface(match, out platformTypeface)) + { + return true; + } + + platformTypeface = null; + return false; + } + + /// + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) + { + if (!SixLaborsSystemFonts.TryGet(familyName, out SixLaborsFontFamily family)) + { + platformTypeface = null; + return false; + } + + SixLaborsFontStyle requestedStyle = ToImageSharpStyle(style, weight); + SixLaborsFontStyle resolvedStyle = requestedStyle; + + if (!family.TryGetMetrics(resolvedStyle, out _)) + { + resolvedStyle = family.TryGetMetrics(SixLaborsFontStyle.Regular, out _) + ? SixLaborsFontStyle.Regular + : family.GetAvailableStyles().Span[0]; + } + + FontSimulations fontSimulations = FontSimulations.None; + + // Avalonia records only the styles missing from the resolved face. SixLabors.Fonts then + // synthesizes those styles while retaining the metrics and tables of that resolved face. + if ((requestedStyle & SixLaborsFontStyle.Bold) != 0 && (resolvedStyle & SixLaborsFontStyle.Bold) == 0) + { + fontSimulations |= FontSimulations.Bold; + } + + if ((requestedStyle & SixLaborsFontStyle.Italic) != 0 && (resolvedStyle & SixLaborsFontStyle.Italic) == 0) + { + fontSimulations |= FontSimulations.Oblique; + } + + return this.TryCreatePlatformTypeface(family, resolvedStyle, fontSimulations, out platformTypeface); + } + + /// + public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) + { + try + { + SixLaborsFontCollection collection = new(); + SixLaborsFontDescription description; + SixLaborsFontFamily family = collection.Add(stream, out description); + family.TryGetMetrics(description.Style, out SixLaborsFontMetrics? metrics); + + platformTypeface = new PlatformTypeface( + description.FontFamilyInvariantCulture, + ToAvaloniaStyle(description.Style), + ToAvaloniaWeight(description.Style), + FontStretch.Normal, + metrics!, + family, + description.Style, + fontSimulations); + + return true; + } + catch + { + platformTypeface = null; + return false; + } + } + + /// + public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) + { + if (!SixLaborsSystemFonts.TryGet(familyName, out SixLaborsFontFamily family)) + { + familyTypefaces = null; + return false; + } + + ReadOnlySpan styles = family.GetAvailableStyles().Span; + Typeface[] typefaces = new Typeface[styles.Length]; + + for (int i = 0; i < styles.Length; i++) + { + SixLaborsFontStyle faceStyle = styles[i]; + typefaces[i] = new Typeface(familyName, ToAvaloniaStyle(faceStyle), ToAvaloniaWeight(faceStyle), FontStretch.Normal); + } + + familyTypefaces = typefaces; + return true; + } + + private static FontStyle ToAvaloniaStyle(SixLaborsFontStyle style) + => (style & SixLaborsFontStyle.Italic) == SixLaborsFontStyle.Italic ? FontStyle.Italic : FontStyle.Normal; + + private static FontWeight ToAvaloniaWeight(SixLaborsFontStyle style) + => (style & SixLaborsFontStyle.Bold) == SixLaborsFontStyle.Bold ? FontWeight.Bold : FontWeight.Normal; + + private static SixLaborsFontStyle ToImageSharpStyle(FontStyle style, FontWeight weight) + { + SixLaborsFontStyle result = (int)weight >= 600 ? SixLaborsFontStyle.Bold : SixLaborsFontStyle.Regular; + + if (style == FontStyle.Italic || style == FontStyle.Oblique) + { + result |= SixLaborsFontStyle.Italic; + } + + return result; + } + + private bool TryCreatePlatformTypeface(SixLaborsFontMatch match, [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) + => this.TryCreatePlatformTypeface(match.Family, match.Style, FontSimulations.None, out platformTypeface); + + private bool TryCreatePlatformTypeface( + SixLaborsFontFamily family, + SixLaborsFontStyle style, + FontSimulations fontSimulations, + [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) + { + if (!family.TryGetMetrics(style, out SixLabors.Fonts.FontMetrics? metrics)) + { + platformTypeface = null; + return false; + } + + platformTypeface = new PlatformTypeface( + metrics.Description.FontFamilyInvariantCulture, + ToAvaloniaStyle(metrics.Description.Style), + ToAvaloniaWeight(metrics.Description.Style), + FontStretch.Normal, + metrics, + family, + metrics.Description.Style, + fontSimulations); + + return true; + } +} + +/// +/// Avalonia platform typeface wrapper for a SixLabors.Fonts face. +/// +internal sealed class PlatformTypeface : IPlatformTypeface +{ + private readonly SixLaborsFontMetrics fontMetrics; + private readonly SixLaborsFontFamily fontFamily; + private readonly SixLaborsFontStyle fontStyle; + + /// + /// Initializes a new platform typeface wrapper. + /// + /// The Avalonia family name. + /// The Avalonia font style. + /// The Avalonia font weight. + /// The Avalonia font stretch. + /// The SixLabors font metrics. + /// The SixLabors font family. + /// The SixLabors font style. + /// The requested font simulations. + public PlatformTypeface( + string familyName, + FontStyle style, + FontWeight weight, + FontStretch stretch, + SixLaborsFontMetrics fontMetrics, + SixLaborsFontFamily fontFamily, + SixLaborsFontStyle fontStyle, + FontSimulations fontSimulations = FontSimulations.None) + { + this.FamilyName = familyName; + this.Style = style; + this.Weight = weight; + this.Stretch = stretch; + this.FontSimulations = fontSimulations; + this.fontMetrics = fontMetrics; + this.fontStyle = fontStyle; + this.fontFamily = fontFamily; + } + + /// + public string FamilyName { get; } + + /// + public FontWeight Weight { get; } + + /// + public FontStyle Style { get; } + + /// + public FontStretch Stretch { get; } + + /// + public FontSimulations FontSimulations { get; } + + /// + /// Creates a SixLabors font for the exact face this typeface wraps, at the given size. + /// + /// The requested font size. + /// The created SixLabors font. + public SixLabors.Fonts.Font CreateFont(float size) + { + SixLaborsFontStyle requestedStyle = this.fontStyle; + + // Avalonia resolves a real face first and records any missing bold or oblique style as a + // simulation. SixLabors.Fonts performs those simulations from the requested style while + // retaining the resolved face metrics, so include the simulation flags in that request. + if ((this.FontSimulations & FontSimulations.Bold) != 0) + { + requestedStyle |= SixLaborsFontStyle.Bold; + } + + if ((this.FontSimulations & FontSimulations.Oblique) != 0) + { + requestedStyle |= SixLaborsFontStyle.Italic; + } + + return this.fontFamily.CreateFont(size, requestedStyle); + } + + /// + public bool TryGetStream([NotNullWhen(true)] out Stream? stream) + { + try + { + stream = this.fontMetrics.OpenStream(); + return true; + } + catch + { + stream = null; + return false; + } + } + + /// + public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory table) + => this.fontMetrics.TryGetTableData(new SixLaborsTag((uint)tag), out table); + + /// + public void Dispose() + { + } +} diff --git a/samples/AvaloniaControlCatalog/GeometryGroupImpl.cs b/samples/AvaloniaControlCatalog/GeometryGroupImpl.cs new file mode 100644 index 00000000..4e38600a --- /dev/null +++ b/samples/AvaloniaControlCatalog/GeometryGroupImpl.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Avalonia.Media; +using Avalonia.Platform; +using SixLabors.ImageSharp.Drawing; + +namespace AvaloniaControlCatalog; + +/// +/// Avalonia geometry group implementation backed by combined ImageSharp paths. +/// +internal sealed class GeometryGroupImpl : GeometryImpl +{ + /// + /// Initializes a new geometry group. + /// + /// The fill rule for the group fill path. + /// The child geometries in the group. + public GeometryGroupImpl(FillRule fillRule, IReadOnlyList children) + : this(CreatePaths(fillRule, children)) + { + } + + private GeometryGroupImpl((IPath StrokePath, IPath? FillPath, IntersectionRule FillRule) paths) + : base(paths.StrokePath, paths.FillPath, paths.FillRule) + { + } + + private static (IPath StrokePath, IPath? FillPath, IntersectionRule FillRule) CreatePaths(FillRule fillRule, IReadOnlyList children) + { + List strokePaths = new(children.Count); + List fillPaths = new(children.Count); + + for (int i = 0; i < children.Count; i++) + { + GeometryImpl geometry = (GeometryImpl)children[i]; + strokePaths.Add(geometry.StrokePath); + + if (geometry.FillPath is not null) + { + fillPaths.Add(geometry.FillPath); + } + } + + return ( + new ComplexPolygon(strokePaths), + fillPaths.Count == 0 ? null : new ComplexPolygon(fillPaths), + fillRule.ToIntersectionRule()); + } +} diff --git a/samples/AvaloniaControlCatalog/GeometryImpl.cs b/samples/AvaloniaControlCatalog/GeometryImpl.cs new file mode 100644 index 00000000..0fd12e9f --- /dev/null +++ b/samples/AvaloniaControlCatalog/GeometryImpl.cs @@ -0,0 +1,280 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using Avalonia; +using Avalonia.Media; +using Avalonia.Platform; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; + +namespace AvaloniaControlCatalog; + +/// +/// Base Avalonia geometry implementation backed by ImageSharp paths. +/// +internal abstract class GeometryImpl : IGeometryImpl +{ + /// + /// Initializes a new geometry from ImageSharp stroke and fill paths. + /// + /// The path used for stroke operations. + /// The path used for fill operations. + /// The fill rule used for the fill path. + protected GeometryImpl(IPath strokePath, IPath? fillPath, IntersectionRule fillRule) + => this.SetPaths(strokePath, fillPath, fillRule); + + /// + /// Gets the stroke path used as the primary geometry path. + /// + public IPath Path => this.StrokePath; + + /// + /// Gets the path used for stroke operations. + /// + public IPath StrokePath { get; private set; } = EmptyPath.OpenPath; + + /// + /// Gets the path used for fill operations. + /// + public IPath? FillPath { get; private set; } = EmptyPath.OpenPath; + + /// + /// Gets the fill rule used for the fill path. + /// + public IntersectionRule FillRule { get; private set; } + + /// + public Rect Bounds { get; private set; } + + /// + public double ContourLength { get; private set; } + + private LinearGeometry strokeGeometry = EmptyPath.OpenPath.ToLinearGeometry(Vector2.One); + + private LinearGeometry? fillGeometry = EmptyPath.OpenPath.ToLinearGeometry(Vector2.One); + + private StrokeCache strokeCache; + + /// + /// Replaces the stroke and fill path with the same path. + /// + /// The replacement path. + protected void SetPath(IPath path) + => this.SetPaths(path, path, this.FillRule); + + /// + /// Replaces the geometry paths and cached path data. + /// + /// The path used for stroke operations. + /// The path used for fill operations. + /// The fill rule used for . + protected void SetPaths(IPath strokePath, IPath? fillPath, IntersectionRule fillRule) + { + this.StrokePath = strokePath; + this.FillPath = fillPath; + this.FillRule = fillRule; + this.strokeGeometry = strokePath.ToLinearGeometry(Vector2.One); + this.fillGeometry = fillPath is null + ? null + : ReferenceEquals(fillPath, strokePath) ? this.strokeGeometry : fillPath.ToLinearGeometry(Vector2.One); + this.Bounds = this.strokeGeometry.Info.Bounds.ToAvaloniaRect(); + this.ContourLength = this.strokeGeometry.ComputeLength(); + this.strokeCache = default; + } + + /// + public Rect GetRenderBounds(IPen? pen) + { + Rect bounds = this.Bounds; + if (this.fillGeometry is not null) + { + bounds = bounds.Union(this.fillGeometry.Info.Bounds.ToAvaloniaRect()); + } + + if (pen is not null) + { + LinearGeometry geometry = this.GetStrokeGeometry(pen); + bounds = bounds.Union(geometry.Info.Bounds.ToAvaloniaRect()); + } + + return bounds; + } + + /// + public IGeometryImpl GetWidenedGeometry(IPen pen) + { + IPath path = this.StrokePath.CreateStrokePath(pen); + + return new StreamGeometryImpl(path, path, this.FillRule); + } + + /// + public bool FillContains(AvaloniaPoint point) + => this.fillGeometry is not null && this.fillGeometry.Contains(point.ToPointF(), this.FillRule); + + /// + public IGeometryImpl? Intersect(IGeometryImpl geometry) + => CombinedGeometryImpl.Create(GeometryCombineMode.Intersect, this, (GeometryImpl)geometry); + + /// + public bool StrokeContains(IPen? pen, AvaloniaPoint point) + => pen is not null && this.GetStrokeGeometry(pen).Contains(point.ToPointF(), IntersectionRule.NonZero); + + /// + public ITransformedGeometryImpl WithTransform(Matrix transform) + => new TransformedGeometryImpl(this, transform); + + /// + public bool TryGetPointAtDistance(double distance, out AvaloniaPoint point) + { + if (!this.TryGetPointAndTangentAtDistance(distance, out point, out _)) + { + point = default; + return false; + } + + return true; + } + + /// + public bool TryGetPointAndTangentAtDistance(double distance, out AvaloniaPoint point, out AvaloniaPoint tangent) + { + if (this.ContourLength <= 0) + { + point = default; + tangent = default; + return false; + } + + if (!this.StrokePath.TryGetPathPointAtDistance((float)distance, Vector2.One, out PathPoint segment)) + { + point = default; + tangent = default; + return false; + } + + point = new AvaloniaPoint(segment.Point.X, segment.Point.Y); + tangent = new AvaloniaPoint(segment.Tangent.X, segment.Tangent.Y); + return true; + } + + /// + public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, [NotNullWhen(true)] out IGeometryImpl? segmentGeometry) + { + if (stopDistance <= startDistance || this.ContourLength <= 0) + { + segmentGeometry = null; + return false; + } + + if (!this.strokeGeometry.TryGetSegment((float)startDistance, (float)stopDistance, startOnBeginFigure, out IPath path)) + { + segmentGeometry = null; + return false; + } + + segmentGeometry = new StreamGeometryImpl(path, null, this.FillRule); + return true; + } + + private LinearGeometry GetStrokeGeometry(IPen pen) + { + LinearGeometry? cachedGeometry = this.strokeCache.Geometry; + if (cachedGeometry is not null && this.strokeCache.Matches(pen)) + { + return cachedGeometry; + } + + LinearGeometry geometry = this.StrokePath.CreateStrokePath(pen).ToLinearGeometry(Vector2.One); + this.strokeCache = new StrokeCache(pen, geometry); + + return geometry; + } + + private struct StrokeCache + { + private double thickness; + private PenLineCap lineCap; + private PenLineJoin lineJoin; + private double miterLimit; + private double dashOffset; + private double[]? dashes; + + /// + /// Initializes cached widened-stroke geometry for a pen. + /// + /// The pen used to create the widened stroke. + /// The widened stroke geometry. + public StrokeCache(IPen pen, LinearGeometry geometry) + { + this.thickness = pen.Thickness; + this.lineCap = pen.LineCap; + this.lineJoin = pen.LineJoin; + this.miterLimit = pen.MiterLimit; + this.dashOffset = pen.DashStyle?.Offset ?? 0; + this.dashes = CopyDashes(pen.DashStyle?.Dashes); + this.Geometry = geometry; + } + + /// + /// Gets the cached widened stroke geometry. + /// + public LinearGeometry? Geometry { get; } + + /// + /// Returns whether the cached geometry was created for the supplied pen. + /// + /// The pen to compare. + /// when the pen matches the cache key. + public bool Matches(IPen pen) + => this.thickness == pen.Thickness && + this.lineCap == pen.LineCap && + this.lineJoin == pen.LineJoin && + this.miterLimit == pen.MiterLimit && + this.dashOffset == (pen.DashStyle?.Offset ?? 0) && + DashesEqual(this.dashes, pen.DashStyle?.Dashes); + + private static double[]? CopyDashes(IReadOnlyList? source) + { + if (source is null || source.Count == 0) + { + return null; + } + + double[] result = new double[source.Count]; + for (int i = 0; i < result.Length; i++) + { + result[i] = source[i]; + } + + return result; + } + + private static bool DashesEqual(double[]? left, IReadOnlyList? right) + { + if (left is null || left.Length == 0) + { + return right is null || right.Count == 0; + } + + if (right is null || left.Length != right.Count) + { + return false; + } + + for (int i = 0; i < left.Length; i++) + { + if (left[i] != right[i]) + { + return false; + } + } + + return true; + } + } + +} diff --git a/samples/AvaloniaControlCatalog/GlobalUsings.cs b/samples/AvaloniaControlCatalog/GlobalUsings.cs new file mode 100644 index 00000000..4493c042 --- /dev/null +++ b/samples/AvaloniaControlCatalog/GlobalUsings.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// Avalonia type aliases shared across the ImageSharp.Drawing rendering backend so the ImageSharp +// types (Brush, Pen, Color, etc.) can be used bare while Avalonia's same-named types stay explicit. +global using AvaloniaColor = Avalonia.Media.Color; +global using AvaloniaMatrix = Avalonia.Matrix; +global using AvaloniaPoint = Avalonia.Point; +global using AvaloniaRect = Avalonia.Rect; +global using AvaloniaRoundedRect = Avalonia.RoundedRect; +global using AvaloniaSize = Avalonia.Size; +global using AvaloniaVector = Avalonia.Vector; +global using AlignmentX = Avalonia.Media.AlignmentX; +global using AlignmentY = Avalonia.Media.AlignmentY; +global using BoxShadows = Avalonia.Media.BoxShadows; +global using FillRule = Avalonia.Media.FillRule; +global using GeometryCombineMode = Avalonia.Media.GeometryCombineMode; +global using GradientSpreadMethod = Avalonia.Media.GradientSpreadMethod; +global using IBlurEffect = Avalonia.Media.IBlurEffect; +global using IBrush = Avalonia.Media.IBrush; +global using IConicGradientBrush = Avalonia.Media.IConicGradientBrush; +global using IDropShadowEffect = Avalonia.Media.IDropShadowEffect; +global using IEffect = Avalonia.Media.IEffect; +global using IGradientBrush = Avalonia.Media.IGradientBrush; +global using IGradientStop = Avalonia.Media.IGradientStop; +global using IImageBrush = Avalonia.Media.IImageBrush; +global using ILinearGradientBrush = Avalonia.Media.ILinearGradientBrush; +global using IPen = Avalonia.Media.IPen; +global using IRadialGradientBrush = Avalonia.Media.IRadialGradientBrush; +global using ISolidColorBrush = Avalonia.Media.ISolidColorBrush; +global using PenLineCap = Avalonia.Media.PenLineCap; +global using PenLineJoin = Avalonia.Media.PenLineJoin; +global using ITileBrush = Avalonia.Media.ITileBrush; +global using RenderOptions = Avalonia.Media.RenderOptions; +global using TileMode = Avalonia.Media.TileMode; diff --git a/samples/AvaloniaControlCatalog/GlyphRunImpl.cs b/samples/AvaloniaControlCatalog/GlyphRunImpl.cs new file mode 100644 index 00000000..124bb2f7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/GlyphRunImpl.cs @@ -0,0 +1,243 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using Avalonia; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Platform; +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using AvaloniaTextHintingMode = Avalonia.Media.TextHintingMode; +using AvaloniaTextOptions = Avalonia.Media.TextOptions; + +namespace AvaloniaControlCatalog; + +/// +/// Avalonia glyph run implementation backed by SixLabors.Fonts glyph ids and positions. +/// +internal sealed class GlyphRunImpl : IGlyphRunImpl +{ + /// + /// Positioned per-glyph metrics measured once at construction. Bounds derive from the + /// entries and scans them for decoration skipping. + /// + private readonly ReadOnlyMemory glyphMetrics; + + /// + /// Memoized result. Avalonia retains glyph runs and redraws + /// decorations with the same limits every paint, so one slot covers the realistic pattern. + /// Reference assignment keeps the read-check-publish sequence safe without locking. + /// + private IntersectionsCache? intersectionsCache; + + /// + /// The hinting mode the run was most recently drawn with; intersections are computed from + /// the same hinted geometry so decoration gaps align with the rendered outlines. + /// + private HintingMode hintingMode = HintingMode.Standard; + + /// + /// Initializes a new glyph run wrapper. + /// + /// The Avalonia glyph typeface. + /// The font rendering em size. + /// The positioned glyph information from Avalonia shaping. + /// The glyph-run baseline origin. + /// The SixLabors font face used to render the run. + public GlyphRunImpl(GlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin, Font font) + { + this.GlyphTypeface = glyphTypeface; + this.FontRenderingEmSize = fontRenderingEmSize; + this.BaselineOrigin = baselineOrigin; + this.Font = font; + + ushort[] glyphIds = new ushort[glyphInfos.Count]; + Vector2[] origins = new Vector2[glyphInfos.Count]; + + double penX = 0; + for (int i = 0; i < glyphInfos.Count; i++) + { + GlyphInfo glyph = glyphInfos[i]; + Avalonia.Vector offset = glyph.GlyphOffset; + + glyphIds[i] = glyph.GlyphIndex; + origins[i] = new Vector2((float)(baselineOrigin.X + penX + offset.X), (float)(baselineOrigin.Y + offset.Y)); + + penX += glyph.GlyphAdvance; + } + + this.GlyphIds = glyphIds; + this.GlyphOrigins = origins; + + // Measure directly from the per-glyph metrics cache; the entries match the boxes + // ImageSharp reports when rendering the same positioned run with the same options. + this.glyphMetrics = TextMeasurer.GetGlyphMetrics( + this.GlyphIds.Span, + this.GlyphOrigins.Span, + new GlyphOptions + { + Font = font, + TextBaseline = TextBaseline.Alphabetic, + ColorFontSupport = ColorFontSupport.ColrV0 | ColorFontSupport.ColrV1 | ColorFontSupport.Svg + }); + + FontRectangle measured = FontRectangle.Empty; + bool hasBounds = false; + foreach (SixLabors.Fonts.GlyphMetrics entry in this.glyphMetrics.Span) + { + if (entry.Bounds == FontRectangle.Empty) + { + continue; + } + + measured = hasBounds ? FontRectangle.Union(measured, entry.Bounds) : entry.Bounds; + hasBounds = true; + } + + this.Bounds = !hasBounds + ? new Rect(baselineOrigin.X, baselineOrigin.Y, penX, 0) + : new Rect(measured.X, measured.Y, measured.Width, measured.Height).Inflate(1); + } + + /// + public GlyphTypeface GlyphTypeface { get; } + + /// + /// Gets the exact SixLabors font face the run was shaped against. + /// + public Font Font { get; } + + /// + public double FontRenderingEmSize { get; } + + /// + public Point BaselineOrigin { get; } + + /// + public Rect Bounds { get; } + + /// + /// Gets the glyph identifiers used by ImageSharp. + /// + public ReadOnlyMemory GlyphIds { get; } + + /// + /// Gets the absolute glyph origins used by ImageSharp. + /// + public ReadOnlyMemory GlyphOrigins { get; } + + /// + /// Creates ImageSharp rich glyph options from the current Avalonia text options. + /// + /// The effective Avalonia text options. + /// The ImageSharp glyph options. + public RichGlyphOptions CreateGlyphOptions(AvaloniaTextOptions textOptions) + { + // Track the hinting mode the run is drawn with so outline-derived measurements + // (decoration intersections) use the same hinted geometry. + this.hintingMode = textOptions.TextHintingMode switch + { + AvaloniaTextHintingMode.None => HintingMode.None, + AvaloniaTextHintingMode.Light => HintingMode.Standard, + AvaloniaTextHintingMode.Strong => HintingMode.Standard, + _ => HintingMode.Standard + }; + + return new RichGlyphOptions + { + Font = this.Font, + HintingMode = this.hintingMode, + + // Avalonia positions glyph runs by their baseline origins, so the run must be + // anchored by the alphabetic baseline rather than the line-box default. + TextBaseline = TextBaseline.Alphabetic, + ColorFontSupport = ColorFontSupport.ColrV0 | ColorFontSupport.ColrV1 | ColorFontSupport.Svg + }; + } + + /// + /// + /// Computed from the exact outline geometry via the Fonts intersection API, matching the + /// precision of the Skia backend's SKTextBlob.GetIntercepts, so text decorations + /// break tightly around descender strokes. + /// + public IReadOnlyList GetIntersections(float lowerLimit, float upperLimit) + { + HintingMode currentHintingMode = this.hintingMode; + IntersectionsCache? cache = this.intersectionsCache; + if (cache is not null + && cache.LowerLimit == lowerLimit + && cache.UpperLimit == upperLimit + && cache.HintingMode == currentHintingMode) + { + return cache.Intersections; + } + + // The limits arrive relative to the baseline; the run origins are absolute. + float bandTop = (float)this.BaselineOrigin.Y + lowerLimit; + float bandBottom = (float)this.BaselineOrigin.Y + upperLimit; + + IReadOnlyList intersections = TextMeasurer.GetIntersections( + this.GlyphIds.Span, + this.GlyphOrigins.Span, + new GlyphOptions + { + Font = this.Font, + HintingMode = currentHintingMode, + TextBaseline = TextBaseline.Alphabetic, + ColorFontSupport = ColorFontSupport.ColrV0 | ColorFontSupport.ColrV1 | ColorFontSupport.Svg + }, + bandTop, + bandBottom).ToArray(); + + this.intersectionsCache = new IntersectionsCache(lowerLimit, upperLimit, currentHintingMode, intersections); + return intersections; + } + + /// + public void Dispose() + { + } + + /// + /// One memoized intersections result for a limit band. + /// + private sealed class IntersectionsCache + { + /// + /// Initializes a new instance of the class. + /// + /// The band's lower limit relative to the baseline. + /// The band's upper limit relative to the baseline. + /// The hinting mode the intersections were computed with. + /// The merged, x-sorted interval pairs. + public IntersectionsCache(float lowerLimit, float upperLimit, HintingMode hintingMode, IReadOnlyList intersections) + { + this.LowerLimit = lowerLimit; + this.UpperLimit = upperLimit; + this.HintingMode = hintingMode; + this.Intersections = intersections; + } + + /// + /// Gets the hinting mode the intersections were computed with. + /// + public HintingMode HintingMode { get; } + + /// + /// Gets the band's lower limit relative to the baseline. + /// + public float LowerLimit { get; } + + /// + /// Gets the band's upper limit relative to the baseline. + /// + public float UpperLimit { get; } + + /// + /// Gets the merged, x-sorted interval pairs. + /// + public IReadOnlyList Intersections { get; } + } +} diff --git a/samples/AvaloniaControlCatalog/IDrawableBitmapImpl.cs b/samples/AvaloniaControlCatalog/IDrawableBitmapImpl.cs new file mode 100644 index 00000000..ef86b7e3 --- /dev/null +++ b/samples/AvaloniaControlCatalog/IDrawableBitmapImpl.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Avalonia.Platform; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace AvaloniaControlCatalog; + +/// +/// Bitmap implementation that can draw itself into an ImageSharp-backed drawing context. +/// +internal interface IDrawableBitmapImpl : IBitmapImpl +{ + /// + /// Draws this bitmap into the supplied drawing context. + /// + /// The drawing context receiving the bitmap. + /// The source rectangle in bitmap pixels. + /// The destination rectangle in drawing coordinates. + /// The resampler used when scaling the bitmap. + /// The opacity applied to the bitmap draw. + /// The alpha composition mode used to blend the bitmap with the destination. + void Draw( + DrawingContextImpl context, + Rectangle sourceRect, + RectangleF destinationRect, + IResampler sampler, + float opacity, + PixelAlphaCompositionMode alphaCompositionMode); +} diff --git a/samples/AvaloniaControlCatalog/Icons.cs b/samples/AvaloniaControlCatalog/Icons.cs new file mode 100644 index 00000000..58d0e0b7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Icons.cs @@ -0,0 +1,194 @@ +namespace ControlCatalog +{ + internal static class Icons + { + // Layers/Composition + public const string Layers = "M12,16L19.36,10.27L21,9L12,2L3,9L4.63,10.27M12,18.54L4.62,12.81L3,14.07L12,21.07L21,14.07L19.37,12.8"; + + // Keyboard + public const string Keyboard = "M19,10H17V8H19M19,13H17V11H19M16,10H14V8H16M16,13H14V11H16M16,17H8V15H16M7,10H5V8H7M7,13H5V11H7M8,11H10V13H8M8,8H10V10H8M11,11H13V13H11M11,8H13V10H11M20,5H4C2.89,5 2,5.89 2,7V17A2,2 0 0,0 4,19H20A2,2 0 0,0 22,17V7C22,5.89 21.1,5 20,5Z"; + + // Blur/Acrylic + public const string Blur = "M14.5,10C16.43,10 18,8.43 18,6.5C18,4.57 16.43,3 14.5,3C12.57,3 11,4.57 11,6.5C11,8.43 12.57,10 14.5,10M14.5,4A2.5,2.5 0 0,1 17,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,4M8.5,14C10.43,14 12,12.43 12,10.5C12,8.57 10.43,7 8.5,7C6.57,7 5,8.57 5,10.5C5,12.43 6.57,14 8.5,14M8.5,8A2.5,2.5 0 0,1 11,10.5A2.5,2.5 0 0,1 8.5,13A2.5,2.5 0 0,1 6,10.5A2.5,2.5 0 0,1 8.5,8M14.5,16A4.5,4.5 0 0,0 10,20.5A4.5,4.5 0 0,0 14.5,25A4.5,4.5 0 0,0 19,20.5A4.5,4.5 0 0,0 14.5,16M14.5,24A3.5,3.5 0 0,1 11,20.5A3.5,3.5 0 0,1 14.5,17A3.5,3.5 0 0,1 18,20.5A3.5,3.5 0 0,1 14.5,24Z"; + + // Adorner/Sparkle + public const string Sparkle = "M12,1L9,9L1,12L9,15L12,23L15,15L23,12L15,9"; + + // Text/AutoComplete + public const string TextInput = "M20,21H4V3H20V21M6,9H14V7H6V9M6,13H18V11H6V13M6,17H14V15H6V17Z"; + + // Border/Square + public const string Border = "M4,4H20V20H4V4M6,8V6H8V8H6M6,12V10H8V12H6M6,16V14H8V16H6M6,20V18H8V20H6M10,20V18H12V20H10M14,20V18H16V20H14M18,20V18H20V20H18M18,16V14H20V16H18M18,12V10H20V12H18M18,8V6H20V8H18M14,8V6H16V8H14M10,8V6H12V8H10Z"; + + // Lightning/Cache + public const string Lightning = "M11,21H7L13,3H17L14,10H20L11,21Z"; + + // Cursor Click/Button + public const string CursorClick = "M10.76,8.69A0.76,0.76 0 0,0 10,9.45V20.9C10,21.32 10.34,21.66 10.76,21.66C10.95,21.66 11.11,21.6 11.24,21.5L13.15,19.95L14.81,23.57C14.94,23.84 15.21,24 15.5,24C15.62,24 15.74,23.97 15.85,23.92L17.28,23.26C17.69,23.07 17.86,22.59 17.66,22.17L16,18.54L18.44,18.19C18.87,18.13 19.1,17.73 18.91,17.37L12.1,8.89C11.95,8.76 11.79,8.69 10.76,8.69M15,10V8H20V10H15M13.83,4.76L16.66,1.93L18.07,3.34L15.24,6.17L13.83,4.76M10,0V5H8V0H10M3.93,14.66L6.76,11.83L8.17,13.24L5.34,16.07L3.93,14.66M3.93,3.34L5.34,1.93L8.17,4.76L6.76,6.17L3.93,3.34M7,10H2V8H7V10Z"; + + // Spinner/ButtonSpinner + public const string Spinner = "M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z"; + + // Calendar + public const string Calendar = "M19,19H5V8H19M16,1V3H8V1H6V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3H18V1M9,15H7V13H9V15M13,15H11V13H13V15M17,15H15V13H17V15M9,11H7V9H9V11M13,11H11V9H13V11M17,11H15V9H17V11Z"; + + // Canvas/Artboard + public const string Canvas = "M17.5,12A1.5,1.5 0 0,1 16,10.5A1.5,1.5 0 0,1 17.5,9A1.5,1.5 0 0,1 19,10.5A1.5,1.5 0 0,1 17.5,12M14.5,8A1.5,1.5 0 0,1 13,6.5A1.5,1.5 0 0,1 14.5,5A1.5,1.5 0 0,1 16,6.5A1.5,1.5 0 0,1 14.5,8M9.5,8A1.5,1.5 0 0,1 8,6.5A1.5,1.5 0 0,1 9.5,5A1.5,1.5 0 0,1 11,6.5A1.5,1.5 0 0,1 9.5,8M6.5,12A1.5,1.5 0 0,1 5,10.5A1.5,1.5 0 0,1 6.5,9A1.5,1.5 0 0,1 8,10.5A1.5,1.5 0 0,1 6.5,12M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A1.5,1.5 0 0,0 13.5,19.5C13.5,19.11 13.35,18.76 13.11,18.5C12.88,18.23 12.73,17.88 12.73,17.5A1.5,1.5 0 0,1 14.23,16H16A5,5 0 0,0 21,11C21,6.58 16.97,3 12,3Z"; + + // Command/Terminal + public const string Terminal = "M20,19V7H4V19H20M20,3A2,2 0 0,1 22,5V19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19V5C2,3.89 2.9,3 4,3H20M13,17V15H18V17H13M9.58,13L5.57,9H8.4L11.7,12.3C12.09,12.69 12.09,13.33 11.7,13.72L8.42,17H5.59L9.58,13Z"; + + // Slides/Carousel + public const string Slides = "M2,6H6V18H2V6M7,6H17V18H7V6M18,6H22V18H18V6Z"; + + // Checkbox + public const string Checkbox = "M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M10,17L5,12L6.41,10.58L10,14.17L17.59,6.58L19,8"; + + // Clipboard + public const string Clipboard = "M19,3H14.82C14.4,1.84 13.3,1 12,1C10.7,1 9.6,1.84 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3"; + + // Palette/Color + public const string Palette = "M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A1.5,1.5 0 0,0 13.5,19.5C13.5,19.11 13.35,18.76 13.11,18.5C12.88,18.23 12.73,17.88 12.73,17.5A1.5,1.5 0 0,1 14.23,16H16A5,5 0 0,0 21,11C21,6.58 16.97,3 12,3M6.5,12A1.5,1.5 0 0,1 5,10.5A1.5,1.5 0 0,1 6.5,9A1.5,1.5 0 0,1 8,10.5A1.5,1.5 0 0,1 6.5,12M9.5,8A1.5,1.5 0 0,1 8,6.5A1.5,1.5 0 0,1 9.5,5A1.5,1.5 0 0,1 11,6.5A1.5,1.5 0 0,1 9.5,8M14.5,8A1.5,1.5 0 0,1 13,6.5A1.5,1.5 0 0,1 14.5,5A1.5,1.5 0 0,1 16,6.5A1.5,1.5 0 0,1 14.5,8M17.5,12A1.5,1.5 0 0,1 16,10.5A1.5,1.5 0 0,1 17.5,9A1.5,1.5 0 0,1 19,10.5A1.5,1.5 0 0,1 17.5,12Z"; + + // Dropdown/ComboBox + public const string Dropdown = "M7,10L12,15L17,10H7Z"; + + // Container/Box + public const string Container = "M2,2H8V4H16V2H22V8H20V16H22V22H16V20H8V22H2V16H4V8H2V2M16,8V6H8V8H6V16H8V18H16V16H18V8H16M4,4V6H6V4H4M18,4V6H20V4H18M4,18V20H6V18H4M18,18V20H20V18H18Z"; + + // Document/Page + public const string Document = "M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"; + + // Menu/Context + public const string Menu = "M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z"; + + // Cursor/Pointer + public const string Cursor = "M13.64,21.97C13.14,22.21 12.54,22 12.31,21.5L10.13,16.76L7.62,18.78C7.45,18.92 7.24,19 7,19A1,1 0 0,1 6,18V3A1,1 0 0,1 7,2C7.24,2 7.47,2.09 7.64,2.23L7.65,2.22L19.14,11.86C19.57,12.22 19.39,12.92 18.83,13.03L14.83,13.78L17,18.5C17.24,19 17,19.59 16.5,19.83L13.64,21.97Z"; + + // Brush/Drawing + public const string Brush = "M20.71,4.63L19.37,3.29C19,2.9 18.35,2.9 17.96,3.29L9,12.25L11.75,15L20.71,6.04C21.1,5.65 21.1,5 20.71,4.63M7,14A3,3 0 0,0 4,17C4,18.31 2.84,19 2,19C2.92,20.22 4.5,21 6,21A4,4 0 0,0 10,17A3,3 0 0,0 7,14Z"; + + // Grid/Table + public const string Grid = "M10,4V8H14V4H10M16,4V8H20V4H16M16,10V14H20V10H16M16,16V20H20V16H16M14,20V16H10V20H14M8,20V16H4V20H8M8,14V10H4V14H8M8,8V4H4V8H8M10,14H14V10H10V14Z"; + + // Shield/Validation + public const string Shield = "M10,17L6,13L7.41,11.59L10,14.17L16.59,7.58L18,9M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1Z"; + + // Clock + public const string Clock = "M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.2,16.2L11,13V7H12.5V12.2L17,14.9L16.2,16.2Z"; + + // Dialog/Message + public const string Dialog = "M20,2H4A2,2 0 0,0 2,4V22L6,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2Z"; + + // DragDrop/Move + public const string DragDrop = "M13,6V11H18V7.75L22.25,12L18,16.25V13H13V18H16.25L12,22.25L7.75,18H11V13H6V16.25L1.75,12L6,7.75V11H11V6H7.75L12,1.75L16.25,6H13Z"; + + // Sidebar/Drawer + public const string Drawer = "M3,2H21A1,1 0 0,1 22,3V21A1,1 0 0,1 21,22H3A1,1 0 0,1 2,21V3A1,1 0 0,1 3,2M10,4V20H20V4H10M4,4V20H8V4H4Z"; + + // Expand + public const string Expand = "M10,21V19H6.41L10.91,14.5L9.5,13.09L5,17.59V14H3V21H10M14.5,10.91L19,6.41V10H21V3H14V5H17.59L13.09,9.5L14.5,10.91Z"; + + // Flyout/Popup + public const string Flyout = "M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M7,9H10V7H7V9M7,13H17V11H7V13M7,17H14V15H7V17Z"; + + // Target/Focus + public const string Target = "M12,8A4,4 0 0,0 8,12A4,4 0 0,0 12,16A4,4 0 0,0 16,12A4,4 0 0,0 12,8M3.05,13H1V11H3.05C3.5,6.83 6.83,3.5 11,3.05V1H13V3.05C17.17,3.5 20.5,6.83 20.95,11H23V13H20.95C20.5,17.17 17.17,20.5 13,20.95V23H11V20.95C6.83,20.5 3.5,17.17 3.05,13M12,5A7,7 0 0,0 5,12A7,7 0 0,0 12,19A7,7 0 0,0 19,12A7,7 0 0,0 12,5Z"; + + // Gesture/Touch + public const string Gesture = "M5,15A2,2 0 0,0 3,17V19H7V17A2,2 0 0,0 5,15M5,11A6,6 0 0,0 -1,17H1A4,4 0 0,1 5,13V11M5,7C0.03,7 -4,11.03 -4,16V17H-2V16C-2,12.13 1.13,9 5,9V7M20.5,9.5L12,18L8.5,14.5L7.08,15.91L12,20.84L21.92,10.91L20.5,9.5Z"; + + // Image/Photo + public const string Image = "M8.5,13.5L11,16.5L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z"; + + // Tag/Label + public const string Tag = "M5.5,7A1.5,1.5 0 0,1 4,5.5A1.5,1.5 0 0,1 5.5,4A1.5,1.5 0 0,1 7,5.5A1.5,1.5 0 0,1 5.5,7M21.41,11.58L12.41,2.58C12.05,2.22 11.55,2 11,2H4C2.89,2 2,2.89 2,4V11C2,11.55 2.22,12.05 2.59,12.41L11.58,21.41C11.95,21.77 12.45,22 13,22C13.55,22 14.05,21.77 14.41,21.41L21.41,14.41C21.78,14.05 22,13.55 22,13C22,12.45 21.77,11.95 21.41,11.58Z"; + + // Transform/Rotate + public const string Transform = "M16.89,15.5L18.31,16.89C19.21,15.73 19.76,14.39 19.93,13H17.91C17.77,13.87 17.43,14.72 16.89,15.5M13,17.9V19.92C14.39,19.75 15.74,19.21 16.9,18.31L15.46,16.87C14.71,17.41 13.87,17.76 13,17.9M19.93,11C19.76,9.61 19.21,8.27 18.31,7.11L16.89,8.53C17.43,9.28 17.77,10.13 17.91,11M15.55,5.55L11,1V5.07C7.06,5.56 4,8.92 4,12.95C4,17.18 7.32,20.56 11.46,20.93C11.64,20.95 11.82,20.97 12,20.97C12.18,20.97 12.36,20.95 12.54,20.93C12.69,20.92 12.83,20.89 13,20.87V18.9C12.82,18.92 12.65,18.95 12.47,18.95C12.32,18.96 12.16,18.97 12,18.97C8.42,18.97 5.53,16.12 5.53,12.58C5.53,9.36 7.88,6.67 11,6.15V10L15.55,5.55Z"; + + // List + public const string List = "M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z"; + + // Bell/Notification + public const string Bell = "M21,19V20H3V19L5,17V11C5,7.9 7.03,5.17 10,4.29C10,4.19 10,4.1 10,4A2,2 0 0,1 12,2A2,2 0 0,1 14,4C14,4.1 14,4.19 14,4.29C16.97,5.17 19,7.9 19,11V17L21,19M14,21A2,2 0 0,1 12,23A2,2 0 0,1 10,21"; + + // Number/Hash + public const string Number = "M5.41,21L6.12,17H2.12L2.47,15H6.47L7.53,9H3.53L3.88,7H7.88L8.59,3H10.59L9.88,7H15.88L16.59,3H18.59L17.88,7H21.88L21.53,9H17.53L16.47,15H20.47L20.12,17H16.12L15.41,21H13.41L14.12,17H8.12L7.41,21H5.41M9.47,9L8.41,15H14.41L15.47,9H9.47Z"; + + // Cube/3D + public const string Cube = "M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L5,8.09V15.91L12,19.85L19,15.91V8.09L12,4.15Z"; + + // Dots/Pagination + public const string Dots = "M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z"; + + // Info/Information + public const string Info = "M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"; + + // Progress/Loading + public const string Progress = "M13,2.03V2.05L13,4.05C17.39,4.59 20.5,8.58 19.96,12.97C19.5,16.61 16.64,19.5 13,19.93V21.93C18.5,21.38 22.5,16.5 21.95,11C21.5,6.25 17.73,2.5 13,2.03M11,2.06C9.05,2.25 7.19,3 5.67,4.26L7.1,5.74C8.22,4.84 9.57,4.26 11,4.06V2.06M4.26,5.67C3,7.19 2.25,9.04 2.05,11H4.05C4.24,9.58 4.8,8.23 5.69,7.1L4.26,5.67M2.06,13C2.26,14.96 3.03,16.81 4.27,18.33L5.69,16.9C4.81,15.77 4.24,14.42 4.06,13H2.06M7.1,18.37L5.67,19.74C7.18,21 9.04,21.79 11,22V20C9.58,19.82 8.23,19.25 7.1,18.37Z"; + + // Radio + public const string Radio = "M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7Z"; + + // Refresh + public const string Refresh = "M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"; + + // Layout/Grid + public const string Layout = "M19,11H13V5H19M19,19H13V13H19M11,11H5V5H11M11,19H5V13H11M3,3V21H21V3"; + + // Scroll + public const string Scroll = "M15,20H9V12H4.16L12,4.16L19.84,12H15V20Z"; + + // Slider/Tune + public const string Slider = "M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z"; + + // Split/SplitView + public const string Split = "M18,4H6C4.89,4 4,4.89 4,6V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18V6C20,4.89 19.1,4 18,4M18,6V18H13V6H18Z"; + + // Tab + public const string Tab = "M21,3H3C1.89,3 1,3.89 1,5V19A2,2 0 0,0 3,21H21C22.1,21 23,20.1 23,19V5C23,3.89 22.1,3 21,3M21,19H3V5H13V9H21V19Z"; + + // Theme/Palette + public const string Theme = "M12,18V6A6,6 0 0,1 18,12A6,6 0 0,1 12,18M20,15.31L23.31,12L20,8.69V4H15.31L12,0.69L8.69,4H4V8.69L0.69,12L4,15.31V20H8.69L12,23.31L15.31,20H20V15.31Z"; + + // Toggle/Switch + public const string Toggle = "M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"; + + // Tooltip/Info bubble + public const string Tooltip = "M4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4A2,2 0 0,1 4,2M11,15H13V13H11V15M11,11H13V5H11V11Z"; + + // Animation/Transition + public const string Transition = "M2,5.27L3.28,4L20,20.72L18.73,22L12.73,16H7V19L3,15L7,11V14H9.73L5,9.27V14H3V5.27M21,9L17,5V8H14V10H17V13L21,9M17,17V14H15V17H17Z"; + + // Tree/Hierarchy + public const string Tree = "M15,20A1,1 0 0,0 16,19V4H8V19A1,1 0 0,0 9,20H2V22H22V20H15Z"; + + // Viewbox/Resize + public const string Viewbox = "M5,15H3V19A2,2 0 0,0 5,21H9V19H5M5,5H9V3H5A2,2 0 0,0 3,5V9H5M19,3H15V5H19V9H21V5A2,2 0 0,0 19,3M19,19H15V21H19A2,2 0 0,0 21,19V15H19"; + + // Puzzle/Embed + public const string Puzzle = "M20.5,11H19V7C19,5.89 18.1,5 17,5H13V3.5A2.5,2.5 0 0,0 10.5,1A2.5,2.5 0 0,0 8,3.5V5H4A2,2 0 0,0 2,7V10.8H3.5C5.04,10.8 6.2,11.96 6.2,13.5C6.2,15.04 5.04,16.2 3.5,16.2H2V20A2,2 0 0,0 4,22H7.8V20.5C7.8,18.96 8.96,17.8 10.5,17.8C12.04,17.8 13.2,18.96 13.2,20.5V22H17A2,2 0 0,0 19,20V16H20.5A2.5,2.5 0 0,0 23,13.5A2.5,2.5 0 0,0 20.5,11Z"; + + // Window/Application + public const string Window = "M4,4H20V20H4V4M6,8V18H18V8H6Z"; + + // Header/Title + public const string Header = "M3,4H5V10H9V4H11V18H9V12H5V18H3V4M13,8H15.31L15.63,5H17.63L17.31,8H19.31L19.63,5H21.63L21.31,8H23V10H21.1L20.9,12H23V14H20.69L20.37,17H18.37L18.69,14H16.69L16.37,17H14.37L14.69,14H13V12H14.9L15.1,10H13V8M17.1,10L16.9,12H18.9L19.1,10H17.1Z"; + + // Monitor/Screen + public const string Monitor = "M21,16H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10V20H8V22H16V20H14V18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2Z"; + + // Navigation/Compass + public const string Navigation = "M12,2L4.5,20.29L5.21,21L12,18L18.79,21L19.5,20.29L12,2Z"; + + // OpenGL/3D Box (same as Cube but outline variant) + public const string Cube3D = "M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z"; + + // Horizontal dots/Pager + public const string HorizontalDots = "M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z"; + + // Slider tune/adjust + public const string Tune = "M3,17V19H9V17H3M3,5V7H13V5H3M13,21V19H21V17H13V15H11V21H13M7,9V11H3V13H7V15H9V9H7M21,13V11H11V13H21M15,9H17V7H21V5H17V3H15V9Z"; + } +} diff --git a/samples/AvaloniaControlCatalog/ImageSharpDrawingAppBuilderExtensions.cs b/samples/AvaloniaControlCatalog/ImageSharpDrawingAppBuilderExtensions.cs new file mode 100644 index 00000000..a4c67297 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ImageSharpDrawingAppBuilderExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Avalonia; +using Avalonia.Platform; + +namespace AvaloniaControlCatalog; + +/// +/// Provides application builder extensions for the ImageSharp.Drawing sample renderer. +/// +internal static class ImageSharpDrawingAppBuilderExtensions +{ + /// + /// Registers the ImageSharp.Drawing rendering and SixLabors.Fonts text shaping + /// subsystems with the application builder. + /// + /// The application builder. + /// The supplied . + public static AppBuilder UseImageSharpDrawing(this AppBuilder builder) + => builder + .UseRenderingSubsystem(PlatformRenderInterface.Initialize, "ImageSharp.Drawing") + .UseTextShapingSubsystem( + () => AvaloniaLocator.CurrentMutable.Bind().ToConstant(new TextShaperImpl()), + "SixLabors.Fonts"); +} diff --git a/samples/AvaloniaControlCatalog/ImageSharpExtensions.cs b/samples/AvaloniaControlCatalog/ImageSharpExtensions.cs new file mode 100644 index 00000000..1e611df7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ImageSharpExtensions.cs @@ -0,0 +1,328 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO.Compression; +using System.Numerics; +using Avalonia.Media.Imaging; +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace AvaloniaControlCatalog; + +/// +/// Conversions between Avalonia and ImageSharp primitive types. +/// +internal static class ImageSharpExtensions +{ + /// + /// Saves an ImageSharp image using the format and settings specified by Avalonia encoder options. + /// + /// The image to save. + /// The stream to which the encoded image is written. + /// The encoder options that select the output format and its settings. + public static void Save(this Image image, Stream stream, BitmapEncoderOptions options) + { + ImageEncoder encoder = options switch + { + PngBitmapEncoderOptions pngOptions => new PngEncoder + { + CompressionLevel = pngOptions.CompressionLevel switch + { + CompressionLevel.Fastest => PngCompressionLevel.BestSpeed, + CompressionLevel.NoCompression => PngCompressionLevel.NoCompression, + CompressionLevel.SmallestSize => PngCompressionLevel.BestCompression, + _ => PngCompressionLevel.DefaultCompression + } + }, + JpegBitmapEncoderOptions jpegOptions => new JpegEncoder + { + Quality = Math.Clamp(jpegOptions.Quality, 1, 100) + }, + _ => throw new ArgumentOutOfRangeException(nameof(options), options, "Unknown encoder options type") + }; + + image.Save(stream, encoder); + } + + /// + /// Converts an Avalonia colour to an ImageSharp colour with the supplied opacity applied. + /// + /// The Avalonia colour. + /// The opacity multiplier. + /// The converted ImageSharp colour. + public static Color ToColor(this AvaloniaColor color, double opacity = 1) + { + byte alpha = (byte)Math.Clamp(color.A * opacity, 0, 255); + return Color.FromPixel(new Bgra32(color.R, color.G, color.B, alpha)); + } + + /// + /// Converts ImageSharp metadata resolution to Avalonia DPI. + /// + /// The ImageSharp image metadata. + /// The converted Avalonia DPI. + public static AvaloniaVector ToDpi(this ImageMetadata metadata) + => metadata.ResolutionUnits switch + { + PixelResolutionUnit.PixelsPerCentimeter => new AvaloniaVector(metadata.HorizontalResolution * 2.54, metadata.VerticalResolution * 2.54), + PixelResolutionUnit.PixelsPerMeter => new AvaloniaVector(metadata.HorizontalResolution * 0.0254, metadata.VerticalResolution * 0.0254), + PixelResolutionUnit.PixelsPerInch => new AvaloniaVector(metadata.HorizontalResolution, metadata.VerticalResolution), + _ => new AvaloniaVector(ImageMetadata.DefaultHorizontalResolution, ImageMetadata.DefaultVerticalResolution) + }; + + /// + /// Converts an Avalonia point to an ImageSharp point. + /// + /// The Avalonia point. + /// The converted ImageSharp point. + public static PointF ToPointF(this AvaloniaPoint point) => new((float)point.X, (float)point.Y); + + /// + /// Converts an Avalonia size to an ImageSharp size. + /// + /// The Avalonia size. + /// The converted ImageSharp size. + public static SizeF ToSizeF(this AvaloniaSize size) => new((float)size.Width, (float)size.Height); + + /// + /// Converts an Avalonia rectangle to an ImageSharp rectangle. + /// + /// The Avalonia rectangle. + /// The converted ImageSharp rectangle. + public static RectangleF ToRectangleF(this AvaloniaRect rect) => new((float)rect.X, (float)rect.Y, (float)rect.Width, (float)rect.Height); + + /// + /// Converts an ImageSharp rectangle to an Avalonia rectangle. + /// + /// The ImageSharp rectangle. + /// The converted Avalonia rectangle. + public static AvaloniaRect ToAvaloniaRect(this RectangleF rect) => new(rect.X, rect.Y, rect.Width, rect.Height); + + /// + /// Converts an Avalonia fill rule to the equivalent ImageSharp intersection rule. + /// + /// The Avalonia fill rule. + /// The converted ImageSharp intersection rule. + public static IntersectionRule ToIntersectionRule(this FillRule fillRule) + => fillRule == FillRule.NonZero ? IntersectionRule.NonZero : IntersectionRule.EvenOdd; + + /// + /// Converts an Avalonia bitmap interpolation mode to the closest ImageSharp resampler. + /// + /// The Avalonia interpolation mode. + /// Whether the image is being enlarged. + /// The converted ImageSharp resampler. + public static IResampler ToResampler(this BitmapInterpolationMode interpolationMode, bool isUpscaling) + => interpolationMode switch + { + BitmapInterpolationMode.None => KnownResamplers.NearestNeighbor, + BitmapInterpolationMode.HighQuality when isUpscaling => KnownResamplers.MitchellNetravali, + _ => KnownResamplers.Triangle + }; + + /// + /// Converts an Avalonia bitmap blending mode to an ImageSharp alpha composition mode. + /// + /// The Avalonia bitmap blending mode. + /// The converted ImageSharp alpha composition mode. + public static PixelAlphaCompositionMode ToAlphaCompositionMode(this BitmapBlendingMode blendingMode) + => blendingMode switch + { + BitmapBlendingMode.Source => PixelAlphaCompositionMode.Src, + BitmapBlendingMode.SourceAtop => PixelAlphaCompositionMode.SrcAtop, + BitmapBlendingMode.SourceIn => PixelAlphaCompositionMode.SrcIn, + BitmapBlendingMode.SourceOut => PixelAlphaCompositionMode.SrcOut, + BitmapBlendingMode.Destination => PixelAlphaCompositionMode.Dest, + BitmapBlendingMode.DestinationAtop => PixelAlphaCompositionMode.DestAtop, + BitmapBlendingMode.DestinationOver => PixelAlphaCompositionMode.DestOver, + BitmapBlendingMode.DestinationIn => PixelAlphaCompositionMode.DestIn, + BitmapBlendingMode.DestinationOut => PixelAlphaCompositionMode.DestOut, + BitmapBlendingMode.Xor => PixelAlphaCompositionMode.Xor, + + // TODO: Implement missing modes. Hue etc. + _ => PixelAlphaCompositionMode.SrcOver + }; + + /// + /// Converts an Avalonia matrix to an ImageSharp-compatible matrix. + /// + /// The Avalonia matrix. + /// The converted matrix. + public static Matrix4x4 ToMatrix4x4(this AvaloniaMatrix matrix) + => new( + (float)matrix.M11, + (float)matrix.M12, + 0, + 0, + (float)matrix.M21, + (float)matrix.M22, + 0, + 0, + 0, + 0, + 1, + 0, + (float)matrix.M31, + (float)matrix.M32, + 0, + 1); + + /// + /// Converts an Avalonia rectangle to an integer ImageSharp rectangle. + /// + /// The Avalonia rectangle. + /// The converted ImageSharp rectangle. + public static Rectangle ToRectangle(this AvaloniaRect r) => (Rectangle)r.ToRectangleF(); + + /// + /// Converts an Avalonia rounded rectangle to an ImageSharp rounded rectangle polygon. + /// + /// The Avalonia rounded rectangle. + /// The converted ImageSharp rounded rectangle polygon. + public static RoundedRectanglePolygon ToRoundedRectanglePath(this AvaloniaRoundedRect rect) + => new( + rect.Rect.ToRectangleF(), + new SizeF((float)rect.RadiiTopLeft.X, (float)rect.RadiiTopLeft.Y), + new SizeF((float)rect.RadiiTopRight.X, (float)rect.RadiiTopRight.Y), + new SizeF((float)rect.RadiiBottomRight.X, (float)rect.RadiiBottomRight.Y), + new SizeF((float)rect.RadiiBottomLeft.X, (float)rect.RadiiBottomLeft.Y)); + + /// + /// Converts an Avalonia gradient spread method to an ImageSharp gradient repetition mode. + /// + /// The Avalonia spread method. + /// The converted ImageSharp repetition mode. + public static GradientRepetitionMode ToGradientRepetitionMode(this GradientSpreadMethod spreadMethod) + => spreadMethod switch + { + GradientSpreadMethod.Reflect => GradientRepetitionMode.Reflect, + GradientSpreadMethod.Repeat => GradientRepetitionMode.Repeat, + _ => GradientRepetitionMode.None + }; + + /// + /// Converts an Avalonia geometry combine mode to an ImageSharp boolean operation. + /// + /// The Avalonia combine mode. + /// The converted ImageSharp boolean operation. + public static BooleanOperation ToBooleanOperation(this GeometryCombineMode combineMode) + => combineMode switch + { + GeometryCombineMode.Intersect => BooleanOperation.Intersection, + GeometryCombineMode.Xor => BooleanOperation.Xor, + GeometryCombineMode.Exclude => BooleanOperation.Difference, + _ => BooleanOperation.Union + }; + + /// + /// Converts an Avalonia pen to ImageSharp stroke options. + /// + /// The Avalonia pen. + /// The converted ImageSharp stroke options. + public static StrokeOptions ToStrokeOptions(this IPen pen) + => new() + { + MiterLimit = pen.MiterLimit, + LineCap = pen.LineCap switch + { + PenLineCap.Round => LineCap.Round, + PenLineCap.Square => LineCap.Square, + _ => LineCap.Butt + }, + LineJoin = pen.LineJoin switch + { + PenLineJoin.Round => LineJoin.Round, + PenLineJoin.Bevel => LineJoin.Bevel, + _ => LineJoin.Miter + } + }; + + /// + /// Converts an Avalonia dash style to an ImageSharp stroke pattern. + /// + /// The Avalonia pen. + /// The converted stroke pattern. + public static float[] ToStrokePattern(this IPen pen) + { + IReadOnlyList? dashes = pen.DashStyle?.Dashes; + if (dashes is null || dashes.Count < 2) + { + return []; + } + + float[] pattern = new float[dashes.Count]; + for (int i = 0; i < pattern.Length; i++) + { + pattern[i] = (float)dashes[i]; + } + + return pattern; + } + + /// + /// Converts an Avalonia dash offset to an ImageSharp stroke pattern offset. + /// + /// The Avalonia pen. + /// The converted stroke pattern offset. + public static float ToStrokePatternOffset(this IPen pen) + => pen.DashStyle is null ? 0 : (float)pen.DashStyle.Offset; + + /// + /// Creates a stroked ImageSharp path from the supplied source path and Avalonia pen. + /// + /// The source ImageSharp path. + /// The Avalonia pen. + /// The stroked ImageSharp path. + public static IPath CreateStrokePath(this IPath path, IPen pen) + { + float[] pattern = pen.ToStrokePattern(); + StrokeOptions options = pen.ToStrokeOptions(); + + return pattern.Length >= 2 + ? path.GenerateOutline((float)pen.Thickness, pattern, pen.ToStrokePatternOffset(), options) + : path.GenerateOutline((float)pen.Thickness, options); + } + + /// + /// Converts an Avalonia pen and resolved brush to an ImageSharp pen. + /// + /// The Avalonia pen. + /// The resolved ImageSharp brush. + /// The converted ImageSharp pen. + public static Pen ToPen(this IPen pen, Brush brush) + { + float[] pattern = pen.ToStrokePattern(); + PenOptions options = new(brush, (float)pen.Thickness, pattern) + { + StrokePatternOffset = pen.ToStrokePatternOffset(), + StrokeOptions = pen.ToStrokeOptions() + }; + + return pattern.Length >= 2 ? new PatternPen(options) : new SolidPen(options); + } + + /// + /// Creates an ImageSharp font for the supplied family name and size. + /// + /// The font family name. + /// The font size. + /// The created ImageSharp font. + public static Font ToFont(this string familyName, float size) + { + if (SystemFonts.TryGet(familyName, out FontFamily family)) + { + return family.CreateFont(size); + } + + return SystemFonts.CreateFont("Segoe UI", size); + } +} diff --git a/samples/AvaloniaControlCatalog/ImmutableBitmapImpl.cs b/samples/AvaloniaControlCatalog/ImmutableBitmapImpl.cs new file mode 100644 index 00000000..e7e6716d --- /dev/null +++ b/samples/AvaloniaControlCatalog/ImmutableBitmapImpl.cs @@ -0,0 +1,274 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace AvaloniaControlCatalog; + +/// +/// ImageSharp-backed immutable bitmap implementation for the sample Avalonia renderer. +/// +internal sealed unsafe class ImmutableBitmapImpl : IDrawableBitmapImpl, IReadableBitmapImpl +{ + private readonly object sync = new(); + private readonly PixelFormat format; + private readonly AlphaFormat alphaFormat; + + /// + /// Initializes a new immutable bitmap from an encoded image stream. + /// + /// The encoded image stream. + public ImmutableBitmapImpl(Stream stream) + : this(SixLabors.ImageSharp.Image.Load(WriteableBitmapImpl.ImageDecoderOptions, stream)) + { + } + + /// + /// Initializes a new immutable bitmap by resizing an existing immutable bitmap. + /// + /// The source bitmap. + /// The destination size in pixels. + /// The interpolation mode used when scaling. + public ImmutableBitmapImpl(ImmutableBitmapImpl source, PixelSize destinationSize, BitmapInterpolationMode interpolationMode) + { + Image image = new(WriteableBitmapImpl.ImageConfiguration, destinationSize.Width, destinationSize.Height); + this.Image = image; + this.Dpi = source.Dpi; + this.PixelSize = destinationSize; + this.format = PixelFormat.Bgra8888; + this.alphaFormat = Avalonia.Platform.AlphaFormat.Premul; + + bool isUpscaling = destinationSize.Width > source.PixelSize.Width || destinationSize.Height > source.PixelSize.Height; + IResampler sampler = interpolationMode.ToResampler(isUpscaling); + Rectangle sourceRect = new(0, 0, source.PixelSize.Width, source.PixelSize.Height); + RectangleF destinationRect = new(0, 0, destinationSize.Width, destinationSize.Height); + + using DrawingContextImpl context = new(image, this.Dpi); + source.Draw(context, sourceRect, destinationRect, sampler, 1, PixelAlphaCompositionMode.Src); + } + + /// + /// Initializes a new immutable bitmap decoded to fit the specified width or height. + /// + /// The encoded image stream. + /// The target width or height. + /// Whether is a width. + /// The interpolation mode used if the decoder resizes. + public ImmutableBitmapImpl(Stream stream, int decodeSize, bool horizontal, BitmapInterpolationMode interpolationMode) + : this( + SixLabors.ImageSharp.Image.Load( + new() + { + Configuration = WriteableBitmapImpl.ImageConfiguration, + TargetSize = horizontal + ? new SixLabors.ImageSharp.Size(decodeSize, 0) + : new SixLabors.ImageSharp.Size(0, decodeSize), + Sampler = interpolationMode.ToResampler(isUpscaling: false) + }, + stream)) + { + } + + /// + /// Initializes a new immutable bitmap from a copy of external pixel data. + /// + /// The bitmap size in pixels. + /// The bitmap DPI. + /// The byte stride between rows. + /// The source pixel format. + /// The source alpha format. + /// The source pixel data. + public ImmutableBitmapImpl(PixelSize size, Vector dpi, int stride, PixelFormat format, AlphaFormat alphaFormat, IntPtr data) + : this(CloneExternalPixels(size, stride, format, alphaFormat, data), dpi, format, alphaFormat) + { + } + + /// + /// Initializes a new immutable bitmap wrapping an existing ImageSharp image. + /// + /// The ImageSharp image backing the bitmap. + public ImmutableBitmapImpl(Image image) + : this(image, image.Metadata.ToDpi()) + { + } + + /// + /// Initializes a new immutable bitmap wrapping an existing ImageSharp image. + /// + /// The ImageSharp image backing the bitmap. + /// The bitmap DPI. + public ImmutableBitmapImpl(Image image, Vector dpi) + : this(image, dpi, PixelFormat.Bgra8888, Avalonia.Platform.AlphaFormat.Premul) + { + } + + /// + /// Initializes a new immutable bitmap with its stored pixel and alpha formats. + /// + /// The ImageSharp image backing the bitmap. + /// The bitmap DPI. + /// The stored pixel format. + /// The stored alpha format. + private ImmutableBitmapImpl(Image image, Vector dpi, PixelFormat format, AlphaFormat alphaFormat) + { + this.Image = image; + this.Dpi = dpi; + this.PixelSize = new PixelSize(image.Width, image.Height); + this.format = format; + this.alphaFormat = alphaFormat; + } + + /// + /// Gets the ImageSharp pixels drawn for this bitmap. + /// + public Image Image { get; } + + /// + public Vector Dpi { get; } + + /// + public PixelSize PixelSize { get; } + + /// + public int Version { get; } = 1; + + /// + public PixelFormat? Format => this.format; + + /// + public AlphaFormat? AlphaFormat => this.alphaFormat; + + /// + public void Draw( + DrawingContextImpl context, + Rectangle sourceRect, + RectangleF destinationRect, + IResampler sampler, + float opacity, + PixelAlphaCompositionMode alphaCompositionMode) + => context.DrawImage(this.Image, sourceRect, destinationRect, sampler, opacity, alphaCompositionMode); + + /// + public ILockedFramebuffer Lock() => new BitmapFramebuffer(this); + + /// + public void Save(Stream stream, BitmapEncoderOptions options) => this.Image.Save(stream, options); + + /// + public void Dispose() => this.Image.Dispose(); + + /// + /// Copies external pixel memory into an ImageSharp image. + /// + private static Image CloneExternalPixels(PixelSize size, int stride, PixelFormat format, AlphaFormat alphaFormat, IntPtr data) + { + if (format == PixelFormats.Bgra8888) + { + return alphaFormat == Avalonia.Platform.AlphaFormat.Premul + ? CopyExternalPixels(size, stride, data) + : CopyExternalPixels(size, stride, data); + } + + if (format == PixelFormats.Rgba8888) + { + return alphaFormat == Avalonia.Platform.AlphaFormat.Premul + ? CopyExternalPixels(size, stride, data) + : CopyExternalPixels(size, stride, data); + } + + // Avalonia's Rgb565 and ImageSharp's Bgr565 share the same packed 5-6-5 byte layout. + return CopyExternalPixels(size, stride, data); + } + + /// + /// Copies external rows into owned ImageSharp storage without changing their pixel representation. + /// + /// The ImageSharp pixel type matching the supplied Avalonia format. + /// The bitmap size in pixels. + /// The signed byte stride between source rows. + /// The address of the first logical source row. + /// The owned image containing the copied bytes. + private static Image CopyExternalPixels(PixelSize size, int stride, IntPtr data) + where TPixel : unmanaged, IPixel + { + Image image = new(WriteableBitmapImpl.ImageConfiguration, size.Width, size.Height); + + for (int y = 0; y < size.Height; y++) + { + Span destinationRow = MemoryMarshal.AsBytes(image.DangerousGetPixelRowMemory(y).Span); + + // Avalonia addresses the first logical row even for bottom-up buffers. Advancing by the + // signed stride therefore preserves logical row order without reinterpreting any pixels. + ReadOnlySpan sourceRow = new((byte*)data + (y * stride), destinationRow.Length); + sourceRow.CopyTo(destinationRow); + } + + return image; + } + + /// + /// Framebuffer view over the bitmap's own contiguous ImageSharp pixel buffer. + /// + private sealed class BitmapFramebuffer : ILockedFramebuffer, IImageVisitor + { + private readonly ImmutableBitmapImpl parent; + private MemoryHandle handle; + + /// + /// Initializes a new framebuffer lock. + /// + /// The bitmap owning the pixel buffer. + public BitmapFramebuffer(ImmutableBitmapImpl parent) + { + this.parent = parent; + Monitor.Enter(parent.sync); + parent.Image.AcceptVisitor(this); + } + + /// + public void Visit(Image image) + where TPixel : unmanaged, IPixel + { + if (!image.DangerousTryGetSinglePixelMemory(out Memory memory)) + { + Monitor.Exit(this.parent.sync); + throw new InvalidOperationException("ImageSharp bitmap buffers must be contiguous to lock them as Avalonia framebuffers."); + } + + this.handle = memory.Pin(); + } + + /// + public IntPtr Address => (IntPtr)this.handle.Pointer; + + /// + public PixelSize Size => this.parent.PixelSize; + + /// + public int RowBytes => ((this.parent.format.BitsPerPixel * this.parent.PixelSize.Width) + 7) / 8; + + /// + public Vector Dpi => this.parent.Dpi; + + /// + public PixelFormat Format => this.parent.format; + + /// + public AlphaFormat AlphaFormat => this.parent.alphaFormat; + + /// + public void Dispose() + { + this.handle.Dispose(); + Monitor.Exit(this.parent.sync); + } + } +} diff --git a/samples/AvaloniaControlCatalog/LineGeometryImpl.cs b/samples/AvaloniaControlCatalog/LineGeometryImpl.cs new file mode 100644 index 00000000..2b2da6aa --- /dev/null +++ b/samples/AvaloniaControlCatalog/LineGeometryImpl.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Avalonia; +using SixLabors.ImageSharp.Drawing; + +namespace AvaloniaControlCatalog; + +/// +/// Avalonia line geometry implementation backed by an ImageSharp open path. +/// +internal sealed class LineGeometryImpl : GeometryImpl +{ + /// + /// Initializes a new line geometry. + /// + /// The line start point. + /// The line end point. + public LineGeometryImpl(Point p1, Point p2) + : base(new PathBuilder().AddLine(p1.ToPointF(), p2.ToPointF()).Build(), null, IntersectionRule.EvenOdd) + { + } +} diff --git a/samples/AvaloniaControlCatalog/MainView.xaml b/samples/AvaloniaControlCatalog/MainView.xaml new file mode 100644 index 00000000..f7b2658b --- /dev/null +++ b/samples/AvaloniaControlCatalog/MainView.xaml @@ -0,0 +1,161 @@ + + + + + + + 20 + 10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + None + BorderOnly + Full + + + + + Default + Light + Dark + + + + + Fluent + Simple + + + + + None + Transparent + Blur + AcrylicBlur + Mica + + + + + LeftToRight + RightToLeft + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/MainView.xaml.cs b/samples/AvaloniaControlCatalog/MainView.xaml.cs new file mode 100644 index 00000000..0f738320 --- /dev/null +++ b/samples/AvaloniaControlCatalog/MainView.xaml.cs @@ -0,0 +1,375 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Styling; +using Avalonia.Threading; +using Avalonia.VisualTree; +using ControlCatalog.Models; +using ControlCatalog.ViewModels; +using AvaloniaCalendar = Avalonia.Controls.Calendar; + +namespace ControlCatalog +{ + public partial class MainView : DrawerPage + { + private readonly TransparentStyles _transparentStyles = new(); + + public MainView() + { + InitializeComponent(); + + Loaded += MainView_Loaded; + Unloaded += MainView_Unloaded; + } + + private const double WideBreakpoint = 1008; + private const double NarrowBreakpoint = 640; + + protected override Type StyleKeyOverride => typeof(MainView); + + private void MainView_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (DataContext == null) + return; + + SizeChanged += OnDrawerSizeChanged; + UpdateAdaptiveLayout(); + } + + private void MainView_Unloaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + SizeChanged -= OnDrawerSizeChanged; + _lastAppliedMode = null; + } + + private SplitViewDisplayMode? _lastAppliedMode; + private bool _updatingLayout; + + private void OnDrawerSizeChanged(object? sender, SizeChangedEventArgs e) + { + if (e.WidthChanged) + UpdateAdaptiveLayout(); + } + + private void UpdateAdaptiveLayout() + { + if (_updatingLayout || DataContext == null) + return; + + var width = Bounds.Width; + if (width <= 0) + return; + + SplitViewDisplayMode targetMode; + if (width >= WideBreakpoint) + targetMode = SplitViewDisplayMode.Inline; + else if (width >= NarrowBreakpoint) + targetMode = SplitViewDisplayMode.CompactInline; + else + targetMode = SplitViewDisplayMode.Overlay; + + if (_lastAppliedMode == targetMode) + return; + + _updatingLayout = true; + try + { + _lastAppliedMode = targetMode; + ViewModel.DisplayMode = targetMode; + + if (targetMode == SplitViewDisplayMode.Inline) + ViewModel.IsDrawerOpened = true; + else if (targetMode == SplitViewDisplayMode.Overlay) + ViewModel.IsDrawerOpened = false; + } + finally + { + _updatingLayout = false; + } + } + + private void Themes_SelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (e.AddedItems.Count > 0 && e.AddedItems[0] is CatalogTheme theme) + { + App.SetCatalogThemes(theme); + } + } + + private void ThemeVariants_SelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (Application.Current is { } app && e.AddedItems.Count > 0 && e.AddedItems[0] is ThemeVariant themeVariant) + { + app.RequestedThemeVariant = themeVariant; + } + } + + private void FlowDirection_SelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (TopLevel.GetTopLevel(this) is { } topLevel && e.AddedItems.Count > 0 && e.AddedItems[0] is FlowDirection flowDirection) + { + topLevel.FlowDirection = flowDirection; + } + } + + private void Decorations_SelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (TopLevel.GetTopLevel(this) is Window window && e.AddedItems.Count > 0 && e.AddedItems[0] is WindowDecorations systemDecorations) + { + window.WindowDecorations = systemDecorations; + } + } + + private void TransparencyLevels_SelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (TopLevel.GetTopLevel(this) is { } topLevel && e.AddedItems.Count > 0 && e.AddedItems[0] is WindowTransparencyLevel transparencyLevel) + { + topLevel.TransparencyLevelHint = [transparencyLevel]; + + if (topLevel.ActualTransparencyLevel != WindowTransparencyLevel.None && + transparencyLevel != WindowTransparencyLevel.None) + { + topLevel.Background = new ImmutableSolidColorBrush(Colors.Gray, 0.2); + if (!topLevel.Styles.Contains(_transparentStyles)) + topLevel.Styles.Add(_transparentStyles); + } + else + { + topLevel.Background = null; + topLevel.Styles.Remove(_transparentStyles); + } + } + } + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + + if (ViewModel != null) + { + ViewModel.Navigator = NavPage; + } + } + + internal MainWindowViewModel ViewModel => (MainWindowViewModel)DataContext!; + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + if (DataContext == null) + return; + + UpdateAdaptiveLayout(); + + var topLevel = TopLevel.GetTopLevel(this)!; + if (topLevel is Window window) + ViewModel.SelectedDecorationIndex = (int)window.WindowDecorations; + + var insets = topLevel.InsetsManager; + if (insets != null) + { + // In real life application these events should be unsubscribed to avoid memory leaks. + ViewModel.SafeAreaPadding = insets.SafeAreaPadding; + insets.SafeAreaChanged += (sender, args) => + { + ViewModel.SafeAreaPadding = insets.SafeAreaPadding; + }; + + ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdgePreference; + ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true; + + ViewModel.PropertyChanged += async (sender, args) => + { + if (args.PropertyName == nameof(ViewModel.DisplayEdgeToEdge)) + { + insets.DisplayEdgeToEdgePreference = ViewModel.DisplayEdgeToEdge; + } + else if (args.PropertyName == nameof(ViewModel.IsSystemBarVisible)) + { + insets.IsSystemBarVisible = ViewModel.IsSystemBarVisible; + } + + // Give the OS some time to apply new values and refresh the view model. + await Task.Delay(100); + ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdgePreference; + ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true; + }; + } + + int startupPageIndex = 0; + string? startupPage = Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_PAGE"); + + if (!string.IsNullOrWhiteSpace(startupPage)) + { + for (int i = 0; i < ViewModel.Pages.Count; i++) + { + if (string.Equals(ViewModel.Pages[i].Header, startupPage, StringComparison.OrdinalIgnoreCase)) + { + startupPageIndex = i; + break; + } + } + } + + ViewModel.SelectedPageIndex = startupPageIndex; + + string? navigateToPage = Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_NAVIGATE_TO_PAGE"); + + if (!string.IsNullOrWhiteSpace(navigateToPage)) + { + // Test-only hook: changing selection after layout exercises the same dirty-region + // repaint path as clicking a navigation item in the drawer. + Dispatcher.UIThread.Post(async () => + { + await Task.Delay(350); + + string[] navigationSequence = navigateToPage.Split( + ',', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + foreach (string pageName in navigationSequence) + { + for (int i = 0; i < ViewModel.Pages.Count; i++) + { + if (string.Equals(ViewModel.Pages[i].Header, pageName, StringComparison.OrdinalIgnoreCase)) + { + ListBox? navigationList = this.GetVisualDescendants() + .OfType() + .FirstOrDefault(x => ReferenceEquals(x.ItemsSource, ViewModel.Pages)); + + Console.Error.WriteLine($"ControlCatalog navigation atTicks={Stopwatch.GetTimestamp()} page={pageName}"); + + if (navigationList is not null) + { + navigationList.SelectedIndex = i; + } + else + { + ViewModel.SelectedPageIndex = i; + } + + if (Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_KEEP_DRAWER_OPEN") == "1") + { + ViewModel.IsDrawerOpened = true; + } + + break; + } + } + + // Leave enough time for the selected page's layout and ordered flushes to + // finish before the next selection measures a distinct navigation. + await Task.Delay(1000); + } + }, DispatcherPriority.Loaded); + } + + if (double.TryParse( + Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_SCROLL_Y"), + NumberStyles.Float, + CultureInfo.InvariantCulture, + out double scrollY)) + { + // Test-only hook: scrolling after layout captures the same dirty-region path that a + // user scroll exercises, while keeping the sample's normal startup path untouched. + Dispatcher.UIThread.Post(async () => + { + await Task.Delay(250); + + ScrollViewer? scrollViewer = this.GetVisualDescendants() + .OfType() + .Where(x => x.Extent.Height > x.Viewport.Height && x.Viewport.Height > 0) + .OrderByDescending(x => x.Viewport.Width) + .ThenByDescending(x => x.Extent.Height - x.Viewport.Height) + .FirstOrDefault(); + + if (scrollViewer is not null) + { + int stepCount = int.TryParse( + Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_SCROLL_STEPS"), + out int parsedStepCount) + ? Math.Max(1, parsedStepCount) + : 1; + + for (int i = 1; i <= stepCount; i++) + { + scrollViewer.Offset = new Vector(scrollViewer.Offset.X, scrollY * i / stepCount); + await Task.Delay(80); + } + } + }, DispatcherPriority.Loaded); + } + + if (int.TryParse( + Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_CALENDAR_SCROLL_MONTHS"), + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int calendarScrollMonths)) + { + // Test-only hook: Calendar navigation invalidates the CalendarItem internals, which + // exercises a different dirty-region path than scrolling the page viewport. + Dispatcher.UIThread.Post(async () => + { + await Task.Delay(250); + + AvaloniaCalendar[] calendars = this.GetVisualDescendants() + .OfType() + .Where(x => x.IsVisible) + .ToArray(); + + int calendarStart = 0; + int calendarEnd = calendars.Length; + + if (int.TryParse( + Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_CALENDAR_SCROLL_INDEX"), + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int calendarIndex)) + { + calendarStart = calendarIndex; + calendarEnd = calendarIndex + 1; + } + + using Pointer pointer = new(Pointer.GetNextFreeId(), PointerType.Mouse, true); + PointerPointProperties properties = new(RawInputModifiers.None, PointerUpdateKind.Other); + int direction = calendarScrollMonths < 0 ? 1 : -1; + int steps = Math.Abs(calendarScrollMonths); + + for (int i = 0; i < steps; i++) + { + for (int j = calendarStart; j < calendarEnd; j++) + { + AvaloniaCalendar calendar = calendars[j]; + Point position = calendar.TranslatePoint( + new Point(calendar.Bounds.Width / 2, calendar.Bounds.Height / 2), + topLevel) ?? default; + + PointerWheelEventArgs args = new( + calendar, + pointer, + topLevel, + position, + (ulong)Environment.TickCount64, + properties, + KeyModifiers.None, + new Vector(0, direction)); + + calendar.RaiseEvent(args); + } + + await Task.Delay(80); + } + }, DispatcherPriority.Loaded); + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/MainWindow.xaml b/samples/AvaloniaControlCatalog/MainWindow.xaml new file mode 100644 index 00000000..d839689d --- /dev/null +++ b/samples/AvaloniaControlCatalog/MainWindow.xaml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/MainWindow.xaml.cs b/samples/AvaloniaControlCatalog/MainWindow.xaml.cs new file mode 100644 index 00000000..d43536a0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/MainWindow.xaml.cs @@ -0,0 +1,74 @@ +using System; +using System.Globalization; +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; + +namespace ControlCatalog +{ + public partial class MainWindow : Window + { + private NativeMenu? _recentMenu; + + public MainWindow() + { + InitializeComponent(); + + if (TryGetStartupSize(out Size startupSize)) + { + Width = startupSize.Width; + Height = startupSize.Height; + } + + _recentMenu = ((NativeMenu.GetMenu(this)?.Items[0] as NativeMenuItem)?.Menu?.Items[2] as NativeMenuItem)?.Menu; + } + + public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit"; + + public static KeyGesture MenuQuitGesture => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? + new KeyGesture(Key.Q, KeyModifiers.Meta) : + new KeyGesture(Key.F4, KeyModifiers.Alt); + + public void OnOpenClicked(object sender, EventArgs args) + { + _recentMenu?.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1))); + } + + public void OnCloseClicked(object sender, EventArgs args) + { + Close(); + } + + /// + /// Reads the optional startup window size used by automated renderer captures. + /// + /// The parsed startup size. + /// when a valid startup size was provided. + private static bool TryGetStartupSize(out Size size) + { + size = default; + string? value = Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_WINDOW_SIZE"); + + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + string[] parts = value.Split('x', 'X', ','); + if (parts.Length != 2) + { + return false; + } + + if (!double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out double width) || + !double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double height)) + { + return false; + } + + size = new Size(width, height); + return true; + } + } +} diff --git a/samples/AvaloniaControlCatalog/MiniMvvm/MiniCommand.cs b/samples/AvaloniaControlCatalog/MiniMvvm/MiniCommand.cs new file mode 100644 index 00000000..57f6d6b4 --- /dev/null +++ b/samples/AvaloniaControlCatalog/MiniMvvm/MiniCommand.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace MiniMvvm +{ + public sealed class MiniCommand : MiniCommand, ICommand + { + private readonly Action? _cb; + private readonly Func? _acb; + private bool _busy; + + public MiniCommand(Action cb) + { + _cb = cb; + } + + public MiniCommand(Func cb) + { + _acb = cb; + } + + private bool Busy + { + get => _busy; + set + { + _busy = value; + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } + } + + + public override event EventHandler? CanExecuteChanged; + + public override bool CanExecute(object? parameter) => !_busy; + + public override async void Execute(object? parameter) + { + if(Busy) + return; + try + { + Busy = true; + if (_cb != null) + _cb((T)parameter!); + else + await _acb!((T)parameter!); + } + finally + { + Busy = false; + } + } + } + + public abstract class MiniCommand : ICommand + { + public static MiniCommand Create(Action cb) => new MiniCommand(_ => cb()); + public static MiniCommand Create(Action cb) => new MiniCommand(cb); + public static MiniCommand CreateFromTask(Func cb) => new MiniCommand(_ => cb()); + + public abstract bool CanExecute(object? parameter); + public abstract void Execute(object? parameter); + public abstract event EventHandler? CanExecuteChanged; + } +} diff --git a/samples/AvaloniaControlCatalog/MiniMvvm/PropertyChangedExtensions.cs b/samples/AvaloniaControlCatalog/MiniMvvm/PropertyChangedExtensions.cs new file mode 100644 index 00000000..cc551d61 --- /dev/null +++ b/samples/AvaloniaControlCatalog/MiniMvvm/PropertyChangedExtensions.cs @@ -0,0 +1,110 @@ +using System; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; +using Avalonia.Reactive; + +namespace MiniMvvm +{ + public static class PropertyChangedExtensions + { + class PropertyObservable : IObservable + { + private readonly INotifyPropertyChanged _target; + private readonly PropertyInfo _info; + + public PropertyObservable(INotifyPropertyChanged target, PropertyInfo info) + { + _target = target; + _info = info; + } + + class Subscription : IDisposable + { + private readonly INotifyPropertyChanged _target; + private readonly PropertyInfo _info; + private readonly IObserver _observer; + + public Subscription(INotifyPropertyChanged target, PropertyInfo info, IObserver observer) + { + _target = target; + _info = info; + _observer = observer; + _target.PropertyChanged += OnPropertyChanged; + _observer.OnNext((T)_info.GetValue(_target)!); + } + + private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == _info.Name) + _observer.OnNext((T)_info.GetValue(_target)!); + } + + public void Dispose() + { + _target.PropertyChanged -= OnPropertyChanged; + _observer.OnCompleted(); + } + } + + public IDisposable Subscribe(IObserver observer) + { + return new Subscription(_target, _info, observer); + } + } + + public static IObservable WhenAnyValue(this TModel model, + Expression> expr) where TModel : INotifyPropertyChanged + { + var l = (LambdaExpression)expr; + var ma = (MemberExpression)l.Body; + var prop = (PropertyInfo)ma.Member; + return new PropertyObservable(model, prop); + } + + public static IObservable WhenAnyValue(this TModel model, + Expression> v1, + Func cb + ) where TModel : INotifyPropertyChanged + { + return model.WhenAnyValue(v1).Select(cb); + } + + public static IObservable WhenAnyValue(this TModel model, + Expression> v1, + Expression> v2, + Func cb + ) where TModel : INotifyPropertyChanged => + Observable.CombineLatest( + model.WhenAnyValue(v1), + model.WhenAnyValue(v2), + cb); + + public static IObservable> WhenAnyValue(this TModel model, + Expression> v1, + Expression> v2 + ) where TModel : INotifyPropertyChanged => + model.WhenAnyValue(v1, v2, (a1, a2) => (a1, a2)); + + public static IObservable WhenAnyValue(this TModel model, + Expression> v1, + Expression> v2, + Expression> v3, + Func cb + ) where TModel : INotifyPropertyChanged => + model.WhenAnyValue(v1) + .CombineLatest( + model.WhenAnyValue(v2), + (l, r) => (l, r)) + .CombineLatest( + model.WhenAnyValue(v3), + (t, r) => cb(t.l, t.r, r)); + + public static IObservable> WhenAnyValue(this TModel model, + Expression> v1, + Expression> v2, + Expression> v3 + ) where TModel : INotifyPropertyChanged => + model.WhenAnyValue(v1, v2, v3, (a1, a2, a3) => (a1, a2, a3)); + } +} diff --git a/samples/AvaloniaControlCatalog/MiniMvvm/ViewModelBase.cs b/samples/AvaloniaControlCatalog/MiniMvvm/ViewModelBase.cs new file mode 100644 index 00000000..1b2f3b2c --- /dev/null +++ b/samples/AvaloniaControlCatalog/MiniMvvm/ViewModelBase.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace MiniMvvm +{ + public class ViewModelBase : INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + + protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (!EqualityComparer.Default.Equals(field, value)) + { + field = value; + RaisePropertyChanged(propertyName); + return true; + } + return false; + } + + protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/samples/AvaloniaControlCatalog/Models/CatalogTheme.cs b/samples/AvaloniaControlCatalog/Models/CatalogTheme.cs new file mode 100644 index 00000000..79b3182d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Models/CatalogTheme.cs @@ -0,0 +1,8 @@ +namespace ControlCatalog.Models +{ + public enum CatalogTheme + { + Fluent, + Simple + } +} diff --git a/samples/AvaloniaControlCatalog/Models/Person.cs b/samples/AvaloniaControlCatalog/Models/Person.cs new file mode 100644 index 00000000..99bc5025 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Models/Person.cs @@ -0,0 +1,125 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using Avalonia.Media; + +namespace ControlCatalog.Models +{ + public class Person : INotifyDataErrorInfo, INotifyPropertyChanged + { + string _firstName = string.Empty; + string _lastName = string.Empty; + bool _isBanned; + private int _age; + + public string FirstName + { + get => _firstName; + set + { + _firstName = value; + if (string.IsNullOrWhiteSpace(value)) + SetError(nameof(FirstName), "First Name Required"); + else + SetError(nameof(FirstName), null); + + OnPropertyChanged(nameof(FirstName)); + } + + } + + public string LastName + { + get => _lastName; + set + { + _lastName = value; + if (string.IsNullOrWhiteSpace(value)) + SetError(nameof(LastName), "Last Name Required"); + else + SetError(nameof(LastName), null); + + OnPropertyChanged(nameof(LastName)); + } + } + + public bool IsBanned + { + get => _isBanned; + set + { + _isBanned = value; + + OnPropertyChanged(nameof(_isBanned)); + } + } + + + /// + /// Gets or sets the age of the person + /// + public int Age + { + get => _age; + set + { + _age = value; + OnPropertyChanged(nameof(Age)); + } + } + + Dictionary> _errorLookup = new Dictionary>(); + + void SetError(string propertyName, string? error) + { + if (string.IsNullOrEmpty(error)) + { + if (_errorLookup.Remove(propertyName)) + OnErrorsChanged(propertyName); + } + else + { + if (_errorLookup.TryGetValue(propertyName, out var errorList)) + { + errorList.Clear(); + errorList.Add(error!); + } + else + { + var errors = new List { error! }; + _errorLookup.Add(propertyName, errors); + } + + OnErrorsChanged(propertyName); + } + } + + public bool HasErrors => _errorLookup.Count > 0; + + public event EventHandler? ErrorsChanged; + public event PropertyChangedEventHandler? PropertyChanged; + + void OnErrorsChanged(string propertyName) + { + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); + } + void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public IEnumerable GetErrors(string? propertyName) + { + if (propertyName is { } && _errorLookup.TryGetValue(propertyName, out var errorList)) + return errorList; + else + return Array.Empty(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Models/StateData.cs b/samples/AvaloniaControlCatalog/Models/StateData.cs new file mode 100644 index 00000000..a7ea655c --- /dev/null +++ b/samples/AvaloniaControlCatalog/Models/StateData.cs @@ -0,0 +1,20 @@ +namespace ControlCatalog.Models; + +public class StateData +{ + public string Name { get; private set; } + public string Abbreviation { get; private set; } + public string Capital { get; private set; } + + public StateData(string name, string abbreviation, string capital) + { + Name = name; + Abbreviation = abbreviation; + Capital = capital; + } + + public override string ToString() + { + return Name; + } +} diff --git a/samples/AvaloniaControlCatalog/NavHeaderItem.xaml b/samples/AvaloniaControlCatalog/NavHeaderItem.xaml new file mode 100644 index 00000000..6867661e --- /dev/null +++ b/samples/AvaloniaControlCatalog/NavHeaderItem.xaml @@ -0,0 +1,101 @@ + + + + + + #99FFFFFF + #FF1F1F1F + #FF000000 + #FF171717 + + + #99000000 + #FFE6E6E6 + #FFFFFFFF + #FFF2F2F2 + + + 36 + 1 1 1 1 #2000, 0 0 1 1 #2fff + + + + Disabled + + Test + + Test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/NuGet.Config b/samples/AvaloniaControlCatalog/NuGet.Config new file mode 100644 index 00000000..026df9e6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/NuGet.Config @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/AcceleratorPage.xaml b/samples/AvaloniaControlCatalog/Pages/AcceleratorPage.xaml new file mode 100644 index 00000000..62fcf0d7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/AcceleratorPage.xaml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Accelerator Support + + + + + This is tab 1 content + + This is tab 1 content + + This is tab 1 content + + + + + This is tab 2 content + + + + + + This is tab 4 content + + + This is fab 5 content + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/AcceleratorPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/AcceleratorPage.xaml.cs new file mode 100644 index 00000000..40afacc0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/AcceleratorPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class AcceleratorPage : ContentPage + { + public AcceleratorPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/AcrylicPage.xaml b/samples/AvaloniaControlCatalog/Pages/AcrylicPage.xaml new file mode 100644 index 00000000..4a68fbfa --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/AcrylicPage.xaml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/AcrylicPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/AcrylicPage.xaml.cs new file mode 100644 index 00000000..adb5b057 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/AcrylicPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class AcrylicPage : ContentPage + { + public AcrylicPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/AdornerLayerPage.xaml b/samples/AvaloniaControlCatalog/Pages/AdornerLayerPage.xaml new file mode 100644 index 00000000..35907c7e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/AdornerLayerPage.xaml @@ -0,0 +1,76 @@ + + + + Rotation + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/AdornerLayerPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/AdornerLayerPage.xaml.cs new file mode 100644 index 00000000..5ee8d444 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/AdornerLayerPage.xaml.cs @@ -0,0 +1,34 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class AdornerLayerPage : ContentPage + { + private Control? _adorner; + + public AdornerLayerPage() + { + InitializeComponent(); + } + + private void RemoveAdorner_OnClick(object? sender, RoutedEventArgs e) + { + var adorner = AdornerLayer.GetAdorner(AdornerButton); + if (adorner is { }) + { + _adorner = adorner; + } + AdornerLayer.SetAdorner(AdornerButton, null); + } + + private void AddAdorner_OnClick(object? sender, RoutedEventArgs e) + { + if (_adorner is { }) + { + AdornerLayer.SetAdorner(AdornerButton, _adorner); + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/AvaloniaControlCatalog/Pages/AutoCompleteBoxPage.xaml new file mode 100644 index 00000000..4edb00b7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -0,0 +1,83 @@ + + + + A control into which the user can input text + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs new file mode 100644 index 00000000..1624fc56 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.LogicalTree; +using ControlCatalog.Models; + +namespace ControlCatalog.Pages +{ + public partial class AutoCompleteBoxPage : ContentPage + { + private static StateData[] BuildAllStates() + { + return new StateData[] + { + new StateData("Alabama","AL","Montgomery"), + new StateData("Alaska","AK","Juneau"), + new StateData("Arizona","AZ","Phoenix"), + new StateData("Arkansas","AR","Little Rock"), + new StateData("California","CA","Sacramento"), + new StateData("Colorado","CO","Denver"), + new StateData("Connecticut","CT","Hartford"), + new StateData("Delaware","DE","Dover"), + new StateData("Florida","FL","Tallahassee"), + new StateData("Georgia","GA","Atlanta"), + new StateData("Hawaii","HI","Honolulu"), + new StateData("Idaho","ID","Boise"), + new StateData("Illinois","IL","Springfield"), + new StateData("Indiana","IN","Indianapolis"), + new StateData("Iowa","IA","Des Moines"), + new StateData("Kansas","KS","Topeka"), + new StateData("Kentucky","KY","Frankfort"), + new StateData("Louisiana","LA","Baton Rouge"), + new StateData("Maine","ME","Augusta"), + new StateData("Maryland","MD","Annapolis"), + new StateData("Massachusetts","MA","Boston"), + new StateData("Michigan","MI","Lansing"), + new StateData("Minnesota","MN","St. Paul"), + new StateData("Mississippi","MS","Jackson"), + new StateData("Missouri","MO","Jefferson City"), + new StateData("Montana","MT","Helena"), + new StateData("Nebraska","NE","Lincoln"), + new StateData("Nevada","NV","Carson City"), + new StateData("New Hampshire","NH","Concord"), + new StateData("New Jersey","NJ","Trenton"), + new StateData("New Mexico","NM","Santa Fe"), + new StateData("New York","NY","Albany"), + new StateData("North Carolina","NC","Raleigh"), + new StateData("North Dakota","ND","Bismarck"), + new StateData("Ohio","OH","Columbus"), + new StateData("Oklahoma","OK","Oklahoma City"), + new StateData("Oregon","OR","Salem"), + new StateData("Pennsylvania","PA","Harrisburg"), + new StateData("Rhode Island","RI","Providence"), + new StateData("South Carolina","SC","Columbia"), + new StateData("South Dakota","SD","Pierre"), + new StateData("Tennessee","TN","Nashville"), + new StateData("Texas","TX","Austin"), + new StateData("Utah","UT","Salt Lake City"), + new StateData("Vermont","VT","Montpelier"), + new StateData("Virginia","VA","Richmond"), + new StateData("Washington","WA","Olympia"), + new StateData("West Virginia","WV","Charleston"), + new StateData("Wisconsin","WI","Madison"), + new StateData("Wyoming","WY","Cheyenne"), + }; + } + public StateData[] States { get; private set; } + + private static LinkedList[] BuildAllSentences() + { + return new string[] + { + "Hello world", + "No this is Patrick", + "Never gonna give you up", + "How does one patch KDE2 under FreeBSD" + } + .Select(x => new LinkedList(x.Split(' '))) + .ToArray(); + } + public LinkedList[] Sentences { get; private set; } + + public AutoCompleteBoxPage() + { + InitializeComponent(); + + States = BuildAllStates(); + Sentences = BuildAllSentences(); + + foreach (AutoCompleteBox box in GetAllAutoCompleteBox().Where(x => x.Name != "CustomAutocompleteBox")) + { + box.ItemsSource = States; + } + + var converter = new FuncMultiValueConverter(parts => + { + return String.Format("{0} ({1})", parts.ToArray()); + }); + var binding = new MultiBinding { Converter = converter }; + binding.Bindings.Add(CompiledBinding.Create(s => s.Name)); + binding.Bindings.Add(CompiledBinding.Create(s => s.Abbreviation)); + + MultiBindingBox.ValueMemberBinding = binding; + + AsyncBox.AsyncPopulator = PopulateAsync; + + CustomAutocompleteBox.ItemsSource = Sentences.SelectMany(x => x); + CustomAutocompleteBox.TextFilter = LastWordContains; + CustomAutocompleteBox.TextSelector = AppendWord; + } + private IEnumerable GetAllAutoCompleteBox() + { + return + this.GetLogicalDescendants() + .OfType(); + } + + private static bool StringContains(string str, string? query) + { + if (query == null) + return false; + return str.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; + } + private async Task> PopulateAsync(string? searchText, CancellationToken cancellationToken) + { + await Task.Delay(TimeSpan.FromSeconds(1.5), cancellationToken); + + return + States.Where(data => StringContains(data.Name, searchText) || StringContains(data.Capital, searchText)) + .ToList(); + } + + private bool LastWordContains(string? searchText, string? item) + { + var words = searchText?.Split(' ') ?? Array.Empty(); + var options = Sentences.Select(x => x.First) + .ToArray?>(); + for (var i = 0; i < words.Length; ++i) + { + var word = words[i]; + for (var j = 0; word is { } && j < options.Length; ++j) + { + if (options[i] is { } option) + { + if (i == words.Length - 1) + { + options[j] = option.Value.ToLower().Contains(word.ToLower()) ? option : null; + } + else + { + options[j] = option.Value.Equals(word, StringComparison.InvariantCultureIgnoreCase) ? option.Next : null; + } + } + } + } + + return options.Any(x => x != null && x.Value == item); + } + private string AppendWord(string? text, string? item) + { + if (item is { }) + { + string[] parts = text?.Split(' ') ?? Array.Empty(); + if (parts.Length == 0) + return item; + + parts[parts.Length - 1] = item; + return string.Join(" ", parts); + } + return string.Empty; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/BitmapCachePage.axaml b/samples/AvaloniaControlCatalog/Pages/BitmapCachePage.axaml new file mode 100644 index 00000000..4298f29e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/BitmapCachePage.axaml @@ -0,0 +1,49 @@ + + + + + + + + + + Render at scale + + Scale + + Enable clear type + + Snap to device pixels + Subpixel offset X + + + + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/BitmapCachePage.axaml.cs b/samples/AvaloniaControlCatalog/Pages/BitmapCachePage.axaml.cs new file mode 100644 index 00000000..be7cf7a7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/BitmapCachePage.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages; + +public partial class BitmapCachePage : ContentPage +{ + public BitmapCachePage() + { + InitializeComponent(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/BorderPage.xaml b/samples/AvaloniaControlCatalog/Pages/BorderPage.xaml new file mode 100644 index 00000000..297e3fcf --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/BorderPage.xaml @@ -0,0 +1,65 @@ + + + + + + + A control which decorates a child with a border and background + + + + Border + + + Background And CenterBorder + + + Background And InnerBorderEdge + + + Background And OuterBorderEdge + + + Rounded Corners + + + Rounded Corners + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/BorderPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/BorderPage.xaml.cs new file mode 100644 index 00000000..71aebf24 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/BorderPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class BorderPage : ContentPage + { + public BorderPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ButtonSpinnerPage.xaml b/samples/AvaloniaControlCatalog/Pages/ButtonSpinnerPage.xaml new file mode 100644 index 00000000..c21a772c --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ButtonSpinnerPage.xaml @@ -0,0 +1,34 @@ + + + + The ButtonSpinner control allows you to add button spinners to any element and then respond to the Spin event to manipulate that element. + + + AllowSpin + ShowButtonSpinner + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ButtonSpinnerPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ButtonSpinnerPage.xaml.cs new file mode 100644 index 00000000..28f4b200 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ButtonSpinnerPage.xaml.cs @@ -0,0 +1,49 @@ +using System; +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class ButtonSpinnerPage : ContentPage + { + public ButtonSpinnerPage() + { + InitializeComponent(); + } + + public void OnSpin(object sender, SpinEventArgs e) + { + var spinner = (ButtonSpinner)sender; + + if (spinner.Content is TextBlock txtBox) + { + int value = Array.IndexOf(_mountains, txtBox.Text); + if (e.Direction == SpinDirection.Increase) + value++; + else + value--; + + if (value < 0) + value = _mountains.Length - 1; + else if (value >= _mountains.Length) + value = 0; + + txtBox.Text = _mountains[value]; + } + + } + + private readonly string[] _mountains = new[] + { + "Everest", + "K2 (Mount Godwin Austen)", + "Kangchenjunga", + "Lhotse", + "Makalu", + "Cho Oyu", + "Dhaulagiri", + "Manaslu", + "Nanga Parbat", + "Annapurna" + }; + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ButtonsPage.xaml b/samples/AvaloniaControlCatalog/Pages/ButtonsPage.xaml new file mode 100644 index 00000000..695ae285 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ButtonsPage.xaml @@ -0,0 +1,292 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A standard button control + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A button control with multiple states: checked, unchecked or indeterminate. + + + + + + + + + + + + + + A button control that raises its Click event repeatedly when it is pressed and held. + + + + + + + + + + + + + + + + A button control that functions as a navigateable hyperlink. + + + + + + + + + + + + + + + + + + + + + + A button with an added drop-down chevron to visually indicate it has a flyout with additional actions. + + + + + + + + + + + + Disabled + + + + + + + + + A button with primary and secondary parts that can each be pressed separately. The primary part behaves like a Button and the secondary part opens a flyout. + + + + + + + + + + + + + + + + + + + + + + + + A button with primary and secondary parts that can each be pressed separately. The primary part behaves like a ToggleButton with two states and the secondary part opens a flyout. + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ButtonsPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ButtonsPage.xaml.cs new file mode 100644 index 00000000..611fc762 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ButtonsPage.xaml.cs @@ -0,0 +1,21 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class ButtonsPage : ContentPage + { + private int repeatButtonClickCount = 0; + + public ButtonsPage() + { + InitializeComponent(); + } + + public void OnRepeatButtonClick(object? sender, RoutedEventArgs args) + { + repeatButtonClickCount++; + RepeatButtonTextBlock.Text = $"Repeat Button: {repeatButtonClickCount}"; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml b/samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml new file mode 100644 index 00000000..769dbc0d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml @@ -0,0 +1,56 @@ + + + A control for selecting dates with a calendar drop-down + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml.cs new file mode 100644 index 00000000..0e4fcbd9 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml.cs @@ -0,0 +1,23 @@ +using System; +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class CalendarDatePickerPage : ContentPage + { + public CalendarDatePickerPage() + { + InitializeComponent(); + + DatePicker1.SelectedDate = DateTime.Today; + DatePicker2.SelectedDate = DateTime.Today.AddDays(10); + DatePicker3.SelectedDate = DateTime.Today.AddDays(20); + DatePicker5.SelectedDate = DateTime.Today; + + DatePicker4.TemplateApplied += (s, e) => + { + DatePicker4.BlackoutDates?.AddDatesInPast(); + }; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml b/samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml new file mode 100644 index 00000000..99727e91 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml @@ -0,0 +1,53 @@ + + + A calendar control for selecting dates + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml.cs new file mode 100644 index 00000000..d9118326 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml.cs @@ -0,0 +1,20 @@ +using System; +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class CalendarPage : ContentPage + { + public CalendarPage() + { + InitializeComponent(); + + var today = DateTime.Today; + DisplayDatesCalendar.DisplayDateStart = today.AddDays(-25); + DisplayDatesCalendar.DisplayDateEnd = today.AddDays(25); + + BlackoutDatesCalendar.BlackoutDates.AddDatesInPast(); + BlackoutDatesCalendar.BlackoutDates.Add(new CalendarDateRange(today.AddDays(6))); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CanvasPage.xaml b/samples/AvaloniaControlCatalog/Pages/CanvasPage.xaml new file mode 100644 index 00000000..a9e34218 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CanvasPage.xaml @@ -0,0 +1,38 @@ + + + A panel which lays out its children by explicit coordinates + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CanvasPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CanvasPage.xaml.cs new file mode 100644 index 00000000..b94aabca --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CanvasPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class CanvasPage : ContentPage + { + public CanvasPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselDemoPage.xaml b/samples/AvaloniaControlCatalog/Pages/CarouselDemoPage.xaml new file mode 100644 index 00000000..792441dd --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselDemoPage.xaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselDemoPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CarouselDemoPage.xaml.cs new file mode 100644 index 00000000..0d59d296 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselDemoPage.xaml.cs @@ -0,0 +1,91 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class CarouselDemoPage : ContentPage + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + // Overview + ("Overview", "First Look", + "Basic CarouselPage with three pages and page indicator.", + () => new CarouselPageFirstLookPage()), + + // Populate + ("Populate", "Data Templates", + "Bind CarouselPage to an ObservableCollection, add or remove pages at runtime, and switch the page template.", + () => new CarouselPageDataTemplatePage()), + + // Appearance + ("Appearance", "Customization", + "Switch slide direction between horizontal and vertical with PageSlide. Page indicator dots update on each selection.", + () => new CarouselPageCustomizationPage()), + + // Features + ("Features", "Page Transitions", + "Animate page switches with CrossFade or PageSlide.", + () => new CarouselPageTransitionsPage()), + ("Features", "Programmatic Selection", + "Jump to any page programmatically with SelectedIndex and respond to SelectionChanged events.", + () => new CarouselPageSelectionPage()), + ("Features", "Gesture & Keyboard", + "Swipe left/right to navigate pages. Toggle IsGestureEnabled and IsKeyboardNavigationEnabled.", + () => new CarouselPageGesturePage()), + ("Features", "Events", + "SelectionChanged, NavigatedTo, and NavigatedFrom events. Swipe or navigate to see the live event log.", + () => new CarouselPageEventsPage()), + + // Performance + ("Performance", "Performance Monitor", + "Track page count, live page instances, and managed heap size. Observe how GC reclaims memory after removing pages.", + () => new CarouselPagePerformancePage()), + + // Showcases + ("Showcases", "Sanctuary", + "Travel discovery app with 3 full-screen immersive pages. Each page has a real background photo, gradient overlay, and themed content. Built as a 1:1 replica of a Stitch design.", + () => new SanctuaryShowcasePage()), + ("Showcases", "Care Companion", + "Healthcare onboarding with CarouselPage (3 pages), then a TabbedPage patient dashboard. Skip or complete onboarding to navigate to the dashboard via RemovePage.", + () => new CareCompanionAppPage()), + + // Carousel (ItemsControl) demos + ("Carousel", "Getting Started", + "Basic Carousel with image items and previous/next navigation buttons.", + () => new CarouselGettingStartedPage()), + ("Carousel", "Transitions", + "Configure page transitions: PageSlide, CrossFade, 3D Rotation, or None.", + () => new CarouselTransitionsPage()), + ("Carousel", "Customization", + "Adjust orientation and transition type to tailor the carousel layout.", + () => new CarouselCustomizationPage()), + ("Carousel", "Gestures & Keyboard", + "Navigate items via swipe gesture and arrow keys. Toggle each input mode on and off.", + () => new CarouselGesturesPage()), + ("Carousel", "Vertical Orientation", + "Carousel with Orientation set to Vertical, navigated with Up/Down keys, swipe, or buttons.", + () => new CarouselVerticalPage()), + ("Carousel", "Multi-Item Peek", + "Adjust ViewportFraction to show multiple items simultaneously with adjacent cards peeking.", + () => new CarouselMultiItemPage()), + ("Carousel", "Data Binding", + "Bind Carousel to an ObservableCollection and add, remove, or shuffle items at runtime.", + () => new CarouselDataBindingPage()), + ("Carousel", "Curated Gallery", + "Editorial art gallery app with DrawerPage navigation, hero Carousel with PipsPager dots, and a horizontal peek carousel for collection highlights.", + () => new CarouselGalleryAppPage()), + }; + + public CarouselDemoPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselPage.xaml b/samples/AvaloniaControlCatalog/Pages/CarouselPage.xaml new file mode 100644 index 00000000..bdb40b90 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselPage.xaml @@ -0,0 +1,119 @@ + + + A swipeable items control that can reveal adjacent pages with ViewportFraction. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Transition + + None + Page Slide + Cross Fade + Rotate 3D + Card Stack + Wave Reveal + Composite (Slide + Fade) + + + Orientation + + Horizontal + Vertical + + + Viewport Fraction + + + + 1.00 + + + + + + + + + Wrap Selection + Swipe Enabled + + + + + + + + Total Items: + 0 + + + Selected Index: + 0 + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CarouselPage.xaml.cs new file mode 100644 index 00000000..c4a6ffa1 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselPage.xaml.cs @@ -0,0 +1,149 @@ +using System; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using ControlCatalog.Pages.Transitions; + +namespace ControlCatalog.Pages +{ + public partial class CarouselPage : ContentPage + { + 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), axis); + break; + case 2: + carousel.PageTransition = new CrossFade(TimeSpan.FromSeconds(0.25)); + break; + case 3: + 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; + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml b/samples/AvaloniaControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml new file mode 100644 index 00000000..a11a1ea6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml.cs new file mode 100644 index 00000000..16f1ba0d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml.cs @@ -0,0 +1,1069 @@ +using System; +using System.Collections.ObjectModel; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Styling; +using AvaCarouselPage = Avalonia.Controls.CarouselPage; + +namespace ControlCatalog.Pages; + +public partial class CareCompanionAppPage : UserControl +{ + static readonly Color Primary = Color.Parse("#137fec"); + static readonly Color PrimaryDark = Color.Parse("#0a5bb5"); + static readonly Color PrimaryLight = Color.Parse("#e0f0ff"); + static readonly Color BgLight = Color.Parse("#f6f7f8"); + static readonly Color TextDark = Color.Parse("#111827"); + static readonly Color TextMuted = Color.Parse("#64748b"); + static readonly Color CardBg = Colors.White; + static readonly Color SuccessGreen = Color.Parse("#10b981"); + static readonly Color WarningAmber = Color.Parse("#f59e0b"); + + NavigationPage? _navPage; + AvaCarouselPage? _onboarding; + ScrollViewer? _infoPanel; + + public CareCompanionAppPage() + { + InitializeComponent(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + _infoPanel = this.FindControl("InfoPanel"); + UpdateInfoVisibility(); + + _navPage = this.FindControl("NavPage"); + if (_navPage == null) return; + + _onboarding = BuildOnboardingCarousel(); + _ = _navPage.PushAsync(_onboarding); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoVisibility(); + } + + void UpdateInfoVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 650; + } + + static TextBlock Txt(string text, double size, FontWeight weight, Color color, + double opacity = 1, TextAlignment align = TextAlignment.Left, + TextWrapping wrap = TextWrapping.NoWrap) + => new TextBlock + { + Text = text, + FontSize = size, + FontWeight = weight, + Foreground = new SolidColorBrush(color), + Opacity = opacity, + TextAlignment = align, + TextWrapping = wrap, + }; + + static Button StyledButton(object content, IBrush bg, IBrush fg, double height, + CornerRadius radius, Thickness margin = default, double fontSize = 14, + FontWeight fontWeight = FontWeight.SemiBold, + IBrush? border = null, Thickness borderThick = default) + { + var btn = new Button + { + Content = content, + Background = bg, + Foreground = fg, + Height = height, + CornerRadius = radius, + Margin = margin, + Padding = new Thickness(16, 0), + HorizontalContentAlignment = HorizontalAlignment.Center, + VerticalContentAlignment = VerticalAlignment.Center, + BorderBrush = border, + BorderThickness = borderThick, + }; + + var over = new Style(x => x.OfType + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml.cs new file mode 100644 index 00000000..05a4097a --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class SanctuaryMainPage : UserControl +{ + public SanctuaryMainPage() + { + InitializeComponent(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml b/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml new file mode 100644 index 00000000..50864e5e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml @@ -0,0 +1,335 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml.cs new file mode 100644 index 00000000..be913706 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml.cs @@ -0,0 +1,70 @@ +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages; + +public partial class SanctuaryShowcasePage : UserControl +{ + public SanctuaryShowcasePage() + { + InitializeComponent(); + } + + private void OnPage1CTA(object? sender, RoutedEventArgs e) + { + DemoCarousel.SelectedIndex = 1; + } + + private void OnPage2CTA(object? sender, RoutedEventArgs e) + { + DemoCarousel.SelectedIndex = 2; + } + + private async void OnPage3CTA(object? sender, RoutedEventArgs e) + { + var nav = this.FindAncestorOfType(); + if (nav == null) + return; + + var carouselWrapper = nav.NavigationStack.LastOrDefault(); + + var headerGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("*, Auto") }; + headerGrid.Children.Add(new TextBlock + { + Text = "Sanctuary", + VerticalAlignment = VerticalAlignment.Center + }); + var closeIcon = Geometry.Parse( + "M4.397 4.397a1 1 0 0 1 1.414 0L12 10.585l6.19-6.188a1 1 0 0 1 1.414 1.414L13.413 12l6.19 6.189a1 1 0 0 1-1.414 1.414L12 13.413l-6.189 6.19a1 1 0 0 1-1.414-1.414L10.585 12 4.397 5.811a1 1 0 0 1 0-1.414z"); + var closeBtn = new Button + { + Content = new PathIcon { Data = closeIcon }, + Background = Brushes.Transparent, + BorderThickness = new Thickness(0), + Padding = new Thickness(8, 4), + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(closeBtn, 1); + headerGrid.Children.Add(closeBtn); + closeBtn.Click += async (_, _) => await nav.PopAsync(null); + + var mainPage = new ContentPage + { + Header = headerGrid, + Content = new SanctuaryMainPage() + }; + NavigationPage.SetHasBackButton(mainPage, false); + + await nav.PushAsync(mainPage); + + if (carouselWrapper != null) + { + nav.RemovePage(carouselWrapper); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CheckBoxPage.xaml b/samples/AvaloniaControlCatalog/Pages/CheckBoxPage.xaml new file mode 100644 index 00000000..4f9c5944 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CheckBoxPage.xaml @@ -0,0 +1,29 @@ + + + A check box control + + + + _Unchecked + _Checked + _Indeterminate + Disabled + + + Three State: Unchecked + Three State: Checked + Three State: Indeterminate + Three State: Disabled + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CheckBoxPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CheckBoxPage.xaml.cs new file mode 100644 index 00000000..b63d7084 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CheckBoxPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class CheckBoxPage : ContentPage + { + public CheckBoxPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ClipboardPage.xaml b/samples/AvaloniaControlCatalog/Pages/ClipboardPage.xaml new file mode 100644 index 00000000..86e99b93 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ClipboardPage.xaml @@ -0,0 +1,31 @@ + + + Example of ClipboardPage capabilities + + + + + + + + + + + + Precise dirty rects + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CompositionPage.axaml.cs b/samples/AvaloniaControlCatalog/Pages/CompositionPage.axaml.cs new file mode 100644 index 00000000..8f5d75f4 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CompositionPage.axaml.cs @@ -0,0 +1,357 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Threading; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.VisualTree; +using Math = System.Math; + +namespace ControlCatalog.Pages; + +public partial class CompositionPage : TabbedPage +{ + private ImplicitAnimationCollection? _implicitAnimations; + private CompositionCustomVisual? _customVisual; + private CompositionSolidColorVisual? _solidVisual; + + public CompositionPage() + { + InitializeComponent(); + AttachAnimatedSolidVisual(SolidVisualHost); + AttachCustomVisual(CustomVisualHost); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + Items.ItemsSource = CreateColorItems(); + } + + private static List CreateColorItems() + { + var list = new List(); + + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 185, 0))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 231, 72, 86))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 120, 215))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 153, 188))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 122, 117, 116))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 118, 118, 118))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 141, 0))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 232, 17, 35))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 99, 177))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 45, 125, 154))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 93, 90, 88))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 76, 74, 72))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 247, 99, 12))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 234, 0, 94))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 142, 140, 216))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 183, 195))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 104, 118, 138))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 105, 121, 126))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 202, 80, 16))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 195, 0, 82))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 107, 105, 214))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 3, 131, 135))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 81, 92, 107))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 74, 84, 89))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 218, 59, 1))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 227, 0, 140))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 135, 100, 184))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 178, 148))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 86, 124, 115))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 100, 124, 100))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 239, 105, 80))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 191, 0, 119))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 116, 77, 169))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 1, 133, 116))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 72, 104, 96))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 82, 94, 84))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 209, 52, 56))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 194, 57, 179))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 177, 70, 194))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 204, 106))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 73, 130, 5))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 132, 117, 69))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 67, 67))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 154, 0, 137))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 136, 23, 152))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 16, 137, 62))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 16, 124, 16))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 126, 115, 95))); + + return list; + } + + private void EnsureImplicitAnimations() + { + if (_implicitAnimations == null) + { + var compositor = ElementComposition.GetElementVisual(this)!.Compositor; + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(400); + + var rotationAnimation = compositor.CreateScalarKeyFrameAnimation(); + rotationAnimation.Target = "RotationAngle"; + rotationAnimation.InsertKeyFrame(.5f, 0.160f); + rotationAnimation.InsertKeyFrame(1f, 0f); + rotationAnimation.Duration = TimeSpan.FromMilliseconds(400); + + var animationGroup = compositor.CreateAnimationGroup(); + animationGroup.Add(offsetAnimation); + animationGroup.Add(rotationAnimation); + + _implicitAnimations = compositor.CreateImplicitAnimationCollection(); + _implicitAnimations["Offset"] = animationGroup; + } + } + + public static void SetEnableAnimations(Border border, bool value) + { + var page = border.FindAncestorOfType(); + if (page == null) + { + border.AttachedToVisualTree += delegate { SetEnableAnimations(border, true); }; + return; + } + + if (ElementComposition.GetElementVisual(page) == null) + return; + + page.EnsureImplicitAnimations(); + if (border.GetVisualParent() is Visual visualParent + && ElementComposition.GetElementVisual(visualParent) is CompositionVisual compositionVisual) + { + compositionVisual.ImplicitAnimations = page._implicitAnimations; + } + } + + void AttachAnimatedSolidVisual(Visual v) + { + void Update() + { + if(_solidVisual == null) + return; + _solidVisual.Size = new (v.Bounds.Width / 3, v.Bounds.Height / 3); + _solidVisual.Offset = new (v.Bounds.Width / 3, v.Bounds.Height / 3, 0); + } + v.AttachedToVisualTree += delegate + { + var compositor = ElementComposition.GetElementVisual(v)?.Compositor; + if(compositor == null || _solidVisual?.Compositor == compositor) + return; + _solidVisual = compositor.CreateSolidColorVisual(); + ElementComposition.SetElementChildVisual(v, _solidVisual); + _solidVisual.Color = Colors.Red; + var animation = _solidVisual.Compositor.CreateColorKeyFrameAnimation(); + animation.InsertKeyFrame(0, Colors.Red); + animation.InsertKeyFrame(0.5f, Colors.Blue); + animation.InsertKeyFrame(1, Colors.Green); + animation.Duration = TimeSpan.FromSeconds(5); + animation.IterationBehavior = AnimationIterationBehavior.Forever; + animation.Direction = PlaybackDirection.Alternate; + _solidVisual.StartAnimation("Color", animation); + + _solidVisual.AnchorPoint = new (0, 0); + + var scale = _solidVisual.Compositor.CreateVector3KeyFrameAnimation(); + scale.Duration = TimeSpan.FromSeconds(5); + scale.IterationBehavior = AnimationIterationBehavior.Forever; + scale.InsertKeyFrame(0, new Vector3(1, 1, 0)); + scale.InsertKeyFrame(0.5f, new Vector3(1.5f, 1.5f, 0)); + scale.InsertKeyFrame(1, new Vector3(1, 1, 0)); + + _solidVisual.StartAnimation("Scale", scale); + + var center = + _solidVisual.Compositor.CreateExpressionAnimation( + "Vector3(this.Target.Size.X * 0.5, this.Target.Size.Y * 0.5, 1)"); + _solidVisual.StartAnimation("CenterPoint", center); + Update(); + }; + v.PropertyChanged += (_, a) => + { + if (a.Property == BoundsProperty) + Update(); + }; + } + + void AttachCustomVisual(Visual v) + { + void Update() + { + if (_customVisual == null) + return; + var h = (float)Math.Min(v.Bounds.Height, v.Bounds.Width / 3); + _customVisual.Size = new (v.Bounds.Width, h); + _customVisual.Offset = new (0, (v.Bounds.Height - h) / 2, 0); + } + v.AttachedToVisualTree += delegate + { + var compositor = ElementComposition.GetElementVisual(v)?.Compositor; + if(compositor == null || _customVisual?.Compositor == compositor) + return; + _customVisual = compositor.CreateCustomVisual(new CustomVisualHandler()); + ElementComposition.SetElementChildVisual(v, _customVisual); + _customVisual.SendHandlerMessage(CustomVisualHandler.StartMessage); + PreciseDirtyRectsCheckboxCustomVisualChanged(this, new()); + Update(); + }; + + v.PropertyChanged += (_, a) => + { + if (a.Property == BoundsProperty) + Update(); + }; + } + + class CustomVisualHandler : CompositionCustomVisualHandler + { + private TimeSpan _animationElapsed; + private TimeSpan? _lastServerTime; + private bool _running; + private bool _preciseDirtyRects; + + public static readonly object StopMessage = new(), + StartMessage = new(), + UsePreciseDirtyRects = new(), + UseNonPreciseDirtyRects = new(); + + private List<(Point center, double size, ImmutableSolidColorBrush brush)> _ellipses = new(); + + void UpdateRects() + { + if (_running) + { + if (_lastServerTime.HasValue) _animationElapsed += (CompositionNow - _lastServerTime.Value); + _lastServerTime = CompositionNow; + } + + _ellipses.Clear(); + + const int cnt = 20; + var maxPointSizeX = EffectiveSize.X / (cnt * 1.6); + var maxPointSizeY = EffectiveSize.Y / 4; + var pointSize = Math.Min(maxPointSizeX, maxPointSizeY); + var animationLength = TimeSpan.FromSeconds(4); + var animationStage = _animationElapsed.TotalSeconds / animationLength.TotalSeconds; + + var sinOffset = Math.Cos(_animationElapsed.TotalSeconds) * 1.5; + + for (var c = 0; c < cnt; c++) + { + var stage = (animationStage + (double)c / cnt) % 1; + var colorStage = + (animationStage + (Math.Sin(_animationElapsed.TotalSeconds * 2) + 1) / 2 + (double)c / cnt) % 1; + var posX = (EffectiveSize.X + pointSize * 3) * stage - pointSize; + var posY = (EffectiveSize.Y - pointSize) * (1 + Math.Sin(stage * 3.14 * 3 + sinOffset)) / 2 + pointSize / 2; + var opacity = Math.Sin(stage * 3.14); + + _ellipses.Add((new Point(posX, posY), pointSize / 2, new ImmutableSolidColorBrush(Color.FromArgb( + 255, + (byte)(255 - 255 * colorStage), + (byte)(255 * Math.Abs(0.5 - colorStage) * 2), + (byte)(255 * colorStage) + ), opacity))); + } + } + + public override void OnRender(ImmediateDrawingContext drawingContext) + { + if (_ellipses.Count == 0) + UpdateRects(); + + foreach(var e in _ellipses) + drawingContext.DrawEllipse(e.brush, null, e.center, e.size, e.size); + } + + public override void OnMessage(object message) + { + if (message == StartMessage) + { + _running = true; + _lastServerTime = null; + RegisterForNextAnimationFrameUpdate(); + } + else if (message == StopMessage) + _running = false; + else if (message == UsePreciseDirtyRects) + _preciseDirtyRects = true; + else if (message == UseNonPreciseDirtyRects) + _preciseDirtyRects = false; + } + + void InvalidateCurrentEllipseRects() + { + foreach (var e in _ellipses) + Invalidate(new Rect(e.center.X - e.size, e.center.Y - e.size, e.size * 2, e.size * 2)); + } + + public override void OnAnimationFrameUpdate() + { + if (_running) + { + if (_preciseDirtyRects) + InvalidateCurrentEllipseRects(); + else + Invalidate(); + UpdateRects(); + if(_preciseDirtyRects) + InvalidateCurrentEllipseRects(); + RegisterForNextAnimationFrameUpdate(); + } + } + } + + private void ButtonThreadSleep(object? sender, RoutedEventArgs e) + { + Thread.Sleep(10000); + } + + private void ButtonStartCustomVisual(object? sender, RoutedEventArgs e) + { + _customVisual?.SendHandlerMessage(CustomVisualHandler.StartMessage); + } + + private void ButtonStopCustomVisual(object? sender, RoutedEventArgs e) + { + _customVisual?.SendHandlerMessage(CustomVisualHandler.StopMessage); + } + + private void PreciseDirtyRectsCheckboxCustomVisualChanged(object sender, RoutedEventArgs e) + { + _customVisual?.SendHandlerMessage(PreciseDirtyRectsCheckboxCustomVisual?.IsChecked == true + ? CustomVisualHandler.UsePreciseDirtyRects + : CustomVisualHandler.UseNonPreciseDirtyRects); + } +} + +public class CompositionPageColorItem +{ + public Color Color { get; private set; } + + public SolidColorBrush ColorBrush + { + get { return new SolidColorBrush(Color); } + } + + public String ColorHexValue + { + get { return Color.ToString().Substring(3).ToUpperInvariant(); } + } + + public CompositionPageColorItem(Color color) + { + Color = color; + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ConnectedAnimationDemoPage.xaml b/samples/AvaloniaControlCatalog/Pages/ConnectedAnimationDemoPage.xaml new file mode 100644 index 00000000..ca4a85af --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ConnectedAnimationDemoPage.xaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ConnectedAnimationDemoPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ConnectedAnimationDemoPage.xaml.cs new file mode 100644 index 00000000..5d666730 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ConnectedAnimationDemoPage.xaml.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class ConnectedAnimationDemoPage : ContentPage + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = []; + + public ConnectedAnimationDemoPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(CreateHomePage(), null); + } + + private ContentPage CreateHomePage() + { + var stack = new StackPanel + { + Margin = new Avalonia.Thickness(12), + Spacing = 16 + }; + + var groups = new Dictionary(); + var groupOrder = new List(); + + foreach (var (group, title, description, factory) in Demos) + { + if (!groups.ContainsKey(group)) + { + groups[group] = new WrapPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Left + }; + groupOrder.Add(group); + } + + var demoFactory = factory; + var demoTitle = title; + + var card = new Button + { + Width = 200, + MinHeight = 80, + Margin = new Avalonia.Thickness(0, 0, 8, 8), + VerticalAlignment = VerticalAlignment.Top, + HorizontalContentAlignment = HorizontalAlignment.Left, + VerticalContentAlignment = VerticalAlignment.Top, + Padding = new Avalonia.Thickness(12, 8), + Content = new StackPanel + { + Spacing = 4, + Children = + { + new TextBlock + { + Text = title, + FontSize = 13, + FontWeight = FontWeight.SemiBold, + TextWrapping = TextWrapping.Wrap + }, + new TextBlock + { + Text = description, + FontSize = 11, + Opacity = 0.6, + TextWrapping = TextWrapping.Wrap + } + } + } + }; + + card.Click += async (s, ev) => + { + var headerGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("*, Auto") }; + headerGrid.Children.Add(new TextBlock + { + Text = demoTitle, + VerticalAlignment = VerticalAlignment.Center + }); + var closeBtn = new Button + { + Content = new PathIcon + { + Data = Geometry.Parse("M4.397 4.397a1 1 0 0 1 1.414 0L12 10.585l6.19-6.188a1 1 0 0 1 1.414 1.414L13.413 12l6.19 6.189a1 1 0 0 1-1.414 1.414L12 13.413l-6.189 6.19a1 1 0 0 1-1.414-1.414L10.585 12 4.397 5.811a1 1 0 0 1 0-1.414z") + }, + Background = Brushes.Transparent, + BorderThickness = new Avalonia.Thickness(0), + Padding = new Avalonia.Thickness(8, 4), + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(closeBtn, 1); + headerGrid.Children.Add(closeBtn); + closeBtn.Click += async (_, _) => await SampleNav.PopAsync(null); + + var page = new ContentPage + { + Header = headerGrid, + Content = demoFactory(), + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + NavigationPage.SetHasBackButton(page, false); + await SampleNav.PushAsync(page, null); + }; + + groups[group].Children.Add(card); + } + + foreach (var groupName in groupOrder) + { + stack.Children.Add(new TextBlock + { + Text = groupName, + FontSize = 13, + FontWeight = FontWeight.SemiBold, + Margin = new Avalonia.Thickness(0, 0, 0, 4), + Opacity = 0.6 + }); + stack.Children.Add(groups[groupName]); + } + + var homePage = new ContentPage + { + Content = new ScrollViewer { Content = stack }, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + + NavigationPage.SetHasNavigationBar(homePage, false); + return homePage; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ContainerQueryPage.xaml b/samples/AvaloniaControlCatalog/Pages/ContainerQueryPage.xaml new file mode 100644 index 00000000..96f7a875 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ContainerQueryPage.xaml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ContainerQueryPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ContainerQueryPage.xaml.cs new file mode 100644 index 00000000..b4372300 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ContainerQueryPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class ContainerQueryPage : ContentPage + { + public ContainerQueryPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ContentDemoPage.xaml b/samples/AvaloniaControlCatalog/Pages/ContentDemoPage.xaml new file mode 100644 index 00000000..bcf0ca67 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ContentDemoPage.xaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ContentDemoPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ContentDemoPage.xaml.cs new file mode 100644 index 00000000..b47dde07 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ContentDemoPage.xaml.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class ContentDemoPage : ContentPage + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + // Overview + ("Overview", "First Look", "Basic ContentPage with header, content, and background inside a NavigationPage.", () => new ContentPageFirstLookPage()), + + // Appearance + ("Appearance", "Customization", "Adjust content alignment, background color, and padding to understand how ContentPage adapts its layout.", () => new ContentPageCustomizationPage()), + + // Features + ("Features", "CommandBar", "Attach a CommandBar to the top or bottom of a ContentPage. Add and remove items at runtime.", () => new ContentPageCommandBarPage()), + ("Features", "Safe Area", "Understand how AutomaticallyApplySafeAreaPadding absorbs platform insets.", () => new ContentPageSafeAreaPage()), + ("Features", "Events", "Observe page lifecycle events: NavigatedTo, NavigatedFrom, and Navigating.", () => new ContentPageEventsPage()), + + // Performance + ("Performance", "Performance Monitor", "Push ContentPages of varying weight (50 KB to 2 MB) and observe live instance count and managed heap. Pop pages and force GC to confirm memory is released.", () => new ContentPagePerformancePage()), + }; + + public ContentDemoPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(CreateHomePage(), null); + } + + private ContentPage CreateHomePage() + { + var stack = new StackPanel + { + Margin = new Avalonia.Thickness(12), + Spacing = 16 + }; + + var groups = new Dictionary(); + var groupOrder = new List(); + + foreach (var (group, title, description, factory) in Demos) + { + if (!groups.ContainsKey(group)) + { + groups[group] = new WrapPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Left + }; + groupOrder.Add(group); + } + + var demoFactory = factory; + var demoTitle = title; + + var card = new Button + { + Width = 170, + MinHeight = 80, + Margin = new Avalonia.Thickness(0, 0, 8, 8), + VerticalAlignment = VerticalAlignment.Top, + HorizontalContentAlignment = HorizontalAlignment.Left, + VerticalContentAlignment = VerticalAlignment.Top, + Padding = new Avalonia.Thickness(12, 8), + Content = new StackPanel + { + Spacing = 4, + Children = + { + new TextBlock + { + Text = title, + FontSize = 13, + FontWeight = FontWeight.SemiBold, + TextWrapping = TextWrapping.Wrap + }, + new TextBlock + { + Text = description, + FontSize = 11, + Opacity = 0.6, + TextWrapping = TextWrapping.Wrap + } + } + } + }; + + card.Click += async (s, e) => + { + var headerGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("*, Auto") }; + headerGrid.Children.Add(new TextBlock + { + Text = demoTitle, + VerticalAlignment = VerticalAlignment.Center + }); + var closeBtn = new Button + { + Content = new PathIcon + { + Data = Geometry.Parse("M4.397 4.397a1 1 0 0 1 1.414 0L12 10.585l6.19-6.188a1 1 0 0 1 1.414 1.414L13.413 12l6.19 6.189a1 1 0 0 1-1.414 1.414L12 13.413l-6.189 6.19a1 1 0 0 1-1.414-1.414L10.585 12 4.397 5.811a1 1 0 0 1 0-1.414z") + }, + Background = Brushes.Transparent, + BorderThickness = new Avalonia.Thickness(0), + Padding = new Avalonia.Thickness(8, 4), + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(closeBtn, 1); + headerGrid.Children.Add(closeBtn); + closeBtn.Click += async (_, _) => await SampleNav.PopAsync(null); + + var page = new ContentPage + { + Header = headerGrid, + Content = demoFactory(), + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + NavigationPage.SetHasBackButton(page, false); + await SampleNav.PushAsync(page, null); + }; + + groups[group].Children.Add(card); + } + + foreach (var groupName in groupOrder) + { + stack.Children.Add(new TextBlock + { + Text = groupName, + FontSize = 13, + FontWeight = FontWeight.SemiBold, + Margin = new Avalonia.Thickness(0, 0, 0, 4), + Opacity = 0.6 + }); + stack.Children.Add(groups[groupName]); + } + + var homePage = new ContentPage + { + Content = new ScrollViewer { Content = stack }, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + + NavigationPage.SetHasNavigationBar(homePage, false); + + return homePage; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ContentPage/ContentPageCommandBarPage.xaml b/samples/AvaloniaControlCatalog/Pages/ContentPage/ContentPageCommandBarPage.xaml new file mode 100644 index 00000000..0a753d25 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ContentPage/ContentPageCommandBarPage.xaml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Hello world + + + Inner context flyout + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ContextFlyoutPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ContextFlyoutPage.xaml.cs new file mode 100644 index 00000000..f25b9341 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ContextFlyoutPage.xaml.cs @@ -0,0 +1,59 @@ +using System; +using System.ComponentModel; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class ContextFlyoutPage : ContentPage + { + public ContextFlyoutPage() + { + InitializeComponent(); + DataContext = new ContextPageViewModel(); + } + + private ContextPageViewModel? _model; + protected override void OnDataContextChanged(EventArgs e) + { + if (_model != null) + _model.View = null; + _model = DataContext as ContextPageViewModel; + if (_model != null) + _model.View = this; + + base.OnDataContextChanged(e); + } + + private void ContextFlyoutPage_Closing(object? sender, CancelEventArgs e) + { + e.Cancel = CancelCloseCheckBox?.IsChecked ?? false; + } + + private void ContextFlyoutPage_Opening(object? sender, EventArgs e) + { + if (e is CancelEventArgs cancelArgs) + { + cancelArgs.Cancel = CancelCloseCheckBox?.IsChecked ?? false; + } + } + + private void CloseFlyout(object? sender, RoutedEventArgs e) + { + TextBox.ContextFlyout?.Hide(); + } + + public void CustomContextRequested(object? sender, ContextRequestedEventArgs e) + { + if (sender is Border border && border.Child is TextBlock textBlock) + { + textBlock.Text = e.TryGetPosition(border, out var point) + ? $"Context was requested with pointer at: {point.X:N0}, {point.Y:N0}" + : "Context was requested without pointer"; + e.Handled = true; + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ContextMenuPage.xaml b/samples/AvaloniaControlCatalog/Pages/ContextMenuPage.xaml new file mode 100644 index 00000000..7e2c1a22 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ContextMenuPage.xaml @@ -0,0 +1,100 @@ + + + A right click menu that can be applied to any control. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ContextMenuPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ContextMenuPage.xaml.cs new file mode 100644 index 00000000..86ce8071 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ContextMenuPage.xaml.cs @@ -0,0 +1,51 @@ +using System; +using System.ComponentModel; +using Avalonia.Controls; +using Avalonia.Input; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class ContextMenuPage : ContentPage + { + public ContextMenuPage() + { + InitializeComponent(); + DataContext = new ContextPageViewModel(); + } + + private ContextPageViewModel? _model; + protected override void OnDataContextChanged(EventArgs e) + { + if (_model != null) + _model.View = null; + _model = DataContext as ContextPageViewModel; + if (_model != null) + _model.View = this; + + base.OnDataContextChanged(e); + } + + private void ContextFlyoutPage_Closing(object? sender, CancelEventArgs e) + { + e.Cancel = CancelCloseCheckBox?.IsChecked ?? false; + } + + private void ContextFlyoutPage_Opening(object? sender, CancelEventArgs e) + { + e.Cancel = CancelOpenCheckBox?.IsChecked ?? false; + } + + public void CustomContextRequested(object? sender, ContextRequestedEventArgs e) + { + if (sender is Border border && border.Child is TextBlock textBlock) + { + textBlock.Text = e.TryGetPosition(border, out var point) + ? $"Context was requested with pointer at: {point.X:N0}, {point.Y:N0}" + : "Context was requested without pointer"; + e.Handled = true; + } + + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CursorPage.xaml b/samples/AvaloniaControlCatalog/Pages/CursorPage.xaml new file mode 100644 index 00000000..ae20193c --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CursorPage.xaml @@ -0,0 +1,31 @@ + + + + Defines a cursor (mouse pointer) + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CursorPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/CursorPage.xaml.cs new file mode 100644 index 00000000..88699ac6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CursorPage.xaml.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class CursorPage : ContentPage + { + public CursorPage() + { + InitializeComponent(); + DataContext = new CursorPageViewModel(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/CustomDrawing.xaml b/samples/AvaloniaControlCatalog/Pages/CustomDrawing.xaml new file mode 100644 index 00000000..f1cb2dab --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/CustomDrawing.xaml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DialogsPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DialogsPage.xaml.cs new file mode 100644 index 00000000..616e3d4f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DialogsPage.xaml.cs @@ -0,0 +1,519 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Security; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Dialogs; +using Avalonia.Layout; +using Avalonia.Platform.Storage; + +namespace ControlCatalog.Pages +{ + public partial class DialogsPage : ContentPage + { + public DialogsPage() + { + InitializeComponent(); + + IStorageFolder? lastSelectedDirectory = null; + IStorageItem? lastSelectedItem = null; + bool ignoreTextChanged = false; + + var results = PickerLastResults; + var resultsVisible = PickerLastResultsVisible; + var bookmarkContainer = BookmarkContainer; + var openedFileContent = OpenedFileContent; + var openMultiple = OpenMultiple; + var currentFolderBox = CurrentFolderBox; + var useSuggestedFilter = UseSuggestedFilter; + var suggestedFilterSelector = SuggestedFilterSelector; + + currentFolderBox.TextChanged += async (sender, args) => + { + if (ignoreTextChanged) return; + + if (Enum.TryParse(currentFolderBox.Text, true, out var folderEnum)) + { + lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolderAsync(folderEnum); + } + else if (!string.IsNullOrWhiteSpace(currentFolderBox.Text)) + { + if (!Uri.TryCreate(currentFolderBox.Text, UriKind.Absolute, out var folderLink)) + { + Uri.TryCreate("file://" + currentFolderBox.Text, UriKind.Absolute, out folderLink); + } + + if (folderLink is not null) + { + try + { + lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPathAsync(folderLink); + } + catch (SecurityException) + { + + } + } + } + }; + + + List? BuildFileTypes() + { + var selectedItem = (FilterSelector.SelectedItem as ComboBoxItem)?.Content + ?? "None"; + + var binLogType = new FilePickerFileType("Binary Log") + { + Patterns = new[] { "*.binlog", "*.buildlog" }, + MimeTypes = new[] { "application/binlog", "application/buildlog" }, + AppleUniformTypeIdentifiers = new[] { "public.data" } + }; + + return selectedItem switch + { + "All + TXT + BinLog" => new List + { + FilePickerFileTypes.All, FilePickerFileTypes.TextPlain, binLogType + }, + "Binlog" => new List { binLogType }, + "TXT extension only" => new List + { + new("TXT") { Patterns = FilePickerFileTypes.TextPlain.Patterns } + }, + "TXT mime only" => new List + { + new("TXT") { MimeTypes = FilePickerFileTypes.TextPlain.MimeTypes } + }, + "TXT apple type id only" => new List + { + new("TXT") + { + AppleUniformTypeIdentifiers = + FilePickerFileTypes.TextPlain.AppleUniformTypeIdentifiers + } + }, + _ => null + }; + } + + List? GetFileTypes() + { + var types = BuildFileTypes(); + UpdateSuggestedFilterSelector(types); + return types; + } + + void UpdateSuggestedFilterSelector(IReadOnlyList? types) + { + var previouslySelected = (suggestedFilterSelector.SelectedItem as ComboBoxItem)?.Tag as FilePickerFileType; + suggestedFilterSelector.Items.Clear(); + suggestedFilterSelector.Items.Add(new ComboBoxItem { Content = "First filter", Tag = null }); + + var desiredIndex = 0; + if (types is { Count: > 0 }) + { + for (var i = 0; i < types.Count; i++) + { + var type = types[i]; + var item = new ComboBoxItem { Content = type.Name, Tag = type }; + suggestedFilterSelector.Items.Add(item); + + if (previouslySelected is not null && ReferenceEquals(previouslySelected, type)) + { + desiredIndex = i + 1; + } + } + } + + suggestedFilterSelector.SelectedIndex = desiredIndex; + } + + FilePickerFileType? GetSuggestedFileType(IReadOnlyList? types) + { + if (useSuggestedFilter.IsChecked == true && types is { Count: > 0 }) + { + if (suggestedFilterSelector.SelectedItem is ComboBoxItem { Tag: FilePickerFileType selectedType } + && types.Any(t => ReferenceEquals(t, selectedType))) + { + return selectedType; + } + + return types.FirstOrDefault(); + } + + return null; + } + + void UpdateSuggestedFilterSelectorState() => + suggestedFilterSelector.IsEnabled = useSuggestedFilter.IsChecked == true; + + useSuggestedFilter.IsCheckedChanged += (_, _) => UpdateSuggestedFilterSelectorState(); + UpdateSuggestedFilterSelectorState(); + + FilterSelector.SelectionChanged += (_, _) => UpdateSuggestedFilterSelector(BuildFileTypes()); + UpdateSuggestedFilterSelector(BuildFileTypes()); + + DecoratedWindow.Click += delegate + { + new DecoratedWindow().Show(); + }; + DecoratedWindowDialog.Click += delegate + { + _ = new DecoratedWindow().ShowDialog(GetWindow()); + }; + Dialog.Click += delegate + { + var window = CreateSampleWindow(); + window.Height = 200; + _ = window.ShowDialog(GetWindow()); + }; + DialogNoTaskbar.Click += delegate + { + var window = CreateSampleWindow(); + window.Height = 200; + window.ShowInTaskbar = false; + _ = window.ShowDialog(GetWindow()); + }; + OwnedWindow.Click += delegate + { + var window = CreateSampleWindow(); + + window.Show(GetWindow()); + }; + + OwnedWindowNoTaskbar.Click += delegate + { + var window = CreateSampleWindow(); + + window.ShowInTaskbar = false; + + window.Show(GetWindow()); + }; + + OpenFilePicker.Click += async delegate + { + var fileTypes = GetFileTypes(); + var result = await GetStorageProvider().OpenFilePickerAsync(new FilePickerOpenOptions() + { + Title = "Open file", + FileTypeFilter = fileTypes, + SuggestedFileType = GetSuggestedFileType(fileTypes), + SuggestedFileName = "FileName", + SuggestedStartLocation = lastSelectedDirectory, + AllowMultiple = openMultiple.IsChecked == true + }); + + await SetPickerResult(result); + }; + SaveFilePicker.Click += async delegate + { + var fileTypes = GetFileTypes(); + var suggestedType = GetSuggestedFileType(fileTypes); + var file = await GetStorageProvider().SaveFilePickerAsync(new FilePickerSaveOptions() + { + Title = "Save file", + FileTypeChoices = fileTypes, + SuggestedFileType = suggestedType, + SuggestedStartLocation = lastSelectedDirectory, + SuggestedFileName = "FileName", + ShowOverwritePrompt = true + }); + + if (file is not null) + { + try + { + // Sync disposal of StreamWriter is not supported on WASM + await using var stream = await file.OpenWriteAsync(); + await using var writer = new System.IO.StreamWriter(stream); + await writer.WriteLineAsync(openedFileContent.Text); + + SetFolder(await file.GetParentAsync()); + } + catch (Exception ex) + { + openedFileContent.Text = ex.ToString(); + } + } + + await SetPickerResult(file is null ? null : new[] { file }); + }; + SaveFilePickerWithResult.Click += async delegate + { + var saveFileTypes = new[] { FilePickerFileTypes.Json, FilePickerFileTypes.Xml }; + var result = await GetStorageProvider().SaveFilePickerWithResultAsync(new FilePickerSaveOptions() + { + Title = "Save file", + FileTypeChoices = saveFileTypes, + SuggestedFileType = GetSuggestedFileType(saveFileTypes), + SuggestedStartLocation = lastSelectedDirectory, + SuggestedFileName = "FileName", + ShowOverwritePrompt = true + }); + + try + { + if (result.File is { } file) + { + // Sync disposal of StreamWriter is not supported on WASM + await using var stream = await file.OpenWriteAsync(); + await using var writer = new System.IO.StreamWriter(stream); + if (result.SelectedFileType == FilePickerFileTypes.Xml) + { + await writer.WriteLineAsync("Test"); + } + else + { + await writer.WriteLineAsync("""{ "sample": "Test" }"""); + } + + SetFolder(await result.File.GetParentAsync()); + } + } + catch (Exception ex) + { + openedFileContent.Text = ex.ToString(); + } + + await SetPickerResult(result.File is null ? null : new[] { result.File }, result.SelectedFileType); + }; + OpenFolderPicker.Click += async delegate + { + var folders = await GetStorageProvider().OpenFolderPickerAsync(new FolderPickerOpenOptions() + { + Title = "Folder file", + SuggestedStartLocation = lastSelectedDirectory, + SuggestedFileName = "FileName", + AllowMultiple = openMultiple.IsChecked == true + }); + + await SetPickerResult(folders); + }; + OpenFileFromBookmark.Click += async delegate + { + var file = bookmarkContainer.Text is not null + ? await GetStorageProvider().OpenFileBookmarkAsync(bookmarkContainer.Text) + : null; + + await SetPickerResult(file is null ? null : new[] { file }); + }; + OpenFolderFromBookmark.Click += async delegate + { + var folder = bookmarkContainer.Text is not null + ? await GetStorageProvider().OpenFolderBookmarkAsync(bookmarkContainer.Text) + : null; + + await SetPickerResult(folder is null ? null : new[] { folder }); + }; + + LaunchUri.Click += async delegate + { + var statusBlock = LaunchStatus; + if (Uri.TryCreate(UriToLaunch.Text, UriKind.Absolute, out var uri)) + { + var result = await TopLevel.GetTopLevel(this)!.Launcher.LaunchUriAsync(uri); + statusBlock.Text = "LaunchUriAsync returned " + result; + } + else + { + statusBlock.Text = "Can't parse the Uri"; + } + }; + + LaunchFile.Click += async delegate + { + var statusBlock = LaunchStatus; + if (lastSelectedItem is not null) + { + var result = await TopLevel.GetTopLevel(this)!.Launcher.LaunchFileAsync(lastSelectedItem); + statusBlock.Text = "LaunchFileAsync returned " + result; + } + else + { + statusBlock.Text = "Please select any file or folder first"; + } + }; + + void SetFolder(IStorageFolder? folder) + { + ignoreTextChanged = true; + lastSelectedDirectory = folder; + lastSelectedItem = folder; + currentFolderBox.Text = folder?.Path is { IsAbsoluteUri: true } abs ? abs.LocalPath : folder?.Path?.ToString(); + ignoreTextChanged = false; + } + async Task SetPickerResult(IReadOnlyCollection? items, FilePickerFileType? selectedType = null) + { + items ??= Array.Empty(); + bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmarkAsync() : "Can't bookmark"; + var mappedResults = new List(); + + string resultText = ""; + if (items.FirstOrDefault() is IStorageItem item) + { + resultText += item is IStorageFile ? "File:" : "Folder:"; + resultText += Environment.NewLine; + + var props = await item.GetBasicPropertiesAsync(); + resultText += @$"Size: {props.Size} + DateCreated: {props.DateCreated} + DateModified: {props.DateModified} + CanBookmark: {item.CanBookmark} + "; + if (item is IStorageFile file) + { + resultText += @$" + Content: + "; + + try + { + resultText += await ReadTextFromFile(file, 500); + } + catch (Exception ex) + { + resultText += ex.ToString(); + } + } + + if (item is IStorageFolder storageFolder) + { + SetFolder(storageFolder); + } + else + { + var parent = await item.GetParentAsync(); + SetFolder(parent); + if (parent is not null) + { + mappedResults.Add(FullPathOrName(parent)); + } + } + + foreach (var selectedItem in items) + { + mappedResults.Add("+> " + FullPathOrName(selectedItem)); + if (selectedItem is IStorageFolder folder) + { + await foreach (var innerItem in folder.GetItemsAsync()) + { + mappedResults.Add("++> " + FullPathOrName(innerItem)); + } + } + } + lastSelectedItem = item; + } + + if (selectedType is not null) + { + resultText += Environment.NewLine + "Selected type: " + selectedType.Name; + } + + openedFileContent.Text = resultText; + results.ItemsSource = mappedResults; + resultsVisible.IsVisible = mappedResults.Any(); + } + } + + internal static async Task ReadTextFromFile(IStorageFile file, int length) + { + await using var stream = await file.OpenReadAsync(); + using var reader = new System.IO.StreamReader(stream); + + // 4GB file test, shouldn't load more than 10000 chars into a memory. + var buffer = ArrayPool.Shared.Rent(length); + try + { + var charsRead = await reader.ReadAsync(buffer, 0, length); + return new string(buffer, 0, charsRead); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + var openedFileContent = OpenedFileContent; + try + { + var storageProvider = GetStorageProvider(); + openedFileContent.Text = $@"CanOpen: {storageProvider.CanOpen} +CanSave: {storageProvider.CanSave} +CanPickFolder: {storageProvider.CanPickFolder}"; + } + catch (Exception ex) + { + openedFileContent.Text = "Storage provider is not available: " + ex.Message; + } + } + + private Window CreateSampleWindow() + { + Button button; + Button dialogButton; + + var window = new Window + { + Height = 200, + Width = 200, + Content = new StackPanel + { + Spacing = 4, + Children = + { + new TextBlock { Text = "Hello world!" }, + (button = new Button + { + HorizontalAlignment = HorizontalAlignment.Center, + Content = "Click to close", + IsDefault = true + }), + (dialogButton = new Button + { + HorizontalAlignment = HorizontalAlignment.Center, + Content = "Dialog", + IsDefault = false + }) + } + }, + WindowStartupLocation = WindowStartupLocation.CenterOwner + }; + + button.Click += (_, __) => window.Close(); + dialogButton.Click += (_, __) => + { + var dialog = CreateSampleWindow(); + dialog.Height = 200; + dialog.ShowDialog(window); + }; + + return window; + } + + private IStorageProvider GetStorageProvider() + { + var forceManaged = ForceManaged.IsChecked ?? false; + return forceManaged + ? new ManagedStorageProvider(GetWindow()) // NOTE: In your production App use 'AppBuilder.UseManagedSystemDialogs()' + : GetTopLevel().StorageProvider; + } + + private static string FullPathOrName(IStorageItem? item) + { + if (item is null) return "(null)"; + return item.Path is { IsAbsoluteUri: true } path ? path.ToString() : item.Name; + } + + Window GetWindow() => TopLevel.GetTopLevel(this) as Window ?? throw new NullReferenceException("Invalid Owner"); + TopLevel GetTopLevel() => TopLevel.GetTopLevel(this) ?? throw new NullReferenceException("Invalid Owner"); + } +#pragma warning restore CS0618 // Type or member is obsolete +} diff --git a/samples/AvaloniaControlCatalog/Pages/DragAndDropPage.xaml b/samples/AvaloniaControlCatalog/Pages/DragAndDropPage.xaml new file mode 100644 index 00000000..7300f8c0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DragAndDropPage.xaml @@ -0,0 +1,61 @@ + + + + + + + + Example of Drag+Drop capabilities + + + + + Drag Me (text) + + + Drag Me (files) + + + Drag Me (bitmap) + + + Drag Me (custom) + + + + + + Drop some text, files, bitmap or custom format here (Copy) + + + Drop some text or custom format here (Move) + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DragAndDropPage.xaml.cs new file mode 100644 index 00000000..d9fe1af1 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DragAndDropPage.xaml.cs @@ -0,0 +1,174 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +using Avalonia.Platform.Storage; + +namespace ControlCatalog.Pages +{ + public partial class DragAndDropPage : ContentPage + { + private readonly DataFormat _customFormat = + DataFormat.CreateStringApplicationFormat("xxx-avalonia-controlcatalog-custom"); + + public DragAndDropPage() + { + InitializeComponent(); + + int textCount = 0; + + SetupDnd( + "Text", + d => d.Add(DataTransferItem.Create(DataFormat.Text, $"Text was dragged {++textCount} times")), + DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link); + + SetupDnd( + "Custom", + d => d.Add(DataTransferItem.Create(_customFormat, "Test123")), + DragDropEffects.Copy | DragDropEffects.Move); + + SetupDnd( + "Files", + async d => + { + var currentFile = Environment.ProcessPath; + + if (File.Exists(currentFile) && + TopLevel.GetTopLevel(this) is { } topLevel && + await topLevel.StorageProvider.TryGetFileFromPathAsync(currentFile) is { } storageFile) + { + d.Add(DataTransferItem.Create(DataFormat.File, storageFile)); + } + }, + DragDropEffects.Copy); + + SetupDnd( + "Bitmap", + d => d.Add(DataTransferItem.Create(DataFormat.Bitmap, new Bitmap(AssetLoader.Open(new Uri("avares://ControlCatalog/Assets/image1.jpg"))))), + DragDropEffects.Copy); + + AddHandlers(CopyTarget, DragDropEffects.Copy); + AddHandlers(MoveTarget, DragDropEffects.Move); + } + + private void SetupDnd(string suffix, Action factory, DragDropEffects effects) => + SetupDnd( + suffix, + o => + { + factory(o); + return Task.CompletedTask; + }, + effects); + + private void SetupDnd(string suffix, Func factory, DragDropEffects effects) + { + var dragMe = this.Get("DragMe" + suffix); + var dragState = this.Get("DragState" + suffix); + + async void DoDrag(object? sender, PointerPressedEventArgs e) + { + var dragData = new DataTransfer(); + await factory(dragData); + + var result = await DragDrop.DoDragDropAsync(e, dragData, effects); + switch (result) + { + case DragDropEffects.Move: + dragState.Text = "Data was moved"; + break; + case DragDropEffects.Copy: + dragState.Text = "Data was copied"; + break; + case DragDropEffects.Link: + dragState.Text = "Data was linked"; + break; + case DragDropEffects.None: + dragState.Text = "The drag operation was canceled"; + break; + default: + dragState.Text = "Unknown result"; + break; + } + } + + dragMe.PointerPressed += DoDrag; + } + + private void AddHandlers(Control target, DragDropEffects allowedEffects) + { + DragDrop.AddDragEnterHandler(target, DragOver); + DragDrop.AddDragOverHandler(target, DragOver); + DragDrop.AddDropHandler(target, Drop); + + void DragOver(object? sender, DragEventArgs e) + { + e.DragEffects &= allowedEffects; + + // Only allow if the dragged data contains text or filenames. + if (!e.DataTransfer.Contains(DataFormat.Text) + && !e.DataTransfer.Contains(DataFormat.File) + && !e.DataTransfer.Contains(DataFormat.Bitmap) + && !e.DataTransfer.Contains(_customFormat)) + e.DragEffects = DragDropEffects.None; + } + + async void Drop(object? sender, DragEventArgs e) + { + e.DragEffects &= allowedEffects; + + if (e.DragEffects == DragDropEffects.None) + return; + + if (e.DataTransfer.Contains(DataFormat.Text)) + { + DropState.Content = e.DataTransfer.TryGetText(); + } + else if (e.DataTransfer.Contains(DataFormat.File)) + { + var files = e.DataTransfer.TryGetFiles() ?? []; + var contentStr = ""; + + foreach (var item in files) + { + if (item is IStorageFile file) + { + var content = await DialogsPage.ReadTextFromFile(file, 500); + contentStr += $"File {item.Name}:{Environment.NewLine}{content}{Environment.NewLine}{Environment.NewLine}"; + } + else if (item is IStorageFolder folder) + { + var childrenCount = 0; + await foreach (var _ in folder.GetItemsAsync()) + { + childrenCount++; + } + contentStr += $"Folder {item.Name}: items {childrenCount}{Environment.NewLine}{Environment.NewLine}"; + } + } + DropState.Content = contentStr; + } + else if (e.DataTransfer.Contains(DataFormat.Bitmap)) + { + var bitmap = e.DataTransfer.TryGetValue(DataFormat.Bitmap); + DropState.Content = new Image + { + Source = bitmap, Width = 400, Height = 300, Stretch = Stretch.Uniform + }; + } + else if (e.DataTransfer.Contains(_customFormat)) + { + DropState.Content = "Custom: " + e.DataTransfer.TryGetValue(_customFormat); + } + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerDemoPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerDemoPage.xaml new file mode 100644 index 00000000..f1edd0bc --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerDemoPage.xaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerDemoPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerDemoPage.xaml.cs new file mode 100644 index 00000000..27eb93ff --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerDemoPage.xaml.cs @@ -0,0 +1,76 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class DrawerDemoPage : ContentPage + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + // Overview + ("Overview", "First Look", "Basic DrawerPage with a navigation drawer, menu items, and detail content.", + () => new DrawerPageFirstLookPage()), + + // Features + ("Features", "Navigation", + "Master-detail pattern: select a drawer menu item to navigate the detail via a NavigationPage with hamburger-to-back-button transition.", + () => new DrawerPageNavigationPage()), + ("Features", "Compact Rail", + "CompactOverlay and CompactInline layout modes: a narrow icon rail is always visible and expands on open. Adjust rail width and open pane width.", + () => new DrawerPageCompactPage()), + ("Features", "Breakpoint", + "DrawerBreakpointLength: below the threshold the drawer switches to Overlay mode automatically. Resize the window or adjust the slider to see the layout switch in real time.", + () => new DrawerPageBreakpointPage()), + ("Features", "Events", + "Opened, Closing, and Closed drawer events plus NavigatedTo and NavigatedFrom page lifecycle events. Enable 'Cancel next close' to prevent the drawer from closing.", + () => new DrawerPageEventsPage()), + ("Features", "RTL Layout", "Right-to-left layout: drawer opens from the right edge with mirrored gestures.", + () => new DrawerPageRtlPage()), + + // Appearance + ("Appearance", "Customization", + "Customize drawer behavior, layout mode, length, colors, header and footer.", + () => new DrawerPageCustomizationPage()), + ("Appearance", "Custom Flyout", + "Dark overlay menu with staggered item animations and CrossFade page transitions on the detail NavigationPage.", + () => new DrawerPageCustomFlyoutPage()), + ("Appearance", "Transitions", + "Configure the detail NavigationPage transition. Choose CrossFade, PageSlide, or CompositePageTransition to animate detail page changes.", + () => new DrawerPageTransitionsPage()), + + // Performance + ("Performance", "Performance Monitor", + "Track detail page swaps, live page instances, and managed heap size. Observe how GC reclaims memory after swapping pages.", + () => new DrawerPagePerformancePage()), + + // Showcases + ("Showcases", "AvaloniaFlix", + "Streaming app with DrawerPage wrapping NavigationPage. Hamburger auto-injected at root, back arrow on detail, and dark themed flyout menu.", + () => new AvaloniaFlixAppPage()), + ("Showcases", "L'Avenir Restaurant", + "Restaurant app with DrawerPage as the root container, NavigationPage for detail navigation, and TabbedPage bottom tabs for Menu, Reservations, and Profile.", + () => new LAvenirAppPage()), + ("Showcases", "EcoTracker", + "Sustainability tracker with CompactInline drawer, eco leaf hamburger icon, crossfade compact/open menu transitions, and green-themed Home, Stats, Habits, and Community pages.", + () => new EcoTrackerAppPage()), + ("Showcases", "ModernApp", + "Travel social app using a top-placement DrawerPage. A slide-down nav pane gives access to Discover, My Trips, Profile, and Settings. Features destination cards, story circles, an experience feed, a stats profile, and a travel gallery.", + () => new ModernAppPage()), + ("Showcases", "Controls Gallery App", + "Controls gallery app using DrawerPage CompactInline mode. Dark Fluent palette, accent pill selection indicator, search box that fades in when open, expandable category groups, and Settings pinned to the footer.", + () => new ControlsGalleryAppPage()), + }; + + public DrawerDemoPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml new file mode 100644 index 00000000..e0ce3ca0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml @@ -0,0 +1,400 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml.cs new file mode 100644 index 00000000..6c771f30 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml.cs @@ -0,0 +1,464 @@ +using System; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class ControlsGalleryAppPage : UserControl +{ + static readonly Color Accent = Color.Parse("#60CDFF"); + static readonly Color ContentBg = Color.Parse("#141414"); + static readonly Color CardBg = Color.Parse("#1F1F1F"); + static readonly Color BorderCol = Color.Parse("#2EFFFFFF"); + static readonly Color TextCol = Color.Parse("#FFFFFF"); + static readonly Color TextSec = Color.Parse("#C8FFFFFF"); + static readonly Color TextMuted = Color.Parse("#80FFFFFF"); + + DrawerPage? _drawer; + NavigationPage? _detailNav; + Button? _selectedBtn; + TextBox? _searchBox; + ContentPage? _preSearchPage; + bool _isSearching; + + public ControlsGalleryAppPage() + { + InitializeComponent(); + + _drawer = this.FindControl("NavDrawer"); + _detailNav = this.FindControl("DetailNav"); + _selectedBtn = this.FindControl + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml.cs new file mode 100644 index 00000000..0da2eb2e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml.cs @@ -0,0 +1,84 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageBreakpointPage : UserControl + { + private bool _isLoaded; + + public DrawerPageBreakpointPage() + { + InitializeComponent(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + _isLoaded = true; + DemoDrawer.PropertyChanged += OnDrawerPropertyChanged; + UpdateStatus(); + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + DemoDrawer.PropertyChanged -= OnDrawerPropertyChanged; + } + + private void OnDrawerPropertyChanged(object? sender, Avalonia.AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == DrawerPage.BoundsProperty) + UpdateStatus(); + } + + private void OnBreakpointChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + if (!_isLoaded) + return; + var value = (int)e.NewValue; + DemoDrawer.DrawerBreakpointLength = value; + BreakpointText.Text = value.ToString(); + UpdateStatus(); + } + + private void OnLayoutChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLayoutBehavior = LayoutCombo.SelectedIndex switch + { + 0 => DrawerLayoutBehavior.Split, + 1 => DrawerLayoutBehavior.CompactInline, + 2 => DrawerLayoutBehavior.CompactOverlay, + _ => DrawerLayoutBehavior.Split + }; + UpdateStatus(); + } + + private void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (!_isLoaded || sender is not Button button) + return; + var item = button.Tag?.ToString() ?? "Home"; + DetailTitleText.Text = item; + DetailPage.Header = item; + if (DemoDrawer.DrawerLayoutBehavior != DrawerLayoutBehavior.Split) + DemoDrawer.IsOpen = false; + } + + private void UpdateStatus() + { + var isVertical = DemoDrawer.DrawerPlacement == DrawerPlacement.Top || + DemoDrawer.DrawerPlacement == DrawerPlacement.Bottom; + var length = isVertical ? DemoDrawer.Bounds.Height : DemoDrawer.Bounds.Width; + var breakpoint = DemoDrawer.DrawerBreakpointLength; + WidthText.Text = $"{(isVertical ? "Height" : "Width")}: {(int)length} px"; + var isOverlay = breakpoint > 0 && length > 0 && length < breakpoint; + ModeText.Text = isOverlay ? + "Mode: Overlay (below breakpoint)" : + $"Mode: {DemoDrawer.DrawerLayoutBehavior} (above breakpoint)"; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml new file mode 100644 index 00000000..fc349726 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml.cs new file mode 100644 index 00000000..b1d84fb5 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml.cs @@ -0,0 +1,78 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageCompactPage : UserControl + { + private bool _isLoaded; + + public DrawerPageCompactPage() + { + InitializeComponent(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + _isLoaded = true; + DemoDrawer.Opened += OnDrawerStatusChanged; + DemoDrawer.Closed += OnDrawerStatusChanged; + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + DemoDrawer.Opened -= OnDrawerStatusChanged; + DemoDrawer.Closed -= OnDrawerStatusChanged; + } + + private void OnDrawerStatusChanged(object? sender, System.EventArgs e) => UpdateStatus(); + + private void OnLayoutChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLayoutBehavior = LayoutCombo.SelectedIndex switch + { + 0 => DrawerLayoutBehavior.CompactOverlay, + 1 => DrawerLayoutBehavior.CompactInline, + _ => DrawerLayoutBehavior.CompactOverlay + }; + } + + private void OnCompactLengthChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.CompactDrawerLength = e.NewValue; + CompactLengthText.Text = ((int)e.NewValue).ToString(); + } + + private void OnDrawerLengthChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLength = e.NewValue; + DrawerLengthText.Text = ((int)e.NewValue).ToString(); + } + + private void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (sender is not Button button) + return; + var item = button.Tag?.ToString() ?? "Home"; + DetailTitleText.Text = item; + DetailPage.Header = item; + DemoDrawer.IsOpen = false; + } + + private void UpdateStatus() + { + StatusText.Text = $"Drawer: {(DemoDrawer.IsOpen ? "Open" : "Closed")}"; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml new file mode 100644 index 00000000..11f584c3 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml.cs new file mode 100644 index 00000000..c05d92ec --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml.cs @@ -0,0 +1,146 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Threading; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageCustomFlyoutPage : UserControl + { + private Ellipse? _bubble1; + private Ellipse? _bubble2; + private DispatcherTimer? _bubbleTimer; + private double _bubblePhase; + + public DrawerPageCustomFlyoutPage() + { + InitializeComponent(); + + _bubble1 = this.FindControl("Bubble1"); + _bubble2 = this.FindControl("Bubble2"); + + DrawerPageControl.PropertyChanged += (_, args) => + { + if (args.Property == DrawerPage.IsOpenProperty) + OnDrawerOpenChanged((bool)args.NewValue!); + }; + + _ = DetailNav.PushAsync(BuildDetailPage("Home"), null); + } + + private Control[] MenuItems => + new Control[] { MenuItem1, MenuItem2, MenuItem3, MenuItem4, MenuItem5, FooterRow }; + + private void OnDrawerOpenChanged(bool isOpen) + { + if (isOpen) + { + StartBubbles(); + + foreach (var item in MenuItems) + { + item.Opacity = 1.0; + if (item.RenderTransform is TranslateTransform tt) + tt.Y = 0; + } + } + else + { + StopBubbles(); + + foreach (var item in MenuItems) + { + var savedItemT = item.Transitions; + item.Transitions = null; + item.Opacity = 0.0; + item.Transitions = savedItemT; + + if (item.RenderTransform is TranslateTransform tt) + { + var savedTT = tt.Transitions; + tt.Transitions = null; + tt.Y = 25; + tt.Transitions = savedTT; + } + } + } + } + + private void StartBubbles() + { + if (_bubbleTimer != null) return; + _bubblePhase = 0; + _bubbleTimer = new DispatcherTimer(DispatcherPriority.Render) + { + Interval = TimeSpan.FromMilliseconds(16) + }; + _bubbleTimer.Tick += OnBubbleTick; + _bubbleTimer.Start(); + } + + private void StopBubbles() + { + if (_bubbleTimer == null) return; + _bubbleTimer.Stop(); + _bubbleTimer.Tick -= OnBubbleTick; + _bubbleTimer = null; + + if (_bubble1 != null) _bubble1.RenderTransform = null; + if (_bubble2 != null) _bubble2.RenderTransform = null; + } + + private void OnBubbleTick(object? sender, EventArgs e) + { + _bubblePhase += 0.012; + + if (_bubble1 != null) + _bubble1.RenderTransform = new TranslateTransform( + x: Math.Sin(_bubblePhase * 0.65) * 10, + y: Math.Sin(_bubblePhase) * 14); + + if (_bubble2 != null) + _bubble2.RenderTransform = new TranslateTransform( + x: Math.Sin(_bubblePhase * 0.45 + 1.8) * 7, + y: Math.Cos(_bubblePhase * 0.85 + 0.6) * 10); + } + + private async void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button button) return; + var tag = button.Tag?.ToString() ?? "Home"; + + DrawerPageControl.IsOpen = false; + + await DetailNav.ReplaceAsync(BuildDetailPage(tag), null); + } + + private static ContentPage BuildDetailPage(string section) + { + var (iconPath, body) = section switch + { + "Home" => + ("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z", + "Welcome back! Here is your dashboard with recent activity, quick actions, and personalized content."), + "Explore" => + ("M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z", + "Discover new places, trending topics, and recommended content tailored to your interests."), + "Messages" => + ("M20,8L12,13L4,8V6L12,11L20,6M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z", + "Your conversations and notifications. Stay connected with the people who matter."), + "Profile" => + ("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z", + "View and edit your profile, manage privacy settings, and control your account preferences."), + "Settings" => + ("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.04 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.04 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", + "Configure application preferences, notifications, and privacy options."), + _ => ("M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z", "") + }; + + var page = NavigationDemoHelper.MakeSectionPage(section, iconPath, section, body, 0); + NavigationPage.SetHasNavigationBar(page, false); + return page; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml new file mode 100644 index 00000000..3adffed5 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs new file mode 100644 index 00000000..d9482523 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs @@ -0,0 +1,326 @@ +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Input.GestureRecognizers; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageCustomizationPage : UserControl + { + private bool _isLoaded; + + private static readonly string[] _iconPaths = + { + // 0 - 3 lines (default hamburger) + "M3 17h18a1 1 0 0 1 .117 1.993L21 19H3a1 1 0 0 1-.117-1.993L3 17h18H3Zm0-6 18-.002a1 1 0 0 1 .117 1.993l-.117.007L3 13a1 1 0 0 1-.117-1.993L3 11l18-.002L3 11Zm0-6h18a1 1 0 0 1 .117 1.993L21 7H3a1 1 0 0 1-.117-1.993L3 5h18H3Z", + // 1 - 2 lines + "M3,13H21V11H3M3,6V8H21V6", + // 2 - 4 squares + "M3,11H11V3H3M3,21H11V13H3M13,21H21V13H13M13,3V11H21V3", + }; + + public DrawerPageCustomizationPage() + { + InitializeComponent(); + EnableMouseSwipeGesture(DemoDrawer); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + _isLoaded = true; + } + + private void OnToggleDrawer(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.IsOpen = !DemoDrawer.IsOpen; + } + + private void OnBehaviorChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerBehavior = BehaviorCombo.SelectedIndex switch + { + 0 => DrawerBehavior.Auto, + 1 => DrawerBehavior.Flyout, + 2 => DrawerBehavior.Locked, + 3 => DrawerBehavior.Disabled, + _ => DrawerBehavior.Auto + }; + } + + private void OnLayoutChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLayoutBehavior = LayoutCombo.SelectedIndex switch + { + 0 => DrawerLayoutBehavior.Overlay, + 1 => DrawerLayoutBehavior.Split, + 2 => DrawerLayoutBehavior.CompactOverlay, + 3 => DrawerLayoutBehavior.CompactInline, + _ => DrawerLayoutBehavior.Overlay + }; + } + + private void OnPlacementChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerPlacement = PlacementCombo.SelectedIndex switch + { + 1 => DrawerPlacement.Right, + 2 => DrawerPlacement.Top, + 3 => DrawerPlacement.Bottom, + _ => DrawerPlacement.Left + }; + } + + private void OnGestureToggled(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (sender is CheckBox check) + DemoDrawer.IsGestureEnabled = check.IsChecked == true; + } + + private void OnDrawerLengthChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLength = e.NewValue; + DrawerLengthText.Text = ((int)e.NewValue).ToString(); + } + + private void OnDrawerBgChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerBackground = DrawerBgCombo.SelectedIndex switch + { + 1 => new SolidColorBrush(Colors.SlateBlue), + 2 => new SolidColorBrush(Colors.DarkCyan), + 3 => new SolidColorBrush(Colors.DarkRed), + 4 => new SolidColorBrush(Colors.DarkGreen), + _ => null + }; + } + + private void OnHeaderBgChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerHeaderBackground = HeaderBgCombo.SelectedIndex switch + { + 1 => new SolidColorBrush(Colors.DodgerBlue), + 2 => new SolidColorBrush(Colors.Orange), + 3 => new SolidColorBrush(Colors.Teal), + 4 => new SolidColorBrush(Colors.Purple), + _ => null + }; + } + + private void OnFooterBgChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerFooterBackground = FooterBgCombo.SelectedIndex switch + { + 1 => new SolidColorBrush(Colors.DimGray), + 2 => new SolidColorBrush(Colors.DarkSlateBlue), + 3 => new SolidColorBrush(Colors.DarkOliveGreen), + 4 => new SolidColorBrush(Colors.Maroon), + _ => null + }; + } + + private void OnIconChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerIcon = new PathIcon { Data = Geometry.Parse(_iconPaths[IconCombo.SelectedIndex]) }; + } + + private void OnBackdropChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.BackdropBrush = BackdropCombo.SelectedIndex switch + { + 1 => new SolidColorBrush(Color.FromArgb(102, 0, 0, 0)), + 2 => new SolidColorBrush(Color.FromArgb(179, 0, 0, 0)), + 3 => new SolidColorBrush(Color.FromArgb(102, 255, 255, 255)), + _ => null + }; + } + + private void OnShowHeaderToggled(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (ShowHeaderCheck.IsChecked == true) + DemoDrawer.DrawerHeader = HeaderTemplateCombo.SelectedIndex == 0 ? DrawerHeaderBorder : (object)"My Application"; + else + DemoDrawer.DrawerHeader = null; + } + + private void OnShowFooterToggled(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (ShowFooterCheck.IsChecked == true) + { + DemoDrawer.DrawerFooter = FooterTemplateCombo.SelectedIndex switch + { + 1 => (object)"v12.0", + 2 => (object)"Avalonia", + _ => DrawerFooterBorder + }; + } + else + { + DemoDrawer.DrawerFooter = null; + } + } + + private void OnHeaderTemplateChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + + switch (HeaderTemplateCombo.SelectedIndex) + { + case 1: + DemoDrawer.DrawerHeader = "My Application"; + DemoDrawer.DrawerHeaderTemplate = new FuncDataTemplate((data, _) => + new Border + { + Padding = new Avalonia.Thickness(16), + Child = new StackPanel + { + Spacing = 2, + Children = + { + new TextBlock { Text = data, FontSize = 18, FontWeight = FontWeight.SemiBold, Foreground = Brushes.White }, + new TextBlock { Text = "Navigation", FontSize = 12, Foreground = Brushes.White, Opacity = 0.7 } + } + } + }); + break; + + case 2: + DemoDrawer.DrawerHeader = "My Application"; + DemoDrawer.DrawerHeaderTemplate = new FuncDataTemplate((data, _) => + { + var initial = data?.Length > 0 ? data[0].ToString().ToUpperInvariant() : "?"; + var avatar = new Border + { + Width = 40, + Height = 40, + CornerRadius = new Avalonia.CornerRadius(20), + Background = new SolidColorBrush(Color.Parse("#1976D2")), + Child = new TextBlock + { + Text = initial, + FontSize = 18, + FontWeight = FontWeight.Bold, + Foreground = Brushes.White, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + var label = new TextBlock { Text = data, FontSize = 14, FontWeight = FontWeight.SemiBold, VerticalAlignment = VerticalAlignment.Center }; + var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 10 }; + row.Children.Add(avatar); + row.Children.Add(label); + return new Border { Padding = new Avalonia.Thickness(12), Child = row }; + }); + break; + + default: + DemoDrawer.DrawerHeader = DrawerHeaderBorder; + DemoDrawer.DrawerHeaderTemplate = null; + break; + } + } + + private void OnFooterTemplateChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + + switch (FooterTemplateCombo.SelectedIndex) + { + case 1: + DemoDrawer.DrawerFooter = "v12.0"; + DemoDrawer.DrawerFooterTemplate = new FuncDataTemplate((data, _) => + new Border + { + Padding = new Avalonia.Thickness(12, 8), + Child = new Border + { + Padding = new Avalonia.Thickness(8, 4), + CornerRadius = new Avalonia.CornerRadius(4), + Background = new SolidColorBrush(Color.Parse("#1976D2")), + Child = new TextBlock { Text = data, FontSize = 11, Foreground = Brushes.White, FontWeight = FontWeight.SemiBold } + } + }); + break; + + case 2: + DemoDrawer.DrawerFooter = "Avalonia"; + DemoDrawer.DrawerFooterTemplate = new FuncDataTemplate((data, _) => + { + var icon = new PathIcon + { + Width = 14, + Height = 14, + Data = Geometry.Parse("M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"), + Opacity = 0.5 + }; + var label = new TextBlock { Text = data, FontSize = 12, Opacity = 0.6, VerticalAlignment = VerticalAlignment.Center }; + var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 6 }; + row.Children.Add(icon); + row.Children.Add(label); + return new Border { Padding = new Avalonia.Thickness(14, 10), Child = row }; + }); + break; + + default: + DemoDrawer.DrawerFooter = DrawerFooterBorder; + DemoDrawer.DrawerFooterTemplate = null; + break; + } + } + + private void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (sender is not Button button) return; + var item = button.Tag?.ToString() ?? "Home"; + + DetailTitleText.Text = item; + + if (DemoDrawer.DrawerBehavior != DrawerBehavior.Locked) + DemoDrawer.IsOpen = false; + } + + private static void EnableMouseSwipeGesture(Control control) + { + var recognizer = control.GestureRecognizers + .OfType() + .FirstOrDefault(); + + if (recognizer is not null) + recognizer.IsMouseEnabled = true; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml new file mode 100644 index 00000000..92a20558 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml.cs new file mode 100644 index 00000000..02148dc4 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageEventsPage : UserControl + { + private readonly Dictionary _sectionPages; + + public DrawerPageEventsPage() + { + InitializeComponent(); + + _sectionPages = new Dictionary + { + ["Home"] = CreateSectionPage("Home"), + ["Profile"] = CreateSectionPage("Profile"), + ["Settings"] = CreateSectionPage("Settings"), + }; + + foreach (var (name, page) in _sectionPages) + { + var label = name; + page.NavigatedTo += (_, _) => Log($"{label}: NavigatedTo"); + page.NavigatedFrom += (_, _) => Log($"{label}: NavigatedFrom"); + } + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + DemoDrawer.Opened += OnDrawerOpened; + DemoDrawer.Closing += OnClosing; + DemoDrawer.Closed += OnDrawerClosed; + // Set Content here so the initial NavigatedTo events fire + // (VisualRoot is null in the constructor, which suppresses lifecycle events). + DemoDrawer.Content = _sectionPages["Home"]; + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + DemoDrawer.Opened -= OnDrawerOpened; + DemoDrawer.Closing -= OnClosing; + DemoDrawer.Closed -= OnDrawerClosed; + } + + private void OnDrawerOpened(object? sender, System.EventArgs e) => Log("Opened"); + private void OnDrawerClosed(object? sender, System.EventArgs e) => Log("Closed"); + + private void OnToggle(object? sender, RoutedEventArgs e) + { + DemoDrawer.IsOpen = !DemoDrawer.IsOpen; + } + + private void OnClosing(object? sender, DrawerClosingEventArgs e) + { + if (CancelCheck.IsChecked == true) + { + e.Cancel = true; + CancelCheck.IsChecked = false; + Log("Closing \u2192 cancelled"); + } + else + { + Log("Closing"); + } + } + + private void OnSelectSection(object? sender, RoutedEventArgs e) + { + if (sender is not Button btn) return; + var section = btn.Tag?.ToString() ?? "Home"; + + if (!_sectionPages.TryGetValue(section, out var page)) return; + if (ReferenceEquals(DemoDrawer.Content, page)) + { + DemoDrawer.IsOpen = false; + return; + } + + Log($"\u2192 {section}"); + DemoDrawer.Content = page; + DemoDrawer.IsOpen = false; + } + + private void OnClearLog(object? sender, RoutedEventArgs e) + { + EventLog.Text = string.Empty; + } + + private void Log(string message) + { + EventLog.Text = $"{message}\n{EventLog.Text}"; + } + + private static ContentPage CreateSectionPage(string header) => new ContentPage + { + Header = header, + Content = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Spacing = 8, + Children = + { + new TextBlock + { + Text = header, + FontSize = 24, + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center, + }, + new TextBlock + { + Text = "Tap a drawer item to navigate.\nWatch the event log in the panel.", + TextWrapping = TextWrapping.Wrap, + Opacity = 0.6, + TextAlignment = TextAlignment.Center, + FontSize = 13, + } + } + } + }; + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageFirstLookPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageFirstLookPage.xaml new file mode 100644 index 00000000..e257137e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageFirstLookPage.xaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageRtlPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageRtlPage.xaml.cs new file mode 100644 index 00000000..36454ef8 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageRtlPage.xaml.cs @@ -0,0 +1,57 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageRtlPage : UserControl + { + public DrawerPageRtlPage() + { + InitializeComponent(); + } + + private void OnRtlToggled(object? sender, RoutedEventArgs e) + { + if (DemoDrawer == null) return; + DemoDrawer.FlowDirection = RtlCheckBox.IsChecked == true + ? FlowDirection.RightToLeft + : FlowDirection.LeftToRight; + } + + private void OnPlacementChanged(object? sender, SelectionChangedEventArgs e) + { + if (DemoDrawer == null) return; + DemoDrawer.DrawerPlacement = PlacementCombo.SelectedIndex switch + { + 0 => DrawerPlacement.Left, + 1 => DrawerPlacement.Right, + _ => DrawerPlacement.Left + }; + } + + private void OnToggleDrawer(object? sender, RoutedEventArgs e) + { + if (DemoDrawer == null) return; + DemoDrawer.IsOpen = !DemoDrawer.IsOpen; + } + + private void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button button) return; + var item = button.Tag?.ToString() ?? "Home"; + + DetailTitleText.Text = item; + DetailDescriptionText.Text = item switch + { + "Home" => "Toggle RTL to see the drawer flip to the right edge.\nGestures are mirrored: drag from right edge to open, drag right to close.", + "Profile" => "View and edit your profile information here.", + "Messages" => "Your messages and notifications appear here.", + "Settings" => "Configure application preferences and options.", + _ => $"Content for {item}" + }; + + DemoDrawer.IsOpen = false; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml new file mode 100644 index 00000000..9776163b --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml.cs new file mode 100644 index 00000000..23634c17 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml.cs @@ -0,0 +1,94 @@ +using System; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageTransitionsPage : UserControl + { + private string _selectedTransition = "None"; + + public DrawerPageTransitionsPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + // Null out the default transition — OnTransitionChanged runs during init before DetailNav exists. + DetailNav.PageTransition = null; + await DetailNav.PushAsync(BuildPage("Home", _selectedTransition), null); + } + + private void OnTransitionChanged(object? sender, SelectionChangedEventArgs e) + { + if (DetailNav == null) return; + + _selectedTransition = TransitionCombo.SelectedIndex switch + { + 1 => "CrossFade", + 2 => "PageSlide (H)", + 3 => "PageSlide (V)", + 4 => "Composite (Slide + Fade)", + _ => "None" + }; + + DetailNav.PageTransition = TransitionCombo.SelectedIndex switch + { + 1 => new CrossFade(TimeSpan.FromMilliseconds(300)), + 2 => new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Horizontal), + 3 => new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Vertical), + 4 => new CompositePageTransition + { + PageTransitions = + { + new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Horizontal), + new CrossFade(TimeSpan.FromMilliseconds(300)) + } + }, + _ => null + }; + } + + private async void OnSectionClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button button) return; + var section = button.Tag?.ToString() ?? "Home"; + + DemoDrawer.IsOpen = false; + + await DetailNav.ReplaceAsync(BuildPage(section, _selectedTransition)); + } + + private static ContentPage BuildPage(string section, string transitionName) + { + var (iconPath, body) = section switch + { + "Home" => + ("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z", + "Your dashboard with recent activity, quick actions, and personalized content."), + "Explore" => + ("M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z", + "Discover new places, trending topics, and recommended content tailored to your interests."), + "Messages" => + ("M20,8L12,13L4,8V6L12,11L20,6M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z", + "Your conversations and notifications. Stay connected with the people who matter."), + "Profile" => + ("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z", + "View and edit your profile, manage privacy settings, and control your account preferences."), + "Settings" => + ("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.04 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.04 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", + "Configure application preferences, notifications, and privacy options."), + _ => ("M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z", "") + }; + + var page = NavigationDemoHelper.MakeSectionPage(section, iconPath, section, body, 0, $"Transition: {transitionName}"); + NavigationPage.SetHasNavigationBar(page, false); + return page; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml new file mode 100644 index 00000000..22320fbc --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml.cs new file mode 100644 index 00000000..49b13b4d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml.cs @@ -0,0 +1,319 @@ +using System; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class EcoTrackerAppPage : UserControl +{ + static readonly Color Primary = Color.Parse("#2E7D32"); + static readonly Color Accent = Color.Parse("#4CAF50"); + static readonly Color BgLight = Color.Parse("#F1F8E9"); + static readonly Color TextDark = Color.Parse("#1A2E1C"); + static readonly Color TextMuted = Color.Parse("#90A4AE"); + + const string LeafPath = + "M12 3C9 6 6 9 6 13C6 17.4 8.7 21 12 22C15.3 21 18 17.4 18 13C18 9 15 6 12 3Z"; + + NavigationPage? _navPage; + DrawerPage? _drawerPage; + ScrollViewer? _infoPanel; + Button? _selectedBtn; + + public EcoTrackerAppPage() + { + InitializeComponent(); + + _infoPanel = this.FindControl("InfoPanel"); + _navPage = this.FindControl("NavPage"); + _drawerPage = this.FindControl("DrawerPageControl"); + _selectedBtn = this.FindControl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerHomeView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerHomeView.xaml.cs new file mode 100644 index 00000000..3f36a4d4 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerHomeView.xaml.cs @@ -0,0 +1,14 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class EcoTrackerHomeView : UserControl +{ + public Action? TreeDetailRequested { get; set; } + + public EcoTrackerHomeView() => InitializeComponent(); + + void OnHeroClick(object? sender, RoutedEventArgs e) => TreeDetailRequested?.Invoke(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml new file mode 100644 index 00000000..82a5e938 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml.cs new file mode 100644 index 00000000..90aa0206 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class EcoTrackerStatsView : UserControl +{ + public EcoTrackerStatsView() => InitializeComponent(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernAppPage.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernAppPage.xaml new file mode 100644 index 00000000..77a0e329 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernAppPage.xaml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernAppPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernAppPage.xaml.cs new file mode 100644 index 00000000..f0d4d7d6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernAppPage.xaml.cs @@ -0,0 +1,118 @@ +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class ModernAppPage : UserControl +{ + // Palette + static readonly Color Primary = Color.Parse("#0dccf2"); + static readonly Color BgLight = Color.Parse("#f5f8f8"); + + static IBrush BgBrush => new SolidColorBrush(BgLight); + + DrawerPage? _drawerPage; + NavigationPage? _navPage; + ScrollViewer? _infoPanel; + TextBlock? _pageTitle; + Button? _selectedNavBtn; + + public ModernAppPage() + { + InitializeComponent(); + + _infoPanel = this.FindControl("InfoPanel"); + _drawerPage = this.FindControl("DrawerPageControl"); + _navPage = this.FindControl("NavPage"); + _pageTitle = this.FindControl("PageTitle"); + + if (_navPage != null) + NavigateToDiscover(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + UpdateInfoPanelVisibility(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoPanelVisibility(); + } + + void UpdateInfoPanelVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 640; + } + + void OnNavClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button btn) return; + SelectNavButton(btn); + _drawerPage!.IsOpen = false; + switch (btn.Tag?.ToString()) + { + case "Discover": NavigateToDiscover(); break; + case "MyTrips": NavigateToMyTrips(); break; + case "Profile": NavigateToProfile(); break; + case "Settings": NavigateToSettings(); break; + } + } + + void OnCloseDrawer(object? sender, RoutedEventArgs e) + { + if (_drawerPage != null) _drawerPage.IsOpen = false; + } + + void SelectNavButton(Button btn) + { + if (_selectedNavBtn != null) + _selectedNavBtn.Background = Brushes.Transparent; + _selectedNavBtn = btn; + btn.Background = new SolidColorBrush(Color.Parse("#1A0dccf2")); + } + + async Task Navigate(ContentPage page) + { + if (_navPage == null) return; + NavigationPage.SetHasBackButton(page, false); + NavigationPage.SetHasNavigationBar(page, false); + await _navPage.PopToRootAsync(); + await _navPage.PushAsync(page); + } + + async void NavigateToDiscover() + { + if (_pageTitle != null) _pageTitle.Text = "Discover"; + SelectNavButton(this.FindControl + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernProfileView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernProfileView.xaml.cs new file mode 100644 index 00000000..ae2cdb94 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernProfileView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class ModernProfileView : UserControl +{ + public ModernProfileView() => InitializeComponent(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml new file mode 100644 index 00000000..a4ee8cd8 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml.cs new file mode 100644 index 00000000..d50ade3d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class ModernSettingsView : UserControl +{ + public ModernSettingsView() => InitializeComponent(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/ExpanderPage.xaml b/samples/AvaloniaControlCatalog/Pages/ExpanderPage.xaml new file mode 100644 index 00000000..322da30a --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ExpanderPage.xaml @@ -0,0 +1,76 @@ + + + Expands to show nested content + + + + + Expanded content + + + + + Expanded content + + + + + Expanded content + + + + + Expanded content + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/FlyoutsPage.axaml.cs b/samples/AvaloniaControlCatalog/Pages/FlyoutsPage.axaml.cs new file mode 100644 index 00000000..d6f54df0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/FlyoutsPage.axaml.cs @@ -0,0 +1,94 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class FlyoutsPage : ContentPage + { + public FlyoutsPage() + { + InitializeComponent(); + + AttachedFlyoutPanel.DoubleTapped += Afp_DoubleTapped; + + SetXamlTexts(); + } + + private void Afp_DoubleTapped(object? sender, RoutedEventArgs e) + { + if (sender is Panel p) + { + FlyoutBase.ShowAttachedFlyout(p); + } + } + + private void SetXamlTexts() + { + var bfxt = ButtonFlyoutXamlText; + bfxt.Text = ""; + + var mfxt = this.MenuFlyoutXamlText; + mfxt.Text = ""; + + var afxt = this.AttachedFlyoutXamlText; + afxt.Text = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n\n In DoubleTapped handler:\n" + + "FlyoutBase.ShowAttachedFlyout(AttachedFlyoutPanel);"; + + var sfxt = this.SharedFlyoutXamlText; + sfxt.Text = "Declare a flyout in Resources:\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n\n\n" + + "Then attach the flyout where you want it:\n" + + " + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/FocusPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/FocusPage.xaml.cs new file mode 100644 index 00000000..18aae471 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/FocusPage.xaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages; + +public partial class FocusPage : ContentPage +{ + public FocusPage() + { + AvaloniaXamlLoader.Load(this); + } +} + diff --git a/samples/AvaloniaControlCatalog/Pages/GesturePage.xaml b/samples/AvaloniaControlCatalog/Pages/GesturePage.xaml new file mode 100644 index 00000000..da9ed5f8 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/GesturePage.xaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/GesturePage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/GesturePage.xaml.cs new file mode 100644 index 00000000..56c09573 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/GesturePage.xaml.cs @@ -0,0 +1,39 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class GesturePage : ContentPage + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + ("Touch / Pen", "Pull Gesture", + "Press and drag from colored border zones. A green ball tracks the pull delta and springs back on release.", + () => new GesturePullPage()), + + ("Multi Touch", "Pinch / Zoom", + "Pinch to scale an image using composition visuals. Scroll to pan when zoomed in.", + () => new GesturePinchZoomPage()), + + ("Multi Touch", "Pinch / Rotation", + "Pinch to rotate a rectangle. The Angle property from the pinch event drives a RotateTransform.", + () => new GesturePinchRotationPage()), + + ("Touch / Pen / Mouse", "Swipe Gesture", + "Swipe horizontally or vertically. Configure direction, threshold, and mouse support. Shows live delta, velocity, and direction.", + () => new GestureSwipePage()), + }; + + public GesturePage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml b/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml new file mode 100644 index 00000000..b46af74c --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml.cs new file mode 100644 index 00000000..55d5e619 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml.cs @@ -0,0 +1,33 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; + +namespace ControlCatalog.Pages +{ + public partial class GesturePinchRotationPage : UserControl + { + private bool _isInit; + + public GesturePinchRotationPage() + { + InitializeComponent(); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + if (_isInit) + { + return; + } + + _isInit = true; + + RotationGesture.AddHandler(InputElement.PinchEvent, (s, e) => + { + AngleSlider.Value = e.Angle; + }); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchZoomPage.xaml b/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchZoomPage.xaml new file mode 100644 index 00000000..6932741e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/Gestures/GesturePinchZoomPage.xaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/LabelsPage.axaml.cs b/samples/AvaloniaControlCatalog/Pages/LabelsPage.axaml.cs new file mode 100644 index 00000000..b26112b3 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/LabelsPage.axaml.cs @@ -0,0 +1,35 @@ +using Avalonia.Controls; +using ControlCatalog.Models; + +namespace ControlCatalog.Pages +{ + public partial class LabelsPage : ContentPage + { + private Person? _person; + + public LabelsPage() + { + CreateDefaultPerson(); + this.InitializeComponent(); + } + + private void CreateDefaultPerson() + { + DataContext = _person = new Person + { + FirstName = "John", + LastName = "Doe", + IsBanned = true, + }; + } + + public void DoSave() + { + + } + public void DoCancel() + { + CreateDefaultPerson(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/LayoutTransformControlPage.xaml b/samples/AvaloniaControlCatalog/Pages/LayoutTransformControlPage.xaml new file mode 100644 index 00000000..d12bbfc3 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/LayoutTransformControlPage.xaml @@ -0,0 +1,28 @@ + + + + Rotation + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/LayoutTransformControlPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/LayoutTransformControlPage.xaml.cs new file mode 100644 index 00000000..e3815238 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/LayoutTransformControlPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class LayoutTransformControlPage : ContentPage + { + public LayoutTransformControlPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ListBoxPage.xaml b/samples/AvaloniaControlCatalog/Pages/ListBoxPage.xaml new file mode 100644 index 00000000..f73a4c18 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ListBoxPage.xaml @@ -0,0 +1,51 @@ + + + + + + + + + + Hosts a collection of ListBoxItem. + Each 5th item is highlighted with nth-child(5n+3) and nth-last-child(5n+4) rules. + + + Multiple + Toggle + AlwaysSelected + AutoScrollToSelectedItem + WrapSelection + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ListBoxPage.xaml.cs new file mode 100644 index 00000000..208b5cce --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ListBoxPage.xaml.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class ListBoxPage : ContentPage + { + public ListBoxPage() + { + InitializeComponent(); + DataContext = new ListBoxPageViewModel(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/MenuPage.xaml b/samples/AvaloniaControlCatalog/Pages/MenuPage.xaml new file mode 100644 index 00000000..d9fb5d81 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/MenuPage.xaml @@ -0,0 +1,99 @@ + + + Exported menu fallback + (Should be only visible on platforms without desktop-global menu bar) + + A window menu + + + + Defined in XAML + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dyanamically generated + + + + + + + + + Mixed + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/MenuPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/MenuPage.xaml.cs new file mode 100644 index 00000000..56a229a5 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/MenuPage.xaml.cs @@ -0,0 +1,28 @@ +using System; +using Avalonia.Controls; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class MenuPage : ContentPage + { + public MenuPage() + { + InitializeComponent(); + DataContext = new MenuPageViewModel(); + } + + private MenuPageViewModel? _model; + protected override void OnDataContextChanged(EventArgs e) + { + if (_model != null) + _model.View = null; + _model = DataContext as MenuPageViewModel; + if (_model != null) + _model.View = this; + + base.OnDataContextChanged(e); + } + + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NativeEmbedPage.xaml b/samples/AvaloniaControlCatalog/Pages/NativeEmbedPage.xaml new file mode 100644 index 00000000..71d622ba --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NativeEmbedPage.xaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + Text + + + Tooltip + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NativeEmbedPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NativeEmbedPage.xaml.cs new file mode 100644 index 00000000..1cd1f78c --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NativeEmbedPage.xaml.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform; + +namespace ControlCatalog.Pages +{ + public partial class NativeEmbedPage : ContentPage + { + public NativeEmbedPage() + { + InitializeComponent(); + } + + public async void ShowPopupDelay(object sender, RoutedEventArgs args) + { + await Task.Delay(3000); + ShowPopup(sender, args); + } + + public void ShowPopup(object sender, RoutedEventArgs args) + { + new ContextMenu() + { + Items = + { + new MenuItem() { Header = "Test" }, new MenuItem() { Header = "Test" } + } + }.Open((Control)sender); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == BoundsProperty) + { + var isMobile = change.GetNewValue().Width < 1200; + FirstPanel.Classes.Set("mobile", isMobile); + SecondPanel.Classes.Set("mobile", isMobile); + } + } + } + + public class EmbedSample : NativeControlHost + { + public static INativeDemoControl? Implementation { get; set; } + + static EmbedSample() + { + + } + + public bool IsSecond { get; set; } + + protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent) + { + return Implementation?.CreateControl(IsSecond, parent, () => base.CreateNativeControlCore(parent)) + ?? base.CreateNativeControlCore(parent); + } + + protected override void DestroyNativeControlCore(IPlatformHandle control) + { + base.DestroyNativeControlCore(control); + } + } + + public interface INativeDemoControl + { + /// Used to specify which control should be displayed as a demo + /// + /// + IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func createDefault); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationDemoHelper.cs b/samples/AvaloniaControlCatalog/Pages/NavigationDemoHelper.cs new file mode 100644 index 00000000..22e52b6f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationDemoHelper.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + /// + /// Shared helpers for ControlCatalog demo pages. + /// + internal static class NavigationDemoHelper + { + /// + /// Pastel background brushes cycled by page index. + /// + internal static readonly IBrush[] PageBrushes = + { + new SolidColorBrush(Color.Parse("#BBDEFB")), + new SolidColorBrush(Color.Parse("#C8E6C9")), + new SolidColorBrush(Color.Parse("#FFE0B2")), + new SolidColorBrush(Color.Parse("#E1BEE7")), + new SolidColorBrush(Color.Parse("#FFCDD2")), + new SolidColorBrush(Color.Parse("#B2EBF2")), + }; + + internal static IBrush GetPageBrush(int index) => + PageBrushes[((index % PageBrushes.Length) + PageBrushes.Length) % PageBrushes.Length]; + + /// + /// Creates a simple demo ContentPage with a centered title and subtitle. + /// + internal static ContentPage MakePage(string header, string body, int colorIndex) => + new ContentPage + { + Header = header, + Background = GetPageBrush(colorIndex), + Content = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Spacing = 8, + Children = + { + new TextBlock + { + Text = header, + FontSize = 20, + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center + }, + new TextBlock + { + Text = body, + FontSize = 13, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + TextAlignment = TextAlignment.Center, + MaxWidth = 260 + } + } + }, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + + /// + /// Creates a demo ContentPage with an icon, title, body, and hint text + /// (used by DrawerPage detail pages). + /// + internal static ContentPage MakeSectionPage( + string header, string iconData, string title, string body, + int colorIndex, string? hint = null) + { + var panel = new StackPanel { Margin = new Thickness(24, 20), Spacing = 12 }; + + panel.Children.Add(new PathIcon + { + Width = 48, + Height = 48, + Data = Geometry.Parse(iconData), + Foreground = new SolidColorBrush(Color.Parse("#0078D4")) + }); + panel.Children.Add(new TextBlock + { + Text = title, + FontSize = 26, + FontWeight = FontWeight.Bold + }); + panel.Children.Add(new TextBlock + { + Text = body, + FontSize = 14, + Opacity = 0.8, + TextWrapping = TextWrapping.Wrap + }); + panel.Children.Add(new Separator { Margin = new Thickness(0, 8) }); + + if (hint != null) + { + panel.Children.Add(new TextBlock + { + Text = hint, + FontSize = 12, + Opacity = 0.45, + FontStyle = FontStyle.Italic, + TextWrapping = TextWrapping.Wrap + }); + } + + return new ContentPage + { + Header = header, + Background = GetPageBrush(colorIndex), + Content = new ScrollViewer { Content = panel }, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + } + + private static readonly Geometry CloseIcon = Geometry.Parse( + "M4.397 4.397a1 1 0 0 1 1.414 0L12 10.585l6.19-6.188a1 1 0 0 1 1.414 1.414L13.413 12l6.19 6.189a1 1 0 0 1-1.414 1.414L12 13.413l-6.189 6.19a1 1 0 0 1-1.414-1.414L10.585 12 4.397 5.811a1 1 0 0 1 0-1.414z"); + + /// + /// Builds the demo gallery home page for NavigationPage/TabbedPage/DrawerPage demo registries. + /// + internal static ContentPage CreateGalleryHomePage( + NavigationPage nav, + (string Group, string Title, string Description, Func Factory)[] demos) + { + var stack = new StackPanel { Margin = new Thickness(12), Spacing = 16 }; + + var groups = new Dictionary(); + var groupOrder = new List(); + + foreach (var (group, title, description, factory) in demos) + { + if (!groups.ContainsKey(group)) + { + groups[group] = new WrapPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Left + }; + groupOrder.Add(group); + } + + var demoFactory = factory; + var demoTitle = title; + + var card = new Button + { + Width = 170, + MinHeight = 80, + Margin = new Thickness(0, 0, 8, 8), + VerticalAlignment = VerticalAlignment.Top, + HorizontalContentAlignment = HorizontalAlignment.Left, + VerticalContentAlignment = VerticalAlignment.Top, + Padding = new Thickness(12, 8), + Content = new StackPanel + { + Spacing = 4, + Children = + { + new TextBlock + { + Text = title, + FontSize = 13, + FontWeight = FontWeight.SemiBold, + TextWrapping = TextWrapping.Wrap + }, + new TextBlock + { + Text = description, + FontSize = 11, + Opacity = 0.6, + TextWrapping = TextWrapping.Wrap + } + } + } + }; + + card.Click += async (_, _) => + { + var headerGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("*, Auto") }; + headerGrid.Children.Add(new TextBlock + { + Text = demoTitle, + VerticalAlignment = VerticalAlignment.Center + }); + var closeBtn = new Button + { + Content = new PathIcon { Data = CloseIcon }, + Background = Brushes.Transparent, + BorderThickness = new Thickness(0), + Padding = new Thickness(8, 4), + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(closeBtn, 1); + headerGrid.Children.Add(closeBtn); + closeBtn.Click += async (_, _) => await nav.PopAsync(null); + + var page = new ContentPage + { + Header = headerGrid, + Content = demoFactory(), + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + NavigationPage.SetHasBackButton(page, false); + await nav.PushAsync(page, null); + }; + + groups[group].Children.Add(card); + } + + foreach (var groupName in groupOrder) + { + stack.Children.Add(new TextBlock + { + Text = groupName, + FontSize = 13, + FontWeight = FontWeight.SemiBold, + Margin = new Thickness(0, 0, 0, 4), + Opacity = 0.6 + }); + stack.Children.Add(groups[groupName]); + } + + var homePage = new ContentPage + { + Content = new ScrollViewer { Content = stack }, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + NavigationPage.SetHasNavigationBar(homePage, false); + return homePage; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationDemoPage.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationDemoPage.xaml new file mode 100644 index 00000000..0f7536b6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationDemoPage.xaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationDemoPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationDemoPage.xaml.cs new file mode 100644 index 00000000..7ca41ab8 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationDemoPage.xaml.cs @@ -0,0 +1,94 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class NavigationDemoPage : ContentPage + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + // Overview + ("Overview", "First Look", "Basic NavigationPage with push/pop navigation and back button support.", + () => new NavigationPageFirstLookPage()), + ("Overview", "Modal Navigation", "Push and pop modal pages that appear on top of the navigation stack.", + () => new NavigationPageModalPage()), + ("Overview", "Navigation Events", + "Subscribe to Pushed, Popped, PoppedToRoot, ModalPushed, and ModalPopped events.", + () => new NavigationPageEventsPage()), + + // Appearance + ("Appearance", "Bar Customization", + "Customize the navigation bar background, foreground, shadow, and visibility.", + () => new NavigationPageAppearancePage()), + ("Appearance", "Header", + "Set page header content: a string, icon, or any custom control in the navigation bar.", + () => new NavigationPageTitlePage()), + + // Data + ("Data", "Pass Data", "Pass data during navigation via constructor arguments or DataContext.", + () => new NavigationPagePassDataPage()), + ("Data", "MVVM Navigation", + "Keep navigation decisions in view models by routing NavigationPage push and pop operations through a small INavigationService.", + () => new NavigationPageMvvmPage()), + + // Features + ("Features", "Attached Methods", + "Per-page navigation bar and back button control via static attached methods.", + () => new NavigationPageAttachedMethodsPage()), + ("Features", "Back Button", "Customize, hide, or intercept the back button.", + () => new NavigationPageBackButtonPage()), + ("Features", "CommandBar", + "Add, remove and position CommandBar items inside the navigation bar or as a bottom bar.", + () => new NavigationPageToolbarPage()), + ("Features", "Transitions", + "Configure page transitions: PageSlide, Parallax Slide, CrossFade, Fade Through, and more.", + () => new NavigationPageTransitionsPage()), + ("Features", "Modal Transitions", "Configure modal transition: PageSlide from bottom, CrossFade, or None.", + () => new NavigationPageModalTransitionsPage()), + ("Features", "Stack Management", "Remove or insert pages anywhere in the navigation stack at runtime.", + () => new NavigationPageStackPage()), + ("Features", "Interactive Header", + "Build a header with a title and live search box that filters page content in real time.", + () => new NavigationPageInteractiveHeaderPage()), + ("Features", "Back Swipe Gesture", "Swipe from the left edge to interactively pop the current page.", + () => new NavigationPageGesturePage()), + ("Features", "Scroll-Aware Bar", + "Hide the navigation bar on downward scroll and reveal it on upward scroll.", + () => new NavigationPageScrollAwarePage()), + + // Performance + ("Performance", "Performance Monitor", + "Track stack depth, live page instances, and managed heap size. Observe how memory is reclaimed after popping pages.", + () => new NavigationPagePerformancePage()), + + // Showcases + ("Showcases", "Pulse Fitness", + "Login flow with RemovePage, TabbedPage dashboard with bottom tabs, and NavigationPage push for workout detail.", + () => new PulseAppPage()), + ("Showcases", "L'Avenir", + "Restaurant app with DrawerPage flyout menu, TabbedPage bottom tabs, and NavigationPage push for dish detail.", + () => new LAvenirAppPage()), + ("Showcases", "AvaloniaFlix", + "Streaming app with dark NavigationPage, hidden nav bar on home, and custom bar tint on movie detail pages.", + () => new AvaloniaFlixAppPage()), + ("Showcases", "Retro Gaming", + "Arcade-style app with NavigationPage header, TabbedPage bottom tabs with CenteredTabPanel, and game detail push.", + () => new RetroGamingAppPage()), + ("Showcases", "Curved Header", + "Shop app with dome-bottomed white header on home (nav bar hidden) and blue curved header on detail (BarLayoutBehavior.Overlay).", + () => new NavigationPageCurvedHeaderPage()), + }; + + public NavigationDemoPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml new file mode 100644 index 00000000..6e209c8b --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml.cs new file mode 100644 index 00000000..a2ae34dd --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml.cs @@ -0,0 +1,191 @@ +using System; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class AvaloniaFlixAppPage : UserControl +{ + NavigationPage? _detailNav; + ScrollViewer? _infoPanel; + + public AvaloniaFlixAppPage() + { + InitializeComponent(); + + _detailNav = this.FindControl("DetailNav"); + if (_detailNav != null) + { + _detailNav.ModalTransition = new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Vertical); + + var homeView = new AvaloniaFlixHomeView(); + homeView.MovieSelected = title => PushDetailPage(title); + homeView.SearchRequested = () => _ = PushSearchPageAsync(); + + var homePage = new ContentPage + { + Content = homeView, + Background = Brushes.Transparent, + Header = BuildHomeHeader(), + }; + NavigationPage.SetTopCommandBar(homePage, BuildHomeCommandBar()); + _ = _detailNav.PushAsync(homePage); + } + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + _infoPanel = this.FindControl("InfoPanel"); + UpdateInfoPanelVisibility(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoPanelVisibility(); + } + + void UpdateInfoPanelVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 650; + } + + TextBlock BuildHomeHeader() => new TextBlock + { + Text = "AVALONIAFLIX", + Foreground = new SolidColorBrush(Color.Parse("#E50914")), + FontSize = 18, + FontWeight = Avalonia.Media.FontWeight.Black, + VerticalAlignment = VerticalAlignment.Center, + }; + + StackPanel BuildHomeCommandBar() + { + var cmdBar = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 12, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + }; + var searchBtn = new Button { Padding = new Thickness(4) }; + searchBtn.Classes.Add("flixTransparent"); + searchBtn.Click += OnSearchClick; + searchBtn.Content = new PathIcon + { + Width = 20, Height = 20, Foreground = Brushes.White, + Data = Avalonia.Media.Geometry.Parse("M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"), + }; + cmdBar.Children.Add(searchBtn); + cmdBar.Children.Add(new Border + { + Width = 30, Height = 30, CornerRadius = new CornerRadius(4), + Background = new SolidColorBrush(Color.Parse("#333333")), + Child = new TextBlock + { + Text = "JD", FontSize = 10, FontWeight = Avalonia.Media.FontWeight.Bold, + Foreground = Brushes.White, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + }, + }); + return cmdBar; + } + + async void PushDetailPage(string title) + { + if (_detailNav == null) return; + + var detailView = new AvaloniaFlixDetailView(title); + + var headerTitle = new TextBlock + { + Text = title, FontSize = 17, FontWeight = Avalonia.Media.FontWeight.Bold, + Foreground = Brushes.White, VerticalAlignment = VerticalAlignment.Center, + }; + + var shareBtnContent = new PathIcon + { + Width = 20, Height = 20, Foreground = Brushes.White, + Data = Avalonia.Media.Geometry.Parse("M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.35C15.11,18.56 15.08,18.78 15.08,19C15.08,20.61 16.39,21.92 18,21.92C19.61,21.92 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z"), + }; + var shareBtn = new Button { Padding = new Thickness(8), Content = shareBtnContent }; + shareBtn.Classes.Add("flixTransparent"); + + var bookmarkBtnContent = new PathIcon + { + Width = 20, Height = 20, Foreground = Brushes.White, + Data = Avalonia.Media.Geometry.Parse("M17,3H7A2,2 0 0,0 5,5V21L12,18L19,21V5C19,3.89 18.1,3 17,3Z"), + }; + var bookmarkBtn = new Button { Padding = new Thickness(8), Content = bookmarkBtnContent }; + bookmarkBtn.Classes.Add("flixTransparent"); + + var detailCmdBar = new StackPanel + { + Orientation = Orientation.Horizontal, Spacing = 8, + VerticalAlignment = VerticalAlignment.Center, + }; + detailCmdBar.Children.Add(shareBtn); + detailCmdBar.Children.Add(bookmarkBtn); + + var detailPage = new ContentPage + { + Content = detailView, + Background = Brushes.Transparent, + Header = headerTitle, + }; + NavigationPage.SetTopCommandBar(detailPage, detailCmdBar); + + await _detailNav.PushAsync(detailPage); + + var drawer = this.FindControl("DrawerPageControl"); + if (drawer is { IsOpen: true }) + drawer.IsOpen = false; + } + + async void OnSearchClick(object? sender, RoutedEventArgs e) + { + await PushSearchPageAsync(); + } + + async Task PushSearchPageAsync() + { + if (_detailNav == null) return; + + var searchView = new AvaloniaFlixSearchView(); + searchView.CloseRequested = async () => await (_detailNav?.PopModalAsync() ?? Task.CompletedTask); + searchView.MovieSelected = async title => + { + if (_detailNav != null && _detailNav.ModalStack.Count > 0) + await _detailNav.PopModalAsync(); + PushDetailPage(title); + }; + + var searchPage = new ContentPage + { + Content = searchView, + Background = new SolidColorBrush(Color.Parse("#0A0A0A")), + }; + NavigationPage.SetHasNavigationBar(searchPage, false); + + await (_detailNav?.PushModalAsync(searchPage) ?? Task.CompletedTask); + } + + void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + var drawer = this.FindControl("DrawerPageControl"); + if (drawer != null) + drawer.IsOpen = false; + + _ = _detailNav?.PopToRootAsync(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml new file mode 100644 index 00000000..e6f53066 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml @@ -0,0 +1,332 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml.cs new file mode 100644 index 00000000..659fea52 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml.cs @@ -0,0 +1,60 @@ +using System; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace ControlCatalog.Pages; + +public partial class AvaloniaFlixDetailView : UserControl +{ + static readonly string[] MovieAssets = + { + "avares://ControlCatalog/Assets/Movies/trending1.jpg", + "avares://ControlCatalog/Assets/Movies/trending2.jpg", + "avares://ControlCatalog/Assets/Movies/toprated1.jpg", + "avares://ControlCatalog/Assets/Movies/toprated2.jpg", + "avares://ControlCatalog/Assets/Movies/toprated3.jpg", + "avares://ControlCatalog/Assets/Movies/toprated4.jpg", + "avares://ControlCatalog/Assets/Movies/continue1.jpg", + "avares://ControlCatalog/Assets/Movies/morelike1.jpg", + "avares://ControlCatalog/Assets/Movies/search1.jpg", + "avares://ControlCatalog/Assets/Movies/hero.jpg", + "avares://ControlCatalog/Assets/Movies/cast1.jpg", + "avares://ControlCatalog/Assets/Movies/cast2.jpg", + }; + + public AvaloniaFlixDetailView() => InitializeComponent(); + + public AvaloniaFlixDetailView(string movieTitle) + { + InitializeComponent(); + + HeroTitleLabel.Text = movieTitle; + + var rng = new Random(movieTitle.GetHashCode()); + int imgIdx = Math.Abs(movieTitle.GetHashCode()) % MovieAssets.Length; + + string year = (2020 + rng.Next(6)).ToString(); + string rating = $"{6.5 + rng.NextDouble() * 3.0:F1}/10"; + int mins = 90 + rng.Next(60); + string duration = $"{mins / 60}h {mins % 60}m"; + + YearLabel.Text = year; + RatingLabel.Text = rating; + DurationLabel.Text = duration; + + try + { + var uri = new Uri(MovieAssets[imgIdx]); + HeroBg.Background = new ImageBrush(new Bitmap(AssetLoader.Open(uri))) + { + Stretch = Stretch.UniformToFill, + }; + } + catch + { + HeroBg.Background = new SolidColorBrush(Color.Parse("#111111")); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml new file mode 100644 index 00000000..4f186e91 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml @@ -0,0 +1,630 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml.cs new file mode 100644 index 00000000..81702bf6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml.cs @@ -0,0 +1,21 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class AvaloniaFlixHomeView : UserControl +{ + public Action? MovieSelected { get; set; } + public Action? SearchRequested { get; set; } + + public AvaloniaFlixHomeView() => InitializeComponent(); + + void OnMovieClick(object? sender, RoutedEventArgs e) + { + string title = "Cyber Dune"; + if (sender is Button btn && btn.Tag is string tag) + title = tag; + MovieSelected?.Invoke(title); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml new file mode 100644 index 00000000..d4c53652 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml.cs new file mode 100644 index 00000000..aac3069b --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages; + +public partial class AvaloniaFlixSearchView : UserControl +{ + public Action? CloseRequested { get; set; } + public Action? MovieSelected { get; set; } + + public AvaloniaFlixSearchView() => InitializeComponent(); + + void OnCloseClick(object? sender, RoutedEventArgs e) + { + if (CloseRequested != null) + { + CloseRequested(); + } + else + { + var nav = this.FindAncestorOfType(); + _ = nav?.PopModalAsync() ?? Task.CompletedTask; + } + } + + void OnMovieClick(object? sender, RoutedEventArgs e) + { + string title = "Neon Horizon"; + if (sender is Button btn && btn.Tag is string tag) + title = tag; + MovieSelected?.Invoke(title); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml new file mode 100644 index 00000000..8810c577 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml.cs new file mode 100644 index 00000000..c8910584 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml.cs @@ -0,0 +1,16 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class CurvedHeaderHomeScrollView : UserControl +{ + public Action? NavigateRequested { get; set; } + + public CurvedHeaderHomeScrollView() => InitializeComponent(); + + void OnShopNowClick(object? sender, RoutedEventArgs e) => NavigateRequested?.Invoke(); + + void OnProductClick(object? sender, RoutedEventArgs e) => NavigateRequested?.Invoke(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderProfileScrollView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderProfileScrollView.xaml new file mode 100644 index 00000000..ecfeb1bb --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/CurvedHeaderProfileScrollView.xaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs new file mode 100644 index 00000000..3ccdcaef --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Shapes; +using Avalonia.Controls.Templates; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Styling; + +namespace ControlCatalog.Pages; + +public partial class LAvenirAppPage : UserControl +{ + static readonly Color Primary = Color.Parse("#4b2bee"); + static readonly Color BgDark = Color.Parse("#131022"); + static readonly Color BgLight = Color.Parse("#f6f6f8"); + static readonly Color TextDark = Color.Parse("#1e293b"); + static readonly Color TextMuted = Color.Parse("#94a3b8"); + static readonly Color BorderLight = Color.Parse("#e2e8f0"); + + NavigationPage? _navPage; + DrawerPage? _drawerPage; + ScrollViewer? _infoPanel; + + public LAvenirAppPage() + { + InitializeComponent(); + + _navPage = this.FindControl("NavPage"); + _drawerPage = this.FindControl("DrawerPageControl"); + + if (_navPage != null) + _ = _navPage.PushAsync(BuildMenuTabbedPage()); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + _infoPanel = this.FindControl("InfoPanel"); + UpdateInfoPanelVisibility(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoPanelVisibility(); + } + + void UpdateInfoPanelVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 650; + } + + TabbedPage BuildMenuTabbedPage() + { + var tp = new TabbedPage + { + Background = new SolidColorBrush(BgLight), + TabPlacement = TabPlacement.Bottom, + PageTransition = new PageSlide(TimeSpan.FromMilliseconds(200)), + }; + tp.Resources["TabItemHeaderFontSize"] = 12.0; + tp.Resources["TabbedPageTabStripBackground"] = Brushes.White; + tp.Resources["TabbedPageTabStripBorderThickness"] = new Thickness(0, 1, 0, 0); + tp.Resources["TabbedPageTabStripBorderBrush"] = new SolidColorBrush(BorderLight); + tp.Resources["TabbedPageTabItemHeaderForegroundSelected"] = new SolidColorBrush(Primary); + tp.Resources["TabbedPageTabItemHeaderForegroundUnselected"] = new SolidColorBrush(TextMuted); + + tp.IndicatorTemplate = new FuncDataTemplate((_, _) => + new Ellipse + { + Width = 5, Height = 5, + Margin = new Thickness(0, 10, 0, 0), + HorizontalAlignment = HorizontalAlignment.Center, + Fill = new SolidColorBrush(Primary), + }); + + tp.Header = new TextBlock + { + Text = "L'Avenir", + FontSize = 18, + FontWeight = FontWeight.Bold, + Foreground = new SolidColorBrush(TextDark), + VerticalAlignment = VerticalAlignment.Center, + TextAlignment = TextAlignment.Center, + }; + + NavigationPage.SetTopCommandBar(tp, new Button + { + Width = 40, + Height = 40, + CornerRadius = new CornerRadius(12), + Background = Brushes.Transparent, + Foreground = new SolidColorBrush(TextDark), + Padding = new Thickness(8), + BorderThickness = new Thickness(0), + Content = new PathIcon + { + Data = Geometry.Parse("M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"), + Width = 18, + Height = 18, + }, + VerticalAlignment = VerticalAlignment.Center, + }); + + var menuView = new LAvenirMenuView(); + menuView.DishSelected = PushDishDetail; + + var menuPage = new ContentPage + { + Content = menuView, + Background = new SolidColorBrush(BgLight), + Header = "Menu", + Icon = new PathIcon { Data = Geometry.Parse("M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2v7zm5-3v8h2.5v8H21V2c-2.76 0-5 2.24-5 4z") }, + }; + + var reservationsPage = new ContentPage + { + Content = new LAvenirReservationsView(), + Background = new SolidColorBrush(BgLight), + Header = "Reservations", + Icon = new PathIcon { Data = Geometry.Parse("M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM9 10H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2z") }, + }; + + var profilePage = new ContentPage + { + Content = new LAvenirProfileView(), + Background = new SolidColorBrush(BgLight), + Header = "Profile", + Icon = new PathIcon { Data = Geometry.Parse("M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z") }, + }; + + tp.Pages = new ObservableCollection { menuPage, reservationsPage, profilePage }; + return tp; + } + + async void PushDishDetail(string name, string price, string description, string imageFile) + { + if (_navPage == null) return; + + var detail = new ContentPage + { + Content = new LAvenirDishDetailView(name, price, description, imageFile), + Background = new SolidColorBrush(BgDark), + Header = name, + }; + NavigationPage.SetBottomCommandBar(detail, BuildFloatingBar(price)); + + _navPage.Background = new SolidColorBrush(BgDark); + _navPage.Resources["NavigationBarBackground"] = new SolidColorBrush(BgDark); + _navPage.Resources["NavigationBarForeground"] = Brushes.White; + + detail.NavigatedFrom += (_, _) => + { + if (_navPage != null) + { + _navPage.Background = new SolidColorBrush(BgLight); + _navPage.Resources["NavigationBarBackground"] = new SolidColorBrush(BgLight); + _navPage.Resources["NavigationBarForeground"] = new SolidColorBrush(TextDark); + } + }; + + await _navPage.PushAsync(detail); + } + + Border BuildFloatingBar(string price) + { + var bar = new Border + { + CornerRadius = new CornerRadius(16), + Background = new SolidColorBrush(Color.FromArgb(178, BgDark.R, BgDark.G, BgDark.B)), + BorderBrush = new SolidColorBrush(Color.FromArgb(51, 255, 255, 255)), + BorderThickness = new Thickness(1), + Padding = new Thickness(16, 12), + Margin = new Thickness(16, 8, 16, 8), + }; + + var barGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto") }; + + var info = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; + info.Children.Add(new TextBlock + { + Text = "Add to Order", + FontSize = 14, + FontWeight = FontWeight.Bold, + Foreground = Brushes.White, + }); + info.Children.Add(new TextBlock + { + Text = price, + FontSize = 12, + FontWeight = FontWeight.Medium, + Foreground = new SolidColorBrush(TextMuted), + }); + barGrid.Children.Add(info); + + var addBtn = new Button + { + Content = "Add", + Width = 80, + Height = 40, + CornerRadius = new CornerRadius(10), + Background = new SolidColorBrush(Primary), + Foreground = Brushes.White, + FontWeight = FontWeight.Bold, + FontSize = 14, + HorizontalContentAlignment = HorizontalAlignment.Center, + VerticalContentAlignment = VerticalAlignment.Center, + }; + var hoverStyle = new Style(x => x.OfType + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirReservationsView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirReservationsView.xaml.cs new file mode 100644 index 00000000..5ea6d72c --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/LAvenirReservationsView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class LAvenirReservationsView : UserControl +{ + public LAvenirReservationsView() => InitializeComponent(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml new file mode 100644 index 00000000..f519b418 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseHomeView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseHomeView.xaml.cs new file mode 100644 index 00000000..5f98cee0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseHomeView.xaml.cs @@ -0,0 +1,25 @@ +using System; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class PulseHomeView : UserControl +{ + public Action? WorkoutDetailRequested { get; set; } + + public PulseHomeView() => InitializeComponent(); + + void OnRecCard1Pressed(object? sender, PointerPressedEventArgs e) => + WorkoutDetailRequested?.Invoke(); + + void OnRecCard2Pressed(object? sender, PointerPressedEventArgs e) => + WorkoutDetailRequested?.Invoke(); + + void OnRecCard3Pressed(object? sender, PointerPressedEventArgs e) => + WorkoutDetailRequested?.Invoke(); + + void OnPlayButtonClicked(object? sender, RoutedEventArgs e) => + WorkoutDetailRequested?.Invoke(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseLoginView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseLoginView.xaml new file mode 100644 index 00000000..23090904 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseLoginView.xaml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutDetailView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutDetailView.xaml.cs new file mode 100644 index 00000000..7999d3d7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutDetailView.xaml.cs @@ -0,0 +1,15 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class PulseWorkoutDetailView : UserControl +{ + public Action? BackRequested { get; set; } + + public PulseWorkoutDetailView() => InitializeComponent(); + + void OnBackClicked(object? sender, RoutedEventArgs e) => + BackRequested?.Invoke(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml new file mode 100644 index 00000000..c1f9659b --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml @@ -0,0 +1,290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml.cs new file mode 100644 index 00000000..fec95be3 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PulseWorkoutsView : UserControl +{ + public PulseWorkoutsView() => InitializeComponent(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml new file mode 100644 index 00000000..23278161 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml.cs new file mode 100644 index 00000000..6e194cf1 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml.cs @@ -0,0 +1,306 @@ +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingAppPage : UserControl +{ + static readonly Color BgColor = Color.Parse("#120a1f"); + static readonly Color SurfaceColor = Color.Parse("#2d1b4e"); + static readonly Color CyanColor = Color.Parse("#00ffff"); + static readonly Color YellowColor = Color.Parse("#ffff00"); + static readonly Color MutedColor = Color.Parse("#7856a8"); + static readonly Color TextColor = Color.Parse("#e0d0ff"); + + NavigationPage? _nav; + ScrollViewer? _infoPanel; + + public RetroGamingAppPage() + { + InitializeComponent(); + + _nav = this.FindControl("RetroNav"); + if (_nav != null) + _ = _nav.PushAsync(BuildHomePage()); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + _infoPanel = this.FindControl("InfoPanel"); + UpdateInfoPanelVisibility(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoPanelVisibility(); + } + + void UpdateInfoPanelVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 650; + } + + ContentPage BuildHomePage() + { + var page = new ContentPage { Background = new SolidColorBrush(BgColor) }; + page.Header = BuildPixelArcadeLogo(); + NavigationPage.SetTopCommandBar(page, BuildNavBarRight()); + + var panel = new Panel(); + panel.Children.Add(BuildHomeTabbedPage()); + panel.Children.Add(BuildSearchFab()); + + page.Content = panel; + return page; + } + + static Control BuildPixelArcadeLogo() + { + var row = new StackPanel + { + Orientation = Avalonia.Layout.Orientation.Horizontal, + Spacing = 10, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + }; + + var iconPanel = new Grid { Width = 36, Height = 30 }; + iconPanel.Children.Add(new Border + { + Width = 36, Height = 20, CornerRadius = new CornerRadius(3), + Background = new SolidColorBrush(Color.Parse("#cc44dd")), + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, + }); + iconPanel.Children.Add(new Border + { + Width = 9, Height = 9, + Background = new SolidColorBrush(SurfaceColor), + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, + Margin = new Thickness(4, 0, 0, 6), + }); + iconPanel.Children.Add(new Border + { + Width = 9, Height = 9, + Background = new SolidColorBrush(SurfaceColor), + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, + Margin = new Thickness(0, 0, 4, 6), + }); + row.Children.Add(iconPanel); + + var textStack = new StackPanel { Spacing = 1 }; + textStack.Children.Add(new TextBlock + { + Text = "PIXEL", + FontFamily = new FontFamily("Courier New, monospace"), + FontSize = 14, FontWeight = FontWeight.Bold, + Foreground = new SolidColorBrush(YellowColor), LineHeight = 16, + }); + textStack.Children.Add(new TextBlock + { + Text = "ARCADE", + FontFamily = new FontFamily("Courier New, monospace"), + FontSize = 14, FontWeight = FontWeight.Bold, + Foreground = new SolidColorBrush(YellowColor), LineHeight = 16, + }); + row.Children.Add(textStack); + return row; + } + + static Control BuildNavBarRight() + { + var row = new StackPanel + { + Orientation = Avalonia.Layout.Orientation.Horizontal, + Spacing = 10, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + }; + row.Children.Add(new PathIcon + { + Width = 16, Height = 16, + Foreground = new SolidColorBrush(TextColor), + Data = Geometry.Parse("M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"), + }); + var avatar = new Border + { + Width = 26, Height = 26, + CornerRadius = new CornerRadius(0), + ClipToBounds = true, + Background = new SolidColorBrush(SurfaceColor), + BorderBrush = new SolidColorBrush(MutedColor), + BorderThickness = new Thickness(1), + }; + avatar.Child = new TextBlock + { + Text = "P1", + FontFamily = new FontFamily("Courier New, monospace"), + FontSize = 7, FontWeight = FontWeight.Bold, + Foreground = new SolidColorBrush(CyanColor), + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + }; + row.Children.Add(avatar); + return row; + } + + TabbedPage BuildHomeTabbedPage() + { + var tp = new TabbedPage + { + Background = new SolidColorBrush(BgColor), + TabPlacement = TabPlacement.Bottom, + PageTransition = new PageSlide(TimeSpan.FromMilliseconds(250)), + }; + tp.Resources["TabItemHeaderFontSize"] = 12.0; + tp.Resources["TabbedPageTabStripBackground"] = new SolidColorBrush(SurfaceColor); + tp.Resources["TabbedPageTabItemHeaderForegroundSelected"] = new SolidColorBrush(Color.Parse("#ad2bee")); + tp.Resources["TabbedPageTabItemHeaderForegroundUnselected"] = new SolidColorBrush(MutedColor); + + var homeView = new RetroGamingHomeView(); + homeView.GameSelected = PushDetailPage; + + var homeTab = new ContentPage + { + Header = "Home", + Icon = new PathIcon { Data = Geometry.Parse("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z") }, + Background = new SolidColorBrush(BgColor), + Content = homeView, + }; + + var gamesView = new RetroGamingGamesView(); + gamesView.GameSelected = PushDetailPage; + + var gamesTab = new ContentPage + { + Header = "Games", + Icon = new PathIcon { Data = Geometry.Parse("M7.97,16L5,19C4.67,19.3 4.23,19.5 3.75,19.5A1.75,1.75 0 0,1 2,17.75V17.5L3,10.12C3.21,7.81 5.14,6 7.5,6H16.5C18.86,6 20.79,7.81 21,10.12L22,17.5V17.75A1.75,1.75 0 0,1 20.25,19.5C19.77,19.5 19.33,19.3 19,19L16.03,16H7.97M7,9V11H5V13H7V15H9V13H11V11H9V9H7M14.5,12A1.5,1.5 0 0,0 13,13.5A1.5,1.5 0 0,0 14.5,15A1.5,1.5 0 0,0 16,13.5A1.5,1.5 0 0,0 14.5,12M17.5,9A1.5,1.5 0 0,0 16,10.5A1.5,1.5 0 0,0 17.5,12A1.5,1.5 0 0,0 19,10.5A1.5,1.5 0 0,0 17.5,9Z") }, + Background = new SolidColorBrush(BgColor), + Content = gamesView, + }; + + var favTab = new ContentPage + { + Header = "Favorites", + Icon = new PathIcon { Data = Geometry.Parse("M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z") }, + Background = new SolidColorBrush(BgColor), + Content = new RetroGamingFavoritesView(), + }; + + var profileTab = new ContentPage + { + Header = "Profile", + Icon = new PathIcon { Data = Geometry.Parse("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z") }, + Background = new SolidColorBrush(BgColor), + Content = new RetroGamingProfileView(), + }; + + tp.Pages = new ObservableCollection { homeTab, gamesTab, favTab, profileTab }; + return tp; + } + + Control BuildSearchFab() + { + var fab = new Button + { + Width = 50, Height = 50, + CornerRadius = new CornerRadius(0), + Background = new SolidColorBrush(YellowColor), + Padding = new Thickness(0), + }; + fab.Classes.Add("retro-fab"); + fab.Content = new PathIcon + { + Width = 22, Height = 22, + Foreground = new SolidColorBrush(BgColor), + Data = Geometry.Parse("M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"), + }; + fab.Click += (_, _) => _ = _nav?.PushModalAsync(BuildSearchModal()); + + return new Border + { + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, + Margin = new Thickness(0, 0, 0, 35), + BoxShadow = new BoxShadows(new BoxShadow + { + Blur = 10, Spread = 1, + Color = Color.FromArgb(140, 255, 255, 0), + }), + Child = fab, + }; + } + + ContentPage BuildSearchModal() + { + var page = new ContentPage { Background = new SolidColorBrush(BgColor) }; + + var searchView = new RetroGamingSearchView(); + searchView.CloseRequested = () => _ = _nav?.PopModalAsync(); + searchView.GameSelected = async title => + { + await (_nav?.PopModalAsync() ?? System.Threading.Tasks.Task.CompletedTask); + PushDetailPage(title); + }; + + page.Content = searchView; + return page; + } + + async void PushDetailPage(string gameTitle) + { + if (_nav == null) return; + + var detailView = new RetroGamingDetailView(gameTitle); + + var page = new ContentPage + { + Background = new SolidColorBrush(BgColor), + Content = detailView, + }; + + NavigationPage.SetBarLayoutBehavior(page, BarLayoutBehavior.Overlay); + page.NavigatedTo += (_, _) => { if (_nav != null) _nav.Resources["NavigationBarBackground"] = Brushes.Transparent; }; + page.NavigatedFrom += (_, _) => { if (_nav != null) _nav.Resources["NavigationBarBackground"] = new SolidColorBrush(SurfaceColor); }; + + var cmdBar = new StackPanel + { + Orientation = Avalonia.Layout.Orientation.Horizontal, + Spacing = 4, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + }; + var heartBtn = new Button(); + heartBtn.Classes.Add("retro-icon-btn"); + heartBtn.Content = new PathIcon + { + Width = 16, Height = 16, + Foreground = new SolidColorBrush(Color.Parse("#ad2bee")), + Data = Geometry.Parse("M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z"), + }; + var shareBtn = new Button(); + shareBtn.Classes.Add("retro-icon-btn"); + shareBtn.Content = new PathIcon + { + Width = 16, Height = 16, + Foreground = new SolidColorBrush(TextColor), + Data = Geometry.Parse("M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.35C15.11,18.56 15.08,18.78 15.08,19C15.08,20.61 16.39,21.92 18,21.92C19.61,21.92 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z"), + }; + cmdBar.Children.Add(heartBtn); + cmdBar.Children.Add(shareBtn); + NavigationPage.SetTopCommandBar(page, cmdBar); + + await _nav.PushAsync(page); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml new file mode 100644 index 00000000..718e8137 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml.cs new file mode 100644 index 00000000..cf49850b --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingDetailView : UserControl +{ + static readonly Dictionary GameAssets = new() + { + { "Cyber Ninja 2084", "hero.jpg" }, + { "Pixel Quest", "pixel_quest.jpg" }, + { "Neon Racer", "neon_racer.jpg" }, + { "Dungeon Bit", "dungeon_bit.jpg" }, + { "Forest Spirit", "forest_spirit.jpg" }, + { "Cyber City", "cyber_city.jpg" }, + { "Neon Ninja", "neon_ninja.jpg" }, + { "Space Voids", "space_voids.jpg" }, + }; + + public RetroGamingDetailView() => InitializeComponent(); + + public RetroGamingDetailView(string gameTitle) + { + InitializeComponent(); + + DetailTitleText.Text = gameTitle.ToUpperInvariant(); + + var filename = GameAssets.TryGetValue(gameTitle, out var f) ? f + : (GameAssets.TryGetValue("Neon Ninja", out var fb) ? fb : null); + + if (filename != null) + { + try + { + var uri = new Uri($"avares://ControlCatalog/Assets/RetroGaming/{filename}"); + using var stream = AssetLoader.Open(uri); + var bmp = new Bitmap(stream); + DetailHeroImageBorder.Background = new ImageBrush(bmp) + { + Stretch = Stretch.UniformToFill, + }; + } + catch + { + SetFallbackBackground(); + } + } + else + { + SetFallbackBackground(); + } + } + + void SetFallbackBackground() + { + var grad = new LinearGradientBrush + { + StartPoint = new Avalonia.RelativePoint(0, 0, Avalonia.RelativeUnit.Relative), + EndPoint = new Avalonia.RelativePoint(1, 1, Avalonia.RelativeUnit.Relative), + }; + grad.GradientStops.Add(new GradientStop(Avalonia.Media.Color.Parse("#3d2060"), 0)); + grad.GradientStops.Add(new GradientStop(Avalonia.Media.Color.Parse("#120a1f"), 1)); + DetailHeroImageBorder.Background = grad; + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml new file mode 100644 index 00000000..ef11d4e7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml.cs new file mode 100644 index 00000000..fd67902a --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingFavoritesView : UserControl +{ + public RetroGamingFavoritesView() => InitializeComponent(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml new file mode 100644 index 00000000..a4e9f403 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml.cs new file mode 100644 index 00000000..6676b792 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml.cs @@ -0,0 +1,39 @@ +using System; +using Avalonia.Controls; +using Avalonia.Layout; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingGamesView : UserControl +{ + public Action? GameSelected { get; set; } + + public RetroGamingGamesView() + { + InitializeComponent(); + + GameCyberNinjaBtn.Click += (_, _) => GameSelected?.Invoke("Cyber Ninja 2084"); + GameNeonRacerBtn.Click += (_, _) => GameSelected?.Invoke("Neon Racer"); + GameDungeonBitBtn.Click += (_, _) => GameSelected?.Invoke("Dungeon Bit"); + GameForestSpiritBtn.Click += (_, _) => GameSelected?.Invoke("Forest Spirit"); + GamePixelQuestBtn.Click += (_, _) => GameSelected?.Invoke("Pixel Quest"); + GameSpaceVoidsBtn.Click += (_, _) => GameSelected?.Invoke("Space Voids"); + GameCyberCityBtn.Click += (_, _) => GameSelected?.Invoke("Cyber City"); + + GamesGrid.SizeChanged += OnGridSizeChanged; + } + + void OnGridSizeChanged(object? sender, SizeChangedEventArgs e) + { + const double defaultWidth = 145; + var available = GamesGrid.Bounds.Width; + if (available <= 0) return; + + bool singleColumn = available < defaultWidth * 2; + foreach (var child in GamesGrid.Children) + { + if (child is Button btn && btn.Content is Border card) + card.Width = singleColumn ? available : defaultWidth; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml new file mode 100644 index 00000000..bf5d1125 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml @@ -0,0 +1,346 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml.cs new file mode 100644 index 00000000..d1693f3c --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml.cs @@ -0,0 +1,22 @@ +using System; +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingHomeView : UserControl +{ + public Action? GameSelected { get; set; } + + public RetroGamingHomeView() + { + InitializeComponent(); + + HeroPlayBtn.Click += (_, _) => GameSelected?.Invoke("Cyber Ninja 2084"); + ContinuePixelQuestBtn.Click += (_, _) => GameSelected?.Invoke("Pixel Quest"); + ContinueSpaceVoidsBtn.Click += (_, _) => GameSelected?.Invoke("Space Voids"); + NewReleaseNeonRacerBtn.Click += (_, _) => GameSelected?.Invoke("Neon Racer"); + NewReleaseDungeonBitBtn.Click += (_, _) => GameSelected?.Invoke("Dungeon Bit"); + NewReleaseForestSpiritBtn.Click += (_, _) => GameSelected?.Invoke("Forest Spirit"); + NewReleaseCyberCityBtn.Click += (_, _) => GameSelected?.Invoke("Cyber City"); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml new file mode 100644 index 00000000..17d7ff2d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml.cs new file mode 100644 index 00000000..f2284bdc --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingProfileView : UserControl +{ + public RetroGamingProfileView() => InitializeComponent(); +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml new file mode 100644 index 00000000..92389efe --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml.cs new file mode 100644 index 00000000..c5db19c0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml.cs @@ -0,0 +1,24 @@ +using System; +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingSearchView : UserControl +{ + public Action? CloseRequested { get; set; } + public Action? GameSelected { get; set; } + + public RetroGamingSearchView() + { + InitializeComponent(); + + CloseBtn.Click += (_, _) => CloseRequested?.Invoke(); + SearchCyberNinjaBtn.Click += (_, _) => GameSelected?.Invoke("Cyber Ninja 2084"); + SearchNeonRacerBtn.Click += (_, _) => GameSelected?.Invoke("Neon Racer"); + SearchDungeonBitBtn.Click += (_, _) => GameSelected?.Invoke("Dungeon Bit"); + SearchForestSpiritBtn.Click += (_, _) => GameSelected?.Invoke("Forest Spirit"); + SearchPixelQuestBtn.Click += (_, _) => GameSelected?.Invoke("Pixel Quest"); + SearchSpaceVoidsBtn.Click += (_, _) => GameSelected?.Invoke("Space Voids"); + SearchCyberCityBtn.Click += (_, _) => GameSelected?.Invoke("Cyber City"); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/CompositeTransition.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/CompositeTransition.cs new file mode 100644 index 00000000..0cddd58e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/CompositeTransition.cs @@ -0,0 +1,120 @@ +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; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages +{ + /// + /// Example custom IPageTransition: horizontal slide combined with cross-fade. + /// Both pages slide and fade simultaneously for a smooth blended effect. + /// + public class CompositeTransition : IPageTransition + { + public CompositeTransition() { } + + public CompositeTransition(TimeSpan duration) + { + Duration = duration; + } + + public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(300); + public Easing TransitionEasing { get; set; } = new LinearEasing(); + + public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return; + + var tasks = new List(); + var parent = GetVisualParent(from, to); + var distance = parent.Bounds.Width > 0 ? parent.Bounds.Width : 500d; + + if (from != null) + { + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = TransitionEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(Visual.OpacityProperty, 1d) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(TranslateTransform.XProperty, forward ? -distance : distance), + new Setter(Visual.OpacityProperty, 0d) + } + } + } + }; + tasks.Add(anim.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = TransitionEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(TranslateTransform.XProperty, forward ? distance : -distance), + new Setter(Visual.OpacityProperty, 0d) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(Visual.OpacityProperty, 1d) + } + } + } + }; + tasks.Add(anim.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (from != null && !cancellationToken.IsCancellationRequested) + from.IsVisible = false; + } + + private static Visual GetVisualParent(Visual? from, Visual? to) + { + var p1 = (from ?? to)!.GetVisualParent(); + if (from != null && to != null && + !ReferenceEquals(from.GetVisualParent(), to.GetVisualParent())) + throw new ArgumentException("Transition elements have different parents."); + return p1 ?? throw new ArgumentException("Transition elements have no parent."); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/FadeThroughTransition.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/FadeThroughTransition.cs new file mode 100644 index 00000000..ece9b1a1 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/FadeThroughTransition.cs @@ -0,0 +1,137 @@ +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 +{ + /// + /// Example custom IPageTransition: a "fade through" with scale. + /// The outgoing page fades out while scaling down; the incoming page fades in while + /// scaling up, producing a smooth depth-aware transition. + /// + public class FadeThroughTransition : IPageTransition + { + public FadeThroughTransition() { } + + public FadeThroughTransition(TimeSpan duration) + { + Duration = duration; + } + + public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(300); + public Easing FadeEasing { get; set; } = new CubicEaseOut(); + + public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return; + + var tasks = new List(); + + if (from != null) + { + from.RenderTransformOrigin = RelativePoint.Center; + from.RenderTransform = new ScaleTransform(1, 1); + } + + if (to != null) + { + to.RenderTransformOrigin = RelativePoint.Center; + to.RenderTransform = new ScaleTransform(1, 1); + to.Opacity = 0; + } + + if (from != null) + { + var outAnim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = FadeEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(Visual.OpacityProperty, 1d), + new Setter(ScaleTransform.ScaleXProperty, 1d), + new Setter(ScaleTransform.ScaleYProperty, 1d) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(Visual.OpacityProperty, 0d), + new Setter(ScaleTransform.ScaleXProperty, forward ? 0.92 : 1.08), + new Setter(ScaleTransform.ScaleYProperty, forward ? 0.92 : 1.08) + } + } + } + }; + tasks.Add(outAnim.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + + var inAnim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = FadeEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(Visual.OpacityProperty, 0d), + new Setter(ScaleTransform.ScaleXProperty, forward ? 1.08 : 0.92), + new Setter(ScaleTransform.ScaleYProperty, forward ? 1.08 : 0.92) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(Visual.OpacityProperty, 1d), + new Setter(ScaleTransform.ScaleXProperty, 1d), + new Setter(ScaleTransform.ScaleYProperty, 1d) + } + } + } + }; + tasks.Add(inAnim.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (to != null && !cancellationToken.IsCancellationRequested) + { + to.Opacity = 1; + to.RenderTransform = null; + } + + if (from != null) + { + if (!cancellationToken.IsCancellationRequested) + from.IsVisible = false; + from.Opacity = 1; + from.RenderTransform = null; + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/PageSlideTransition.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/PageSlideTransition.cs new file mode 100644 index 00000000..dfbe8478 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/PageSlideTransition.cs @@ -0,0 +1,97 @@ +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; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages +{ + /// + /// Example custom IPageTransition: a directional page slide. + /// Both pages slide together in the specified axis direction. + /// Demonstrates how to implement a custom horizontal or vertical slide from scratch. + /// + public class PageSlideTransition : IPageTransition + { + public enum SlideAxis { Horizontal, Vertical } + + public PageSlideTransition() { } + + public PageSlideTransition(TimeSpan duration, SlideAxis axis = SlideAxis.Horizontal) + { + Duration = duration; + Axis = axis; + } + + public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(300); + public SlideAxis Axis { get; set; } = SlideAxis.Horizontal; + public Easing SlideEasing { get; set; } = new LinearEasing(); + + public Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) => + Axis == SlideAxis.Horizontal + ? StartAxis(from, to, forward, cancellationToken, TranslateTransform.XProperty, () => GetVisualParent(from, to).Bounds.Width) + : StartAxis(from, to, forward, cancellationToken, TranslateTransform.YProperty, () => GetVisualParent(from, to).Bounds.Height); + + private async Task StartAxis( + Visual? from, Visual? to, bool forward, CancellationToken cancellationToken, + Avalonia.AvaloniaProperty prop, Func getDistance) + { + if (cancellationToken.IsCancellationRequested) + return; + var tasks = new List(); + var distance = getDistance() is > 0 and var d ? d : 500d; + + if (from != null) + { + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = SlideEasing, + Duration = Duration, + Children = + { + new KeyFrame { Cue = new Cue(0d), Setters = { new Setter(prop, 0d) } }, + new KeyFrame { Cue = new Cue(1d), Setters = { new Setter(prop, forward ? -distance : distance) } } + } + }; + tasks.Add(anim.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = SlideEasing, + Duration = Duration, + Children = + { + new KeyFrame { Cue = new Cue(0d), Setters = { new Setter(prop, forward ? distance : -distance) } }, + new KeyFrame { Cue = new Cue(1d), Setters = { new Setter(prop, 0d) } } + } + }; + tasks.Add(anim.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (from != null && !cancellationToken.IsCancellationRequested) + from.IsVisible = false; + } + + private static Visual GetVisualParent(Visual? from, Visual? to) + { + var p1 = (from ?? to)!.GetVisualParent(); + if (from != null && to != null && + !ReferenceEquals(from.GetVisualParent(), to.GetVisualParent())) + throw new ArgumentException("Transition elements have different parents."); + return p1 ?? throw new ArgumentException("Transition elements have no parent."); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/ParallaxSlideTransition.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/ParallaxSlideTransition.cs new file mode 100644 index 00000000..7326c6e9 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPage/Transitions/ParallaxSlideTransition.cs @@ -0,0 +1,121 @@ +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; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages +{ + /// + /// Example custom IPageTransition: a parallax slide. + /// The incoming page slides full-width from the right while the outgoing page shifts ~30% + /// to the left with a subtle opacity fade, producing a depth-layered push effect. + /// + public class ParallaxSlideTransition : IPageTransition + { + public ParallaxSlideTransition() { } + + public ParallaxSlideTransition(TimeSpan duration) + { + Duration = duration; + } + + public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(350); + public Easing SlideEasing { get; set; } = new CubicEaseOut(); + + public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return; + + var tasks = new List(); + var parent = GetVisualParent(from, to); + var distance = parent.Bounds.Width > 0 ? parent.Bounds.Width : 500d; + + if (from != null) + { + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = SlideEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(Visual.OpacityProperty, 1d) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(TranslateTransform.XProperty, forward ? -distance * 0.3 : distance), + new Setter(Visual.OpacityProperty, forward ? 0.7 : 1d) + } + } + } + }; + tasks.Add(anim.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = SlideEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(TranslateTransform.XProperty, forward ? distance : -distance * 0.3), + new Setter(Visual.OpacityProperty, forward ? 1d : 0.7) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(Visual.OpacityProperty, 1d) + } + } + } + }; + tasks.Add(anim.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (from != null && !cancellationToken.IsCancellationRequested) + from.IsVisible = false; + } + + private static Visual GetVisualParent(Visual? from, Visual? to) + { + var p1 = (from ?? to)!.GetVisualParent(); + if (from != null && to != null && + !ReferenceEquals(from.GetVisualParent(), to.GetVisualParent())) + throw new ArgumentException("Transition elements have different parents."); + return p1 ?? throw new ArgumentException("Transition elements have no parent."); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NavigationPerformanceMonitorHelper.cs b/samples/AvaloniaControlCatalog/Pages/NavigationPerformanceMonitorHelper.cs new file mode 100644 index 00000000..3178c35f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NavigationPerformanceMonitorHelper.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Threading; + +namespace ControlCatalog.Pages +{ + /// + /// Shared helpers for the performance-monitor demo pages + /// (NavigationPage, TabbedPage, DrawerPage, ContentPage). + /// + internal sealed class NavigationPerformanceMonitorHelper + { + internal static readonly IBrush PositiveDeltaBrush = new SolidColorBrush(Color.Parse("#D32F2F")); + internal static readonly IBrush NegativeDeltaBrush = new SolidColorBrush(Color.Parse("#388E3C")); + internal static readonly IBrush ZeroDeltaBrush = new SolidColorBrush(Color.Parse("#757575")); + internal static readonly IBrush CurrentBorderBrush = new SolidColorBrush(Color.Parse("#0078D4")); + internal static readonly IBrush DefaultBorderBrush = new SolidColorBrush(Color.Parse("#CCCCCC")); + + private readonly List> _trackedPages = new(); + private double _previousHeapMB; + private DispatcherTimer? _autoRefreshTimer; + + internal readonly Stopwatch OpStopwatch = new(); + internal int TotalCreated; + + /// + /// Track a newly-created page via WeakReference and increment TotalCreated. + /// + internal void TrackPage(Page page) + { + TotalCreated++; + _trackedPages.Add(new WeakReference(page)); + } + + /// + /// Count live (not yet GC'd) tracked page instances. + /// + internal int CountLiveInstances() + { + int alive = 0; + for (int i = _trackedPages.Count - 1; i >= 0; i--) + { + if (_trackedPages[i].TryGetTarget(out _)) + alive++; + else + _trackedPages.RemoveAt(i); + } + return alive; + } + + /// + /// Update heap and delta text blocks. Call from RefreshAll(). + /// + internal void UpdateHeapDelta(TextBlock heapText, TextBlock deltaText) + { + var heapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0); + heapText.Text = $"Managed Heap: {heapMB:##0.0} MB"; + + var delta = heapMB - _previousHeapMB; + if (Math.Abs(delta) < 0.05) + { + deltaText.Text = "(no change)"; + deltaText.Foreground = ZeroDeltaBrush; + } + else + { + var sign = delta > 0 ? "+" : ""; + deltaText.Text = $"({sign}{delta:0.0} MB)"; + deltaText.Foreground = delta > 0 ? PositiveDeltaBrush : NegativeDeltaBrush; + } + _previousHeapMB = heapMB; + } + + /// + /// Initialize previous heap baseline. + /// + internal void InitHeap() + { + _previousHeapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0); + } + + /// + /// Stop the stopwatch and write elapsed ms to the given TextBlock. + /// + internal void StopMetrics(TextBlock lastOpText) + { + if (!OpStopwatch.IsRunning) return; + OpStopwatch.Stop(); + lastOpText.Text = $"Last Op: {OpStopwatch.ElapsedMilliseconds} ms"; + } + + /// + /// Force full GC, then invoke the refresh callback. + /// + internal void ForceGC(Action refresh) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + refresh(); + } + + /// + /// Start a 2-second auto-refresh timer. + /// + internal void StartAutoRefresh(Action refresh) + { + if (_autoRefreshTimer != null) return; + _autoRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) }; + _autoRefreshTimer.Tick += (_, _) => refresh(); + _autoRefreshTimer.Start(); + } + + /// + /// Stop the auto-refresh timer. + /// + internal void StopAutoRefresh() + { + _autoRefreshTimer?.Stop(); + _autoRefreshTimer = null; + } + + /// + /// Toggle auto-refresh based on a CheckBox. + /// + internal void OnAutoRefreshChanged(CheckBox check, Action refresh) + { + if (check.IsChecked == true) + StartAutoRefresh(refresh); + else + StopAutoRefresh(); + } + + /// + /// Append a timestamped log entry to a StackPanel inside a ScrollViewer. + /// + internal void LogOperation(string action, string detail, + StackPanel logPanel, ScrollViewer logScroll, string? extraInfo = null) + { + var heapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0); + var timing = OpStopwatch.ElapsedMilliseconds; + var extra = extraInfo != null ? $" {extraInfo}," : ""; + + logPanel.Children.Add(new TextBlock + { + Text = $"{DateTime.Now:HH:mm:ss} [{action}] {detail} —{extra} heap {heapMB:##0.0} MB, {timing} ms", + FontSize = 10, + FontFamily = new FontFamily("Cascadia Mono,Consolas,Menlo,monospace"), + Padding = new Thickness(6, 2), + TextTrimming = TextTrimming.CharacterEllipsis, + }); + logScroll.ScrollToEnd(); + } + + /// + /// Build a tracked ContentPage with a 50 KB dummy allocation. + /// + internal ContentPage BuildTrackedPage(string title, int index, int allocBytes = 51200) + { + var page = NavigationDemoHelper.MakePage(title, + $"Stack position #{index}\nPush more pages ...", index); + page.Tag = new byte[allocBytes]; + TrackPage(page); + return page; + } + + /// + /// Create a reusable stack/history row (badge + title + label). + /// + internal static (Border Container, Border Badge, TextBlock IndexText, + TextBlock TitleText, TextBlock BadgeText) CreateStackRow() + { + var indexText = new TextBlock + { + FontSize = 10, FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + }; + var badge = new Border + { + Width = 22, Height = 22, + CornerRadius = new CornerRadius(11), + VerticalAlignment = VerticalAlignment.Center, + Child = indexText, + }; + var titleText = new TextBlock + { + VerticalAlignment = VerticalAlignment.Center, + TextTrimming = TextTrimming.CharacterEllipsis, + Margin = new Thickness(6, 0, 0, 0), + }; + var badgeText = new TextBlock + { + FontSize = 10, Opacity = 0.5, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(4, 0, 0, 0), + IsVisible = false, + }; + + var row = new DockPanel(); + row.Children.Add(badge); + row.Children.Add(titleText); + row.Children.Add(badgeText); + + var container = new Border + { + CornerRadius = new CornerRadius(6), + Padding = new Thickness(8, 6), + Child = row, + }; + + return (container, badge, indexText, titleText, badgeText); + } + + /// + /// Update a stack row with page data. + /// + internal static void UpdateStackRow( + (Border Container, Border Badge, TextBlock IndexText, + TextBlock TitleText, TextBlock BadgeText) row, + int stackIndex, string title, bool isCurrent, bool isRoot) + { + row.Badge.Background = NavigationDemoHelper.GetPageBrush(stackIndex); + row.IndexText.Text = (stackIndex + 1).ToString(); + row.TitleText.Text = title; + row.TitleText.FontWeight = isCurrent ? FontWeight.SemiBold : FontWeight.Normal; + + string? label = isCurrent ? "current" : (isRoot ? "root" : null); + row.BadgeText.Text = label ?? ""; + row.BadgeText.IsVisible = label != null; + + row.Container.BorderBrush = isCurrent ? CurrentBorderBrush : DefaultBorderBrush; + row.Container.BorderThickness = new Thickness(isCurrent ? 2 : 1); + } + + /// + /// Sync a StackPanel of stack rows with data, growing/shrinking the row cache as needed. + /// + internal static void RefreshStackPanel( + StackPanel panel, + List<(Border Container, Border Badge, TextBlock IndexText, + TextBlock TitleText, TextBlock BadgeText)> rowCache, + IReadOnlyList stack, Page? currentPage) + { + int count = stack.Count; + + while (rowCache.Count < count) + rowCache.Add(CreateStackRow()); + + while (panel.Children.Count > count) + panel.Children.RemoveAt(panel.Children.Count - 1); + while (panel.Children.Count < count) + panel.Children.Add(rowCache[panel.Children.Count].Container); + + for (int displayIdx = 0; displayIdx < count; displayIdx++) + { + int stackIdx = count - 1 - displayIdx; + var page = stack[stackIdx]; + bool isCurrent = ReferenceEquals(page, currentPage); + bool isRoot = stackIdx == 0; + + var row = rowCache[displayIdx]; + if (!ReferenceEquals(panel.Children[displayIdx], row.Container)) + panel.Children[displayIdx] = row.Container; + + UpdateStackRow(row, stackIdx, page.Header?.ToString() ?? "(untitled)", isCurrent, isRoot); + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/NotificationsPage.xaml b/samples/AvaloniaControlCatalog/Pages/NotificationsPage.xaml new file mode 100644 index 00000000..61d8afde --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/NotificationsPage.xaml @@ -0,0 +1,25 @@ + + + TopLevel bound notification manager. + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/OpenGlPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/OpenGlPage.xaml.cs new file mode 100644 index 00000000..76a79290 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/OpenGlPage.xaml.cs @@ -0,0 +1,82 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Controls; +using Avalonia.Rendering.Composition; +using ControlCatalog.Pages.OpenGl; + +// ReSharper disable StringLiteralTypo + +namespace ControlCatalog.Pages +{ + public partial class OpenGlPage : ContentPage + { + public OpenGlPage() + { + InitializeComponent(); + GL.Init(Knobs); + + AttachedToVisualTree += delegate + { + if (TopLevel.GetTopLevel(this) is Window) + Snapshot.IsVisible = true; + }; + } + + private async void SnapshotClick(object sender, RoutedEventArgs e) + { + var v = ElementComposition.GetElementVisual(this)!; + var snap = await v.Compositor.CreateCompositionVisualSnapshot(v, 1.5); + await new Window() + { + Content = new ScrollViewer() + { + HorizontalScrollBarVisibility = ScrollBarVisibility.Auto, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + Content = new Image() + { + Source = snap + } + } + }.ShowDialog((Window)TopLevel.GetTopLevel(this)!); + } + } + + public class OpenGlPageControl : OpenGlControlBase + { + private OpenGlContent _content = new(); + private GlPageKnobs? _knobs; + + public void Init(GlPageKnobs knobs) + { + _knobs = knobs; + _knobs.PropertyChanged += KnobsPropertyChanged; + } + + private void KnobsPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == GlPageKnobs.YawProperty + || change.Property == GlPageKnobs.RollProperty + || change.Property == GlPageKnobs.PitchProperty + || change.Property == GlPageKnobs.DiscoProperty) + RequestNextFrameRendering(); + } + + protected override unsafe void OnOpenGlInit(GlInterface GL) => _content.Init(GL, GlVersion); + + protected override void OnOpenGlDeinit(GlInterface GL) => _content.Deinit(GL); + + protected override unsafe void OnOpenGlRender(GlInterface gl, int fb) + { + if (_knobs == null) + return; + _content.OnOpenGlRender(gl, fb, new PixelSize((int)Bounds.Width, (int)Bounds.Height), + _knobs.Yaw, _knobs.Pitch, _knobs.Roll, _knobs.Disco); + if (_knobs.Disco > 0.01) + RequestNextFrameRendering(); + _knobs?.Info = _content.Info; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml new file mode 100644 index 00000000..b75b5c37 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs new file mode 100644 index 00000000..f42bb10c --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerCarouselPage : UserControl +{ + public PipsPagerCarouselPage() + { + InitializeComponent(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml new file mode 100644 index 00000000..3e32f253 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml.cs new file mode 100644 index 00000000..26682b9d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerCustomButtonThemesPage : UserControl +{ + public PipsPagerCustomButtonThemesPage() + { + InitializeComponent(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml new file mode 100644 index 00000000..260536d7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs new file mode 100644 index 00000000..a9276f11 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerCustomColorsPage : UserControl +{ + public PipsPagerCustomColorsPage() + { + InitializeComponent(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml new file mode 100644 index 00000000..fe748b24 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs new file mode 100644 index 00000000..cce9e6c5 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerCustomTemplatesPage : UserControl +{ + public PipsPagerCustomTemplatesPage() + { + InitializeComponent(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml new file mode 100644 index 00000000..a69c1016 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs new file mode 100644 index 00000000..d9716539 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs @@ -0,0 +1,29 @@ +using System.Collections.ObjectModel; +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerEventsPage : UserControl +{ + private readonly ObservableCollection _events = new(); + + public PipsPagerEventsPage() + { + InitializeComponent(); + + EventLog.ItemsSource = _events; + + EventPager.PropertyChanged += (_, e) => + { + if (e.Property != PipsPager.SelectedPageIndexProperty) + return; + + var newIndex = (int)e.NewValue!; + StatusText.Text = $"Selected: {newIndex}"; + _events.Insert(0, $"SelectedPageIndex changed to {newIndex}"); + + if (_events.Count > 20) + _events.RemoveAt(_events.Count - 1); + }; + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml new file mode 100644 index 00000000..5eead2fb --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs new file mode 100644 index 00000000..80a1569f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerGettingStartedPage : UserControl +{ + public PipsPagerGettingStartedPage() + { + InitializeComponent(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml new file mode 100644 index 00000000..5cc416d4 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs new file mode 100644 index 00000000..2dc936b5 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerLargeCollectionPage : UserControl +{ + public PipsPagerLargeCollectionPage() + { + InitializeComponent(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPagerPage.xaml b/samples/AvaloniaControlCatalog/Pages/PipsPagerPage.xaml new file mode 100644 index 00000000..aee21a7d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPagerPage.xaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PipsPagerPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PipsPagerPage.xaml.cs new file mode 100644 index 00000000..5a3b142f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PipsPagerPage.xaml.cs @@ -0,0 +1,54 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class PipsPagerPage : ContentPage + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + ("Getting Started", "First Look", + "Default PipsPager with horizontal and vertical orientation, with and without navigation buttons.", + () => new PipsPagerGettingStartedPage()), + + ("Features", "Carousel Integration", + "Bind SelectedPageIndex to a Carousel's SelectedIndex for two-way synchronized page navigation.", + () => new PipsPagerCarouselPage()), + ("Features", "Large Collections", + "Use MaxVisiblePips to limit visible indicators when the page count is large. Pips scroll automatically.", + () => new PipsPagerLargeCollectionPage()), + ("Features", "Events", + "Monitor SelectedPageIndex changes to react to user navigation.", + () => new PipsPagerEventsPage()), + + ("Appearance", "Custom Colors", + "Override pip indicator colors using resource keys for normal, selected, and hover states.", + () => new PipsPagerCustomColorsPage()), + ("Appearance", "Custom Button Themes", + "Replace the default chevron navigation buttons with custom button themes.", + () => new PipsPagerCustomButtonThemesPage()), + ("Appearance", "Custom Templates", + "Override pip item templates to create squares, pills, numbers, or any custom shape.", + () => new PipsPagerCustomTemplatesPage()), + + ("Showcases", "Care Companion", + "A health care onboarding flow using PipsPager as the page indicator for a CarouselPage.", + () => new CareCompanionAppPage()), + ("Showcases", "Sanctuary", + "A travel discovery app using PipsPager as the page indicator for a CarouselPage.", + () => new SanctuaryShowcasePage()), + }; + + public PipsPagerPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PlatformInfoPage.xaml b/samples/AvaloniaControlCatalog/Pages/PlatformInfoPage.xaml new file mode 100644 index 00000000..f86d53b7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PlatformInfoPage.xaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PlatformInfoPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PlatformInfoPage.xaml.cs new file mode 100644 index 00000000..db1bbef4 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PlatformInfoPage.xaml.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class PlatformInfoPage : ContentPage + { + public PlatformInfoPage() + { + InitializeComponent(); + DataContext = new PlatformInformationViewModel(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PointerCanvas.cs b/samples/AvaloniaControlCatalog/Pages/PointerCanvas.cs new file mode 100644 index 00000000..1311a1a1 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PointerCanvas.cs @@ -0,0 +1,236 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Threading; + +namespace ControlCatalog.Pages; + +public class PointerCanvas : Control +{ + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + private int _events; + private IDisposable? _statusUpdated; + private Dictionary _pointers = new(); + private PointerPointProperties? _lastProperties; + private PointerUpdateKind? _lastNonOtherUpdateKind; + class PointerPoints + { + struct CanvasPoint + { + public IBrush? Brush; + public Point Point; + public double Radius; + public double? Pressure; + } + + readonly CanvasPoint[] _points = new CanvasPoint[1000]; + int _index; + + public void Render(DrawingContext context, bool drawPoints) + { + CanvasPoint? prev = null; + for (var c = 0; c < _points.Length; c++) + { + var i = (c + _index) % _points.Length; + var pt = _points[i]; + var pressure = (pt.Pressure ?? prev?.Pressure ?? 0.5); + var thickness = pressure * 10; + var radius = pressure * pt.Radius; + + if (drawPoints) + { + if (pt.Brush != null) + { + context.DrawEllipse(pt.Brush, null, pt.Point, radius, radius); + } + } + else + { + if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null + && prev.Value.Pressure != null && pt.Pressure != null) + { + var linePen = new Pen(Brushes.Black, thickness, null, PenLineCap.Round, PenLineJoin.Round); + context.DrawLine(linePen, prev.Value.Point, pt.Point); + } + } + prev = pt; + } + + } + + void AddPoint(Point pt, IBrush brush, double radius, float? pressure = null) + { + _points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius, Pressure = pressure }; + _index = (_index + 1) % _points.Length; + } + + public void HandleEvent(PointerEventArgs e, Visual v) + { + e.Handled = true; + e.PreventGestureRecognition(); + var currentPoint = e.GetCurrentPoint(v); + if (e.RoutedEvent == PointerPressedEvent) + AddPoint(currentPoint.Position, Brushes.Green, 10); + else if (e.RoutedEvent == PointerReleasedEvent) + AddPoint(currentPoint.Position, Brushes.Red, 10); + else + { + var pts = e.GetIntermediatePoints(v); + for (var c = 0; c < pts.Count; c++) + { + var pt = pts[c]; + AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black, + c == pts.Count - 1 ? 5 : 2, pt.Properties.Pressure); + } + } + } + } + + private int _threadSleep; + public static readonly DirectProperty ThreadSleepProperty = + AvaloniaProperty.RegisterDirect(nameof(ThreadSleep), c => c.ThreadSleep, (c, v) => c.ThreadSleep = v); + + public int ThreadSleep + { + get => _threadSleep; + set => SetAndRaise(ThreadSleepProperty, ref _threadSleep, value); + } + + private bool _drawOnlyPoints; + public static readonly DirectProperty DrawOnlyPointsProperty = + AvaloniaProperty.RegisterDirect(nameof(DrawOnlyPoints), c => c.DrawOnlyPoints, (c, v) => c.DrawOnlyPoints = v); + + public bool DrawOnlyPoints + { + get => _drawOnlyPoints; + set => SetAndRaise(DrawOnlyPointsProperty, ref _drawOnlyPoints, value); + } + + private string? _status; + public static readonly DirectProperty StatusProperty = + AvaloniaProperty.RegisterDirect(nameof(Status), c => c.Status, (c, v) => c.Status = v, + defaultBindingMode: Avalonia.Data.BindingMode.TwoWay); + + public string? Status + { + get => _status; + set => SetAndRaise(StatusProperty, ref _status, value); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + _statusUpdated = DispatcherTimer.Run(() => + { + if (_stopwatch.Elapsed.TotalMilliseconds > 250) + { + Status = $@"Events per second: {(_events / _stopwatch.Elapsed.TotalSeconds)} +PointerUpdateKind: {_lastProperties?.PointerUpdateKind} +Last PointerUpdateKind != Other: {_lastNonOtherUpdateKind} +IsLeftButtonPressed: {_lastProperties?.IsLeftButtonPressed} +IsRightButtonPressed: {_lastProperties?.IsRightButtonPressed} +IsMiddleButtonPressed: {_lastProperties?.IsMiddleButtonPressed} +IsXButton1Pressed: {_lastProperties?.IsXButton1Pressed} +IsXButton2Pressed: {_lastProperties?.IsXButton2Pressed} +IsBarrelButtonPressed: {_lastProperties?.IsBarrelButtonPressed} +IsEraser: {_lastProperties?.IsEraser} +IsInverted: {_lastProperties?.IsInverted} +Pressure: {_lastProperties?.Pressure} +XTilt: {_lastProperties?.XTilt} +YTilt: {_lastProperties?.YTilt} +Twist: {_lastProperties?.Twist}"; + _stopwatch.Restart(); + _events = 0; + } + + return true; + }, TimeSpan.FromMilliseconds(10)); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _statusUpdated?.Dispose(); + } + + void HandleEvent(PointerEventArgs e) + { + _events++; + + if (_threadSleep != 0) + { + Thread.Sleep(_threadSleep); + } + InvalidateVisual(); + + var lastPointer = e.GetCurrentPoint(this); + _lastProperties = lastPointer.Properties; + + if (_lastProperties?.PointerUpdateKind != PointerUpdateKind.Other) + { + _lastNonOtherUpdateKind = _lastProperties?.PointerUpdateKind; + } + + if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch) + { + _pointers.Remove(e.Pointer.Id); + return; + } + + if (e.Pointer.Type != PointerType.Pen + || lastPointer.Properties.Pressure > 0) + { + if (!_pointers.TryGetValue(e.Pointer.Id, out var pt)) + _pointers[e.Pointer.Id] = pt = new PointerPoints(); + pt.HandleEvent(e, this); + } + } + + public override void Render(DrawingContext context) + { + context.FillRectangle(Brushes.White, Bounds); + foreach (var pt in _pointers.Values) + pt.Render(context, _drawOnlyPoints); + base.Render(context); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + if (e.ClickCount == 2) + { + _pointers.Clear(); + InvalidateVisual(); + return; + } + + HandleEvent(e); + base.OnPointerPressed(e); + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + HandleEvent(e); + base.OnPointerMoved(e); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + HandleEvent(e); + base.OnPointerReleased(e); + } + + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + _lastProperties = null; + base.OnPointerCaptureLost(e); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PointerContactsTab.cs b/samples/AvaloniaControlCatalog/Pages/PointerContactsTab.cs new file mode 100644 index 00000000..323ac685 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PointerContactsTab.cs @@ -0,0 +1,109 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace ControlCatalog.Pages; + +public class PointerContactsTab : Control +{ + class PointerInfo + { + public Point Point { get; set; } + public Color Color { get; set; } + } + + private static Color[] AllColors = new[] + { + Colors.Aqua, + Colors.Beige, + Colors.Chartreuse, + Colors.Coral, + Colors.Fuchsia, + Colors.Crimson, + Colors.Lavender, + Colors.Orange, + Colors.Orchid, + Colors.ForestGreen, + Colors.SteelBlue, + Colors.PapayaWhip, + Colors.PaleVioletRed, + Colors.Goldenrod, + Colors.Maroon, + Colors.Moccasin, + Colors.Navy, + Colors.Wheat, + Colors.Violet, + Colors.Sienna, + Colors.Indigo, + Colors.Honeydew + }; + + private Dictionary _pointers = new Dictionary(); + + public PointerContactsTab() + { + ClipToBounds = true; + } + + void UpdatePointer(PointerEventArgs e) + { + if (!_pointers.TryGetValue(e.Pointer, out var info)) + { + if (e.RoutedEvent == PointerMovedEvent) + return; + var colors = AllColors.Except(_pointers.Values.Select(c => c.Color)).ToArray(); + var color = colors[new Random().Next(0, colors.Length - 1)]; + _pointers[e.Pointer] = info = new PointerInfo { Color = color }; + } + + info.Point = e.GetPosition(this); + InvalidateVisual(); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + UpdatePointer(e); + e.Pointer.Capture(this); + e.Handled = true; + e.PreventGestureRecognition(); + base.OnPointerPressed(e); + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + UpdatePointer(e); + e.Handled = true; + base.OnPointerMoved(e); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + _pointers.Remove(e.Pointer); + e.Handled = true; + InvalidateVisual(); + } + + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + _pointers.Remove(e.Pointer); + InvalidateVisual(); + } + + public override void Render(DrawingContext context) + { + context.FillRectangle(Brushes.Transparent, new Rect(default, Bounds.Size)); + foreach (var pt in _pointers.Values) + { + var brush = new ImmutableSolidColorBrush(pt.Color); + + context.DrawEllipse(brush, null, pt.Point, 75, 75); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/PointersPage.xaml b/samples/AvaloniaControlCatalog/Pages/PointersPage.xaml new file mode 100644 index 00000000..2f958b90 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PointersPage.xaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + Capture 1 + + + Capture 2 + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/PointersPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/PointersPage.xaml.cs new file mode 100644 index 00000000..388c8fa3 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/PointersPage.xaml.cs @@ -0,0 +1,59 @@ +using System; +using Avalonia.Controls; +using Avalonia.Input; + +namespace ControlCatalog.Pages; + +public partial class PointersPage : TabbedPage +{ + public PointersPage() + { + InitializeComponent(); + } + + private void Border_PointerUpdated(object? sender, PointerEventArgs e) + { + if (sender is Border border && border.Child is TextBlock textBlock) + { + var position = e.GetPosition(border); + textBlock.Text = @$"Type: {e.Pointer.Type} +Captured: {e.Pointer.Captured == sender} +PointerId: {e.Pointer.Id} +Position: {(int)position.X} {(int)position.Y}"; + e.Handled = true; + } + } + + private void Border_PointerCaptureLost(object? sender, PointerCaptureLostEventArgs e) + { + if (sender is Border border && border.Child is TextBlock textBlock) + { + textBlock.Text = @$"Type: {e.Pointer.Type} +Captured: {e.Pointer.Captured == sender} +PointerId: {e.Pointer.Id} +Position: ??? ???"; + e.Handled = true; + + } + } + + private void Border_PointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (e.Pointer.Captured == sender) + { + e.Pointer.Capture(null); + e.Handled = true; + } + else if (e.Pointer.Captured is not null) + { + throw new InvalidOperationException("How?"); + } + } + + private void Border_PointerPressed(object? sender, PointerPressedEventArgs e) + { + e.Pointer.Capture(sender as Border); + e.Handled = true; + e.PreventGestureRecognition(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ProgressBarPage.xaml b/samples/AvaloniaControlCatalog/Pages/ProgressBarPage.xaml new file mode 100644 index 00000000..3b80b5a6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ProgressBarPage.xaml @@ -0,0 +1,43 @@ + + + A progress bar control + + + Maximum + + + + Minimum + + + + Progress Text Format + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ProgressBarPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ProgressBarPage.xaml.cs new file mode 100644 index 00000000..244161a6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ProgressBarPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class ProgressBarPage : ContentPage + { + public ProgressBarPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/RadioButtonPage.xaml b/samples/AvaloniaControlCatalog/Pages/RadioButtonPage.xaml new file mode 100644 index 00000000..efb63ea1 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/RadioButtonPage.xaml @@ -0,0 +1,41 @@ + + + Allows the selection of a single option of many + + + + _Option 1 + O_ption 2 + Op_tion 3 + Disabled + + + Three States: Option 1 + Three States: Option 2 + Three States: Option 3 + Disabled + + + Group A: Option 1 + Group A: Disabled + Group B: Option 1 + Group B: Option 3 + + + Group A: Option 2 + Group B: Option 2 + Group B: Option 4 + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/RadioButtonPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/RadioButtonPage.xaml.cs new file mode 100644 index 00000000..8199a7ca --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/RadioButtonPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class RadioButtonPage : ContentPage + { + public RadioButtonPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/RefreshContainerPage.axaml b/samples/AvaloniaControlCatalog/Pages/RefreshContainerPage.axaml new file mode 100644 index 00000000..a905e4ef --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/RefreshContainerPage.axaml @@ -0,0 +1,44 @@ + + + + + + Used pull direction: + + + + TopToBottom + LeftToRight + RightToLeft + BottomToTop + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/RefreshContainerPage.axaml.cs b/samples/AvaloniaControlCatalog/Pages/RefreshContainerPage.axaml.cs new file mode 100644 index 00000000..2c886fa9 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/RefreshContainerPage.axaml.cs @@ -0,0 +1,28 @@ +using Avalonia.Controls; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class RefreshContainerPage : ContentPage + { + private RefreshContainerViewModel _viewModel; + + public RefreshContainerPage() + { + InitializeComponent(); + + _viewModel = new RefreshContainerViewModel(); + + DataContext = _viewModel; + } + + private async void RefreshContainerPage_RefreshRequested(object? sender, RefreshRequestedEventArgs e) + { + var deferral = e.GetDeferral(); + + await _viewModel.AddToTop(); + + deferral.Complete(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/RelativePanelPage.axaml b/samples/AvaloniaControlCatalog/Pages/RelativePanelPage.axaml new file mode 100644 index 00000000..ccc39bef --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/RelativePanelPage.axaml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/RelativePanelPage.axaml.cs b/samples/AvaloniaControlCatalog/Pages/RelativePanelPage.axaml.cs new file mode 100644 index 00000000..3c59deb7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/RelativePanelPage.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class RelativePanelPage : ContentPage + { + public RelativePanelPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ScreenPage.cs b/samples/AvaloniaControlCatalog/Pages/ScreenPage.cs new file mode 100644 index 00000000..97911529 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ScreenPage.cs @@ -0,0 +1,174 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Net.Http.Headers; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Threading; + +namespace ControlCatalog.Pages +{ + public class ScreenPage : ContentPage + { + private double _leftMost; + private double _topMost; + private IBrush _primaryBrush = SolidColorBrush.Parse("#FF0078D7"); + private IBrush _defaultBrush = Brushes.LightGray; + private IPen _activePen = new Pen(Brushes.Black); + private IPen _defaultPen = new Pen(Brushes.DarkGray); + + public ScreenPage() + { + var button = new Button(); + button.Content = "Request ScreenDetails"; + button.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Top; + button.Click += async (sender, args) => + { + var success = TopLevel.GetTopLevel(this)!.Screens is { } screens ? + await screens.RequestScreenDetails() : + false; + button.Content = "Request ScreenDetails: " + (success ? "Granted" : "Denied"); + }; + Content = button; + } + + protected override bool BypassFlowDirectionPolicies => true; + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + var topLevel = TopLevel.GetTopLevel(this); + if (topLevel is Window w) + { + w.PositionChanged += (_, _) => InvalidateVisual(); + } + + if (topLevel?.Screens is { } screens) + { + screens.Changed += (_, _) => + { + Console.WriteLine("Screens Changed"); + InvalidateVisual(); + }; + } + } + + public override void Render(DrawingContext context) + { + base.Render(context); + double beginOffset = (Content as Visual)?.Bounds.Height + 10 ?? 0; + + var topLevel = TopLevel.GetTopLevel(this)!; + if (topLevel.Screens is not { } screens) + { + var formattedText = CreateFormattedText("Current platform doesn't support Screens API."); + context.DrawText(formattedText, new Point(15, 15 + beginOffset)); + return; + } + + var activeScreen = screens.ScreenFromTopLevel(topLevel); + double maxBottom = 0; + + for (int i = 0; i + + + Allows for horizontal and vertical content scrolling. Supports snapping on touch and pointer wheel scrolling. + + + + + + + + + + + + + + + + + + + + + + + + + + + Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen. + + + + + + + + + + + + + + + + Vertical Snapping + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Horizontal Snapping + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ScrollViewerPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ScrollViewerPage.xaml.cs new file mode 100644 index 00000000..cfdd5bab --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ScrollViewerPage.xaml.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using MiniMvvm; + +namespace ControlCatalog.Pages +{ + public class ScrollViewerPageViewModel : ViewModelBase + { + private bool _allowAutoHide; + private bool _enableInertia; + private ScrollBarVisibility _horizontalScrollVisibility; + private ScrollBarVisibility _verticalScrollVisibility; + private SnapPointsType _snapPointsType; + private SnapPointsAlignment _snapPointsAlignment; + private bool _areSnapPointsRegular; + + public ScrollViewerPageViewModel() + { + AvailableVisibility = new List + { + ScrollBarVisibility.Auto, + ScrollBarVisibility.Visible, + ScrollBarVisibility.Hidden, + ScrollBarVisibility.Disabled, + }; + + AvailableSnapPointsType = new List() + { + SnapPointsType.None, + SnapPointsType.Mandatory, + SnapPointsType.MandatorySingle + }; + + AvailableSnapPointsAlignment = new List() + { + SnapPointsAlignment.Near, + SnapPointsAlignment.Center, + SnapPointsAlignment.Far, + }; + + HorizontalScrollVisibility = ScrollBarVisibility.Auto; + VerticalScrollVisibility = ScrollBarVisibility.Auto; + AllowAutoHide = true; + EnableInertia = true; + } + + public bool AllowAutoHide + { + get => _allowAutoHide; + set => this.RaiseAndSetIfChanged(ref _allowAutoHide, value); + } + + public bool EnableInertia + { + get => _enableInertia; + set => this.RaiseAndSetIfChanged(ref _enableInertia, value); + } + + public ScrollBarVisibility HorizontalScrollVisibility + { + get => _horizontalScrollVisibility; + set => this.RaiseAndSetIfChanged(ref _horizontalScrollVisibility, value); + } + + public ScrollBarVisibility VerticalScrollVisibility + { + get => _verticalScrollVisibility; + set => this.RaiseAndSetIfChanged(ref _verticalScrollVisibility, value); + } + + public List AvailableVisibility { get; } + + public bool AreSnapPointsRegular + { + get => _areSnapPointsRegular; + set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value); + } + + public SnapPointsType SnapPointsType + { + get => _snapPointsType; + set => this.RaiseAndSetIfChanged(ref _snapPointsType, value); + } + + public SnapPointsAlignment SnapPointsAlignment + { + get => _snapPointsAlignment; + set => this.RaiseAndSetIfChanged(ref _snapPointsAlignment, value); + } + public List AvailableSnapPointsType { get; } + public List AvailableSnapPointsAlignment { get; } + } + + public partial class ScrollViewerPage : TabbedPage + { + public ScrollViewerPage() + { + InitializeComponent(); + + DataContext = new ScrollViewerPageViewModel(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/SliderPage.xaml b/samples/AvaloniaControlCatalog/Pages/SliderPage.xaml new file mode 100644 index 00000000..445564cb --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/SliderPage.xaml @@ -0,0 +1,84 @@ + + + A control that lets the user select from a range of values by moving a Thumb control along a Track. + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/SliderPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/SliderPage.xaml.cs new file mode 100644 index 00000000..f65f7143 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/SliderPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class SliderPage : ContentPage + { + public SliderPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/SplitViewPage.xaml b/samples/AvaloniaControlCatalog/Pages/SplitViewPage.xaml new file mode 100644 index 00000000..144688e3 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/SplitViewPage.xaml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + Left + Right + Top + Bottom + + + + + + Inline + CompactInline + Overlay + CompactOverlay + + + + + CatalogChromeMediumColor + Red + Blue + Green + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/SplitViewPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/SplitViewPage.xaml.cs new file mode 100644 index 00000000..4917a13c --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/SplitViewPage.xaml.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class SplitViewPage : ContentPage + { + public SplitViewPage() + { + InitializeComponent(); + DataContext = new SplitViewPageViewModel(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TabControlPage.xaml b/samples/AvaloniaControlCatalog/Pages/TabControlPage.xaml new file mode 100644 index 00000000..e7aa18ea --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabControlPage.xaml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + This is the first page in the TabControl. + + + + + + This is the second page in the TabControl. + + + + + You should not see this. + + + + + + + + + + + + + + + + + + + + + + Tab Placement: + + Left + Bottom + Right + Top + + Set TabItem.ContentTemplate + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/TabControlPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/TabControlPage.xaml.cs new file mode 100644 index 00000000..366b5216 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabControlPage.xaml.cs @@ -0,0 +1,47 @@ +using System; +using Avalonia.Controls; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class TabControlPage : ContentPage + { + public TabControlPage() + { + InitializeComponent(); + + DataContext = new TabControlPageViewModel + { + Tabs = new[] + { + new TabControlPageViewModelItem + { + Header = "Arch", + Text = "This is the first templated tab page.", + Image = LoadBitmap("avares://ControlCatalog/Assets/delicate-arch-896885_640.jpg"), + }, + new TabControlPageViewModelItem + { + Header = "Leaf", + Text = "This is the second templated tab page.", + Image = LoadBitmap("avares://ControlCatalog/Assets/maple-leaf-888807_640.jpg"), + }, + new TabControlPageViewModelItem + { + Header = "Disabled", + Text = "You should not see this.", + IsEnabled = false, + }, + }, + TabPlacement = Dock.Top, + }; + } + + private static Bitmap LoadBitmap(string uri) + { + return new Bitmap(AssetLoader.Open(new Uri(uri))); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TabStripPage.xaml b/samples/AvaloniaControlCatalog/Pages/TabStripPage.xaml new file mode 100644 index 00000000..f4a47af3 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabStripPage.xaml @@ -0,0 +1,35 @@ + + + A control which displays a selectable strip of tabs + + + + Defined in XAML + + Item 1 + Item 2 + Disabled + + + + + Dynamically generated + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/TabStripPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/TabStripPage.xaml.cs new file mode 100644 index 00000000..d8301098 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabStripPage.xaml.cs @@ -0,0 +1,33 @@ +using Avalonia.Controls; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class TabStripPage : ContentPage + { + public TabStripPage() + { + InitializeComponent(); + + DataContext = new TabControlPageViewModel + { + Tabs = new[] + { + new TabControlPageViewModelItem() + { + Header = "Item 1", + }, + new TabControlPageViewModelItem + { + Header = "Item 2", + }, + new TabControlPageViewModelItem + { + Header = "Disabled", + IsEnabled = false, + }, + } + }; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TabbedDemoPage.xaml b/samples/AvaloniaControlCatalog/Pages/TabbedDemoPage.xaml new file mode 100644 index 00000000..58a3b4fe --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabbedDemoPage.xaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/TabbedDemoPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/TabbedDemoPage.xaml.cs new file mode 100644 index 00000000..88692c97 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabbedDemoPage.xaml.cs @@ -0,0 +1,96 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class TabbedDemoPage : ContentPage + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + // Overview + ("Overview", "First Look", + "Basic TabbedPage with three tabs, tab placement selector, and selection status.", + () => new TabbedPageFirstLookPage()), + + // Populate + ("Populate", "Page Collection", + "Populate a TabbedPage by adding ContentPage objects directly to the Pages collection.", + () => new TabbedPageCollectionPage()), + ("Populate", "Data Templates", + "Bind TabbedPage to an ObservableCollection, add or remove tabs at runtime, and switch the page template.", + () => new TabbedPageDataTemplatePage()), + + // Appearance + ("Appearance", "Tab Customization", + "Customize tab placement, bar background, selected and unselected tab colors.", + () => new TabbedPageCustomizationPage()), + ("Appearance", "Custom Tab Bar", + "VYNTRA-style custom tab bar with floating pill, brand colours, and system-adaptive theme using only resource overrides and styles.", + () => new TabbedPageCustomTabBarPage()), + ("Appearance", "FAB Tab Bar", + "Social-media-style bottom nav with a central floating action button that triggers a command, not a tab.", + () => new TabbedPageFabPage()), + ("Appearance", "Fluid Nav Bar", + "Inspired by the Flutter fluid_nav_bar vignette. Color themes with animated indicator and icons.", + () => new TabbedPageFluidNavPage()), + + // Features + ("Features", "Programmatic Selection", + "Preset the initial tab with SelectedIndex, jump to any tab programmatically, and respond to SelectionChanged events.", + () => new TabbedPageProgrammaticPage()), + ("Features", "Placement", "Switch the tab bar between Top, Bottom, Left, and Right placements.", + () => new TabbedPagePlacementPage()), + ("Features", "Page Transitions", + "Animate tab switches with CrossFade, PageSlide, or composite transitions.", + () => new TabbedPageTransitionsPage()), + ("Features", "Keyboard Navigation", + "Keyboard shortcuts to navigate between tabs, with a toggle to enable or disable.", + () => new TabbedPageKeyboardPage()), + ("Features", "Swipe Gestures", + "Swipe left/right (Top/Bottom) or up/down (Left/Right) to navigate. Toggle IsGestureEnabled.", + () => new TabbedPageGesturePage()), + ("Features", "Events", + "SelectionChanged, NavigatedTo, and NavigatedFrom events. Switch tabs to see the live event log.", + () => new TabbedPageEventsPage()), + ("Features", "Disabled Tabs", + "IsTabEnabled attached property: disable individual tabs so they cannot be selected.", + () => new TabbedPageDisabledTabsPage()), + + // Performance + ("Performance", "Performance Monitor", + "Track tab count, live page instances, and managed heap size. Observe how GC reclaims memory after removing tabs.", + () => new TabbedPagePerformancePage()), + + // Composition + ("Composition", "With NavigationPage", + "Embed a NavigationPage inside each TabbedPage tab for drill-down navigation.", + () => new TabbedPageWithNavigationPage()), + ("Composition", "With DrawerPage", + "Combine TabbedPage with DrawerPage: a global navigation drawer sits over tabbed content.", + () => new TabbedPageWithDrawerPage()), + + // Showcases + ("Showcases", "Pulse Fitness", + "Fitness app with bottom TabbedPage navigation, NavigationPage drill-down inside tabs, and workout detail screens.", + () => new PulseAppPage()), + ("Showcases", "L'Avenir Restaurant", + "Restaurant app with DrawerPage root, NavigationPage detail, and TabbedPage bottom tabs for Menu, Reservations, and Profile.", + () => new LAvenirAppPage()), + ("Showcases", "Retro Gaming", + "Arcade-style app with NavigationPage header, TabbedPage bottom tabs with CenteredTabPanel, and game detail push.", + () => new RetroGamingAppPage()), + }; + + public TabbedDemoPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TabbedPage/CenteredTabPanel.cs b/samples/AvaloniaControlCatalog/Pages/TabbedPage/CenteredTabPanel.cs new file mode 100644 index 00000000..6ca5e0ec --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabbedPage/CenteredTabPanel.cs @@ -0,0 +1,63 @@ +using System; +using Avalonia; +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + /// + /// A custom panel that arranges N children in N+1 equally-sized slots, leaving the + /// middle slot empty. Intended for tab bars that need a central action button + /// overlaid on the gap. + /// + /// For N children the split is ⌊N/2⌋ items on the left and ⌈N/2⌉ on the right. + /// Example – 4 tabs: [0][1][ gap ][2][3] (5 equal columns, center is free). + /// + public class CenteredTabPanel : Panel + { + protected override Size MeasureOverride(Size availableSize) + { + int count = Children.Count; + if (count == 0) + return default; + + int slots = count + 1; + bool infiniteWidth = double.IsInfinity(availableSize.Width); + double slotWidth = infiniteWidth ? 60.0 : availableSize.Width / slots; + + double maxHeight = 0; + foreach (var child in Children) + { + child.Measure(new Size(slotWidth, availableSize.Height)); + maxHeight = Math.Max(maxHeight, child.DesiredSize.Height); + } + + // When given finite width, fill it. When infinite (inside a ScrollViewer), + // return a small positive width so the parent allocates real space. + double desiredWidth = infiniteWidth ? slotWidth * slots : availableSize.Width; + if (double.IsNaN(maxHeight) || double.IsInfinity(maxHeight)) + maxHeight = 0; + + return new Size(desiredWidth, maxHeight); + } + + protected override Size ArrangeOverride(Size finalSize) + { + int count = Children.Count; + if (count == 0) + return finalSize; + + int slots = count + 1; + double slotW = finalSize.Width / slots; + int leftCount = count / 2; // items placed to the left of the gap + + for (int i = 0; i < count; i++) + { + // Skip the center slot (leftCount), reserved for the FAB. + int slot = i < leftCount ? i : i + 1; + Children[i].Arrange(new Rect(slot * slotW, 0, slotW, finalSize.Height)); + } + + return finalSize; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavBar.cs b/samples/AvaloniaControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavBar.cs new file mode 100644 index 00000000..a68e57c9 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavBar.cs @@ -0,0 +1,620 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Skia; +using Avalonia.Threading; +using SkiaSharp; + +namespace ControlCatalog.Pages +{ + /// + /// A fluid navigation bar that replicates the Flutter fluid_nav_bar vignette. + /// The bar background has a bezier "dip" that travels to the selected tab. + /// Each icon is drawn progressively using SKPathMeasure for the fill animation. + /// + public class FluidNavBar : Control, Avalonia.Rendering.ICustomHitTest + { + internal const double NominalHeight = 56.0; + internal const double CircleRadius = 25.0; + internal const double ActiveFloat = 16.0; // px the circle rises + internal const double IconDrawScale = 0.9; // icon scale within circle + internal const double ScaleCurveScale = 0.50; + internal const double FloatLinearPIn = 0.28; + internal const double FillLinearPIn = 0.25; + internal const double XAnimDuration = 0.620; // s — X bump travel + internal const double YDipDuration = 0.300; // s — dip down + internal const double YBounceDelay = 0.500; // s — wait before bounce + internal const double YBounceDuration = 1.200; // s — elastic bounce up + internal const double FloatUpDuration = 1.666; // s — circle rising + internal const double FloatDownDuration = 0.833; // s — circle falling + + public static readonly StyledProperty> ItemsProperty = + AvaloniaProperty.Register>( + nameof(Items), new List()); + + public static readonly StyledProperty SelectedIndexProperty = + AvaloniaProperty.Register(nameof(SelectedIndex), 0); + + public static readonly StyledProperty BarColorProperty = + AvaloniaProperty.Register(nameof(BarColor), Colors.White); + + public static readonly StyledProperty ButtonColorProperty = + AvaloniaProperty.Register(nameof(ButtonColor), Colors.White); + + public static readonly StyledProperty ActiveIconColorProperty = + AvaloniaProperty.Register(nameof(ActiveIconColor), Colors.Black); + + public static readonly StyledProperty InactiveIconColorProperty = + AvaloniaProperty.Register( + nameof(InactiveIconColor), Color.FromArgb(140, 120, 120, 120)); + + private double _xCurrent = -1; // -1 = not yet initialised + private double _lastWidth = -1; // tracks width changes for resize correction + private double _xStart, _xTarget, _xAnimStartSec; + private double _yValue = 1.0; // 0 = deepest dip, 1 = flat + private double _yDipStartSec; + private bool _yBounceStarted; + private double _yBounceStartSec; + + // per-item (length = Items.Count after OnItemsChanged) + private double[] _floatProgress = Array.Empty(); + private double[] _floatStartSec = Array.Empty(); + private bool[] _floatGoingUp = Array.Empty(); + + // Parsed Skia paths — owned here, disposed on detach / items change + private SKPath?[] _parsedPaths = Array.Empty(); + + private DispatcherTimer? _animTimer; + private readonly Stopwatch _clock = Stopwatch.StartNew(); + private bool _animating; + + public IList Items + { + get => GetValue(ItemsProperty); + set => SetValue(ItemsProperty, value); + } + + public int SelectedIndex + { + get => GetValue(SelectedIndexProperty); + set => SetValue(SelectedIndexProperty, value); + } + + public Color BarColor + { + get => GetValue(BarColorProperty); + set => SetValue(BarColorProperty, value); + } + + public Color ButtonColor + { + get => GetValue(ButtonColorProperty); + set => SetValue(ButtonColorProperty, value); + } + + public Color ActiveIconColor + { + get => GetValue(ActiveIconColorProperty); + set => SetValue(ActiveIconColorProperty, value); + } + + public Color InactiveIconColor + { + get => GetValue(InactiveIconColorProperty); + set => SetValue(InactiveIconColorProperty, value); + } + + public event EventHandler? SelectionChanged; + + public FluidNavBar() + { + ClipToBounds = false; + Height = NominalHeight; + Cursor = new Cursor(StandardCursorType.Hand); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ItemsProperty) + OnItemsChanged(); + else if (change.Property == SelectedIndexProperty) + OnSelectedIndexChanged(change.GetOldValue(), change.GetNewValue()); + else if (change.Property == BarColorProperty + || change.Property == ButtonColorProperty + || change.Property == ActiveIconColorProperty + || change.Property == InactiveIconColorProperty) + InvalidateVisual(); + } + + public bool HitTest(Point point) + { + return point.X >= 0 && point.X <= Bounds.Width + && point.Y >= -(ActiveFloat + CircleRadius) + && point.Y <= Bounds.Height; + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + var n = Items?.Count ?? 0; + if (n == 0 || Bounds.Width <= 0) return; + + var pos = e.GetPosition(this); + var index = (int)(pos.X / (Bounds.Width / n)); + index = Math.Clamp(index, 0, n - 1); + + if (index != SelectedIndex) + { + SetCurrentValue(SelectedIndexProperty, index); + SelectionChanged?.Invoke(this, index); + } + + e.Handled = true; + } + + protected override Size MeasureOverride(Size availableSize) + { + var w = double.IsPositiveInfinity(availableSize.Width) ? 300 : availableSize.Width; + return new Size(w, NominalHeight); + } + + protected override Size ArrangeOverride(Size finalSize) + { + var w = finalSize.Width; + if (w > 0) + { + if (_xCurrent < 0 || _lastWidth < 0) + { + // First layout — snap everything to the current selection + _xCurrent = IndexToX(SelectedIndex, w); + _xTarget = _xCurrent; + _xStart = _xCurrent; + } + else if (Math.Abs(w - _lastWidth) > 0.5) + { + // Width changed (resize) — scale pixel positions proportionally + // so the bump stays over the correct slot + var ratio = w / _lastWidth; + _xCurrent = _xCurrent * ratio; + _xStart = _xStart * ratio; + _xTarget = IndexToX(SelectedIndex, w); + InvalidateVisual(); + } + _lastWidth = w; + } + return new Size(w > 0 ? w : 300, NominalHeight); + } + + public override void Render(DrawingContext context) + { + var w = Bounds.Width; + var h = Bounds.Height; + + if (w <= 0 || h <= 0 || Items == null || Items.Count == 0) return; + + var n = Items.Count; + + // Initialise _xCurrent if layout didn't run yet + if (_xCurrent < 0) + { + _xCurrent = IndexToX(SelectedIndex, w); + _xTarget = _xCurrent; + } + + // Snapshot per-item animation state for this frame + var slotCenters = new double[n]; + var floatOffsets = new double[n]; + var scaleYValues = new double[n]; + var fillAmounts = new double[n]; + + for (int i = 0; i < n; i++) + slotCenters[i] = IndexToX(i, w); + + for (int i = 0; i < n; i++) + { + var p = i < _floatProgress.Length ? _floatProgress[i] : (i == SelectedIndex ? 1.0 : 0.0); + var goUp = i < _floatGoingUp.Length ? _floatGoingUp[i] : (i == SelectedIndex); + + // Float offset — uses LinearPoint(0.28, 0) to delay start, then elastic/quintic easing + var linearP = LinearPoint(p, FloatLinearPIn, 0.0); + var floatEased = goUp ? ElasticOut(linearP, 0.38) : EaseInQuint(linearP); + floatOffsets[i] = ActiveFloat * floatEased; + + // Scale Y squish via CenteredElastic curves + var centered = goUp ? CenteredElasticOut(p, 0.6) : CenteredElasticIn(p, 0.6); + scaleYValues[i] = 0.75 + centered * ScaleCurveScale; + + // Icon fill — LinearPoint(0.25, 1.0) adds a slight draw delay vs float + fillAmounts[i] = LinearPoint(p, FillLinearPIn, 1.0); + } + + // Clamp scaleY to sane range to avoid SVG-transform oddities + for (int i = 0; i < n; i++) + scaleYValues[i] = Math.Max(0.1, Math.Min(1.5, scaleYValues[i])); + + var op = new FluidNavBarRenderOp( + new Rect(0, -(ActiveFloat + CircleRadius), w, h + ActiveFloat + CircleRadius), + (float)w, (float)h, + (float)_xCurrent, (float)_yValue, + slotCenters, floatOffsets, scaleYValues, fillAmounts, + _parsedPaths, + BarColor, ButtonColor, ActiveIconColor, InactiveIconColor); + + context.Custom(op); + } + + private void OnItemsChanged() + { + foreach (var p in _parsedPaths) p?.Dispose(); + + var n = Items?.Count ?? 0; + _parsedPaths = new SKPath?[n]; + _floatProgress = new double[n]; + _floatStartSec = new double[n]; + _floatGoingUp = new bool[n]; + + for (int i = 0; i < n; i++) + { + var svg = Items![i].SvgPath; + if (!string.IsNullOrEmpty(svg)) + _parsedPaths[i] = SKPath.ParseSvgPathData(svg); + } + + var sel = Math.Clamp(SelectedIndex, 0, Math.Max(0, n - 1)); + for (int i = 0; i < n; i++) + { + _floatProgress[i] = i == sel ? 1.0 : 0.0; + _floatGoingUp[i] = i == sel; + } + + _xCurrent = -1; // force re-init on next arrange/render + InvalidateVisual(); + } + + private void OnSelectedIndexChanged(int oldIndex, int newIndex) + { + var n = _floatProgress.Length; + if (n == 0) return; + + newIndex = Math.Clamp(newIndex, 0, n - 1); + oldIndex = Math.Clamp(oldIndex, 0, n - 1); + if (oldIndex == newIndex) return; + + var now = _clock.Elapsed.TotalSeconds; + + // X: slide bump from old to new position + if (_xCurrent < 0 && Bounds.Width > 0) + _xCurrent = IndexToX(oldIndex, Bounds.Width); + + _xStart = _xCurrent; + _xTarget = Bounds.Width > 0 ? IndexToX(newIndex, Bounds.Width) : _xStart; + _xAnimStartSec = now; + + // Y: dip then elastic bounce + _yValue = 1.0; + _yDipStartSec = now; + _yBounceStarted = false; + + // Per-button float + _floatGoingUp[oldIndex] = false; + _floatStartSec[oldIndex] = now; + _floatGoingUp[newIndex] = true; + _floatStartSec[newIndex] = now; + + StartAnimation(); + } + + private void StartAnimation() + { + if (_animating) return; + _animating = true; + _animTimer = new DispatcherTimer( + TimeSpan.FromSeconds(1.0 / 60.0), + DispatcherPriority.Render, + OnAnimTick); + _animTimer.Start(); + } + + private void StopAnimation() + { + _animTimer?.Stop(); + _animTimer = null; + _animating = false; + } + + private void OnAnimTick(object? sender, EventArgs e) + { + var now = _clock.Elapsed.TotalSeconds; + var anyActive = false; + + var xElapsed = now - _xAnimStartSec; + if (xElapsed < XAnimDuration) + { + _xCurrent = _xStart + (_xTarget - _xStart) * (xElapsed / XAnimDuration); + anyActive = true; + } + else + { + _xCurrent = _xTarget; + } + + var yDipElapsed = now - _yDipStartSec; + if (yDipElapsed < YDipDuration) + { + _yValue = 1.0 - yDipElapsed / YDipDuration; + anyActive = true; + } + else + { + _yValue = 0.0; + + if (!_yBounceStarted && yDipElapsed >= YBounceDelay) + { + _yBounceStarted = true; + _yBounceStartSec = now; + } + + if (_yBounceStarted) + { + var bt = now - _yBounceStartSec; + if (bt < YBounceDuration) + { + _yValue = ElasticOut(bt / YBounceDuration, 0.38); + anyActive = true; + } + else + { + _yValue = 1.0; + } + } + } + + for (int i = 0; i < _floatProgress.Length; i++) + { + var elapsed = now - _floatStartSec[i]; + var duration = _floatGoingUp[i] ? FloatUpDuration : FloatDownDuration; + if (elapsed < duration) + { + var t = elapsed / duration; + _floatProgress[i] = _floatGoingUp[i] ? t : 1.0 - t; + anyActive = true; + } + else + { + _floatProgress[i] = _floatGoingUp[i] ? 1.0 : 0.0; + } + } + + InvalidateVisual(); + + if (!anyActive) + StopAnimation(); + } + + private double IndexToX(int index, double width) + { + var n = Items?.Count ?? 1; + if (n <= 0) n = 1; + return (index + 0.5) * (width / n); + } + + + internal static double ElasticOut(double t, double period = 0.4) + { + if (t <= 0) return 0; + if (t >= 1) return 1; + var s = period / 4.0; + return Math.Pow(2.0, -10.0 * t) * Math.Sin((t - s) * 2.0 * Math.PI / period) + 1.0; + } + + private static double CenteredElasticOut(double t, double period = 0.4) + { + return Math.Pow(2.0, -10.0 * t) * Math.Sin(t * 2.0 * Math.PI / period) + 0.5; + } + + private static double CenteredElasticIn(double t, double period = 0.4) + { + return -Math.Pow(2.0, 10.0 * (t - 1.0)) * Math.Sin((t - 1.0) * 2.0 * Math.PI / period) + 0.5; + } + + internal static double LinearPoint(double x, double pIn, double pOut) + { + if (pIn <= 0) return pOut; + var lowerScale = pOut / pIn; + var upperScale = (1.0 - pOut) / (1.0 - pIn); + var upperOff = 1.0 - upperScale; + return x < pIn ? x * lowerScale : x * upperScale + upperOff; + } + + private static double EaseInQuint(double t) => t * t * t * t * t; + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + StopAnimation(); + } + + private sealed class FluidNavBarRenderOp : ICustomDrawOperation + { + private readonly float _w, _h, _xCenter, _normY; + private readonly double[] _slots, _floatOff, _scaleY, _fill; + private readonly SKPath?[] _paths; + private readonly Color _bar, _btn, _active, _inactive; + + public Rect Bounds { get; } + + public FluidNavBarRenderOp( + Rect bounds, + float w, float h, + float xCenter, float normY, + double[] slots, double[] floatOff, double[] scaleY, double[] fill, + SKPath?[] paths, + Color bar, Color btn, Color active, Color inactive) + { + Bounds = bounds; + _w = w; _h = h; + _xCenter = xCenter; _normY = normY; + _slots = slots; _floatOff = floatOff; + _scaleY = scaleY; _fill = fill; + _paths = paths; + _bar = bar; _btn = btn; + _active = active; _inactive = inactive; + } + + public bool HitTest(Point p) => false; + public bool Equals(ICustomDrawOperation? other) => false; + public void Dispose() { } + + public void Render(ImmediateDrawingContext context) + { + var lease = context.TryGetFeature(); + if (lease == null) return; + + using var l = lease.Lease(); + var canvas = l.SkCanvas; + + int save = canvas.Save(); + try + { + DrawBackground(canvas); + for (int i = 0; i < _slots.Length; i++) + DrawButton(canvas, i); + } + finally + { + canvas.RestoreToCount(save); + } + } + + private void DrawBackground(SKCanvas canvas) + { + const float rTop = 54f, rBot = 44f; + const float hcTop = 0.6f, hcBot = 0.5f; + const float pcTop = 0.35f, pcBot = 0.85f; + const float tY = -10f, bY = 54f; + const float tD = 0f, bD = 6f; + + float norm = (float)(LinearPoint(_normY, 0.5, 2.0) / 2.0); + + float r = Lerp(rTop, rBot, norm); + float anchr = Lerp(r * hcTop, r * hcBot, (float)LinearPoint(norm, 0.5, 0.75)); + float dipc = Lerp(r * pcTop, r * pcBot, (float)LinearPoint(norm, 0.5, 0.80)); + float y = Lerp(tY, bY, (float)LinearPoint(norm, 0.2, 0.70)); + float dist = Lerp(tD, bD, (float)LinearPoint(norm, 0.5, 0.00)); + float x0 = _xCenter - dist / 2f; + float x1 = _xCenter + dist / 2f; + + using var path = new SKPath(); + path.MoveTo(0, 0); + path.LineTo(x0 - r, 0); + path.CubicTo(x0 - r + anchr, 0, x0 - dipc, y, x0, y); + path.LineTo(x1, y); + path.CubicTo(x1 + dipc, y, x1 + r - anchr, 0, x1 + r, 0); + path.LineTo(_w, 0); + path.LineTo(_w, _h); + path.LineTo(0, _h); + path.Close(); + + using var paint = new SKPaint { Color = ToSK(_bar), IsAntialias = true }; + canvas.DrawPath(path, paint); + } + + private void DrawButton(SKCanvas canvas, int i) + { + var cx = (float)_slots[i]; + var cy = _h / 2f; + var fo = (float)_floatOff[i]; + var sy = (float)_scaleY[i]; + var fa = (float)_fill[i]; + + const float r = (float)CircleRadius; + + // Circle — just translated up, not scaled + using var cp = new SKPaint { Color = ToSK(_btn), IsAntialias = true }; + canvas.DrawCircle(cx, cy - fo, r, cp); + + // Icon + if (i < _paths.Length && _paths[i] != null) + DrawIcon(canvas, _paths[i]!, cx, cy - fo, sy, fa); + } + + private void DrawIcon(SKCanvas canvas, SKPath path, float cx, float cy, + float scaleY, float fillAmount) + { + const float s = (float)IconDrawScale; + + int save = canvas.Save(); + canvas.Translate(cx, cy); + canvas.Scale(s, s * scaleY); + + // Grey background stroke (full path, unselected look) + using var bg = new SKPaint + { + Style = SKPaintStyle.Stroke, + StrokeWidth = 2.4f, + StrokeCap = SKStrokeCap.Round, + StrokeJoin = SKStrokeJoin.Round, + Color = ToSK(_inactive), + IsAntialias = true + }; + canvas.DrawPath(path, bg); + + // Foreground stroke, trimmed progressively with SKPathMeasure + if (fillAmount > 0f) + { + using var fg = new SKPaint + { + Style = SKPaintStyle.Stroke, + StrokeWidth = 2.4f, + StrokeCap = SKStrokeCap.Round, + StrokeJoin = SKStrokeJoin.Round, + Color = ToSK(_active), + IsAntialias = true + }; + DrawTrimmedPath(canvas, path, fillAmount, fg); + } + + canvas.RestoreToCount(save); + } + + // Iterates all contours and draws each trimmed to fillAmount of its length. + // Direct port of Flutter's extractPartialPath behavior. + private static void DrawTrimmedPath(SKCanvas canvas, SKPath path, + float fillAmount, SKPaint paint) + { + using var measure = new SKPathMeasure(path, false); + do + { + var len = measure.Length; + if (len <= 0f) continue; + + using var seg = new SKPath(); + if (measure.GetSegment(0f, len * fillAmount, seg, true)) + canvas.DrawPath(seg, paint); + } + while (measure.NextContour()); + } + + private static float Lerp(float a, float b, float t) => a + (b - a) * t; + + private static double LinearPoint(double x, double pIn, double pOut) + { + if (pIn <= 0) return pOut; + var lo = pOut / pIn; + var hi = (1.0 - pOut) / (1.0 - pIn); + return x < pIn ? x * lo : x * hi + (1.0 - hi); + } + + private static SKColor ToSK(Color c) => new SKColor(c.R, c.G, c.B, c.A); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavItem.cs b/samples/AvaloniaControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavItem.cs new file mode 100644 index 00000000..e2c98005 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavItem.cs @@ -0,0 +1,14 @@ +namespace ControlCatalog.Pages +{ + public class FluidNavItem + { + public string SvgPath { get; } + public string Label { get; } + + public FluidNavItem(string svgPath, string label) + { + SvgPath = svgPath; + Label = label; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageCollectionPage.xaml b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageCollectionPage.xaml new file mode 100644 index 00000000..80e63a08 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageCollectionPage.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs new file mode 100644 index 00000000..b52bfd4d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs @@ -0,0 +1,45 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class TabbedPageFabPage : UserControl + { + private static readonly StreamGeometry FeedGeometry = + StreamGeometry.Parse("M12.9942 2.79444C12.4118 2.30208 11.5882 2.30208 11.0058 2.79444L3.50582 9.39444C3.18607 9.66478 3 10.0634 3 10.4828V20.25C3 20.9404 3.55964 21.5 4.25 21.5H8.25C8.94036 21.5 9.5 20.9404 9.5 20.25V14.75C9.5 14.6119 9.61193 14.5 9.75 14.5H14.25C14.3881 14.5 14.5 14.6119 14.5 14.75V20.25C14.5 20.9404 15.0596 21.5 15.75 21.5H19.75C20.4404 21.5 21 20.9404 21 20.25V10.4828C21 10.0634 20.8139 9.66478 20.4942 9.39444L12.9942 2.79444Z"); + private static readonly StreamGeometry DiscoverGeometry = + StreamGeometry.Parse("M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm4.24 5.76-3.03 6.55-6.55 3.03L9.69 10.8l6.55-3.04zM12 13.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"); + private static readonly StreamGeometry AlertsGeometry = + StreamGeometry.Parse("M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"); + private static readonly StreamGeometry ProfileGeometry = + StreamGeometry.Parse("M12 2C9.243 2 7 4.243 7 7s2.243 5 5 5 5-2.243 5-5-2.243-5-5-5zM12 14c-5.523 0-10 3.582-10 8a1 1 0 001 1h18a1 1 0 001-1c0-4.418-4.477-8-10-8z"); + + private int _postCount; + + public TabbedPageFabPage() + { + InitializeComponent(); + SetupIcons(); + + FabButton.Click += OnFabClicked; + TriggerFabButton.Click += OnFabClicked; + } + + private void SetupIcons() + { + FeedPage.Icon = new PathIcon { Data = FeedGeometry }; + DiscoverPage.Icon = new PathIcon { Data = DiscoverGeometry }; + AlertsPage.Icon = new PathIcon { Data = AlertsGeometry }; + ProfilePage.Icon = new PathIcon { Data = ProfileGeometry }; + } + + private void OnFabClicked(object? sender, RoutedEventArgs e) + { + _postCount++; + StatusText.Text = _postCount == 1 + ? "Post created! Check your feed." + : $"{_postCount} posts created!"; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFirstLookPage.xaml b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFirstLookPage.xaml new file mode 100644 index 00000000..1dabb6ba --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageFirstLookPage.xaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithDrawerPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithDrawerPage.xaml.cs new file mode 100644 index 00000000..88f688d7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithDrawerPage.xaml.cs @@ -0,0 +1,114 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class TabbedPageWithDrawerPage : UserControl + { + public TabbedPageWithDrawerPage() + { + InitializeComponent(); + Loaded += (_, _) => ShowSection("Home"); + } + + private void OnSectionSelected(object? sender, RoutedEventArgs e) + { + if (sender is Button btn && btn.Tag is string section) + { + ShowSection(section); + DemoDrawer.IsOpen = false; + } + } + + private void ShowSection(string section) + { + SectionHost.Content = section switch + { + "Home" => CreateHomeTabbed(), + _ => CreatePlainPage(section) + }; + } + + private static Control CreatePlainPage(string section) + { + var (subtitle, icon) = section switch + { + "Explore" => ("Discover new content.", "📍"), + "Favorites" => ("Items you've saved.", "❤"), + _ => (string.Empty, string.Empty) + }; + + return new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Spacing = 8, + Children = + { + new TextBlock + { + Text = section, + FontSize = 22, + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center + }, + new TextBlock + { + Text = subtitle, + FontSize = 13, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + TextAlignment = TextAlignment.Center, + MaxWidth = 300 + } + } + }; + } + + private static TabbedPage CreateHomeTabbed() => new() + { + TabPlacement = TabPlacement.Bottom, + Pages = new[] + { + new ContentPage + { + Header = "Featured", + Content = new TextBlock + { + Text = "Featured content", + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + FontSize = 18, + Opacity = 0.7 + } + }, + new ContentPage + { + Header = "Recent", + Content = new TextBlock + { + Text = "Recent activity", + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + FontSize = 18, + Opacity = 0.7 + } + }, + new ContentPage + { + Header = "Popular", + Content = new TextBlock + { + Text = "Popular right now", + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + FontSize = 18, + Opacity = 0.7 + } + } + } + }; + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml new file mode 100644 index 00000000..22878ec3 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml.cs new file mode 100644 index 00000000..2b7a5a3a --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml.cs @@ -0,0 +1,100 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class TabbedPageWithNavigationPage : UserControl + { + private bool _initialized; + + public TabbedPageWithNavigationPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + if (_initialized) + return; + + _initialized = true; + await BrowseNav.PushAsync(CreateListPage("Browse", "Items", BrowseNav), null); + await SearchNav.PushAsync(CreateListPage("Search", "Results", SearchNav), null); + await AccountNav.PushAsync(CreateListPage("Account", "Options", AccountNav), null); + } + + private static ContentPage CreateListPage(string tabName, string listTitle, NavigationPage nav) + { + var list = new ListBox + { + Margin = new Avalonia.Thickness(8), + Items = + { + $"{listTitle} item 1", + $"{listTitle} item 2", + $"{listTitle} item 3", + $"{listTitle} item 4", + $"{listTitle} item 5" + } + }; + + list.SelectionChanged += async (_, args) => + { + if (args.AddedItems.Count == 0) return; + var item = args.AddedItems[0]?.ToString() ?? string.Empty; + list.SelectedItem = null; + + var detail = new ContentPage + { + Header = item, + Content = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Spacing = 8, + Children = + { + new TextBlock + { + Text = item, + FontSize = 20, + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center + }, + new TextBlock + { + Text = $"Detail view for \"{item}\" in the {tabName} tab.", + FontSize = 13, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + TextAlignment = Avalonia.Media.TextAlignment.Center, + MaxWidth = 280 + } + } + } + }; + + await nav.PushAsync(detail, nav.PageTransition); + }; + + var page = new ContentPage + { + Header = tabName, + Content = list, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + NavigationPage.SetHasNavigationBar(page, false); + return page; + } + + private void OnPlacementChanged(object? sender, SelectionChangedEventArgs e) + { + if (DemoTabs == null) return; + DemoTabs.TabPlacement = PlacementCombo.SelectedIndex == 0 ? TabPlacement.Top : TabPlacement.Bottom; + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TableViewPage.axaml b/samples/AvaloniaControlCatalog/Pages/TableViewPage.axaml new file mode 100644 index 00000000..bd730eb6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TableViewPage.axaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/TableViewPage.axaml.cs b/samples/AvaloniaControlCatalog/Pages/TableViewPage.axaml.cs new file mode 100644 index 00000000..af753dd7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TableViewPage.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages; + +public partial class TableViewPage : ContentPage +{ + public TableViewPage() + { + InitializeComponent(); + DataContext = new TableViewPageViewModel(); + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TextBlockPage.xaml b/samples/AvaloniaControlCatalog/Pages/TextBlockPage.xaml new file mode 100644 index 00000000..3418cffe --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TextBlockPage.xaml @@ -0,0 +1,141 @@ + + + A control that can display text + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This is a + TextBlock + with several + Span elements, + + using a variety of styles + . + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/TextBlockPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/TextBlockPage.xaml.cs new file mode 100644 index 00000000..d10d255a --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TextBlockPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class TextBlockPage : ContentPage + { + public TextBlockPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TextBoxPage.xaml b/samples/AvaloniaControlCatalog/Pages/TextBoxPage.xaml new file mode 100644 index 00000000..9b5a412f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TextBoxPage.xaml @@ -0,0 +1,105 @@ + + + + + + + + + + Custom context flyout + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/TextBoxPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/TextBoxPage.xaml.cs new file mode 100644 index 00000000..3c38a05c --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TextBoxPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class TextBoxPage : ContentPage + { + public TextBoxPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ThemePage.axaml b/samples/AvaloniaControlCatalog/Pages/ThemePage.axaml new file mode 100644 index 00000000..ccc5769f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ThemePage.axaml @@ -0,0 +1,80 @@ + + + + + + + Black + + + White + + + #ffe5ea + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nested ToolTips + + + + ToolTip replaced on the fly + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ToolTipPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ToolTipPage.xaml.cs new file mode 100644 index 00000000..e436407d --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ToolTipPage.xaml.cs @@ -0,0 +1,42 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class ToolTipPage : ContentPage + { + public ToolTipPage() + { + InitializeComponent(); + } + + private void ToolTipOpening(object? sender, CancelRoutedEventArgs args) + { + ((Control)args.Source!).SetValue(ToolTip.TipProperty, "New tip set from ToolTipOpening."); + } + + public void CustomPlacementCallback(CustomPopupPlacement placement) + { + var r = new Random().Next(); + + placement.Anchor = (r % 4) switch + { + 1 => PopupAnchor.Top, + 2 => PopupAnchor.Left, + 3 => PopupAnchor.Right, + _ => PopupAnchor.Bottom, + }; + placement.Gravity = (r % 4) switch + { + 1 => PopupGravity.Top, + 2 => PopupGravity.Left, + 3 => PopupGravity.Right, + _ => PopupGravity.Bottom, + }; + placement.Offset = new Point(r % 20, r % 20); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/TransitioningContentControlPage.axaml b/samples/AvaloniaControlCatalog/Pages/TransitioningContentControlPage.axaml new file mode 100644 index 00000000..5435ed27 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TransitioningContentControlPage.axaml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + 8 + + + + + The TransitioningContentControl control allows you to show a page transition whenever the Content changes. + + + + + + + + + + + + + + + + + + + + + + + + + Single + Multiple + Toggle + AlwaysSelected + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/TreeViewPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/TreeViewPage.xaml.cs new file mode 100644 index 00000000..d03590fe --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/TreeViewPage.xaml.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class TreeViewPage : ContentPage + { + public TreeViewPage() + { + InitializeComponent(); + DataContext = new TreeViewPageViewModel(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/ViewboxPage.xaml b/samples/AvaloniaControlCatalog/Pages/ViewboxPage.xaml new file mode 100644 index 00000000..3848e405 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ViewboxPage.xaml @@ -0,0 +1,54 @@ + + + + + A control used to scale single child. + + + + + + + + + + + + + + + + + + + + + + Uniform + UniformToFill + Fill + None + + + + + + + + Both + DownOnly + UpOnly + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/ViewboxPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/ViewboxPage.xaml.cs new file mode 100644 index 00000000..f7b1ea3f --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/ViewboxPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class ViewboxPage : ContentPage + { + public ViewboxPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/AvaloniaControlCatalog/Pages/WindowCustomizationsPage.xaml new file mode 100644 index 00000000..5114dd16 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/WindowCustomizationsPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/WindowCustomizationsPage.xaml.cs new file mode 100644 index 00000000..2ee8677e --- /dev/null +++ b/samples/AvaloniaControlCatalog/Pages/WindowCustomizationsPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class WindowCustomizationsPage : ContentPage + { + public WindowCustomizationsPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/Pages/teapot.bin b/samples/AvaloniaControlCatalog/Pages/teapot.bin new file mode 100644 index 0000000000000000000000000000000000000000..589eeb912dca4141b6c4d55a767446fe3aad67cf GIT binary patch literal 15464 zcmZ{q37pN<`^V4ujxGBxEsU%!$lk)-@6|6Mg{Y=dO=M6iDxu9(L`oEu8DtAB))pnq z{oWQugvy#~ETsimN*m_?KF{~g+~@xOzwY~Wf6wPRozFSvp67he+!??0hQ`=nwQtO5 z-o|Y2w@`H1ZO#4dkG(Uc5Ep#@sgdbHDbQ%@McGZSl|iGQ+n;+}eJrz0tq8++Jy) zwttJidHt`_KRf#y|3u-qSonDhL_4o7*cEELCP%mLSN#twUEsf$krNcH;gYoBLD3rA z_(fy(@*fC_)+6nS@!KqaQ2M*ICHwF9TizFrnYzxy{=B7YWG=T?LvM(S*X`ATo*&5g zZm(MYu_heT?bZDC(%`=5`Bb~Sx!*CFZSYPurthUu+Tt|?ddvei6t5@H zQIf)VUI!I0K;$>#o}OtNw?TE(p}w4%FFJ-d%w@ z=V5zde0Ank|NVhF|7xzofqDi5^$Z5;84T1j7^r73P}B%K z(RYi!PxKw4?+vZhn0{VltAFa&vi`JTz5K$uzlb)x+Uz~(8#5%M?c@Qz&GMp7yY}2^ zr+gc1jOA?@=9?kqqE5T^ypgU?X3(nDL);4$MZ0$O+5KsDhK!#VjrpxwE%Du$E$`dx zPph&j9JA-Qe(Pfa?QWdCDfdtA?-j+HJL*rrb+!89&8t!-I{m5^;@xmpXkX)od% zlHt}9&Kr)uzR?V7wlU_`=Gu$qnzmym{WF8)I>UL@Gv!ZvY`4ti_Nv8O$7FoBSL-_) z?hy8$y~ZR@W=Zms&%h))441)p`OwzFJ$L=ThqoteRKtRiO4NPJ?Y#O74Wj8A!BC-9;yAooJW(rg-(;qRw{8nChITjIYjo%3SLF zr_8IK!9YEOJY&VrV4$ACKs|#g_m8?O0(CzG>P`sMJ;2#des9rti@s0v9is0It<{)* zt2_Vl&5&t9n|?!mJF868X?G6SmoMNuNPe(k{Y~5lzl(P5*^Afsw#U|>f7?!c@7gKa zwTE}$z99MiYG2^m)o1rxR(nt8YSn9}zy6t5663G&vr9cE;|%EZmLKk|8|QUj?SHi8 z-V4N=x1~aKW4Bu3&2D6)1D0Fy_CNGv;(l}9qVHGg{&Alxs2!E_#^*AoylD4XOwI|p ze~fW!F8Y2IdevEt<{rrl-2S*V$T*2H`!|}z82N!)j~>7EO=AMtE7s=Pi{=XNK)udz zUiD1*EhnFlxn!@(|0LsYSkGQPxkbj5y^`-&;a;id+RyvFPQ0k5*`2D!i|W|wCB7pi zY8T!u&g;Hksr#qkJn>RGW7E$Tujs5w;tku~9TnDBd(|2OwT8f|G1YnkJ-%8S-@B4? zsddsnIj`EQK*H>sE0C+=qiKiA^w41_v7 zSE3WOPV|@hrg+uaj;phsGNwA`DdVd%FJl(X72bh*o#DLd84T1j7^r73P|qNDl=~eg z+$(ig1nRC}p9|y}_nK*}=eMA9&wAWTZ-{p7T^qOb&Eda-rF;4Pte}%<*Dkd)JY&4n z=k;rJ9W7&48$aK_x%;g$&f@*g`R6S>8gb)P+uFg`UbW}`O_%NGcfA8a=f2zhDRWjt z+6YuFL)ru8;&D2l6L1clYS2S;59iLLe0E|OUC@GSKlX#W5(w?Yw z*e9%SsKc86t5==jK%L=~G1Yla8DE|4f1OJ`Ux9kQ0`+_a>iJ4pr+W4S_3Q`gc@NYx z9@Bf9a-XQXLf;Snb|={MD*TMA?7MSi3o6A6BWI#BBW6asxOc2}sB_xJx#Q5hKJ6X% zw!8kB5u1Ju-qL7ZvFvzZ^l(JHe!qB{zZdPs988}{(r(iSCA9mQTLInLm;YAzl6c7; z++Xywvu=!gIgP^hA$RjLx>VSnJ}lv-PgYrer|3pI>wKbp+$`!E>JeG-Q;pk)y50kE z8>=qbW<8?T;C{Ad(#{%Ybzq!pL!I7_cJ57SADP$(l1VuUuUoV7Cf5*CYl!JFO>#Xk ztHw9Uwb4I0ms)2mb>8B=ikakI;Vs^)m`Uyxa~1DZOw}}|Y8q2@jH%kuzqnqElRO(n zoq?3P>sqJOSJgT>W+Z3ep!m|X{g+DZOm_Snv%Q1cbMwqC@e}Rl#$3Bi@8k9yZA?z? zmGPVz=LFI}=t{k6qTRV_PM-nNuD_e9&mJ*94vE*wxh`MMRY5<_)dt4oERMJ;e(K%! zcsWbyO&Mo&sMR^i889MV;;fx`Ib*H)ET8^?H#3rRUH5p-7uSXJX7L>|XNqWDFSF?( z&QB>>znyg_^OmF!uM2y`3zzg2ZPU+X-sz&XSDyj3hL|2xt%oy^9AB+1mNHjzoiRPH z+N+q_E6zaiUd7a2;Vs^)lyjnL8dG(QsoK%MxL%BtJRAI`ReT0?-Hoob{@xzAzc2aW z-l9*9dNY=ndoW^}rAJL>)7aFfwnmJ9cC?D=M(x})-XdBu$c$`WKR03vnntDm?ms+# zY1Cfb#4nNFH)2e?s12I=uGwZ?O&|C7`PTIBl+d)xysi1ZSI4)r8b#CmZ(;*WRq}1Y z#q=4Hk)6FWVrN|*Ev$Qne=_zW-dhshA^eT}pl`Fr@^>57S+!}j@TV4mTc=I$7%iI< z&v$Dxi@Qg?qvd2hHtU|`8f4zYIWZSTy__C_th3;9`ZSS!Fn({^OR{G7Ts)BQ7Ogq- zsx`#a8t|(zlk1^gYW(EdVkvVa*Gd2Xo>%rNPQDP6gC^9s8|P%a?6bKeGg|oM1axI; z-Ge=M+L++U%xHSsr~Tuj4rOpp7WcOWy%KfVo?#1$&X}uL@n?^i-r3Q@4Nv#y%tKG%t>f7H)U{Mo>B8FS|dw zr^AGBJq3f*8j|%gC32#LUq3^CUXya7HfvI-Pk$(y_ICKWY{A{hbERvYTtiH)A!hWL z$@RFOYsKTMwdI?Xxr*1BpE_^RXCD$%d&L|ONP?LT)k z7HM+08T+`_ziQ0DNX;=#+4r~o<4p!jf9Ex3XNoucjPA2+lJ;=@4=4J#HRnYBSodvA z&-=f|S-fp0bA@ZrV;&g$e(`!-d(rrVJFHAuTev?tk@(3f{2SQ+-=CUyR}6~w=d`hV zWE{6Ym+jswwvgf|>ho#&MC)!9y&OFdtS z_7lFVoqE1v>iLSP=PRb}ffTQL_G9XK&sWcQ{=eND`mWITgT52AR%2$Yyk#`sOXm5R z7xj;nFw;c4&(fi6bD!^}{@@N9BVNvBqFsC5pmF@Y>3RR9i>vrHcu}-#?=pLa@4hcB zzM?+ui$uG2_1Poa=9ZW75BJ;{>9(8iK#c#&kW2kmtvbp$xi>uFpTDcGc&jg-?5q8c z#Ot)mckAr4q?Et=!Sdqu$KS`l-RDN!dU9WT-Orl1n)cCt=iM=WcDnz+L3p<~?YzcR zO|(y!Iiwx0F)I@7?)%=R6t6m;ktWmsk#XD_^qB9}anIUx->oN{Hyl5A#kE1v+Vort zKN&!Oz0Po6^&CZxH@P5iYj%4z=kO>Q-|f}Z+{Ycx@5nfAuhg@gabxo<#p`NX zx1{^t7uGTFr}g4>wd+!Ek9b|Z^s|3z)OF%DysST;KJK$P{k3>Q9sYkCoYr2ohJ3Y# zm>yHDC#J_&Yh#||Txy-PC+Agr6;pc^Q+pMty$aM`1*)boRntJ#k$>+iu3ez&6@>K- z?;Ldo675oV(XQ^Ct3NoBK%Mi1HymG``LKUtE_MDB{lj_HGZ<6P zU`#!O{99h}GZ?66Fi>|zzPc-7>VAl+J0Vc_K%nm&eYfcQMBgF$-tcS5Ni!a#A=8K@ zpd_j!u@sa>l_r*f(@@rwHRZrVdBpNiLVV>-1t=xH3Z^2IAuDStnaWU3e3eZVC@;P$ zrYck*t3a#<6;YK;byEW>i?4>M3026d5NknIR8`{XPz_bvoMFy{vruOd&xSgvI>fq= zj!GxigL6=4nEIvxoGHErrXidyzJ{g|)D>SN(-`WJ)i+JdxzIp-=bH1NAz4G>`OpZ} zhwqq>=E z&2?~<_^vb8LwE6AZ!)2$_%h86(2ML^bECNlt`px)rVm_Cc0KWC$V6olZ-E<7H=0{b z7ThGhEOQ(5A?riz3pb0eujvQ3klk+jn`{_>8bBNfgHVHrcfg&fJBfqgE>wSWx48$h z#dnVx0t3Z2#0-Ty#5dI33xmn-HustP;U4kbZ-&7TvLVC=U?^%RaX8$Iy3dR-BjJAW zjWiF!FtTC9hu{J6J!Bq+;bf1PN6je6LFEu1gU3;i6Q6+5sL{kR@FeO{GuDiQQQ{kC zo`T23_mmkAPl#{4c^bx$jWy4h2{2B46U?*l6xmb6i7*~Do;V4fMm=MmGta{W@jY)Q z!?R@15~sjK@l7!=z$CI4%}Zt~v&=j*A7+bhzF7cs$mS4Vg}JD?#Mj^z)I9ULc?0H)?+vpM7LY9< zE`nFZx5zAp*T|NbrDhqtiF%Xx7QBsmoA?eaM=dA53-6(pnib}KSSG&r%}RJnd@Ic= zct?Dz%xZX-Y=y};{LvVP^TkX6D^V+naae^~Mf?C(qw>v%<|BxS?<4au1mgSHtbw@r z)|gM=1F}!eTC)y5Lw!bE4;xS$h@Zno)JEba*o<0hzA#_HI`MsJzJm4Q`^s#A&&9XJ zY=uo^UzlxXJA5g=?PdpjMfMeOCu~7&A$|>8QQOQe^9^hl-#6x4*g>{~_#NyN-*=_} zz9!pk_L#k}54Df@J^X=$#`9DxJkJ7SK)L9&CyV{iy{i1-`)iaKn5H^P-D;3(Nq;-7F#e1Dq1;5V|r z%|GS@oJ5@@o`OPDA(0=k*4je;$5;>2$o?@UY)LpFzLK^SoDyFtTN(_RA(jD;tb{$y zmW7h2lEiXQ3RQ|&9!jH16DvR&)M>V&tpsJoSIJg}a^kCOt3Y}2Rk2l}0$DX%-PVAb zsG7uDa60OAVr@7Bbq4WFI15$Xo^9(u4e`~nb)lB{>e_UuExvSH56&bz+n!_VLmlze zw+)~!SzTg7NJpg;8$mtPIkvHF0`;M;` zE+S^ok6*0dQgQe;3hl|-6FWi&adfno+5gzfZ6~+_bp^3AbU}3?c7-cZR}#C??<#vW zbP~tawmWndM|ax;x{9NR?Mc6DY%jQ49KCFB=uXz1crElm^&nnHzw2!#^b$v=y#acY z^(Nj3*NWptdz0;BZ??C4QqCO;k1RtY5Ca!@`P%-Ku*4lOOf%w+h&)_5ReP-9g8nRFA2Kzaz72oG}BdjA^N8ALTp*|yShV`fo_6z$Z zd@jB(?N_joY$I_CY!crVyA?K*ZL{0$4%ms>N&Fgip>`3!fp1aY62F52)ONeu?tvZR z+hg~_*W%l2_rW*f+h@Or@5pxBAMAeEBfkCiN7zfYm-rLxL+vB}4Bw-Eu)o*?uwQ%! z>_PaE>__4u_(^<+?62@M*JAwa%wVr#OM4d#Y!6{UsE#Z{}<2heR zuM}7^ODqi@$|IJ6G}LKcS+5+FN0ld5fQqP!#7a;ZRhd`?s-ntz)x7FZPJGq98c;!e zHN2WoNqjZET2O_ons>Ta8>)-1ws!{9Age(<6KbMr63>EKsMEc(y*f}^e099Ka0b~K z#B?}QeCb|2IE(BYufEp+8loB!8$n}KV`39H7j-W2JUAa!-@CwT3Jt{9)N2Nf#MjJg z4o$?@+`ACYBfG$B;kAUO;%n)(f@WmRh^?VHsyVR@T!?Dnwe{LTOYybyE`nBMt%w&x zYw=y|T>@>$F7?`b9UudhL5zTp@`+LCi0VkZ4E}>^?_KV7f)3*AD zdtKl%vdg`$-j&cvd{=ti;0m%Uh*v>pRA=JV&;`}i>+bb{E5+Bt>j~Y+x)HB|tHgJW z*9)#D>+N0ZUB|n(cRggHGKn|9ji?)mH$flN&Gfm2_s!m|aJ@Kg^|Igwab$V7!A)d+ z>C=yQU+;FfRUEf_{UM7iigZoj#=<@*YVcu}KM;ybw5inF7BfODtAK8QSd5HIe-or3l91nYs zzzDJt#7AKyY7~8Pc#rZPgNMcOnD;n5LiPyp33wDW+8g6N$$PXn7RI5*5ubwbsPV+7 z;ThBf`aH{ff;SPyiesWT37!(iB=0$Rn(TS{Oy>Q(Hw7k&V~Y0zOd^{^d=Z{Qy+og> zykGKiVTw3%y*zk<>;>X9co8+-o8i68d%8ChW}#*gXTu!S9O7Jf1vQU8^Lfwn7Qjq# zEbv~1+2VNBdkyB2y-uGuc)#u~gazVQ=q-X*$zCNchSyL_=(Cjf5^otS6vr~}O;|*> zi1-#PM!oI5<1L4GQSTDpgB7S1#P?w(Y9(OK8AoSAg+NpDo*?aK0tlwed?`+kHojuTL&MLeN6le)`;&jZ#{fM zw!!<{+X$Obn~0m?3)C0HFX1cHSHvx_74^Bd&D#ze#kbwt0h`6Q!`lg8if^a)HEbc< z=I!#nf$ie^#`_j_knJFT2Rl(ai3RXAYL~a$+XLT-Z;!VZz9svXxDUP)-#+hqC?Na6 z+wc7dKcRji{tUmMejy%!gQ$bVL+~qVzjxR>0zZoHh<6lz7T;0t7#tAaG4D4xM0VKw z-8&9P#CP2L1CEj%CH@J=P{)XW!EdPFy}!MG;JEny@lL=WWPcD(!k^+h>79bV$O`$> zGr!pK6j%0o%# literal 0 HcmV?d00001 diff --git a/samples/AvaloniaControlCatalog/PlatformRenderInterface.cs b/samples/AvaloniaControlCatalog/PlatformRenderInterface.cs new file mode 100644 index 00000000..271678b8 --- /dev/null +++ b/samples/AvaloniaControlCatalog/PlatformRenderInterface.cs @@ -0,0 +1,315 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.IO; +using System.Numerics; +using Avalonia; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; +using Avalonia.Platform; +using Avalonia.Platform.Surfaces; +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Text; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Transforms; +using AvaloniaGlyphRun = Avalonia.Media.GlyphRun; + +namespace AvaloniaControlCatalog; + +/// +/// Avalonia rendering platform implementation backed by ImageSharp.Drawing. +/// +internal sealed class PlatformRenderInterface : IPlatformRenderInterface +{ + /// + /// Registers the ImageSharp.Drawing platform renderer and font manager with Avalonia. + /// + private static readonly object WebGpuLogGate = new(); + + private static readonly string WebGpuLogPath = + System.IO.Path.Combine(AppContext.BaseDirectory, "webgpu-errors.log"); + + public static void Initialize() + { + // Surface native WebGPU validation/device-lost errors instead of letting them be + // silently swallowed. Without this the GPU backend can corrupt the surface or lose + // the device with no diagnostic, which reads as a mysterious hang or blanked output. + // + // This is a WinExe with no attached console, and the callback can fire from a native + // thread immediately before the process dies, so the only reliable sink is a file that + // is opened, written, flushed, and closed on every call. Debug.WriteLine is kept for the + // case where a debugger is attached (run in Debug so the wgpu validation layer is active). + File.WriteAllText(WebGpuLogPath, $"WebGPU error log started {DateTime.Now:O}{Environment.NewLine}"); + WebGPUEnvironment.UncapturedError = static (type, message) => AppendWebGpuLog($"[WebGPU {type}] {message}"); + + // The uncaptured-error callback above does NOT catch the validation error that loses the + // device (the binding panics on the failing wgpuQueueSubmit before it routes through us). + // wgpu's own log callback DOES surface that root error, so wire it at Warn level. + try + { + WebGPUEnvironment.EnableNativeLogging(AppendWebGpuLog); + } + catch (Exception ex) + { + AppendWebGpuLog($"failed to wire wgpu log callback: {ex.Message}"); + } + + PlatformRenderInterface platform = new(); + + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(platform) + .Bind().ToConstant(new FontManagerImpl()); + } + + private static void AppendWebGpuLog(string message) + { + string line = $"{DateTime.Now:HH:mm:ss.fff} {message}"; + System.Diagnostics.Debug.WriteLine(line); + try + { + lock (WebGpuLogGate) + { + File.AppendAllText(WebGpuLogPath, line + Environment.NewLine); + } + } + catch (IOException) + { + // Never let a logging failure escape the native callback boundary. + } + } + + /// + public bool SupportsIndividualRoundRects => true; + + /// + public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; + + /// + public PixelFormat DefaultPixelFormat => PixelFormat.Bgra8888; + + /// + public bool SupportsRegions => true; + + /// + public IGeometryImpl CreateEllipseGeometry(Rect rect) + => new EllipseGeometryImpl(rect); + + /// + public IGeometryImpl CreateLineGeometry(AvaloniaPoint p1, AvaloniaPoint p2) + => new LineGeometryImpl(p1, p2); + + /// + public IGeometryImpl CreateRectangleGeometry(Rect rect) => new RectangleGeometryImpl(rect); + + /// + public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl(); + + /// + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + => new GeometryGroupImpl(fillRule, children); + + /// + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) + => CombinedGeometryImpl.Create(combineMode, (GeometryImpl)g1, (GeometryImpl)g2); + + /// + public IGeometryImpl BuildGlyphRunGeometry(AvaloniaGlyphRun glyphRun) + { + SixLabors.Fonts.Font font = ((PlatformTypeface)glyphRun.GlyphTypeface.PlatformTypeface).CreateFont((float)glyphRun.FontRenderingEmSize); + GlyphRunImpl glyphRunImpl = new(glyphRun.GlyphTypeface, glyphRun.FontRenderingEmSize, glyphRun.GlyphInfos, glyphRun.BaselineOrigin, font); + RichGlyphOptions options = new() + { + Font = font, + HintingMode = HintingMode.None, + + // Avalonia positions glyph runs by their baseline origins, so the run must be + // anchored by the alphabetic baseline rather than the line-box default. + TextBaseline = TextBaseline.Alphabetic, + ColorFontSupport = ColorFontSupport.ColrV0 | ColorFontSupport.ColrV1 | ColorFontSupport.Svg + }; + + ComplexPolygon glyphPath = new(TextBuilder.GeneratePaths(glyphRunImpl.GlyphIds.Span, glyphRunImpl.GlyphOrigins.Span, options)); + + return new StreamGeometryImpl(glyphPath, glyphPath, IntersectionRule.NonZero); + } + + /// + public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, AvaloniaVector dpi) + => new RenderTargetBitmapImpl(size, dpi); + + /// + public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, AvaloniaVector dpi, PixelFormat format, AlphaFormat alphaFormat) + => new WriteableBitmapImpl(size, dpi, format, alphaFormat); + + /// + public IBitmapImpl LoadBitmap(string fileName) + { + using FileStream stream = File.OpenRead(fileName); + + return this.LoadBitmap(stream); + } + + /// + public IBitmapImpl LoadBitmap(Stream stream) + => new ImmutableBitmapImpl(stream); + + /// + public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + => new WriteableBitmapImpl(stream, width, horizontal: true, interpolationMode); + + /// + public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + => new WriteableBitmapImpl(stream, height, horizontal: false, interpolationMode); + + /// + public IWriteableBitmapImpl LoadWriteableBitmap(string fileName) + { + using FileStream stream = File.OpenRead(fileName); + + return this.LoadWriteableBitmap(stream); + } + + /// + public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream) + => new WriteableBitmapImpl(stream); + + /// + public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + => new ImmutableBitmapImpl(stream, width, horizontal: true, interpolationMode); + + /// + public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + => new ImmutableBitmapImpl(stream, height, horizontal: false, interpolationMode); + + /// + public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + ImmutableBitmapImpl? bitmap = bitmapImpl as ImmutableBitmapImpl; + + if (bitmap is not null) + { + return new ImmutableBitmapImpl(bitmap, destinationSize, interpolationMode); + } + + throw new InvalidOperationException("Invalid source bitmap type."); + } + + /// + public unsafe IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, AvaloniaVector dpi, int stride) + => new ImmutableBitmapImpl(size, dpi, stride, format, alphaFormat, data); + + /// + public IGlyphRunImpl CreateGlyphRun(GlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, AvaloniaPoint baselineOrigin) + { + // Resolve the exact SixLabors face this run was shaped against (via the public PlatformTypeface) + // so the glyph ids line up - mirroring how the Skia backend carries the SKTypeface. + SixLabors.Fonts.Font font = ((PlatformTypeface)glyphTypeface.PlatformTypeface).CreateFont((float)fontRenderingEmSize); + + return new GlyphRunImpl(glyphTypeface, fontRenderingEmSize, glyphInfos, baselineOrigin, font); + } + + /// + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsApiContext) + => new PlatformRenderInterfaceContext(graphicsApiContext is not null); + + /// + public bool IsSupportedBitmapPixelFormat(PixelFormat format) + => format == PixelFormats.Rgb565 + || format == PixelFormats.Bgra8888 + || format == PixelFormats.Rgba8888; + + /// + public IPlatformRenderInterfaceRegion CreateRegion() => new RegionImpl(); +} + +/// +/// ImageSharp.Drawing render-context implementation used by Avalonia render targets and layers. +/// +internal sealed class PlatformRenderInterfaceContext : IPlatformRenderInterfaceContext +{ + private readonly bool hasPlatformGraphicsContext; + private readonly WebGPUSurfaceSession webGpuSurfaceSession = new(); + private bool useWebGpuBackend; + + /// + /// Initializes a new backend context. + /// + /// Whether Avalonia created a platform graphics context for this renderer. + public PlatformRenderInterfaceContext(bool hasPlatformGraphicsContext) + => this.hasPlatformGraphicsContext = hasPlatformGraphicsContext; + + /// + public bool IsLost => this.webGpuSurfaceSession.IsLost; + + /// + public IReadOnlyDictionary PublicFeatures { get; } = new Dictionary(); + + /// + public PixelSize? MaxOffscreenRenderTargetPixelSize => null; + + /// + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + { + bool useWebGpu = DrawingContextImpl.BackendMode == DrawingBackendMode.WebGpu + || (DrawingContextImpl.BackendMode == DrawingBackendMode.Auto && this.hasPlatformGraphicsContext); + + if (useWebGpu) + { + foreach (IPlatformRenderSurface surface in surfaces) + { + if (surface is INativePlatformHandleSurface nativeSurface && + WebGPURenderTargetImpl.TryCreate(this.webGpuSurfaceSession, nativeSurface, out WebGPURenderTargetImpl? renderTarget)) + { + this.useWebGpuBackend = true; + return renderTarget; + } + } + + if (DrawingContextImpl.BackendMode == DrawingBackendMode.WebGpu) + { + throw new NotSupportedException("WebGPU rendering requires an Avalonia native surface with enough platform handles to create a WebGPU surface."); + } + } + + foreach (IPlatformRenderSurface surface in surfaces) + { + if (surface is IFramebufferPlatformSurface framebufferSurface) + { + this.useWebGpuBackend = false; + return new RenderTargetImpl(framebufferSurface.CreateFramebufferRenderTarget()); + } + } + + throw new NotSupportedException("ImageSharp.Drawing sample renderer requires a framebuffer surface."); + } + + /// + public IDrawingContextLayerImpl CreateOffscreenRenderTarget(PixelSize pixelSize, AvaloniaVector scaling, bool enableTextAntialiasing) + { + AvaloniaVector dpi = scaling * 96; + if (this.useWebGpuBackend) + { + return new WebGPULayerImpl(this.webGpuSurfaceSession, pixelSize, dpi); + } + + return new RenderTargetBitmapImpl(pixelSize, dpi); + } + + /// + public object? TryGetFeature(Type featureType) => null; + + /// + public void Dispose() + { + this.webGpuSurfaceSession.Dispose(); + } + +} diff --git a/samples/AvaloniaControlCatalog/Program.cs b/samples/AvaloniaControlCatalog/Program.cs new file mode 100644 index 00000000..6510d7da --- /dev/null +++ b/samples/AvaloniaControlCatalog/Program.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using Avalonia; +using ControlCatalog; +using SixLabors.ImageSharp; + +namespace AvaloniaControlCatalog; + +internal static class Program +{ + [STAThread] + public static void Main(string[] args) + => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + + private static AppBuilder BuildAvaloniaApp() + { + DrawingContextImpl.BackendMode = Environment.GetEnvironmentVariable("IMAGESHARP_DRAWING_BACKEND")?.ToLowerInvariant() switch + { + "cpu" => DrawingBackendMode.Cpu, + "webgpu" => DrawingBackendMode.WebGpu, + _ => DrawingBackendMode.Auto + }; + + // DrawingContextImpl.BackendMode = DrawingBackendMode.Cpu; + + if (int.TryParse(Environment.GetEnvironmentVariable("IMAGESHARP_DRAWING_MAX_PARALLELISM"), out int maxParallelism)) + { + Configuration.Default.MaxDegreeOfParallelism = maxParallelism; + } + + AppBuilder builder = AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont(); + + if (DrawingContextImpl.BackendMode == DrawingBackendMode.Cpu) + { + // This sample-only override must run before Avalonia creates the backend context; + // otherwise Win32 creates a platform graphics context and the top-level surfaces + // represent the GPU/composited path instead of the framebuffer path we need to test. + builder = builder.With(new Win32PlatformOptions + { + RenderingMode = [Win32RenderingMode.Software] + }); + } + + // Temporary backend switch for visual comparison: ISD_BACKEND=skia uses Avalonia's default (Skia). + if (!string.Equals(Environment.GetEnvironmentVariable("ISD_BACKEND"), "skia", StringComparison.OrdinalIgnoreCase)) + { + builder = builder.UseImageSharpDrawing(); + } + + return builder; + } +} diff --git a/samples/AvaloniaControlCatalog/RectangleGeometryImpl.cs b/samples/AvaloniaControlCatalog/RectangleGeometryImpl.cs new file mode 100644 index 00000000..bf87fab9 --- /dev/null +++ b/samples/AvaloniaControlCatalog/RectangleGeometryImpl.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Avalonia; +using SixLabors.ImageSharp.Drawing; + +namespace AvaloniaControlCatalog; + +/// +/// Avalonia rectangle geometry implementation backed by an ImageSharp rectangle path. +/// +internal sealed class RectangleGeometryImpl : GeometryImpl +{ + /// + /// Initializes a new rectangle geometry. + /// + /// The rectangle bounds. + public RectangleGeometryImpl(Rect rect) + : this(new RectanglePolygon(rect.ToRectangleF())) + { + } + + private RectangleGeometryImpl(IPath path) + : base(path, path, IntersectionRule.EvenOdd) + { + } +} diff --git a/samples/AvaloniaControlCatalog/RegionImpl.cs b/samples/AvaloniaControlCatalog/RegionImpl.cs new file mode 100644 index 00000000..1f32d0c4 --- /dev/null +++ b/samples/AvaloniaControlCatalog/RegionImpl.cs @@ -0,0 +1,106 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Avalonia; +using Avalonia.Platform; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; + +namespace AvaloniaControlCatalog; + +/// +/// ImageSharp-backed region implementation for Avalonia clipping and hit testing. +/// +internal sealed class RegionImpl : IPlatformRenderInterfaceRegion +{ + private readonly Region region = new(); + private readonly List rects = []; + private bool rectsValid = true; + + /// + public bool IsEmpty => this.region.IsEmpty; + + /// + public LtrbPixelRect Bounds + { + get + { + Rectangle bounds = this.region.Bounds; + return new() + { + Left = bounds.Left, + Top = bounds.Top, + Right = bounds.Right, + Bottom = bounds.Bottom + }; + } + } + + /// + public IList Rects + { + get + { + if (this.rectsValid) + { + return this.rects; + } + + this.rects.Clear(); + IReadOnlyList rectangles = this.region.Rectangles; + for (int i = 0; i < rectangles.Count; i++) + { + Rectangle rectangle = rectangles[i]; + this.rects.Add(new LtrbPixelRect + { + Left = rectangle.Left, + Top = rectangle.Top, + Right = rectangle.Right, + Bottom = rectangle.Bottom + }); + } + + this.rectsValid = true; + return this.rects; + } + } + + /// + public void AddRect(LtrbPixelRect rect) + { + this.region.Add(Rectangle.FromLTRB(rect.Left, rect.Top, rect.Right, rect.Bottom)); + this.rectsValid = false; + } + + /// + public void Reset() + { + this.region.Clear(); + this.rects.Clear(); + this.rectsValid = true; + } + + /// + public bool Intersects(LtrbRect rect) + => this.region.Intersects( + Rectangle.FromLTRB( + (int)rect.Left, + (int)rect.Top, + (int)Math.Ceiling(rect.Right), + (int)Math.Ceiling(rect.Bottom))); + + /// + public bool Contains(AvaloniaPoint pt) + => this.region.Contains((int)pt.X, (int)pt.Y); + + /// + /// Converts the current region to an ImageSharp path. + /// + /// The ImageSharp path representing the region. + public IPath ToPath() => this.region.ToPath(); + + /// + public void Dispose() + { + } +} diff --git a/samples/AvaloniaControlCatalog/RenderTargetBitmapImpl.cs b/samples/AvaloniaControlCatalog/RenderTargetBitmapImpl.cs new file mode 100644 index 00000000..adb350fa --- /dev/null +++ b/samples/AvaloniaControlCatalog/RenderTargetBitmapImpl.cs @@ -0,0 +1,106 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Avalonia; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Platform.Surfaces; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace AvaloniaControlCatalog; + +/// +/// ImageSharp-backed render-target bitmap implementation for offscreen layers and render-target bitmaps. +/// +internal sealed class RenderTargetBitmapImpl : IRenderTargetBitmapImpl, IDrawingContextLayerImpl, IFramebufferPlatformSurface, IDrawableBitmapImpl +{ + private readonly WriteableBitmapImpl bitmap; + private readonly RenderTargetImpl renderTarget; + + /// + /// Initializes a new render-target bitmap. + /// + /// The bitmap size in pixels. + /// The bitmap DPI. + public RenderTargetBitmapImpl(PixelSize size, Vector dpi) + { + this.bitmap = new WriteableBitmapImpl(size, dpi); + this.renderTarget = new RenderTargetImpl(this.CreateFramebufferRenderTarget()); + } + + /// + /// Gets the ImageSharp pixels drawn for this render target. + /// + public Image Image => this.bitmap.Image; + + /// + public Vector Dpi => this.bitmap.Dpi; + + /// + public PixelSize PixelSize => this.bitmap.PixelSize; + + /// + public int Version => this.bitmap.Version; + + /// + public PixelFormat? Format => this.bitmap.Format; + + /// + public AlphaFormat? AlphaFormat => this.bitmap.AlphaFormat; + + /// + public bool CanBlit => true; + + /// + public bool IsCorrupted => false; + + /// + public void Blit(IDrawingContextImpl context) + { + DrawingContextImpl drawingContext = (DrawingContextImpl)context; + drawingContext.Blit(this.Image, this.PixelSize); + } + + /// + public void Draw( + DrawingContextImpl context, + Rectangle sourceRect, + RectangleF destinationRect, + IResampler sampler, + float opacity, + PixelAlphaCompositionMode alphaCompositionMode) + => context.DrawImage(this.Image, sourceRect, destinationRect, sampler, opacity, alphaCompositionMode); + + /// + public IDrawingContextImpl CreateDrawingContext() + => this.renderTarget.CreateDrawingContext( + new IRenderTarget.RenderTargetSceneInfo(this.PixelSize, this.Dpi.X / 96D, Avalonia.Rendering.Composition.CompositionTransparencyLevel.None), + out _); + + /// + public ILockedFramebuffer Lock() => this.bitmap.Lock(); + + /// + public IFramebufferRenderTarget CreateFramebufferRenderTarget() + => new FuncFramebufferRenderTarget( + (IRenderTarget.RenderTargetSceneInfo _, out FramebufferLockProperties properties) => + { + // Dirty-rect rendering clears and redraws only the active clip, so pixels outside + // that clip must remain from the previous lock until the layer is copied out. + properties = new FramebufferLockProperties(PreviousFrameIsRetained: true); + return this.Lock(); + }, + retainsFrameContents: true); + + /// + public void Save(Stream stream, BitmapEncoderOptions options) => this.bitmap.Save(stream, options); + + /// + public void Dispose() + { + this.renderTarget.Dispose(); + this.bitmap.Dispose(); + } +} diff --git a/samples/AvaloniaControlCatalog/RenderTargetImpl.cs b/samples/AvaloniaControlCatalog/RenderTargetImpl.cs new file mode 100644 index 00000000..ea0449e7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/RenderTargetImpl.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Avalonia.Platform; +using Avalonia.Platform.Surfaces; + +namespace AvaloniaControlCatalog; + +/// +/// Framebuffer render target for the ImageSharp.Drawing sample renderer. +/// +internal sealed class RenderTargetImpl : IRenderTarget +{ + private IFramebufferRenderTarget? framebufferRenderTarget; + + /// + /// Initializes a new framebuffer render target wrapper. + /// + /// The Avalonia framebuffer render target. + public RenderTargetImpl(IFramebufferRenderTarget framebufferRenderTarget) + => this.framebufferRenderTarget = framebufferRenderTarget; + + /// + public RenderTargetProperties Properties => new() + { + IsSuitableForDirectRendering = true, + RetainsPreviousFrameContents = this.framebufferRenderTarget?.RetainsFrameContents == true + }; + + /// + public PlatformRenderTargetState PlatformRenderTargetState + => this.framebufferRenderTarget?.State ?? PlatformRenderTargetState.Disposed; + + /// + public IDrawingContextImpl CreateDrawingContext(IRenderTarget.RenderTargetSceneInfo sceneInfo, out RenderTargetDrawingContextProperties properties) + { + if (this.framebufferRenderTarget is null) + { + throw new ObjectDisposedException(nameof(RenderTargetImpl)); + } + + ILockedFramebuffer framebuffer = this.framebufferRenderTarget.Lock(sceneInfo, out FramebufferLockProperties lockProperties); + + properties = new RenderTargetDrawingContextProperties + { + PreviousFrameIsRetained = lockProperties.PreviousFrameIsRetained + }; + + return new DrawingContextImpl(framebuffer); + } + + /// + public void Dispose() + { + this.framebufferRenderTarget?.Dispose(); + this.framebufferRenderTarget = null; + } +} diff --git a/samples/AvaloniaControlCatalog/ScrollPage.xaml b/samples/AvaloniaControlCatalog/ScrollPage.xaml new file mode 100644 index 00000000..6d5ecb81 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ScrollPage.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/StreamGeometryImpl.cs b/samples/AvaloniaControlCatalog/StreamGeometryImpl.cs new file mode 100644 index 00000000..64de0db7 --- /dev/null +++ b/samples/AvaloniaControlCatalog/StreamGeometryImpl.cs @@ -0,0 +1,216 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Avalonia; +using Avalonia.Media; +using Avalonia.Platform; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; + +namespace AvaloniaControlCatalog; + +/// +/// Mutable stream geometry implementation backed by ImageSharp path builders. +/// +internal sealed class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl +{ + private IPath strokePath = EmptyPath.OpenPath; + private IPath? fillPath = EmptyPath.OpenPath; + + /// + /// Initializes an empty stream geometry. + /// + public StreamGeometryImpl() + : base(EmptyPath.OpenPath, EmptyPath.OpenPath, IntersectionRule.EvenOdd) + { + } + + internal StreamGeometryImpl(IPath strokePath, IPath? fillPath, IntersectionRule fillRule) + : base(strokePath, fillPath, fillRule) + { + this.strokePath = strokePath; + this.fillPath = fillPath; + } + + /// + public IStreamGeometryImpl Clone() => new StreamGeometryImpl(this.strokePath, this.fillPath, this.FillRule); + + /// + public IStreamGeometryContextImpl Open() => new StreamGeometryImplContext(this); + + private void SetStreamPaths(IPath strokePath, IPath? fillPath, IntersectionRule fillRule) + { + this.strokePath = strokePath; + this.fillPath = fillPath; + this.SetPaths(strokePath, fillPath, fillRule); + } + + /// + /// Stream geometry context that records Avalonia path commands into ImageSharp path builders. + /// + private sealed class StreamGeometryImplContext : IStreamGeometryContextImpl + { + private readonly StreamGeometryImpl owner; + private readonly PathBuilder strokeBuilder = new(); + private readonly PathBuilder fillBuilder = new(); + private PointF startPoint; + private bool isFilled; + private bool isFigureBroken; + private bool hasFill; + private IntersectionRule fillRule = IntersectionRule.EvenOdd; + + /// + /// Initializes a new stream geometry context. + /// + /// The stream geometry updated when the context is disposed. + public StreamGeometryImplContext(StreamGeometryImpl owner) + => this.owner = owner; + + /// + public void ArcTo(AvaloniaPoint point, AvaloniaSize size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, bool isStroked = true) + { + PointF endPoint = point.ToPointF(); + bool sweep = sweepDirection == SweepDirection.Clockwise; + + // Avalonia's rotationAngle is nominally radians, but the Skia backend passes the raw value + // straight to SKPath.ArcTo, whose x-axis rotation is degrees - so callers (e.g. the rounded + // rectangle builder, which passes PiOver2) are effectively calibrated against a degrees-valued + // axis rotation. Match Skia by forwarding the value unchanged rather than converting to degrees. + // See https://github.com/AvaloniaUI/Avalonia/issues/21445. + float rotation = (float)rotationAngle; + + if (isStroked) + { + this.strokeBuilder.ArcTo((float)size.Width, (float)size.Height, rotation, isLargeArc, sweep, endPoint); + } + else + { + this.BreakStrokeFigure(endPoint); + } + + if (this.isFilled) + { + this.fillBuilder.ArcTo((float)size.Width, (float)size.Height, rotation, isLargeArc, sweep, endPoint); + } + } + + /// + public void BeginFigure(AvaloniaPoint startPoint, bool isFilled = true) + { + this.startPoint = startPoint.ToPointF(); + this.isFilled = isFilled; + this.isFigureBroken = false; + this.strokeBuilder.MoveTo(this.startPoint); + + if (isFilled) + { + this.hasFill = true; + this.fillBuilder.MoveTo(this.startPoint); + } + } + + /// + public void CubicBezierTo(AvaloniaPoint controlPoint1, AvaloniaPoint controlPoint2, AvaloniaPoint endPoint, bool isStroked = true) + { + PointF imageEndPoint = endPoint.ToPointF(); + + if (isStroked) + { + this.strokeBuilder.CubicBezierTo( + controlPoint1.ToPointF(), + controlPoint2.ToPointF(), + imageEndPoint); + } + else + { + this.BreakStrokeFigure(imageEndPoint); + } + + if (this.isFilled) + { + this.fillBuilder.CubicBezierTo( + controlPoint1.ToPointF(), + controlPoint2.ToPointF(), + imageEndPoint); + } + } + + /// + public void QuadraticBezierTo(AvaloniaPoint controlPoint, AvaloniaPoint endPoint, bool isStroked = true) + { + PointF imageEndPoint = endPoint.ToPointF(); + + if (isStroked) + { + this.strokeBuilder.QuadraticBezierTo(controlPoint.ToPointF(), imageEndPoint); + } + else + { + this.BreakStrokeFigure(imageEndPoint); + } + + if (this.isFilled) + { + this.fillBuilder.QuadraticBezierTo(controlPoint.ToPointF(), imageEndPoint); + } + } + + /// + public void LineTo(AvaloniaPoint point, bool isStroked = true) + { + PointF imagePoint = point.ToPointF(); + + if (isStroked) + { + this.strokeBuilder.LineTo(imagePoint); + } + else + { + this.BreakStrokeFigure(imagePoint); + } + + if (this.isFilled) + { + this.fillBuilder.LineTo(imagePoint); + } + } + + /// + public void EndFigure(bool isClosed) + { + if (isClosed) + { + if (this.isFigureBroken) + { + this.strokeBuilder.LineTo(this.startPoint); + this.isFigureBroken = false; + } + else + { + this.strokeBuilder.CloseFigure(); + } + + if (this.isFilled) + { + this.fillBuilder.CloseFigure(); + } + } + } + + /// + public void SetFillRule(FillRule fillRule) + { + this.fillRule = fillRule.ToIntersectionRule(); + } + + /// + public void Dispose() + => this.owner.SetStreamPaths(this.strokeBuilder.Build(), this.hasFill ? this.fillBuilder.Build() : null, this.fillRule); + + private void BreakStrokeFigure(PointF point) + { + this.isFigureBroken = true; + this.strokeBuilder.MoveTo(point); + } + } +} diff --git a/samples/AvaloniaControlCatalog/TextShaperImpl.cs b/samples/AvaloniaControlCatalog/TextShaperImpl.cs new file mode 100644 index 00000000..5a945225 --- /dev/null +++ b/samples/AvaloniaControlCatalog/TextShaperImpl.cs @@ -0,0 +1,185 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Concurrent; +using System.Globalization; +using Avalonia; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Platform; +using SixLaborsFont = SixLabors.Fonts.Font; +using SixLaborsShapedGlyph = SixLabors.Fonts.ShapedGlyph; +using SixLaborsTag = SixLabors.Fonts.Tables.AdvancedTypographic.Tag; +using SixLaborsTextDirection = SixLabors.Fonts.TextDirection; +using SixLaborsTextShaper = SixLabors.Fonts.TextShaper; +using SixLaborsTextShapingBuffer = SixLabors.Fonts.TextShapingBuffer; + +namespace AvaloniaControlCatalog; + +/// +/// Avalonia text shaper implementation backed by SixLabors.Fonts. +/// +internal sealed class TextShaperImpl : ITextShaperImpl +{ + [ThreadStatic] + private static SixLaborsTextShapingBuffer? shapingBuffer; + + /// + public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) + { + if (text.Length == 0) + { + return new ShapedBuffer(text, 0, options.GlyphTypeface, options.FontRenderingEmSize, options.BidiLevel); + } + + GlyphTypeface glyphTypeface = options.GlyphTypeface; + + if (glyphTypeface.TextShaperTypeface is not TextShaperTypeface shaperTypeface) + { + throw new NotSupportedException("The provided GlyphTypeface is not supported by this text shaper."); + } + + ReadOnlySpan textSpan = text.Span; + + // A trailing break character must keep its cluster for caret navigation while + // rendering as nothing, and a trailing carriage return and line feed pair + // shares one cluster anchored at the carriage return. + int trailingBreakStart = -1; + if (new Codepoint(textSpan[^1]).IsBreakChar) + { + trailingBreakStart = textSpan.Length > 1 && textSpan[^2] == '\r' && textSpan[^1] == '\n' + ? textSpan.Length - 2 + : textSpan.Length - 1; + } + + SixLaborsTextShapingBuffer buffer = shapingBuffer ??= new SixLaborsTextShapingBuffer(); + buffer.Add(textSpan); + buffer.TextDirection = (options.BidiLevel & 1) == 0 + ? SixLaborsTextDirection.LeftToRight + : SixLaborsTextDirection.RightToLeft; + buffer.Language = options.Culture ?? CultureInfo.CurrentCulture; + + SixLaborsFont font = shaperTypeface.GetFont((float)options.FontRenderingEmSize); + SixLaborsTag[] features = GetFeatures(options); + + if (features.Length > 0) + { + SixLaborsTextShaper.ShapeRun(font, buffer, features); + } + else + { + SixLaborsTextShaper.ShapeRun(font, buffer); + } + + int bufferLength = buffer.Count; + ShapedBuffer shapedBuffer = new(text, bufferLength, glyphTypeface, options.FontRenderingEmSize, options.BidiLevel); + + ushort invisibleGlyph = trailingBreakStart >= 0 ? glyphTypeface.CharacterToGlyphMap[' '] : (ushort)0; + + for (int i = 0; i < bufferLength; i++) + { + ref readonly SixLaborsShapedGlyph shapedGlyph = ref buffer[i]; + + ushort glyphIndex = shapedGlyph.GlyphId; + int glyphCluster = shapedGlyph.StringIndex; + double glyphAdvance = shapedGlyph.AdvanceWidth + options.LetterSpacing; + + // Shaping offsets are Y-up while Avalonia positions glyphs Y-down. + Vector glyphOffset = new(shapedGlyph.Offset.X, -shapedGlyph.Offset.Y); + + if (trailingBreakStart >= 0 && glyphCluster >= trailingBreakStart) + { + glyphIndex = invisibleGlyph; + glyphCluster = trailingBreakStart; + glyphAdvance = options.LetterSpacing; + glyphOffset = default; + } + + shapedBuffer[i] = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); + } + + return shapedBuffer; + } + + /// + public ITextShaperTypeface CreateTypeface(GlyphTypeface glyphTypeface) + { + if (glyphTypeface.PlatformTypeface is not PlatformTypeface platformTypeface) + { + throw new NotSupportedException("The provided GlyphTypeface is not supported by this text shaper."); + } + + return new TextShaperTypeface(platformTypeface); + } + + /// + /// Maps the requested font features to shaping feature tags. + /// + /// The text shaper options. + /// The feature tags to turn on. + private static SixLaborsTag[] GetFeatures(TextShaperOptions options) + { + if (options.FontFeatures is null || options.FontFeatures.Count == 0) + { + return []; + } + + // Shaping accepts whole-run feature enablement only, so disabled features and + // sub-run feature ranges cannot be expressed and are not forwarded. + int enabled = 0; + for (int i = 0; i < options.FontFeatures.Count; i++) + { + if (options.FontFeatures[i].Value != 0) + { + enabled++; + } + } + + if (enabled == 0) + { + return []; + } + + SixLaborsTag[] features = new SixLaborsTag[enabled]; + int index = 0; + for (int i = 0; i < options.FontFeatures.Count; i++) + { + FontFeature fontFeature = options.FontFeatures[i]; + if (fontFeature.Value != 0) + { + features[index++] = SixLaborsTag.Parse(fontFeature.Tag); + } + } + + return features; + } +} + +/// +/// Text shaper typeface caching the SixLabors fonts created for a platform typeface. +/// +internal sealed class TextShaperTypeface : ITextShaperTypeface +{ + private readonly PlatformTypeface platformTypeface; + private readonly ConcurrentDictionary fonts = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The platform typeface to shape with. + public TextShaperTypeface(PlatformTypeface platformTypeface) => this.platformTypeface = platformTypeface; + + /// + /// Gets the SixLabors font for this typeface at the given size. + /// + /// The requested font size. + /// The font. + public SixLaborsFont GetFont(float size) + => this.fonts.GetOrAdd(size, static (requestedSize, typeface) => typeface.CreateFont(requestedSize), this.platformTypeface); + + /// + public void Dispose() + { + } +} diff --git a/samples/AvaloniaControlCatalog/TransformedGeometryImpl.cs b/samples/AvaloniaControlCatalog/TransformedGeometryImpl.cs new file mode 100644 index 00000000..0070e516 --- /dev/null +++ b/samples/AvaloniaControlCatalog/TransformedGeometryImpl.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using Avalonia; +using Avalonia.Media; +using Avalonia.Platform; +using SixLabors.ImageSharp.Drawing; + +namespace AvaloniaControlCatalog; + +/// +/// Geometry wrapper that preserves the original source geometry and applied transform. +/// +internal sealed class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl +{ + /// + /// Initializes a new transformed geometry wrapper. + /// + /// The untransformed source geometry. + /// The transform applied to . + public TransformedGeometryImpl(GeometryImpl source, Matrix transform) + : this(source, transform, transform.ToMatrix4x4()) + { + } + + private TransformedGeometryImpl(GeometryImpl source, Matrix transform, Matrix4x4 matrix) + : this(source, transform, matrix, source.StrokePath.Transform(matrix)) + { + } + + private TransformedGeometryImpl(GeometryImpl source, Matrix transform, Matrix4x4 matrix, IPath strokePath) + : base( + strokePath, + ReferenceEquals(source.StrokePath, source.FillPath) ? strokePath : source.FillPath?.Transform(matrix), + source.FillRule) + { + this.SourceGeometry = source; + this.Transform = transform; + } + + /// + public IGeometryImpl SourceGeometry { get; } + + /// + public Matrix Transform { get; } +} diff --git a/samples/AvaloniaControlCatalog/TransparentStyles.axaml b/samples/AvaloniaControlCatalog/TransparentStyles.axaml new file mode 100644 index 00000000..adf92446 --- /dev/null +++ b/samples/AvaloniaControlCatalog/TransparentStyles.axaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/TransparentStyles.axaml.cs b/samples/AvaloniaControlCatalog/TransparentStyles.axaml.cs new file mode 100644 index 00000000..85310f4b --- /dev/null +++ b/samples/AvaloniaControlCatalog/TransparentStyles.axaml.cs @@ -0,0 +1,5 @@ +using Avalonia.Styling; + +namespace ControlCatalog; + +public class TransparentStyles : Styles; diff --git a/samples/AvaloniaControlCatalog/ViewModels/ApplicationViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/ApplicationViewModel.cs new file mode 100644 index 00000000..509feca5 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/ApplicationViewModel.cs @@ -0,0 +1,26 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class ApplicationViewModel : ViewModelBase + { + public ApplicationViewModel() + { + ExitCommand = MiniCommand.Create(() => + { + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) + { + lifetime.Shutdown(); + } + }); + + RestoreDefault = MiniCommand.Create(() => { }); + } + + public MiniCommand ExitCommand { get; } + + public MiniCommand RestoreDefault { get; } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/ComboBoxPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/ComboBoxPageViewModel.cs new file mode 100644 index 00000000..7d415e5d --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/ComboBoxPageViewModel.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Selection; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class ComboBoxPageViewModel : ViewModelBase + { + private bool _wrapSelection; + private string _textValue = string.Empty; + private IdAndName? _selectedItem = null; + + public bool WrapSelection + { + get => _wrapSelection; + set => this.RaiseAndSetIfChanged(ref _wrapSelection, value); + } + + public string TextValue + { + get => _textValue; + set => this.RaiseAndSetIfChanged(ref _textValue, value); + } + + public IdAndName? SelectedItem + { + get => _selectedItem; + set => this.RaiseAndSetIfChanged(ref _selectedItem, value); + } + + public ObservableCollection Values { get; set; } = new ObservableCollection + { + new IdAndName(){ Id = "Id 1", Name = "Name 1", SearchText = "A" }, + new IdAndName(){ Id = "Id 2", Name = "Name 2", SearchText = "B" }, + new IdAndName(){ Id = "Id 3", Name = "Name 3", SearchText = "C" }, + new IdAndName(){ Id = "Id 4", Name = "Name 4", SearchText = "D" }, + new IdAndName(){ Id = "Id 5", Name = "Name 5", SearchText = "E" }, + }; + } + + public class IdAndName + { + public string? Id { get; set; } + public string? Name { get; set; } + public string? SearchText { get; set; } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/ContextPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/ContextPageViewModel.cs new file mode 100644 index 00000000..03156f69 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/ContextPageViewModel.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.VisualTree; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class ContextPageViewModel + { + public Control? View { get; set; } + public ContextPageViewModel() + { + OpenCommand = MiniCommand.CreateFromTask(Open); + SaveCommand = MiniCommand.Create(Save); + OpenRecentCommand = MiniCommand.Create(OpenRecent); + + MenuItems = new[] + { + new MenuItemViewModel { Header = "_Open...", Command = OpenCommand }, + new MenuItemViewModel { Header = "Save", Command = SaveCommand }, + new MenuItemViewModel { Header = "-" }, + new MenuItemViewModel + { + Header = "Recent", + Items = new[] + { + new MenuItemViewModel + { + Header = "File1.txt", + Command = OpenRecentCommand, + CommandParameter = @"c:\foo\File1.txt" + }, + new MenuItemViewModel + { + Header = "File2.txt", + Command = OpenRecentCommand, + CommandParameter = @"c:\foo\File2.txt" + }, + } + }, + }; + } + + public IReadOnlyList MenuItems { get; set; } + public MiniCommand OpenCommand { get; } + public MiniCommand SaveCommand { get; } + public MiniCommand OpenRecentCommand { get; } + + public async Task Open() + { + var window = TopLevel.GetTopLevel(View) as Window; + if (window == null) + return; + + var result = await window.StorageProvider.OpenFilePickerAsync(new Avalonia.Platform.Storage.FilePickerOpenOptions() { AllowMultiple = true }); + + foreach (var file in result) + { + System.Diagnostics.Debug.WriteLine($"Opened: {file.Name}"); + } + } + + public void Save() + { + System.Diagnostics.Debug.WriteLine("Save"); + } + + public void OpenRecent(string path) + { + System.Diagnostics.Debug.WriteLine($"Open recent: {path}"); + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/CursorPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/CursorPageViewModel.cs new file mode 100644 index 00000000..dfcdd436 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/CursorPageViewModel.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia; +using Avalonia.Input; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class CursorPageViewModel : ViewModelBase + { + public CursorPageViewModel() + { + StandardCursors = Enum.GetValues(typeof(StandardCursorType)) + .Cast() + .Select(x => new StandardCursorModel(x)) + .ToList(); + + var s = AssetLoader.Open(new Uri("avares://ControlCatalog/Assets/avalonia-32.png")); + var bitmap = new Bitmap(s); + CustomCursor = new Cursor(bitmap, new PixelPoint(16, 16)); + } + + public IEnumerable StandardCursors { get; } + + public Cursor CustomCursor { get; } + } + + public class StandardCursorModel + { + public StandardCursorModel(StandardCursorType type) + { + Type = type; + Cursor = new Cursor(type); + } + + public StandardCursorType Type { get; } + + public Cursor Cursor { get; } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/DataValidationViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/DataValidationViewModel.cs new file mode 100644 index 00000000..3b2c3769 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/DataValidationViewModel.cs @@ -0,0 +1,45 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using MiniMvvm; + +namespace ControlCatalog.ViewModels; + +public class DataValidationViewModel : ViewModelBase +{ + private string? _DataAnnotationsSample; + + [Required] + [EmailAddress] + [MinLength(5)] + public string? DataAnnotationsSample + { + get => _DataAnnotationsSample; + set => RaiseAndSetIfChanged(ref _DataAnnotationsSample, value); + } + + public Func Converter { get; } = new Func(o => + { + return $"Error: {o}"; + }); + + + private string? _ExceptionInsideSetterSample; + + public string? ExceptionInsideSetterSample + { + get => _ExceptionInsideSetterSample; + set + { + if (value is null || value.Length < 5) + throw new ArgumentOutOfRangeException(nameof(value), "Give me 5 or more letter please :-)"); + + RaiseAndSetIfChanged(ref _ExceptionInsideSetterSample, value); + } + } + + public Func ExceptionConverter { get; } = new Func(o => + { + return o is Exception ex ? $"Huh, there was an Exception: {ex.Message}" : "Something went really wrong!"; + }); +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/ExpanderPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/ExpanderPageViewModel.cs new file mode 100644 index 00000000..aa7f710e --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/ExpanderPageViewModel.cs @@ -0,0 +1,27 @@ +using Avalonia; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class ExpanderPageViewModel : ViewModelBase + { + private object _cornerRadius = AvaloniaProperty.UnsetValue; + private bool _rounded; + + public object CornerRadius + { + get => _cornerRadius; + private set => RaiseAndSetIfChanged(ref _cornerRadius, value); + } + + public bool Rounded + { + get => _rounded; + set + { + if (RaiseAndSetIfChanged(ref _rounded, value)) + CornerRadius = _rounded ? new CornerRadius(25) : AvaloniaProperty.UnsetValue; + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/ListBoxPageViewModel.cs new file mode 100644 index 00000000..a1dd17af --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/ListBoxPageViewModel.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Selection; +using ControlCatalog.Pages; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class ListBoxPageViewModel : ViewModelBase + { + private bool _multiple; + private bool _toggle; + private bool _alwaysSelected; + private bool _autoScrollToSelectedItem = true; + private bool _wrapSelection; + private int _counter; + private IObservable _selectionMode; + + public ListBoxPageViewModel() + { + Items = new ObservableCollection(Enumerable.Range(1, 10000).Select(i => GenerateItem())); + + Selection = new SelectionModel(); + Selection.Select(1); + + _selectionMode = this.WhenAnyValue( + x => x.Multiple, + x => x.Toggle, + x => x.AlwaysSelected, + (m, t, a) => + (m ? Avalonia.Controls.SelectionMode.Multiple : 0) | + (t ? Avalonia.Controls.SelectionMode.Toggle : 0) | + (a ? Avalonia.Controls.SelectionMode.AlwaysSelected : 0)); + + AddItemCommand = MiniCommand.Create(() => + { + var item = GenerateItem(); + Items.Add(item); + Selection.Clear(); + Selection.Select(Items.Count - 1); + }); + + RemoveItemCommand = MiniCommand.Create(() => + { + var items = Selection.SelectedItems.ToList(); + + foreach (var item in items) + { + Items.Remove(item!); + } + }); + + SelectRandomItemCommand = MiniCommand.Create(() => + { + var random = new Random(); + + using (Selection.BatchUpdate()) + { + Selection.Clear(); + Selection.Select(random.Next(Items.Count - 1)); + } + }); + } + + public ObservableCollection Items { get; } + public SelectionModel Selection { get; } + public IObservable SelectionMode => _selectionMode; + + public bool Multiple + { + get => _multiple; + set => this.RaiseAndSetIfChanged(ref _multiple, value); + } + + public bool Toggle + { + get => _toggle; + set => this.RaiseAndSetIfChanged(ref _toggle, value); + } + + public bool AlwaysSelected + { + get => _alwaysSelected; + set => this.RaiseAndSetIfChanged(ref _alwaysSelected, value); + } + + public bool AutoScrollToSelectedItem + { + get => _autoScrollToSelectedItem; + set => this.RaiseAndSetIfChanged(ref _autoScrollToSelectedItem, value); + } + + public bool WrapSelection + { + get => _wrapSelection; + set => this.RaiseAndSetIfChanged(ref _wrapSelection, value); + } + + public MiniCommand AddItemCommand { get; } + public MiniCommand RemoveItemCommand { get; } + public MiniCommand SelectRandomItemCommand { get; } + + private ItemModel GenerateItem() => new ItemModel(_counter++); + } + + /// + /// An Item model for the + /// + public class ItemModel + { + /// + /// Creates a new ItemModel with the given ID + /// + /// The ID to display + public ItemModel(int id) + { + ID = id; + } + + /// + /// The ID of this Item + /// + public int ID { get; } + + public override string ToString() + { + return $"Item {ID}"; + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/MainWindowViewModel.cs new file mode 100644 index 00000000..5d14cca1 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,150 @@ +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Dialogs; +using System; +using System.ComponentModel.DataAnnotations; +using Avalonia; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + partial class MainWindowViewModel : ViewModelBase + { + private WindowState _windowState; + private WindowState[] _windowStates = Array.Empty(); + private bool _extendClientAreaEnabled; + private double _titleBarHeight; + private bool _isSystemBarVisible; + private bool _displayEdgeToEdge; + private Thickness _safeAreaPadding; + private bool _canResize; + private bool _canMinimize; + private bool _canMaximize; + private int _selectedDecorationIndex; + + public MainWindowViewModel() + { + AboutCommand = MiniCommand.CreateFromTask(async () => + { + var dialog = new AboutAvaloniaDialog(); + + if ((App.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow is { } mainWindow) + { + await dialog.ShowDialog(mainWindow); + } + }); + ExitCommand = MiniCommand.Create(() => + { + (App.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.Shutdown(); + }); + + WindowState = WindowState.Normal; + + WindowStates = new WindowState[] + { + WindowState.Minimized, + WindowState.Normal, + WindowState.Maximized, + WindowState.FullScreen, + }; + + TitleBarHeight = -1; + CanResize = true; + CanMinimize = true; + CanMaximize = true; + + Filter(); + } + + public bool ExtendClientAreaEnabled + { + get { return _extendClientAreaEnabled; } + set { RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); } + } + + public double TitleBarHeight + { + get { return _titleBarHeight; } + set { RaiseAndSetIfChanged(ref _titleBarHeight, value); } + } + + public WindowState WindowState + { + get { return _windowState; } + set { RaiseAndSetIfChanged(ref _windowState, value); } + } + + public WindowState[] WindowStates + { + get { return _windowStates; } + set { RaiseAndSetIfChanged(ref _windowStates, value); } + } + + public bool IsSystemBarVisible + { + get { return _isSystemBarVisible; } + set { RaiseAndSetIfChanged(ref _isSystemBarVisible, value); } + } + + public bool DisplayEdgeToEdge + { + get { return _displayEdgeToEdge; } + set { RaiseAndSetIfChanged(ref _displayEdgeToEdge, value); } + } + + public Thickness SafeAreaPadding + { + get { return _safeAreaPadding; } + set { RaiseAndSetIfChanged(ref _safeAreaPadding, value); } + } + + public bool CanResize + { + get { return _canResize; } + set { RaiseAndSetIfChanged(ref _canResize, value); } + } + + public bool CanMinimize + { + get { return _canMinimize; } + set { RaiseAndSetIfChanged(ref _canMinimize, value); } + } + + public bool CanMaximize + { + get { return _canMaximize; } + set { RaiseAndSetIfChanged(ref _canMaximize, value); } + } + + public int SelectedDecorationIndex + { + get { return _selectedDecorationIndex; } + set { RaiseAndSetIfChanged(ref _selectedDecorationIndex, value); } + } + + public MiniCommand AboutCommand { get; } + + public MiniCommand ExitCommand { get; } + + private DateTime? _validatedDateExample; + + /// + /// A required DateTime which should demonstrate validation for the DateTimePicker + /// + [Required] + public DateTime? ValidatedDateExample + { + get => _validatedDateExample; + set => RaiseAndSetIfChanged(ref _validatedDateExample, value); + } + + public Win32Properties.WindowCornerPreference[] Win32WindowCornerPreferences { get; } = + Enum.GetValues(); + + public Win32Properties.WindowCornerPreference Win32WindowCornerPreference + { + get; + set { RaiseAndSetIfChanged(ref field, value); } + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/MainWindowViewModel_PageList.cs b/samples/AvaloniaControlCatalog/ViewModels/MainWindowViewModel_PageList.cs new file mode 100644 index 00000000..89d20de6 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/MainWindowViewModel_PageList.cs @@ -0,0 +1,248 @@ +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Dialogs; +using System; +using System.ComponentModel.DataAnnotations; +using Avalonia; +using MiniMvvm; +using Avalonia.Collections; +using ControlCatalog.Pages; +using System.Globalization; +using System.Text; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; + +namespace ControlCatalog.ViewModels +{ + partial class MainWindowViewModel + { + private int _selectedPageIndex; + private bool _isDrawerOpened = true; + private bool _ignoreListChange = false; + private string? _query = ""; + private PageItem? _currentItem; + private SplitViewDisplayMode _displayMode; + + private List _items = new() + { + new PageItem("Composition", () => new CompositionPage(), Icons.Layers), + new PageItem("Accelerator", () => new AcceleratorPage(), Icons.Keyboard), + new PageItem("Acrylic", () => new AcrylicPage(), Icons.Blur), + new PageItem("AdornerLayer", () => new AdornerLayerPage(), Icons.Sparkle), + new PageItem("AutoCompleteBox",() => new AutoCompleteBoxPage(), Icons.TextInput), + new PageItem("Border",() => new BorderPage(), Icons.Border), + new PageItem("BitmapCache",() => new BitmapCachePage(), Icons.Lightning), + new PageItem("Buttons",() => new ButtonsPage(), Icons.CursorClick), + new PageItem("ButtonSpinner",() => new ButtonSpinnerPage(), Icons.Spinner), + new PageItem("Calendar",() => new CalendarPage(), Icons.Calendar), + new PageItem("Canvas",() => new CanvasPage(), Icons.Canvas), + new PageItem("CommandBar",() => new CommandBarPage(), Icons.Terminal), + new PageItem("Carousel",() => new Pages.CarouselPage(), Icons.Slides), + new PageItem("CarouselPage",() => new CarouselDemoPage(), Icons.Slides), + new PageItem("CheckBox",() => new CheckBoxPage(), Icons.Checkbox), + new PageItem("Clipboard",() => new ClipboardPage(), Icons.Clipboard), + new PageItem("ColorPicker",() => new ColorPickerPage(), Icons.Palette), + new PageItem("ComboBox",() => new ComboBoxPage(), Icons.Dropdown), + new PageItem("Container Queries",() => new ContainerQueryPage(), Icons.Container), + new PageItem("ContentPage",() => new ContentDemoPage(), Icons.Document), + new PageItem("ContextFlyout",() => new ContextFlyoutPage(), Icons.Menu), + new PageItem("ContextMenu",() => new ContextMenuPage(), Icons.Menu), + new PageItem("Cursor",() => new CursorPage(), Icons.Cursor), + new PageItem("Custom Drawing",() => new CustomDrawing(), Icons.Brush), + new PageItem("Data Validation",() => new DataValidationPage(), Icons.Shield), + new PageItem("Date/Time Picker",() => new DateTimePickerPage(), Icons.Clock), + new PageItem("CalendarDatePicker",() => new CalendarDatePickerPage(), Icons.Calendar), + new PageItem("Dialogs",() => new DialogsPage(), Icons.Dialog), + new PageItem("Drag+Drop",() => new DragAndDropPage(), Icons.DragDrop), + new PageItem("DrawerPage",() => new DrawerDemoPage(), Icons.Drawer), + new PageItem("Expander",() => new ExpanderPage(), Icons.Expand), + new PageItem("Flyouts",() => new FlyoutsPage(), Icons.Flyout), + new PageItem("Focus",() => new FocusPage(), Icons.Target), + new PageItem("Gestures",() => new GesturePage(), Icons.Gesture), + new PageItem("Image",() => new ImagePage(), Icons.Image), + new PageItem("Label",() => new LabelsPage(), Icons.Tag), + new PageItem("LayoutTransformControl",() => new LayoutTransformControlPage(), Icons.Transform), + new PageItem("ListBox",() => new ListBoxPage(), Icons.List), + new PageItem("Menu",() => new MenuPage(), Icons.Menu), + new PageItem("NavigationPage",() => new NavigationDemoPage(), Icons.Navigation), + new PageItem("Notifications",() => new NotificationsPage(), Icons.Bell), + new PageItem("NumericUpDown",() => new NumericUpDownPage(), Icons.Number), + new PageItem("OpenGL",() => new OpenGlPage(), Icons.Cube3D), + new PageItem("OpenGL Lease",() => new OpenGlLeasePage(), Icons.Cube3D), + new PageItem("PipsPager",() => new PipsPagerPage(), Icons.HorizontalDots), + new PageItem("Platform Information",() => new PlatformInfoPage(), Icons.Info), + new PageItem("Pointers",() => new PointersPage(), Icons.Cursor), + new PageItem("ProgressBar",() => new ProgressBarPage(), Icons.Progress), + new PageItem("RadioButton",() => new RadioButtonPage(), Icons.Radio), + new PageItem("RefreshContainer",() => new RefreshContainerPage(), Icons.Refresh), + new PageItem("RelativePanel",() => new RelativePanelPage(), Icons.Layout), + new PageItem("ScrollViewer",() => new ScrollViewerPage(), Icons.Scroll), + new PageItem("Slider",() => new SliderPage(), Icons.Tune), + new PageItem("SplitView",() => new SplitViewPage(), Icons.Split), + new PageItem("TabbedPage",() => new TabbedDemoPage(), Icons.Tab), + new PageItem("TabControl",() => new TabControlPage(), Icons.Tab), + new PageItem("TabStrip",() => new TabStripPage(), Icons.Tab), + new PageItem("TableView",() => new TableViewPage(), Icons.Grid), + new PageItem("TextBox",() => new TextBoxPage(), Icons.TextInput), + new PageItem("TextBlock",() => new TextBlockPage(), Icons.TextInput), + new PageItem("Theme Variants",() => new ThemePage(), Icons.Theme), + new PageItem("ToggleSwitch",() => new ToggleSwitchPage(), Icons.Toggle), + new PageItem("ToolTip",() => new ToolTipPage(), Icons.Tooltip), + new PageItem("TransitioningContentControl",() => new TransitioningContentControlPage(), Icons.Transition), + new PageItem("TreeView",() => new TreeViewPage(), Icons.Tree), + new PageItem("Viewbox",() => new ViewboxPage(), Icons.Viewbox), + new PageItem("Native Embed",() => new NativeEmbedPage(), Icons.Puzzle), + new PageItem("Window Customizations",() => new WindowCustomizationsPage(), Icons.Window), + new PageItem("HeaderedContentControl",() => new HeaderedContentPage(), Icons.Header), + new PageItem("Screens",() => new ScreenPage(), Icons.Monitor), + }; + + public AvaloniaList Pages { get; } = new AvaloniaList(); + + public void Filter(string? query = "") + { + try + { + _ignoreListChange = true; + Pages.Clear(); + + if (string.IsNullOrWhiteSpace(query)) + { + Pages.AddRange(_items); + } + else + { + var querySearchKey = PageItem.CreateSearchKey(query); + + if (querySearchKey.Length == 0) + { + Pages.AddRange(_items); + } + else + { + foreach (var item in _items) + { + if (item.MatchesSearch(querySearchKey)) + { + Pages.Add(item); + } + } + } + } + } + finally + { + _ignoreListChange = false; + if (_currentItem != null) + { + var newIndex = Pages.IndexOf(_currentItem); + if (newIndex != -1) + { + SelectedPageIndex = newIndex; + } + } + } + } + + public INavigation? Navigator { get; internal set; } + + public int SelectedPageIndex + { + get { return _selectedPageIndex; } + set + { + RaiseAndSetIfChanged(ref _selectedPageIndex, value); + + if (!_ignoreListChange) + { + NavigateTo(_selectedPageIndex); + + if (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay) + IsDrawerOpened = false; + } + } + } + + public bool IsDrawerOpened + { + get { return _isDrawerOpened; } + set { RaiseAndSetIfChanged(ref _isDrawerOpened, value); } + } + + public SplitViewDisplayMode DisplayMode + { + get { return _displayMode; } + set { RaiseAndSetIfChanged(ref _displayMode, value); } + } + + public string? Query + { + get { return _query; } + set + { + RaiseAndSetIfChanged(ref _query, value); + + Filter(value); + } + } + + private async void NavigateTo(int pageIndex) + { + if (pageIndex < 0 || pageIndex >= Pages.Count || Navigator is null) + return; + + var item = Pages[pageIndex]; + + if (item != null) + { + var view = item.Factory(); + if (view is Page page && view.GetType() != Navigator.NavigationStack.LastOrDefault()?.GetType()) + { + _currentItem = item; + await Navigator.ReplaceAsync(page); + } + } + } + } + + class PageItem(string header, Func factory, string? iconData = null) + { + public string Header { get; } = header; + public Func Factory { get; } = factory; + public string? IconData { get; } = iconData; + private string SearchKey { get; } = CreateSearchKey(header); + + public bool IsVisible { get; set; } = true; + + public bool MatchesSearch(string searchKey) + { + return SearchKey.Contains(searchKey, StringComparison.Ordinal); + } + + public static string CreateSearchKey(string value) + { + var normalizedValue = value.Normalize(NormalizationForm.FormKD); + var builder = new StringBuilder(normalizedValue.Length); + + foreach (var c in normalizedValue) + { + var category = CharUnicodeInfo.GetUnicodeCategory(c); + + if (category is UnicodeCategory.NonSpacingMark or + UnicodeCategory.SpacingCombiningMark or + UnicodeCategory.EnclosingMark) + { + continue; + } + + if (char.IsLetterOrDigit(c)) + { + builder.Append(char.ToUpperInvariant(c)); + } + } + + return builder.ToString(); + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/MenuItemViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/MenuItemViewModel.cs new file mode 100644 index 00000000..190bb4c8 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/MenuItemViewModel.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Windows.Input; + +namespace ControlCatalog.ViewModels +{ + public class MenuItemViewModel + { + public string? Header { get; set; } + public ICommand? Command { get; set; } + public object? CommandParameter { get; set; } + public IList? Items { get; set; } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/MenuPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/MenuPageViewModel.cs new file mode 100644 index 00000000..f6b406d0 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/MenuPageViewModel.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.VisualTree; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class MenuPageViewModel + { + public Control? View { get; set; } + public MenuPageViewModel() + { + OpenCommand = MiniCommand.CreateFromTask(Open); + SaveCommand = MiniCommand.Create(Save); + OpenRecentCommand = MiniCommand.Create(OpenRecent); + + var recentItems = new[] + { + new MenuItemViewModel + { + Header = "File1.txt", + Command = OpenRecentCommand, + CommandParameter = @"c:\foo\File1.txt" + }, + new MenuItemViewModel + { + Header = "File2.txt", + Command = OpenRecentCommand, + CommandParameter = @"c:\foo\File2.txt" + }, + }; + + RecentItems = recentItems; + MenuItems = new[] + { + new MenuItemViewModel + { + Header = "_File", + Items = new[] + { + new MenuItemViewModel { Header = "O_pen...", Command = OpenCommand }, + new MenuItemViewModel { Header = "Save", Command = SaveCommand }, + new MenuItemViewModel { Header = "-" }, + new MenuItemViewModel + { + Header = "Recent", + Items = recentItems + }, + } + }, + new MenuItemViewModel + { + Header = "_Edit", + Items = new[] + { + new MenuItemViewModel { Header = "_Copy" }, + new MenuItemViewModel { Header = "_Paste" }, + } + } + }; + } + + public IReadOnlyList MenuItems { get; set; } + public IReadOnlyList RecentItems { get; set; } + public MiniCommand OpenCommand { get; } + public MiniCommand SaveCommand { get; } + public MiniCommand OpenRecentCommand { get; } + + public async Task Open() + { + var window = TopLevel.GetTopLevel(View) as Window; + if (window == null) + return; + var result = await window.StorageProvider.OpenFilePickerAsync(new Avalonia.Platform.Storage.FilePickerOpenOptions() { AllowMultiple = true }); + + if (result != null) + { + foreach (var file in result) + { + System.Diagnostics.Debug.WriteLine($"Opened: {file.Name}"); + } + } + } + + public void Save() + { + System.Diagnostics.Debug.WriteLine("Save"); + } + + public void OpenRecent(string path) + { + System.Diagnostics.Debug.WriteLine($"Open recent: {path}"); + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/NotificationViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/NotificationViewModel.cs new file mode 100644 index 00000000..40af6e03 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/NotificationViewModel.cs @@ -0,0 +1,44 @@ +using Avalonia.Controls.Notifications; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class NotificationViewModel + { + public WindowNotificationManager? NotificationManager { get; set; } + + public NotificationViewModel() + { + ShowCustomManagedNotificationCommand = MiniCommand.Create(() => + { + NotificationManager?.Show(new NotificationViewModel() { Title = "Hey There!", Message = "Did you know that Avalonia now supports Custom In-Window Notifications?" , NotificationManager = NotificationManager}, NotificationType.Warning); + }); + + ShowManagedNotificationCommand = MiniCommand.Create(() => + { + NotificationManager?.Show(new Avalonia.Controls.Notifications.Notification("Welcome", "Avalonia now supports Notifications.", NotificationType.Information)); + }); + + YesCommand = MiniCommand.Create(() => + { + NotificationManager?.Show(new Avalonia.Controls.Notifications.Notification("Avalonia Notifications", "Start adding notifications to your app today.")); + }); + + NoCommand = MiniCommand.Create(() => + { + NotificationManager?.Show(new Avalonia.Controls.Notifications.Notification("Avalonia Notifications", "Start adding notifications to your app today. To find out more visit...")); + }); + } + + public string? Title { get; set; } + public string? Message { get; set; } + + public MiniCommand YesCommand { get; } + + public MiniCommand NoCommand { get; } + + public MiniCommand ShowCustomManagedNotificationCommand { get; } + + public MiniCommand ShowManagedNotificationCommand { get; } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/PlatformInformationViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/PlatformInformationViewModel.cs new file mode 100644 index 00000000..21c87bab --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/PlatformInformationViewModel.cs @@ -0,0 +1,58 @@ +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Platform; +using MiniMvvm; + +namespace ControlCatalog.ViewModels; + +public class PlatformInformationViewModel : ViewModelBase +{ + public PlatformInformationViewModel() + { + /* NOTE: + * ------------ + * The below API is not meant to be used in production Apps. + * If you need to consume this info, please use: + * - OperatingSystem ( https://learn.microsoft.com/en-us/dotnet/api/system.operatingsystem | if .NET 5 or greater) + * - or RuntimeInformation ( https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.runtimeinformation ) + */ + + var runtimeInfo = AvaloniaLocator.Current.GetService()?.GetRuntimeInfo(); + + if (runtimeInfo is { } info) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER"))) + { + if (info.IsDesktop) + { + PlatformInfo = "Platform: Desktop (browser)"; + } + else if (info.IsMobile) + { + PlatformInfo = "Platform: Mobile (browser)"; + } + else + { + PlatformInfo = "Platform: Unknown (browser) - please report"; + } + } + else + { + if (info.IsDesktop) + { + PlatformInfo = "Platform: Desktop (native)"; + } + else if (info.IsMobile) + { + PlatformInfo = "Platform: Mobile (native)"; + } + else + { + PlatformInfo = "Platform: Unknown (native) - please report"; + } + } + } + } + + public string? PlatformInfo { get; } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/RefreshContainerViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/RefreshContainerViewModel.cs new file mode 100644 index 00000000..aa15d775 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/RefreshContainerViewModel.cs @@ -0,0 +1,25 @@ +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Controls.Notifications; +using ControlCatalog.Pages; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class RefreshContainerViewModel : ViewModelBase + { + public ObservableCollection Items { get; } + + public RefreshContainerViewModel() + { + Items = new ObservableCollection(Enumerable.Range(1, 200).Select(i => $"Item {i}")); + } + + public async Task AddToTop() + { + await Task.Delay(3000); + Items.Insert(0, $"Item {200 - Items.Count}"); + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/SplitViewPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/SplitViewPageViewModel.cs new file mode 100644 index 00000000..9664331a --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/SplitViewPageViewModel.cs @@ -0,0 +1,54 @@ +using System; +using Avalonia.Controls; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class SplitViewPageViewModel : ViewModelBase + { + private int _displayMode = 3; //CompactOverlay + private int _placement = 0; //Left + + public int Placement + { + get => _placement; + set + { + this.RaiseAndSetIfChanged(ref _placement, value); + this.RaisePropertyChanged(nameof(PanePlacement)); + } + } + + public int DisplayMode + { + get => _displayMode; + set + { + this.RaiseAndSetIfChanged(ref _displayMode, value); + this.RaisePropertyChanged(nameof(CurrentDisplayMode)); + } + } + + public SplitViewPanePlacement PanePlacement => + _placement switch + { + 0 => SplitViewPanePlacement.Left, + 1 => SplitViewPanePlacement.Right, + 2 => SplitViewPanePlacement.Top, + 3 => SplitViewPanePlacement.Bottom, + _ => SplitViewPanePlacement.Left + }; + + public SplitViewDisplayMode CurrentDisplayMode + { + get + { + if (Enum.IsDefined(typeof(SplitViewDisplayMode), _displayMode)) + { + return (SplitViewDisplayMode)_displayMode; + } + return SplitViewDisplayMode.CompactOverlay; + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/TabControlPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/TabControlPageViewModel.cs new file mode 100644 index 00000000..b2334625 --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/TabControlPageViewModel.cs @@ -0,0 +1,26 @@ +using Avalonia.Controls; +using Avalonia.Media.Imaging; +using MiniMvvm; + +namespace ControlCatalog.ViewModels; + +public class TabControlPageViewModel : ViewModelBase +{ + private Dock _tabPlacement; + + public TabControlPageViewModelItem[]? Tabs { get; set; } + + public Dock TabPlacement + { + get { return _tabPlacement; } + set { this.RaiseAndSetIfChanged(ref _tabPlacement, value); } + } +} + +public class TabControlPageViewModelItem +{ + public string? Header { get; set; } + public string? Text { get; set; } + public Bitmap? Image { get; set; } + public bool IsEnabled { get; set; } = true; +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/TableViewPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/TableViewPageViewModel.cs new file mode 100644 index 00000000..1a969cee --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/TableViewPageViewModel.cs @@ -0,0 +1,246 @@ +using System.Collections.ObjectModel; +using MiniMvvm; + +namespace ControlCatalog.ViewModels; + +public sealed class TableViewPageViewModel : ViewModelBase +{ + public ObservableCollection Countries { get; } = + [ + new("Afghanistan", "ASIA (EX. NEAR EAST)", 31056997, 647500, 700), + new("Albania", "EASTERN EUROPE", 3581655, 28748, 4500), + new("Algeria", "NORTHERN AFRICA", 32930091, 2381740, 6000), + new("American Samoa", "OCEANIA", 57794, 199, 8000), + new("Andorra", "WESTERN EUROPE", 71201, 468, 19000), + new("Angola", "SUB-SAHARAN AFRICA", 12127071, 1246700, 1900), + new("Anguilla", "LATIN AMER. & CARIB", 13477, 102, 8600), + new("Antigua & Barbuda", "LATIN AMER. & CARIB", 69108, 443, 11000), + new("Argentina", "LATIN AMER. & CARIB", 39921833, 2766890, 11200), + new("Armenia", "C.W. OF IND. STATES", 2976372, 29800, 3500), + new("Aruba", "LATIN AMER. & CARIB", 71891, 193, 28000), + new("Australia", "OCEANIA", 20264082, 7686850, 29000), + new("Austria", "WESTERN EUROPE", 8192880, 83870, 30000), + new("Azerbaijan", "C.W. OF IND. STATES", 7961619, 86600, 3400), + new("The Bahamas", "LATIN AMER. & CARIB", 303770, 13940, 16700), + new("Bahrain", "NEAR EAST", 698585, 665, 16900), + new("Bangladesh", "ASIA (EX. NEAR EAST)", 147365352, 144000, 1900), + new("Barbados", "LATIN AMER. & CARIB", 279912, 431, 15700), + new("Belarus", "C.W. OF IND. STATES", 10293011, 207600, 6100), + new("Belgium", "WESTERN EUROPE", 10379067, 30528, 29100), + new("Belize", "LATIN AMER. & CARIB", 287730, 22966, 4900), + new("Benin", "SUB-SAHARAN AFRICA", 7862944, 112620, 1100), + new("Bermuda", "NORTHERN AMERICA", 65773, 53, 36000), + new("Bhutan", "ASIA (EX. NEAR EAST)", 2279723, 47000, 1300), + new("Bolivia", "LATIN AMER. & CARIB", 8989046, 1098580, 2400), + new("Bosnia & Herzegovina", "EASTERN EUROPE", 4498976, 51129, 6100), + new("Botswana", "SUB-SAHARAN AFRICA", 1639833, 600370, 9000), + new("Brazil", "LATIN AMER. & CARIB", 188078227, 8511965, 7600), + new("British Virgin Is.", "LATIN AMER. & CARIB", 23098, 153, 16000), + new("Brunei", "ASIA (EX. NEAR EAST)", 379444, 5770, 18600), + new("Bulgaria", "EASTERN EUROPE", 7385367, 110910, 7600), + new("Burkina Faso", "SUB-SAHARAN AFRICA", 13902972, 274200, 1100), + new("Burma", "ASIA (EX. NEAR EAST)", 47382633, 678500, 1800), + new("Burundi", "SUB-SAHARAN AFRICA", 8090068, 27830, 600), + new("Cambodia", "ASIA (EX. NEAR EAST)", 13881427, 181040, 1900), + new("Cameroon", "SUB-SAHARAN AFRICA", 17340702, 475440, 1800), + new("Canada", "NORTHERN AMERICA", 33098932, 9984670, 29800), + new("Cape Verde", "SUB-SAHARAN AFRICA", 420979, 4033, 1400), + new("Cayman Islands", "LATIN AMER. & CARIB", 45436, 262, 35000), + new("Central African Rep.", "SUB-SAHARAN AFRICA", 4303356, 622984, 1100), + new("Chad", "SUB-SAHARAN AFRICA", 9944201, 1284000, 1200), + new("Chile", "LATIN AMER. & CARIB", 16134219, 756950, 9900), + new("China", "ASIA (EX. NEAR EAST)", 1313973713, 9596960, 5000), + new("Colombia", "LATIN AMER. & CARIB", 43593035, 1138910, 6300), + new("Comoros", "SUB-SAHARAN AFRICA", 690948, 2170, 700), + new("Congo, Dem.Rep.", "SUB - SAHARAN AFRICA", 62660551, 2345410, 700), + new("Congo, Repub.of the", "SUB - SAHARAN AFRICA", 3702314, 342000, 700), + new("Cook Islands", "OCEANIA", 21388, 240, 5000), + new("Costa Rica", "LATIN AMER. & CARIB", 4075261, 51100, 9100), + new("Cote d'Ivoire", "SUB-SAHARAN AFRICA", 17654843, 322460, 1400), + new("Croatia", "EASTERN EUROPE", 4494749, 56542, 10600), + new("Cuba", "LATIN AMER. & CARIB", 11382820, 110860, 2900), + new("Cyprus", "NEAR EAST", 784301, 9250, 19200), + new("Czech Republic", "EASTERN EUROPE", 10235455, 78866, 15700), + new("Denmark", "WESTERN EUROPE", 5450661, 43094, 31100), + new("Djibouti", "SUB-SAHARAN AFRICA", 486530, 23000, 1300), + new("Dominica", "LATIN AMER. & CARIB", 68910, 754, 5400), + new("Dominican Republic", "LATIN AMER. & CARIB", 9183984, 48730, 6000), + new("East Timor", "ASIA (EX. NEAR EAST)", 1062777, 15007, 500), + new("Ecuador", "LATIN AMER. & CARIB", 13547510, 283560, 3300), + new("Egypt", "NORTHERN AFRICA", 78887007, 1001450, 4000), + new("El Salvador", "LATIN AMER. & CARIB", 6822378, 21040, 4800), + new("Equatorial Guinea", "SUB-SAHARAN AFRICA", 540109, 28051, 2700), + new("Eritrea", "SUB-SAHARAN AFRICA", 4786994, 121320, 700), + new("Estonia", "BALTICS", 1324333, 45226, 12300), + new("Ethiopia", "SUB-SAHARAN AFRICA", 74777981, 1127127, 700), + new("Faroe Islands", "WESTERN EUROPE", 47246, 1399, 22000), + new("Fiji", "OCEANIA", 905949, 18270, 5800), + new("Finland", "WESTERN EUROPE", 5231372, 338145, 27400), + new("France", "WESTERN EUROPE", 60876136, 547030, 27600), + new("French Guiana", "LATIN AMER. & CARIB", 199509, 91000, 8300), + new("French Polynesia", "OCEANIA", 274578, 4167, 17500), + new("Gabon", "SUB-SAHARAN AFRICA", 1424906, 267667, 5500), + new("Gambia, The", "SUB - SAHARAN AFRICA", 1641564, 11300, 1700), + new("Gaza Strip", "NEAR EAST", 1428757, 360, 600), + new("Georgia", "C.W. OF IND. STATES", 4661473, 69700, 2500), + new("Germany", "WESTERN EUROPE", 82422299, 357021, 27600), + new("Ghana", "SUB-SAHARAN AFRICA", 22409572, 239460, 2200), + new("Gibraltar", "WESTERN EUROPE", 27928, 7, 17500), + new("Greece", "WESTERN EUROPE", 10688058, 131940, 20000), + new("Greenland", "NORTHERN AMERICA", 56361, 2166086, 20000), + new("Grenada", "LATIN AMER. & CARIB", 89703, 344, 5000), + new("Guadeloupe", "LATIN AMER. & CARIB", 452776, 1780, 8000), + new("Guam", "OCEANIA", 171019, 541, 21000), + new("Guatemala", "LATIN AMER. & CARIB", 12293545, 108890, 4100), + new("Guernsey", "WESTERN EUROPE", 65409, 78, 20000), + new("Guinea", "SUB-SAHARAN AFRICA", 9690222, 245857, 2100), + new("Guinea-Bissau", "SUB-SAHARAN AFRICA", 1442029, 36120, 800), + new("Guyana", "LATIN AMER. & CARIB", 767245, 214970, 4000), + new("Haiti", "LATIN AMER. & CARIB", 8308504, 27750, 1600), + new("Honduras", "LATIN AMER. & CARIB", 7326496, 112090, 2600), + new("Hong Kong", "ASIA (EX. NEAR EAST)", 6940432, 1092, 28800), + new("Hungary", "EASTERN EUROPE", 9981334, 93030, 13900), + new("Iceland", "WESTERN EUROPE", 299388, 103000, 30900), + new("India", "ASIA (EX. NEAR EAST)", 1095351995, 3287590, 2900), + new("Indonesia", "ASIA (EX. NEAR EAST)", 245452739, 1919440, 3200), + new("Iran", "ASIA (EX. NEAR EAST)", 68688433, 1648000, 7000), + new("Iraq", "NEAR EAST", 26783383, 437072, 1500), + new("Ireland", "WESTERN EUROPE", 4062235, 70280, 29600), + new("Isle of Man", "WESTERN EUROPE", 75441, 572, 21000), + new("Israel", "NEAR EAST", 6352117, 20770, 19800), + new("Italy", "WESTERN EUROPE", 58133509, 301230, 26700), + new("Jamaica", "LATIN AMER. & CARIB", 2758124, 10991, 3900), + new("Japan", "ASIA (EX. NEAR EAST)", 127463611, 377835, 28200), + new("Jersey", "WESTERN EUROPE", 91084, 116, 24800), + new("Jordan", "NEAR EAST", 5906760, 92300, 4300), + new("Kazakhstan", "C.W. OF IND. STATES", 15233244, 2717300, 6300), + new("Kenya", "SUB-SAHARAN AFRICA", 34707817, 582650, 1000), + new("Kiribati", "OCEANIA", 105432, 811, 800), + new("North Korea", "ASIA(EX.NEAR EAST)", 23113019, 120540, 1300), + new("South Korea", "ASIA(EX.NEAR EAST)", 48846823, 98480, 17800), + new("Kuwait", "NEAR EAST", 2418393, 17820, 19000), + new("Kyrgyzstan", "C.W. OF IND. STATES", 5213898, 198500, 1600), + new("Laos", "ASIA (EX. NEAR EAST)", 6368481, 236800, 1700), + new("Latvia", "BALTICS", 2274735, 64589, 10200), + new("Lebanon", "NEAR EAST", 3874050, 10400, 4800), + new("Lesotho", "SUB-SAHARAN AFRICA", 2022331, 30355, 3000), + new("Liberia", "SUB-SAHARAN AFRICA", 3042004, 111370, 1000), + new("Libya", "NORTHERN AFRICA", 5900754, 1759540, 6400), + new("Liechtenstein", "WESTERN EUROPE", 33987, 160, 25000), + new("Lithuania", "BALTICS", 3585906, 65200, 11400), + new("Luxembourg", "WESTERN EUROPE", 474413, 2586, 55100), + new("Macau", "ASIA (EX. NEAR EAST)", 453125, 28, 19400), + new("Macedonia", "EASTERN EUROPE", 2050554, 25333, 6700), + new("Madagascar", "SUB-SAHARAN AFRICA", 18595469, 587040, 800), + new("Malawi", "SUB-SAHARAN AFRICA", 13013926, 118480, 600), + new("Malaysia", "ASIA (EX. NEAR EAST)", 24385858, 329750, 9000), + new("Maldives", "ASIA (EX. NEAR EAST)", 359008, 300, 3900), + new("Mali", "SUB-SAHARAN AFRICA", 11716829, 1240000, 900), + new("Malta", "WESTERN EUROPE", 400214, 316, 17700), + new("Marshall Islands", "OCEANIA", 60422, 11854, 1600), + new("Martinique", "LATIN AMER. & CARIB", 436131, 1100, 14400), + new("Mauritania", "SUB-SAHARAN AFRICA", 3177388, 1030700, 1800), + new("Mauritius", "SUB-SAHARAN AFRICA", 1240827, 2040, 11400), + new("Mayotte", "SUB-SAHARAN AFRICA", 201234, 374, 2600), + new("Mexico", "LATIN AMER. & CARIB", 107449525, 1972550, 9000), + new("Micronesia, Fed.St.", "OCEANIA", 108004, 702, 2000), + new("Moldova", "C.W. OF IND. STATES", 4466706, 33843, 1800), + new("Monaco", "WESTERN EUROPE", 32543, 2, 27000), + new("Mongolia", "ASIA (EX. NEAR EAST)", 2832224, 1564116, 1800), + new("Montserrat", "LATIN AMER. & CARIB", 9439, 102, 3400), + new("Morocco", "NORTHERN AFRICA", 33241259, 446550, 4000), + new("Mozambique", "SUB-SAHARAN AFRICA", 19686505, 801590, 1200), + new("Namibia", "SUB-SAHARAN AFRICA", 2044147, 825418, 7200), + new("Nauru", "OCEANIA", 13287, 21, 5000), + new("Nepal", "ASIA (EX. NEAR EAST)", 28287147, 147181, 1400), + new("Netherlands", "WESTERN EUROPE", 16491461, 41526, 28600), + new("Netherlands Antilles", "LATIN AMER. & CARIB", 221736, 960, 11400), + new("New Caledonia", "OCEANIA", 219246, 19060, 15000), + new("New Zealand", "OCEANIA", 4076140, 268680, 21600), + new("Nicaragua", "LATIN AMER. & CARIB", 5570129, 129494, 2300), + new("Niger", "SUB-SAHARAN AFRICA", 12525094, 1267000, 800), + new("Nigeria", "SUB-SAHARAN AFRICA", 131859731, 923768, 900), + new("N. Mariana Islands", "OCEANIA", 82459, 477, 12500), + new("Norway", "WESTERN EUROPE", 4610820, 323802, 37800), + new("Oman", "NEAR EAST", 3102229, 212460, 13100), + new("Pakistan", "ASIA (EX. NEAR EAST)", 165803560, 803940, 2100), + new("Palau", "OCEANIA", 20579, 458, 9000), + new("Panama", "LATIN AMER. & CARIB", 3191319, 78200, 6300), + new("Papua New Guinea", "OCEANIA", 5670544, 462840, 2200), + new("Paraguay", "LATIN AMER. & CARIB", 6506464, 406750, 4700), + new("Peru", "LATIN AMER. & CARIB", 28302603, 1285220, 5100), + new("Philippines", "ASIA (EX. NEAR EAST)", 89468677, 300000, 4600), + new("Poland", "EASTERN EUROPE", 38536869, 312685, 11100), + new("Portugal", "WESTERN EUROPE", 10605870, 92391, 18000), + new("Puerto Rico", "LATIN AMER. & CARIB", 3927188, 13790, 16800), + new("Qatar", "NEAR EAST", 885359, 11437, 21500), + new("Reunion", "SUB-SAHARAN AFRICA", 787584, 2517, 5800), + new("Romania", "EASTERN EUROPE", 22303552, 237500, 7000), + new("Russia", "C.W. OF IND. STATES", 142893540, 17075200, 8900), + new("Rwanda", "SUB-SAHARAN AFRICA", 8648248, 26338, 1300), + new("Saint Helena", "SUB-SAHARAN AFRICA", 7502, 413, 2500), + new("Saint Kitts & Nevis", "LATIN AMER. & CARIB", 39129, 261, 8800), + new("Saint Lucia", "LATIN AMER. & CARIB", 168458, 616, 5400), + new("St Pierre & Miquelon", "NORTHERN AMERICA", 7026, 242, 6900), + new("Saint Vincent and the Grenadines", "LATIN AMER. & CARIB", 117848, 389, 2900), + new("Samoa", "OCEANIA", 176908, 2944, 5600), + new("San Marino", "WESTERN EUROPE", 29251, 61, 34600), + new("Sao Tome & Principe", "SUB-SAHARAN AFRICA", 193413, 1001, 1200), + new("Saudi Arabia", "NEAR EAST", 27019731, 1960582, 11800), + new("Senegal", "SUB-SAHARAN AFRICA", 11987121, 196190, 1600), + new("Serbia", "EASTERN EUROPE", 9396411, 88361, 2200), + new("Seychelles", "SUB-SAHARAN AFRICA", 81541, 455, 7800), + new("Sierra Leone", "SUB-SAHARAN AFRICA", 6005250, 71740, 500), + new("Singapore", "ASIA (EX. NEAR EAST)", 4492150, 693, 23700), + new("Slovakia", "EASTERN EUROPE", 5439448, 48845, 13300), + new("Slovenia", "EASTERN EUROPE", 2010347, 20273, 19000), + new("Solomon Islands", "OCEANIA", 552438, 28450, 1700), + new("Somalia", "SUB-SAHARAN AFRICA", 8863338, 637657, 500), + new("South Africa", "SUB-SAHARAN AFRICA", 44187637, 1219912, 10700), + new("Spain", "WESTERN EUROPE", 40397842, 504782, 22000), + new("Sri Lanka", "ASIA (EX. NEAR EAST)", 20222240, 65610, 3700), + new("Sudan", "SUB-SAHARAN AFRICA", 41236378, 2505810, 1900), + new("Suriname", "LATIN AMER. & CARIB", 439117, 163270, 4000), + new("Swaziland", "SUB-SAHARAN AFRICA", 1136334, 17363, 4900), + new("Sweden", "WESTERN EUROPE", 9016596, 449964, 26800), + new("Switzerland", "WESTERN EUROPE", 7523934, 41290, 32700), + new("Syria", "NEAR EAST", 18881361, 185180, 3300), + new("Taiwan", "ASIA (EX. NEAR EAST)", 23036087, 35980, 23400), + new("Tajikistan", "C.W. OF IND. STATES", 7320815, 143100, 1000), + new("Tanzania", "SUB-SAHARAN AFRICA", 37445392, 945087, 600), + new("Thailand", "ASIA (EX. NEAR EAST)", 64631595, 514000, 7400), + new("Togo", "SUB-SAHARAN AFRICA", 5548702, 56785, 1500), + new("Tonga", "OCEANIA", 114689, 748, 2200), + new("Trinidad & Tobago", "LATIN AMER. & CARIB", 1065842, 5128, 9500), + new("Tunisia", "NORTHERN AFRICA", 10175014, 163610, 6900), + new("Turkey", "NEAR EAST", 70413958, 780580, 6700), + new("Turkmenistan", "C.W. OF IND. STATES", 5042920, 488100, 5800), + new("Turks & Caicos Is", "LATIN AMER. & CARIB", 21152, 430, 9600), + new("Tuvalu", "OCEANIA", 11810, 26, 1100), + new("Uganda", "SUB-SAHARAN AFRICA", 28195754, 236040, 1400), + new("Ukraine", "C.W. OF IND. STATES", 46710816, 603700, 5400), + new("United Arab Emirates", "NEAR EAST", 2602713, 82880, 23200), + new("United Kingdom", "WESTERN EUROPE", 60609153, 244820, 27700), + new("United States", "NORTHERN AMERICA", 298444215, 9631420, 37800), + new("Uruguay", "LATIN AMER. & CARIB", 3431932, 176220, 12800), + new("Uzbekistan", "C.W. OF IND. STATES", 27307134, 447400, 1700), + new("Vanuatu", "OCEANIA", 208869, 12200, 2900), + new("Venezuela", "LATIN AMER. & CARIB", 25730435, 912050, 4800), + new("Vietnam", "ASIA (EX. NEAR EAST)", 84402966, 329560, 2500), + new("Virgin Islands", "LATIN AMER. & CARIB", 108605, 1910, 17200), + new("Wallis and Futuna", "OCEANIA", 16025, 274, 3700), + new("West Bank", "NEAR EAST", 2460492, 5860, 800), + new("Yemen", "NEAR EAST", 21456188, 527970, 800), + new("Zambia", "SUB-SAHARAN AFRICA", 11502010, 752614, 800), + new("Zimbabwe", "SUB-SAHARAN AFRICA", 12236805, 390580, 1900) + ]; + + public sealed class Country(string name, string region, int population, int area, int gdp) + { + public string Name { get; set; } = name; + public string Region { get; set; } = region; + public int Population { get; set; } = population; + public int Area { get; set; } = area; // Square miles + public int GDP { get; set; } = gdp; + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs new file mode 100644 index 00000000..5bad6c4b --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs @@ -0,0 +1,317 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Styling; +using Avalonia.VisualTree; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class TransitioningContentControlPageViewModel : ViewModelBase + { + public TransitioningContentControlPageViewModel() + { + var images = new string[] + { + "delicate-arch-896885_640.jpg", + "hirsch-899118_640.jpg", + "maple-leaf-888807_640.jpg" + }; + + foreach (var image in images) + { + var path = $"avares://ControlCatalog/Assets/{image}"; + Images.Add(new Bitmap(AssetLoader.Open(new Uri(path)))); + } + + SetupTransitions(); + + _SelectedTransition = PageTransitions[1]; + _SelectedImage = Images[0]; + } + + public List PageTransitions { get; } = new List(); + + public List Images { get; } = new List(); + + + private Bitmap _SelectedImage; + private bool _Reversed; + + /// + /// Gets or Sets the selected image + /// + public Bitmap SelectedImage + { + get { return _SelectedImage; } + set { this.RaiseAndSetIfChanged(ref _SelectedImage, value); } + } + + + private PageTransition _SelectedTransition; + + /// + /// Gets or sets the transition to play + /// + public PageTransition SelectedTransition + { + get { return _SelectedTransition; } + set { this.RaiseAndSetIfChanged(ref _SelectedTransition, value); } + } + + + + private bool _ClipToBounds; + + /// + /// Gets or sets if the content should be clipped to bounds + /// + public bool ClipToBounds + { + get { return _ClipToBounds; } + set { this.RaiseAndSetIfChanged(ref _ClipToBounds, value); } + } + + + private int _Duration = 500; + + /// + /// Gets or Sets the duration + /// + public int Duration + { + get { return _Duration; } + set + { + this.RaiseAndSetIfChanged(ref _Duration , value); + SetupTransitions(); + } + } + + /// + /// Gets or sets a value indicating whether the animation is reversed. + /// + public bool Reversed + { + get => _Reversed; + set => this.RaiseAndSetIfChanged(ref _Reversed, value); + } + + private void SetupTransitions() + { + if (PageTransitions.Count == 0) + { + PageTransitions.AddRange(new[] + { + new PageTransition("None"), + new PageTransition("CrossFade"), + new PageTransition("Slide horizontally"), + new PageTransition("Slide vertically"), + new PageTransition("Composite"), + new PageTransition("Custom") + }); + } + + PageTransitions[1].Transition = new CrossFade(TimeSpan.FromMilliseconds(Duration)); + PageTransitions[2].Transition = new PageSlide(TimeSpan.FromMilliseconds(Duration), PageSlide.SlideAxis.Horizontal); + PageTransitions[3].Transition = new PageSlide(TimeSpan.FromMilliseconds(Duration), PageSlide.SlideAxis.Vertical); + + var compositeTransition = new CompositePageTransition(); + compositeTransition.PageTransitions.Add(PageTransitions[1].Transition!); + compositeTransition.PageTransitions.Add(PageTransitions[2].Transition!); + compositeTransition.PageTransitions.Add(PageTransitions[3].Transition!); + PageTransitions[4].Transition = compositeTransition; + + PageTransitions[5].Transition = new CustomTransition(TimeSpan.FromMilliseconds(Duration)); + } + + public void NextImage() + { + Reversed = false; + var index = Images.IndexOf(SelectedImage) + 1; + + if (index >= Images.Count) + { + index = 0; + } + + SelectedImage = Images[index]; + } + + public void PrevImage() + { + Reversed = true; + var index = Images.IndexOf(SelectedImage) - 1; + + if (index < 0) + { + index = Images.Count-1; + } + + SelectedImage = Images[index]; + } + } + + public class PageTransition : ViewModelBase + { + public PageTransition(string displayTitle) + { + DisplayTitle = displayTitle; + } + + public string DisplayTitle { get; } + + + private IPageTransition? _Transition; + + /// + /// Gets or sets the transition + /// + public IPageTransition? Transition + { + get { return _Transition; } + set { this.RaiseAndSetIfChanged(ref _Transition, value); } + } + + public override string ToString() + { + return DisplayTitle; + } + + } + + public class CustomTransition : IPageTransition + { + /// + /// Initializes a new instance of the class. + /// + public CustomTransition() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public CustomTransition(TimeSpan duration) + { + Duration = duration; + } + + /// + /// Gets the duration of the animation. + /// + public TimeSpan Duration { get; set; } + + public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + var tasks = new List(); + var parent = GetVisualParent(from, to); + var scaleProperty = ScaleTransform.ScaleYProperty; + + if (from != null) + { + var animation = new Animation + { + Children = + { + new KeyFrame + { + Setters = { new Setter { Property = scaleProperty, Value = 1d } }, + Cue = new Cue(0d) + }, + new KeyFrame + { + Setters = + { + new Setter + { + Property = scaleProperty, + Value = 0d + } + }, + Cue = new Cue(1d) + } + }, + Duration = Duration + }; + tasks.Add(animation.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + var animation = new Animation + { + Children = + { + new KeyFrame + { + Setters = + { + new Setter + { + Property = scaleProperty, + Value = 0d + } + }, + Cue = new Cue(0d) + }, + new KeyFrame + { + Setters = { new Setter { Property = scaleProperty, Value = 1d } }, + Cue = new Cue(1d) + } + }, + Duration = Duration + }; + tasks.Add(animation.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (from != null && !cancellationToken.IsCancellationRequested) + { + from.IsVisible = false; + } + } + + /// + /// Gets the common visual parent of the two control. + /// + /// The from control. + /// The to control. + /// The common parent. + /// + /// The two controls do not share a common parent. + /// + /// + /// Any one of the parameters may be null, but not both. + /// + private static Visual GetVisualParent(Visual? from, Visual? to) + { + var p1 = (from ?? to)!.GetVisualParent(); + var p2 = (to ?? from)!.GetVisualParent(); + + if (p1 != null && p2 != null && p1 != p2) + { + throw new ArgumentException("Controls for PageSlide must have same parent."); + } + + return p1 ?? throw new InvalidOperationException("Cannot determine visual parent."); + } + } +} diff --git a/samples/AvaloniaControlCatalog/ViewModels/TreeViewPageViewModel.cs b/samples/AvaloniaControlCatalog/ViewModels/TreeViewPageViewModel.cs new file mode 100644 index 00000000..7b24f61e --- /dev/null +++ b/samples/AvaloniaControlCatalog/ViewModels/TreeViewPageViewModel.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using Avalonia.Controls; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class TreeViewPageViewModel : ViewModelBase + { + private readonly Node _root; + private SelectionMode _selectionMode; + + public TreeViewPageViewModel() + { + _root = new Node(); + + Items = _root.Children; + SelectedItems = new ObservableCollection(); + + AddItemCommand = MiniCommand.Create(AddItem); + RemoveItemCommand = MiniCommand.Create(RemoveItem); + SelectRandomItemCommand = MiniCommand.Create(SelectRandomItem); + } + + public ObservableCollection Items { get; } + public ObservableCollection SelectedItems { get; } + public MiniCommand AddItemCommand { get; } + public MiniCommand RemoveItemCommand { get; } + public MiniCommand SelectRandomItemCommand { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set + { + SelectedItems.Clear(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + + private void AddItem() + { + var parentItem = SelectedItems.Count > 0 ? (Node)SelectedItems[0] : _root; + parentItem.AddItem(); + } + + private void RemoveItem() + { + while (SelectedItems.Count > 0) + { + var lastItem = SelectedItems[0]; + RecursiveRemove(Items, lastItem); + } + + bool RecursiveRemove(ObservableCollection items, Node selectedItem) + { + if (items.Remove(selectedItem)) + { + return true; + } + + foreach (Node item in items) + { + if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem)) + { + return true; + } + } + + return false; + } + } + + private void SelectRandomItem() + { + var random = new Random(); + var depth = random.Next(4); + var indexes = Enumerable.Range(0, depth).Select(x => random.Next(10)); + var node = _root; + + foreach (var i in indexes) + { + node = node.Children[i]; + } + + SelectedItems.Clear(); + SelectedItems.Add(node); + } + + public class Node + { + private ObservableCollection? _children; + private int _childIndex = 10; + + public Node() + { + Header = "Item"; + } + + public Node(Node parent, int index) + { + Parent = parent; + Header = parent.Header + ' ' + index; + } + + public Node? Parent { get; } + public string Header { get; } + public bool AreChildrenInitialized => _children != null; + public ObservableCollection Children => _children ??= CreateChildren(); + public void AddItem() => Children.Add(new Node(this, _childIndex++)); + public void RemoveItem(Node child) => Children.Remove(child); + public override string ToString() => Header; + + private ObservableCollection CreateChildren() + { + return new ObservableCollection( + Enumerable.Range(0, 10).Select(i => new Node(this, i))); + } + } + } +} diff --git a/samples/AvaloniaControlCatalog/Views/CustomNotificationView.xaml b/samples/AvaloniaControlCatalog/Views/CustomNotificationView.xaml new file mode 100644 index 00000000..f06ea029 --- /dev/null +++ b/samples/AvaloniaControlCatalog/Views/CustomNotificationView.xaml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/Pages/DialogsPage.xaml.cs b/samples/AvaloniaControlCatalog/Pages/DialogsPage.xaml.cs index 616e3d4f..626f6310 100644 --- a/samples/AvaloniaControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/AvaloniaControlCatalog/Pages/DialogsPage.xaml.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; -using Avalonia.Dialogs; using Avalonia.Layout; using Avalonia.Platform.Storage; @@ -498,13 +497,7 @@ private Window CreateSampleWindow() return window; } - private IStorageProvider GetStorageProvider() - { - var forceManaged = ForceManaged.IsChecked ?? false; - return forceManaged - ? new ManagedStorageProvider(GetWindow()) // NOTE: In your production App use 'AppBuilder.UseManagedSystemDialogs()' - : GetTopLevel().StorageProvider; - } + private IStorageProvider GetStorageProvider() => GetTopLevel().StorageProvider; private static string FullPathOrName(IStorageItem? item) { diff --git a/samples/AvaloniaControlCatalog/Program.cs b/samples/AvaloniaControlCatalog/Program.cs index 6510d7da..859a31b2 100644 --- a/samples/AvaloniaControlCatalog/Program.cs +++ b/samples/AvaloniaControlCatalog/Program.cs @@ -5,6 +5,7 @@ using Avalonia; using ControlCatalog; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Renderers.Avalonia; namespace AvaloniaControlCatalog; @@ -16,15 +17,13 @@ public static void Main(string[] args) private static AppBuilder BuildAvaloniaApp() { - DrawingContextImpl.BackendMode = Environment.GetEnvironmentVariable("IMAGESHARP_DRAWING_BACKEND")?.ToLowerInvariant() switch + DrawingBackendMode backendMode = Environment.GetEnvironmentVariable("IMAGESHARP_DRAWING_BACKEND")?.ToLowerInvariant() switch { "cpu" => DrawingBackendMode.Cpu, "webgpu" => DrawingBackendMode.WebGpu, _ => DrawingBackendMode.Auto }; - // DrawingContextImpl.BackendMode = DrawingBackendMode.Cpu; - if (int.TryParse(Environment.GetEnvironmentVariable("IMAGESHARP_DRAWING_MAX_PARALLELISM"), out int maxParallelism)) { Configuration.Default.MaxDegreeOfParallelism = maxParallelism; @@ -34,7 +33,7 @@ private static AppBuilder BuildAvaloniaApp() .UsePlatformDetect() .WithInterFont(); - if (DrawingContextImpl.BackendMode == DrawingBackendMode.Cpu) + if (backendMode == DrawingBackendMode.Cpu) { // This sample-only override must run before Avalonia creates the backend context; // otherwise Win32 creates a platform graphics context and the top-level surfaces @@ -48,7 +47,7 @@ private static AppBuilder BuildAvaloniaApp() // Temporary backend switch for visual comparison: ISD_BACKEND=skia uses Avalonia's default (Skia). if (!string.Equals(Environment.GetEnvironmentVariable("ISD_BACKEND"), "skia", StringComparison.OrdinalIgnoreCase)) { - builder = builder.UseImageSharpDrawing(); + builder = builder.UseImageSharpDrawing(backendMode); } return builder; diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index b1d51c34..89c9d7a5 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -17,30 +17,29 @@ 3.0 - 1 - + - <_WebGpuVersionPrefix>$(WebGpuVersionMajor).$(MinVerMinor).$(MinVerPatch) - <_WebGpuPreReleaseSuffix> - <_WebGpuPreReleaseSuffix Condition="'$(MinVerPreRelease)' != ''">-$(MinVerPreRelease) - <_WebGpuBuildMetadataSuffix> - <_WebGpuBuildMetadataSuffix Condition="'$(MinVerBuildMetadata)' != ''">+$(MinVerBuildMetadata) - - $(WebGpuVersionMajor).0.0.0 - $(WebGpuVersionMajor).$(MinVerMinor).$(MinVerPatch).0 - $(_WebGpuVersionPrefix)$(_WebGpuPreReleaseSuffix) + <_IndependentPackageVersionPrefix>$(IndependentPackageVersionMajor).$(MinVerMinor).$(MinVerPatch) + <_IndependentPackagePreReleaseSuffix> + <_IndependentPackagePreReleaseSuffix Condition="'$(MinVerPreRelease)' != ''">-$(MinVerPreRelease) + <_IndependentPackageBuildMetadataSuffix> + <_IndependentPackageBuildMetadataSuffix Condition="'$(MinVerBuildMetadata)' != ''">+$(MinVerBuildMetadata) + + $(IndependentPackageVersionMajor).0.0.0 + $(IndependentPackageVersionMajor).$(MinVerMinor).$(MinVerPatch).0 + $(_IndependentPackageVersionPrefix)$(_IndependentPackagePreReleaseSuffix) $(Version) - $(Version)$(_WebGpuBuildMetadataSuffix) + $(Version)$(_IndependentPackageBuildMetadataSuffix) diff --git a/samples/AvaloniaControlCatalog/CombinedGeometryImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/CombinedGeometryImpl.cs similarity index 96% rename from samples/AvaloniaControlCatalog/CombinedGeometryImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/CombinedGeometryImpl.cs index 1148cc19..c3d2fad0 100644 --- a/samples/AvaloniaControlCatalog/CombinedGeometryImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/CombinedGeometryImpl.cs @@ -5,7 +5,7 @@ using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// Avalonia combined geometry implementation backed by ImageSharp path clipping. diff --git a/samples/AvaloniaControlCatalog/DrawingBackendMode.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/DrawingBackendMode.cs similarity index 82% rename from samples/AvaloniaControlCatalog/DrawingBackendMode.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/DrawingBackendMode.cs index 366f40b2..f1fc2f5c 100644 --- a/samples/AvaloniaControlCatalog/DrawingBackendMode.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/DrawingBackendMode.cs @@ -1,12 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// -/// Selects the rendering backend used by the ImageSharp.Drawing Avalonia sample. +/// Selects the rendering backend used by the ImageSharp.Drawing Avalonia renderer. /// -internal enum DrawingBackendMode +public enum DrawingBackendMode { /// /// Uses WebGPU when Avalonia exposes a compatible native surface; otherwise uses the CPU framebuffer. diff --git a/samples/AvaloniaControlCatalog/DrawingContextImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/DrawingContextImpl.cs similarity index 97% rename from samples/AvaloniaControlCatalog/DrawingContextImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/DrawingContextImpl.cs index a1d2187a..bb785e61 100644 --- a/samples/AvaloniaControlCatalog/DrawingContextImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/DrawingContextImpl.cs @@ -19,7 +19,7 @@ using AvaloniaTextRenderingMode = Avalonia.Media.TextRenderingMode; using BoxShadow = Avalonia.Media.BoxShadow; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// Avalonia drawing context implementation backed by an ImageSharp.Drawing canvas. @@ -181,7 +181,7 @@ private DrawingContextImpl(Image image, AvaloniaVector dpi, bool ownsIm internal DrawingCanvas Canvas => this.canvas; /// - public void Clear(Avalonia.Media.Color color) + public void Clear(global::Avalonia.Media.Color color) { this.Trace(() => $"Clear color={Describe(color)}"); this.canvas.Clear(Brushes.Solid(color.ToColor())); @@ -261,7 +261,7 @@ public void DrawBitmap(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskR } /// - public void DrawLine(IPen? pen, Avalonia.Point p1, Avalonia.Point p2) + public void DrawLine(IPen? pen, AvaloniaPoint p1, AvaloniaPoint p2) { Rect targetRect = new Rect(p1, p2).Normalize(); Pen? strokePen = this.CreatePen(pen, targetRect); @@ -328,7 +328,7 @@ public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows } /// - public void DrawRectangle(Avalonia.Media.IExperimentalAcrylicMaterial material, RoundedRect rect) + public void DrawRectangle(global::Avalonia.Media.IExperimentalAcrylicMaterial material, RoundedRect rect) { if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0) { @@ -919,16 +919,16 @@ internal void DisposeTransientResources() return this.CreateImageBrush(imageBrush, targetRect); } - if (brush is Avalonia.Media.ISceneBrush sceneBrush) + if (brush is global::Avalonia.Media.ISceneBrush sceneBrush) { - using Avalonia.Media.ISceneBrushContent? content = sceneBrush.CreateContent(); + using global::Avalonia.Media.ISceneBrushContent? content = sceneBrush.CreateContent(); return content is null ? null : this.CreateSceneBrushContent(content, targetRect); } - if (brush is Avalonia.Media.ISceneBrushContent sceneBrushContent) + if (brush is global::Avalonia.Media.ISceneBrushContent sceneBrushContent) { return this.CreateSceneBrushContent(sceneBrushContent, targetRect); } @@ -1355,14 +1355,14 @@ private void FillPath(Brush brush, IPath path, IntersectionRule fillRule, float // Skia feeds TileBrushCalculator the bitmap size in the current drawing DPI, then draws // from source pixels into that logical size. Keep the same split here: layout in Avalonia // DIPs, source sampling through the bitmap implementation. - Avalonia.Size sourceSize = bitmap.PixelSize.ToSizeWithDpi(this.dpi); + AvaloniaSize sourceSize = bitmap.PixelSize.ToSizeWithDpi(this.dpi); return this.CreateTileBrush(bitmap, sourceSize, brush, targetRect); } /// /// Converts an Avalonia scene brush into an ImageSharp tiled image brush. /// - private ImageBrush? CreateSceneBrushContent(Avalonia.Media.ISceneBrushContent content, Rect targetRect) + private ImageBrush? CreateSceneBrushContent(global::Avalonia.Media.ISceneBrushContent content, Rect targetRect) { Rect contentRect = content.Rect; int width = (int)Math.Ceiling(contentRect.Width); @@ -1396,7 +1396,7 @@ private void FillPath(Brush brush, IPath path, IntersectionRule fillRule, float : Matrix.CreateTranslation(-contentRect.X, -contentRect.Y); context.PushRenderOptions(this.currentRenderOptions); - context.Clear(Avalonia.Media.Colors.Transparent); + context.Clear(global::Avalonia.Media.Colors.Transparent); content.Render(context, transform); context.PopRenderOptions(); } @@ -1411,7 +1411,7 @@ private void FillPath(Brush brush, IPath path, IntersectionRule fillRule, float /// private ImageBrush? CreateTileBrush( IDrawableBitmapImpl sourceBitmap, - Avalonia.Size sourceSize, + AvaloniaSize sourceSize, ITileBrush brush, Rect targetRect) { @@ -1426,7 +1426,7 @@ private void FillPath(Brush brush, IPath path, IntersectionRule fillRule, float using (DrawingContextImpl context = new(tileImage, this.dpi)) { - context.Clear(Avalonia.Media.Colors.Transparent); + context.Clear(global::Avalonia.Media.Colors.Transparent); context.PushClip(layout.IntermediateClip); context.PushRenderOptions(this.currentRenderOptions); context.Transform = layout.IntermediateTransform; @@ -1473,10 +1473,10 @@ private static ColorStop[] ToColorStops(IReadOnlyList gradientSto /// /// Calculates the ImageSharp tile image that represents Avalonia's tile brush viewbox/viewport rules. /// - private static TileBrushLayout CalculateTileBrushLayout(ITileBrush brush, Avalonia.Size sourceSize, Rect targetRect) + private static TileBrushLayout CalculateTileBrushLayout(ITileBrush brush, AvaloniaSize sourceSize, Rect targetRect) { Rect sourceRect = brush.SourceRect.ToPixels(sourceSize); - Avalonia.Size targetSize = targetRect.Size; + AvaloniaSize targetSize = targetRect.Size; Rect destinationRect = brush.DestinationRect.ToPixels(targetSize); if (sourceRect.Width <= 0 || sourceRect.Height <= 0 || destinationRect.Width <= 0 || destinationRect.Height <= 0) @@ -1484,12 +1484,12 @@ private static TileBrushLayout CalculateTileBrushLayout(ITileBrush brush, Avalon return default; } - AvaloniaVector scale = Avalonia.Media.MediaExtensions.CalculateScaling( + AvaloniaVector scale = global::Avalonia.Media.MediaExtensions.CalculateScaling( brush.Stretch, destinationRect.Size, sourceRect.Size); - Avalonia.Size scaledSize = new(sourceRect.Width * scale.X, sourceRect.Height * scale.Y); + AvaloniaSize scaledSize = new(sourceRect.Width * scale.X, sourceRect.Height * scale.Y); AvaloniaVector translate = CalculateTileTranslate( brush.AlignmentX, brush.AlignmentY, @@ -1497,7 +1497,7 @@ private static TileBrushLayout CalculateTileBrushLayout(ITileBrush brush, Avalon destinationRect.Size); bool isTiled = brush.TileMode != TileMode.None; - Avalonia.Size intermediateSize = isTiled ? destinationRect.Size : targetSize; + AvaloniaSize intermediateSize = isTiled ? destinationRect.Size : targetSize; Matrix intermediateTransform = Matrix.CreateTranslation(-sourceRect.Position) * Matrix.CreateScale(scale) * @@ -1537,7 +1537,7 @@ private static TileBrushLayout CalculateTileBrushLayout(ITileBrush brush, Avalon /// /// Calculates Avalonia tile alignment translation for the scaled source size. /// - private static AvaloniaVector CalculateTileTranslate(AlignmentX alignmentX, AlignmentY alignmentY, Avalonia.Size sourceSize, Avalonia.Size destinationSize) + private static AvaloniaVector CalculateTileTranslate(AlignmentX alignmentX, AlignmentY alignmentY, AvaloniaSize sourceSize, AvaloniaSize destinationSize) { double x = alignmentX switch { @@ -1723,7 +1723,7 @@ private static string GetCaptureDirectory() /// /// Formats an Avalonia color for diagnostic traces. /// - private static string Describe(Avalonia.Media.Color color) + private static string Describe(global::Avalonia.Media.Color color) => $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}"; /// diff --git a/samples/AvaloniaControlCatalog/EllipseGeometryImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/EllipseGeometryImpl.cs similarity index 93% rename from samples/AvaloniaControlCatalog/EllipseGeometryImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/EllipseGeometryImpl.cs index fd459953..fa09244e 100644 --- a/samples/AvaloniaControlCatalog/EllipseGeometryImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/EllipseGeometryImpl.cs @@ -4,7 +4,7 @@ using Avalonia; using SixLabors.ImageSharp.Drawing; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// Avalonia ellipse geometry implementation backed by an ImageSharp ellipse path. diff --git a/samples/AvaloniaControlCatalog/FontManagerImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/FontManagerImpl.cs similarity index 99% rename from samples/AvaloniaControlCatalog/FontManagerImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/FontManagerImpl.cs index 72fcb9ff..32a66cd6 100644 --- a/samples/AvaloniaControlCatalog/FontManagerImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/FontManagerImpl.cs @@ -16,7 +16,7 @@ using SixLaborsFontStyle = SixLabors.Fonts.FontStyle; using SixLaborsSystemFonts = SixLabors.Fonts.SystemFonts; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// Avalonia font manager implementation backed by SixLabors.Fonts. diff --git a/samples/AvaloniaControlCatalog/GeometryGroupImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/GeometryGroupImpl.cs similarity index 96% rename from samples/AvaloniaControlCatalog/GeometryGroupImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/GeometryGroupImpl.cs index 4e38600a..e910a3a1 100644 --- a/samples/AvaloniaControlCatalog/GeometryGroupImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/GeometryGroupImpl.cs @@ -5,7 +5,7 @@ using Avalonia.Platform; using SixLabors.ImageSharp.Drawing; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// Avalonia geometry group implementation backed by combined ImageSharp paths. diff --git a/samples/AvaloniaControlCatalog/GeometryImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/GeometryImpl.cs similarity index 90% rename from samples/AvaloniaControlCatalog/GeometryImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/GeometryImpl.cs index 0fd12e9f..1baac130 100644 --- a/samples/AvaloniaControlCatalog/GeometryImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/GeometryImpl.cs @@ -10,7 +10,7 @@ using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// Base Avalonia geometry implementation backed by ImageSharp paths. @@ -115,6 +115,37 @@ public IGeometryImpl GetWidenedGeometry(IPen pen) public bool FillContains(AvaloniaPoint point) => this.fillGeometry is not null && this.fillGeometry.Contains(point.ToPointF(), this.FillRule); + /// + public IntersectionResult GetFillIntersectionResult(IGeometryImpl geometry) + { + if (geometry is not GeometryImpl other || this.FillPath is null || other.FillPath is null) + { + return IntersectionResult.Empty; + } + + Region region = new(this.FillPath, this.FillRule); + Region otherRegion = new(other.FillPath, other.FillRule); + + // Containment is tested before intersection so equal regions produce FullyInside + // rather than FullyContains. + if (region.Contains(otherRegion)) + { + return IntersectionResult.FullyInside; + } + + if (otherRegion.Contains(region)) + { + return IntersectionResult.FullyContains; + } + + if (region.Intersects(otherRegion)) + { + return IntersectionResult.Intersects; + } + + return IntersectionResult.Empty; + } + /// public IGeometryImpl? Intersect(IGeometryImpl geometry) => CombinedGeometryImpl.Create(GeometryCombineMode.Intersect, this, (GeometryImpl)geometry); diff --git a/src/ImageSharp.Drawing.Renderers.Avalonia/GlobalUsings.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/GlobalUsings.cs new file mode 100644 index 00000000..d2ace35f --- /dev/null +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/GlobalUsings.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// Avalonia type aliases shared across the ImageSharp.Drawing Avalonia renderer so the ImageSharp +// types (Brush, Pen, Color, etc.) can be used bare while Avalonia's same-named types stay explicit. +global using AvaloniaColor = global::Avalonia.Media.Color; +global using AvaloniaMatrix = global::Avalonia.Matrix; +global using AvaloniaPixelFormats = global::Avalonia.Platform.PixelFormats; +global using AvaloniaPoint = global::Avalonia.Point; +global using AvaloniaRect = global::Avalonia.Rect; +global using AvaloniaRoundedRect = global::Avalonia.RoundedRect; +global using AvaloniaSize = global::Avalonia.Size; +global using AvaloniaVector = global::Avalonia.Vector; +global using AlignmentX = global::Avalonia.Media.AlignmentX; +global using AlignmentY = global::Avalonia.Media.AlignmentY; +global using BoxShadows = global::Avalonia.Media.BoxShadows; +global using FillRule = global::Avalonia.Media.FillRule; +global using GeometryCombineMode = global::Avalonia.Media.GeometryCombineMode; +global using GradientSpreadMethod = global::Avalonia.Media.GradientSpreadMethod; +global using IBlurEffect = global::Avalonia.Media.IBlurEffect; +global using IBrush = global::Avalonia.Media.IBrush; +global using IConicGradientBrush = global::Avalonia.Media.IConicGradientBrush; +global using IDropShadowEffect = global::Avalonia.Media.IDropShadowEffect; +global using IEffect = global::Avalonia.Media.IEffect; +global using IGradientBrush = global::Avalonia.Media.IGradientBrush; +global using IGradientStop = global::Avalonia.Media.IGradientStop; +global using IImageBrush = global::Avalonia.Media.IImageBrush; +global using ILinearGradientBrush = global::Avalonia.Media.ILinearGradientBrush; +global using IPen = global::Avalonia.Media.IPen; +global using IRadialGradientBrush = global::Avalonia.Media.IRadialGradientBrush; +global using ISolidColorBrush = global::Avalonia.Media.ISolidColorBrush; +global using PenLineCap = global::Avalonia.Media.PenLineCap; +global using PenLineJoin = global::Avalonia.Media.PenLineJoin; +global using ITileBrush = global::Avalonia.Media.ITileBrush; +global using RenderOptions = global::Avalonia.Media.RenderOptions; +global using TileMode = global::Avalonia.Media.TileMode; diff --git a/samples/AvaloniaControlCatalog/GlyphRunImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/GlyphRunImpl.cs similarity index 97% rename from samples/AvaloniaControlCatalog/GlyphRunImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/GlyphRunImpl.cs index 124bb2f7..eb3ddebd 100644 --- a/samples/AvaloniaControlCatalog/GlyphRunImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/GlyphRunImpl.cs @@ -11,7 +11,7 @@ using AvaloniaTextHintingMode = Avalonia.Media.TextHintingMode; using AvaloniaTextOptions = Avalonia.Media.TextOptions; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// Avalonia glyph run implementation backed by SixLabors.Fonts glyph ids and positions. @@ -45,7 +45,7 @@ internal sealed class GlyphRunImpl : IGlyphRunImpl /// The positioned glyph information from Avalonia shaping. /// The glyph-run baseline origin. /// The SixLabors font face used to render the run. - public GlyphRunImpl(GlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin, Font font) + public GlyphRunImpl(GlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, AvaloniaPoint baselineOrigin, Font font) { this.GlyphTypeface = glyphTypeface; this.FontRenderingEmSize = fontRenderingEmSize; @@ -59,7 +59,7 @@ public GlyphRunImpl(GlyphTypeface glyphTypeface, double fontRenderingEmSize, IRe for (int i = 0; i < glyphInfos.Count; i++) { GlyphInfo glyph = glyphInfos[i]; - Avalonia.Vector offset = glyph.GlyphOffset; + AvaloniaVector offset = glyph.GlyphOffset; glyphIds[i] = glyph.GlyphIndex; origins[i] = new Vector2((float)(baselineOrigin.X + penX + offset.X), (float)(baselineOrigin.Y + offset.Y)); @@ -112,7 +112,7 @@ public GlyphRunImpl(GlyphTypeface glyphTypeface, double fontRenderingEmSize, IRe public double FontRenderingEmSize { get; } /// - public Point BaselineOrigin { get; } + public AvaloniaPoint BaselineOrigin { get; } /// public Rect Bounds { get; } diff --git a/samples/AvaloniaControlCatalog/IDrawableBitmapImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/IDrawableBitmapImpl.cs similarity index 95% rename from samples/AvaloniaControlCatalog/IDrawableBitmapImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/IDrawableBitmapImpl.cs index ef86b7e3..eaa690fc 100644 --- a/samples/AvaloniaControlCatalog/IDrawableBitmapImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/IDrawableBitmapImpl.cs @@ -6,7 +6,7 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Transforms; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// Bitmap implementation that can draw itself into an ImageSharp-backed drawing context. diff --git a/src/ImageSharp.Drawing.Renderers.Avalonia/ImageSharp.Drawing.Renderers.Avalonia.csproj b/src/ImageSharp.Drawing.Renderers.Avalonia/ImageSharp.Drawing.Renderers.Avalonia.csproj new file mode 100644 index 00000000..4d522eb0 --- /dev/null +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/ImageSharp.Drawing.Renderers.Avalonia.csproj @@ -0,0 +1,42 @@ + + + + SixLabors.ImageSharp.Drawing.Renderers.Avalonia + SixLabors.ImageSharp.Drawing.Renderers.Avalonia + SixLabors.ImageSharp.Drawing.Renderers.Avalonia + SixLabors.ImageSharp.Drawing.Renderers.Avalonia + + 1 + sixlabors.imagesharp.drawing.128.png + LICENSE + https://github.com/SixLabors/ImageSharp.Drawing/ + $(RepositoryUrl) + ImageSharp Drawing Avalonia Graphics Renderer Shapes Paths Text + An ImageSharp.Drawing renderer for Avalonia applications. + Debug;Release + net8.0 + enable + Nullable + false + true + true + $(NoWarn);AVA3001 + 12.2.999-cibuild0068621-alpha + https://nuget-feed-all.avaloniaui.net/v3/index.json;$(RestoreAdditionalProjectSources) + + + + + + + + + + + + + + + + + diff --git a/samples/AvaloniaControlCatalog/ImageSharpDrawingAppBuilderExtensions.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/ImageSharpDrawingAppBuilderExtensions.cs similarity index 62% rename from samples/AvaloniaControlCatalog/ImageSharpDrawingAppBuilderExtensions.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/ImageSharpDrawingAppBuilderExtensions.cs index a4c67297..4f00a45a 100644 --- a/samples/AvaloniaControlCatalog/ImageSharpDrawingAppBuilderExtensions.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/ImageSharpDrawingAppBuilderExtensions.cs @@ -4,23 +4,30 @@ using Avalonia; using Avalonia.Platform; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// -/// Provides application builder extensions for the ImageSharp.Drawing sample renderer. +/// Provides application builder extensions for the ImageSharp.Drawing renderer. /// -internal static class ImageSharpDrawingAppBuilderExtensions +public static class ImageSharpDrawingAppBuilderExtensions { /// /// Registers the ImageSharp.Drawing rendering and SixLabors.Fonts text shaping /// subsystems with the application builder. /// /// The application builder. + /// The rendering backend selection. /// The supplied . - public static AppBuilder UseImageSharpDrawing(this AppBuilder builder) - => builder + public static AppBuilder UseImageSharpDrawing(this AppBuilder builder, DrawingBackendMode backendMode) + { + // Avalonia registers one rendering subsystem for the application, so the backend selection + // must be fixed before Avalonia creates any platform rendering contexts. + DrawingContextImpl.BackendMode = backendMode; + + return builder .UseRenderingSubsystem(PlatformRenderInterface.Initialize, "ImageSharp.Drawing") .UseTextShapingSubsystem( () => AvaloniaLocator.CurrentMutable.Bind().ToConstant(new TextShaperImpl()), "SixLabors.Fonts"); + } } diff --git a/samples/AvaloniaControlCatalog/ImageSharpExtensions.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/ImageSharpExtensions.cs similarity index 99% rename from samples/AvaloniaControlCatalog/ImageSharpExtensions.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/ImageSharpExtensions.cs index 1e611df7..7280ab02 100644 --- a/samples/AvaloniaControlCatalog/ImageSharpExtensions.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/ImageSharpExtensions.cs @@ -16,7 +16,7 @@ using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// Conversions between Avalonia and ImageSharp primitive types. diff --git a/samples/AvaloniaControlCatalog/ImmutableBitmapImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/ImmutableBitmapImpl.cs similarity index 95% rename from samples/AvaloniaControlCatalog/ImmutableBitmapImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/ImmutableBitmapImpl.cs index e7e6716d..38041f6e 100644 --- a/samples/AvaloniaControlCatalog/ImmutableBitmapImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/ImmutableBitmapImpl.cs @@ -11,7 +11,7 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Transforms; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// ImageSharp-backed immutable bitmap implementation for the sample Avalonia renderer. @@ -43,8 +43,8 @@ public ImmutableBitmapImpl(ImmutableBitmapImpl source, PixelSize destinationSize this.Image = image; this.Dpi = source.Dpi; this.PixelSize = destinationSize; - this.format = PixelFormat.Bgra8888; - this.alphaFormat = Avalonia.Platform.AlphaFormat.Premul; + this.format = AvaloniaPixelFormats.Bgra8888; + this.alphaFormat = global::Avalonia.Platform.AlphaFormat.Premul; bool isUpscaling = destinationSize.Width > source.PixelSize.Width || destinationSize.Height > source.PixelSize.Height; IResampler sampler = interpolationMode.ToResampler(isUpscaling); @@ -106,7 +106,7 @@ public ImmutableBitmapImpl(Image image) /// The ImageSharp image backing the bitmap. /// The bitmap DPI. public ImmutableBitmapImpl(Image image, Vector dpi) - : this(image, dpi, PixelFormat.Bgra8888, Avalonia.Platform.AlphaFormat.Premul) + : this(image, dpi, AvaloniaPixelFormats.Bgra8888, global::Avalonia.Platform.AlphaFormat.Premul) { } @@ -170,16 +170,16 @@ public void Draw( /// private static Image CloneExternalPixels(PixelSize size, int stride, PixelFormat format, AlphaFormat alphaFormat, IntPtr data) { - if (format == PixelFormats.Bgra8888) + if (format == AvaloniaPixelFormats.Bgra8888) { - return alphaFormat == Avalonia.Platform.AlphaFormat.Premul + return alphaFormat == global::Avalonia.Platform.AlphaFormat.Premul ? CopyExternalPixels(size, stride, data) : CopyExternalPixels(size, stride, data); } - if (format == PixelFormats.Rgba8888) + if (format == AvaloniaPixelFormats.Rgba8888) { - return alphaFormat == Avalonia.Platform.AlphaFormat.Premul + return alphaFormat == global::Avalonia.Platform.AlphaFormat.Premul ? CopyExternalPixels(size, stride, data) : CopyExternalPixels(size, stride, data); } diff --git a/samples/AvaloniaControlCatalog/LineGeometryImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/LineGeometryImpl.cs similarity index 83% rename from samples/AvaloniaControlCatalog/LineGeometryImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/LineGeometryImpl.cs index 2b2da6aa..0a2d959f 100644 --- a/samples/AvaloniaControlCatalog/LineGeometryImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/LineGeometryImpl.cs @@ -4,7 +4,7 @@ using Avalonia; using SixLabors.ImageSharp.Drawing; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// Avalonia line geometry implementation backed by an ImageSharp open path. @@ -16,7 +16,7 @@ internal sealed class LineGeometryImpl : GeometryImpl /// /// The line start point. /// The line end point. - public LineGeometryImpl(Point p1, Point p2) + public LineGeometryImpl(AvaloniaPoint p1, AvaloniaPoint p2) : base(new PathBuilder().AddLine(p1.ToPointF(), p2.ToPointF()).Build(), null, IntersectionRule.EvenOdd) { } diff --git a/samples/AvaloniaControlCatalog/PlatformRenderInterface.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/PlatformRenderInterface.cs similarity index 97% rename from samples/AvaloniaControlCatalog/PlatformRenderInterface.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/PlatformRenderInterface.cs index 271678b8..34415261 100644 --- a/samples/AvaloniaControlCatalog/PlatformRenderInterface.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/PlatformRenderInterface.cs @@ -22,7 +22,7 @@ using SixLabors.ImageSharp.Processing.Processors.Transforms; using AvaloniaGlyphRun = Avalonia.Media.GlyphRun; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// Avalonia rendering platform implementation backed by ImageSharp.Drawing. @@ -93,7 +93,7 @@ private static void AppendWebGpuLog(string message) public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; /// - public PixelFormat DefaultPixelFormat => PixelFormat.Bgra8888; + public PixelFormat DefaultPixelFormat => AvaloniaPixelFormats.Bgra8888; /// public bool SupportsRegions => true; @@ -222,9 +222,9 @@ public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsCon /// public bool IsSupportedBitmapPixelFormat(PixelFormat format) - => format == PixelFormats.Rgb565 - || format == PixelFormats.Bgra8888 - || format == PixelFormats.Rgba8888; + => format == AvaloniaPixelFormats.Rgb565 + || format == AvaloniaPixelFormats.Bgra8888 + || format == AvaloniaPixelFormats.Rgba8888; /// public IPlatformRenderInterfaceRegion CreateRegion() => new RegionImpl(); diff --git a/samples/AvaloniaControlCatalog/RectangleGeometryImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/RectangleGeometryImpl.cs similarity index 91% rename from samples/AvaloniaControlCatalog/RectangleGeometryImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/RectangleGeometryImpl.cs index bf87fab9..e4145b28 100644 --- a/samples/AvaloniaControlCatalog/RectangleGeometryImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/RectangleGeometryImpl.cs @@ -4,7 +4,7 @@ using Avalonia; using SixLabors.ImageSharp.Drawing; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// Avalonia rectangle geometry implementation backed by an ImageSharp rectangle path. diff --git a/samples/AvaloniaControlCatalog/RegionImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/RegionImpl.cs similarity index 97% rename from samples/AvaloniaControlCatalog/RegionImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/RegionImpl.cs index 1f32d0c4..4ef69115 100644 --- a/samples/AvaloniaControlCatalog/RegionImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/RegionImpl.cs @@ -6,7 +6,7 @@ using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// ImageSharp-backed region implementation for Avalonia clipping and hit testing. diff --git a/samples/AvaloniaControlCatalog/RenderTargetBitmapImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/RenderTargetBitmapImpl.cs similarity index 95% rename from samples/AvaloniaControlCatalog/RenderTargetBitmapImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/RenderTargetBitmapImpl.cs index adb350fa..e2b9896c 100644 --- a/samples/AvaloniaControlCatalog/RenderTargetBitmapImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/RenderTargetBitmapImpl.cs @@ -9,7 +9,7 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Transforms; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// ImageSharp-backed render-target bitmap implementation for offscreen layers and render-target bitmaps. @@ -76,7 +76,7 @@ public void Draw( /// public IDrawingContextImpl CreateDrawingContext() => this.renderTarget.CreateDrawingContext( - new IRenderTarget.RenderTargetSceneInfo(this.PixelSize, this.Dpi.X / 96D, Avalonia.Rendering.Composition.CompositionTransparencyLevel.None), + new IRenderTarget.RenderTargetSceneInfo(this.PixelSize, this.Dpi.X / 96D, global::Avalonia.Rendering.Composition.CompositionTransparencyLevel.None), out _); /// diff --git a/samples/AvaloniaControlCatalog/RenderTargetImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/RenderTargetImpl.cs similarity index 97% rename from samples/AvaloniaControlCatalog/RenderTargetImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/RenderTargetImpl.cs index ea0449e7..0cd1b492 100644 --- a/samples/AvaloniaControlCatalog/RenderTargetImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/RenderTargetImpl.cs @@ -4,7 +4,7 @@ using Avalonia.Platform; using Avalonia.Platform.Surfaces; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// Framebuffer render target for the ImageSharp.Drawing sample renderer. diff --git a/samples/AvaloniaControlCatalog/StreamGeometryImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/StreamGeometryImpl.cs similarity index 99% rename from samples/AvaloniaControlCatalog/StreamGeometryImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/StreamGeometryImpl.cs index 64de0db7..edd0ae2c 100644 --- a/samples/AvaloniaControlCatalog/StreamGeometryImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/StreamGeometryImpl.cs @@ -7,7 +7,7 @@ using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// Mutable stream geometry implementation backed by ImageSharp path builders. diff --git a/samples/AvaloniaControlCatalog/TextShaperImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/TextShaperImpl.cs similarity index 97% rename from samples/AvaloniaControlCatalog/TextShaperImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/TextShaperImpl.cs index 5a945225..246b9bd6 100644 --- a/samples/AvaloniaControlCatalog/TextShaperImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/TextShaperImpl.cs @@ -15,7 +15,7 @@ using SixLaborsTextShaper = SixLabors.Fonts.TextShaper; using SixLaborsTextShapingBuffer = SixLabors.Fonts.TextShapingBuffer; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// Avalonia text shaper implementation backed by SixLabors.Fonts. @@ -96,7 +96,7 @@ public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions optio glyphOffset = default; } - shapedBuffer[i] = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); + shapedBuffer[i] = new global::Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); } return shapedBuffer; diff --git a/samples/AvaloniaControlCatalog/TransformedGeometryImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/TransformedGeometryImpl.cs similarity index 96% rename from samples/AvaloniaControlCatalog/TransformedGeometryImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/TransformedGeometryImpl.cs index 0070e516..e196d2de 100644 --- a/samples/AvaloniaControlCatalog/TransformedGeometryImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/TransformedGeometryImpl.cs @@ -7,7 +7,7 @@ using Avalonia.Platform; using SixLabors.ImageSharp.Drawing; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// Geometry wrapper that preserves the original source geometry and applied transform. diff --git a/samples/AvaloniaControlCatalog/WebGPULayerImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/WebGPULayerImpl.cs similarity index 95% rename from samples/AvaloniaControlCatalog/WebGPULayerImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/WebGPULayerImpl.cs index b8a60b6d..fb5720c0 100644 --- a/samples/AvaloniaControlCatalog/WebGPULayerImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/WebGPULayerImpl.cs @@ -10,7 +10,7 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Transforms; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// WebGPU-backed Avalonia drawing layer for the ImageSharp.Drawing sample renderer. @@ -62,10 +62,10 @@ public WebGPULayerImpl(WebGPURenderTarget renderTarget, PixelSize size, Vector d public bool IsCorrupted => false; /// - public PixelFormat? Format => PixelFormat.Bgra8888; + public PixelFormat? Format => AvaloniaPixelFormats.Bgra8888; /// - public AlphaFormat? AlphaFormat => Avalonia.Platform.AlphaFormat.Premul; + public AlphaFormat? AlphaFormat => global::Avalonia.Platform.AlphaFormat.Premul; /// public bool HasRenderContextAffinity => true; diff --git a/samples/AvaloniaControlCatalog/WebGPURenderTargetImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/WebGPURenderTargetImpl.cs similarity index 98% rename from samples/AvaloniaControlCatalog/WebGPURenderTargetImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/WebGPURenderTargetImpl.cs index 9eb9dc01..4078704a 100644 --- a/samples/AvaloniaControlCatalog/WebGPURenderTargetImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/WebGPURenderTargetImpl.cs @@ -9,7 +9,7 @@ using SixLabors.ImageSharp.Drawing.Processing.Backends; using ImageSharpSize = SixLabors.ImageSharp.Size; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// WebGPU render target bound directly to an Avalonia native window surface. diff --git a/samples/AvaloniaControlCatalog/WriteableBitmapImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/WriteableBitmapImpl.cs similarity index 94% rename from samples/AvaloniaControlCatalog/WriteableBitmapImpl.cs rename to src/ImageSharp.Drawing.Renderers.Avalonia/WriteableBitmapImpl.cs index f4c818be..f14b200e 100644 --- a/samples/AvaloniaControlCatalog/WriteableBitmapImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/WriteableBitmapImpl.cs @@ -11,7 +11,7 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Transforms; -namespace AvaloniaControlCatalog; +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; /// /// ImageSharp-backed writeable bitmap implementation for the sample Avalonia renderer. @@ -116,10 +116,10 @@ private WriteableBitmapImpl(Image image) public int Version => this.version; /// - public PixelFormat? Format => PixelFormats.Bgra8888; + public PixelFormat? Format => AvaloniaPixelFormats.Bgra8888; /// - public AlphaFormat? AlphaFormat => Avalonia.Platform.AlphaFormat.Premul; + public AlphaFormat? AlphaFormat => global::Avalonia.Platform.AlphaFormat.Premul; /// public void Draw( @@ -186,10 +186,10 @@ public BitmapFramebuffer(WriteableBitmapImpl parent) public Vector Dpi => this.parent.Dpi; /// - public PixelFormat Format => PixelFormats.Bgra8888; + public PixelFormat Format => AvaloniaPixelFormats.Bgra8888; /// - public AlphaFormat AlphaFormat => Avalonia.Platform.AlphaFormat.Premul; + public AlphaFormat AlphaFormat => global::Avalonia.Platform.AlphaFormat.Premul; /// public void Dispose() diff --git a/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj b/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj index 1d76c205..ba66f647 100644 --- a/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj +++ b/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj @@ -5,6 +5,8 @@ SixLabors.ImageSharp.Drawing.WebGPU SixLabors.ImageSharp.Drawing.Processing.Backends SixLabors.ImageSharp.Drawing.WebGPU + + 1 sixlabors.imagesharp.drawing.128.png LICENSE https://github.com/SixLabors/ImageSharp.Drawing/ diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 16b60759..ec46721c 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -45,7 +45,7 @@ - + @@ -53,11 +53,11 @@ Enable when referencing the local imagesharp source to prevent missing global usings for the ImageSharp namespaces. --> - + diff --git a/src/ImageSharp.Drawing/Region.cs b/src/ImageSharp.Drawing/Region.cs index 72a1e920..839fc461 100644 --- a/src/ImageSharp.Drawing/Region.cs +++ b/src/ImageSharp.Drawing/Region.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.Numerics; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Drawing.Helpers; namespace SixLabors.ImageSharp.Drawing; @@ -17,8 +18,8 @@ namespace SixLabors.ImageSharp.Drawing; /// public sealed class Region { - // The canonical model is the same shape used by SkRegion: a sorted set of horizontal - // Y bands, where each band owns sorted, non-overlapping X intervals. This preserves + // The canonical model is a sorted set of horizontal Y bands, where each band owns + // sorted, non-overlapping X intervals. This preserves // disjoint islands, L shapes, holes, and stair-step edges without collapsing anything // to the bounding rectangle. private readonly List bands = []; @@ -45,6 +46,331 @@ public Region() public Region(Rectangle rectangle) : this() => this.Add(rectangle); + /// + /// Initializes a new instance of the class containing the integer coverage of the specified path. + /// + /// The path whose filled area is added to the region. + /// The rule used to determine the filled area of the path. + /// The resulting region contains non-antialiased integer coverage. + public Region(IPath path, IntersectionRule intersectionRule) + : this() + { + RectangleF pathBounds = path.Bounds; + + // The finite integer clip ceilings each edge independently to match the path-region + // conversion contract; deriving right and bottom from rounded sizes can widen the clip. + int clipLeft = (int)MathF.Ceiling(pathBounds.Left); + int clipTop = (int)MathF.Ceiling(pathBounds.Top); + int clipRight = (int)MathF.Ceiling(pathBounds.Right); + int clipBottom = (int)MathF.Ceiling(pathBounds.Bottom); + + if (clipLeft >= clipRight || clipTop >= clipBottom) + { + return; + } + + LinearGeometry geometry = path.ToLinearGeometry(Vector2.One); + ReadOnlySpan contours = geometry.GetContours(); + int maximumEdgeCount = geometry.Info.PointCount; + + if (maximumEdgeCount == 0) + { + return; + } + + const int stackEdgeBufferSizeInBytes = 512; + int stackEdgeCapacity = stackEdgeBufferSizeInBytes / Unsafe.SizeOf(); + + // Each stored point can contribute at most one edge after the contour is implicitly closed. + // The fixed byte budget bounds per-call stack use while keeping small paths allocation-free; + // larger paths receive one exact, constructor-local array that dies with the conversion. + Span edges = maximumEdgeCount <= stackEdgeCapacity + ? stackalloc RegionEdge[maximumEdgeCount] + : new RegionEdge[maximumEdgeCount]; + + int edgeCount = 0; + + for (int i = 0; i < contours.Length; i++) + { + LinearContour contour = contours[i]; + + if (contour.PointCount < 2) + { + continue; + } + + ReadOnlySpan points = geometry.GetContourPoints(contour); + + // Filled contours are implicitly closed. Starting with the final point emits the + // closing edge without copying the contour or appending a duplicate endpoint. + PointF previous = points[^1]; + + for (int p = 0; p < points.Length; p++) + { + PointF current = points[p]; + + // Edge endpoints use signed 26.6 coordinates. The current X crossing and its + // per-row delta use signed 16.16 coordinates so every scanline advances by one add. + long x0 = (long)(previous.X * 64F); + long y0 = (long)(previous.Y * 64F); + long x1 = (long)(current.X * 64F); + long y1 = (long)(current.Y * 64F); + previous = current; + + int winding = 1; + + if (y0 > y1) + { + long temporary = x0; + x0 = x1; + x1 = temporary; + + temporary = y0; + y0 = y1; + y1 = temporary; + winding = -1; + } + + // Adding one half selects rows by their pixel centres. The bottom row is + // exclusive so adjoining edges contribute their shared vertex exactly once. + int top = (int)((y0 + 32) >> 6); + int bottom = (int)((y1 + 32) >> 6); + + if (top == bottom || top >= clipBottom || bottom <= clipTop) + { + continue; + } + + long slope = ((x1 - x0) << 16) / (y1 - y0); + long distanceToFirstCentre = ((long)top << 6) + 32 - y0; + long x = (x0 + ((slope * distanceToFirstCentre) >> 16)) << 10; + int firstY = Math.Max(top, clipTop); + int lastY = Math.Min(bottom - 1, clipBottom - 1); + + // Advance from the edge's natural first row to the clipped first row in 16.16 + // units. Converting before subtraction prevents the row distance from wrapping. + x += slope * ((long)firstY - top); + + edges[edgeCount++] = new RegionEdge + { + FirstY = firstY, + LastY = lastY, + X = x, + DxDy = slope, + Winding = winding, + Previous = -1, + Next = -1 + }; + } + } + + if (edgeCount == 0) + { + return; + } + + edges[..edgeCount].Sort(); + + // The sorted list contains active edges followed by edges for future rows. Relinking + // crossings in place avoids a second edge-sized order buffer. + for (int i = 0; i < edgeCount; i++) + { + edges[i].Previous = i - 1; + edges[i].Next = i + 1 < edgeCount ? i + 1 : -1; + } + + int activeHead = 0; + int firstFutureEdge = 0; + int y = edges[0].FirstY; + int windingMask = intersectionRule == IntersectionRule.EvenOdd ? 1 : -1; + bool hasBounds = false; + int regionLeft = 0; + int regionTop = 0; + int regionRight = 0; + int regionBottom = 0; + + while (activeHead >= 0 && y < clipBottom) + { + // When no edge spans the vertical gap, skip directly to the next populated row. + if (activeHead == firstFutureEdge && y < edges[firstFutureEdge].FirstY) + { + y = edges[firstFutureEdge].FirstY; + } + + // Future edges are ordered by their first row and initial X. Move only the edges + // beginning on this row into active X order; existing active edges were restored + // to that order while walking the preceding row. + int newEdge = firstFutureEdge; + + while (newEdge >= 0 && edges[newEdge].FirstY <= y) + { + int nextNewEdge = edges[newEdge].Next; + MoveEdgeBackward(edges, newEdge, ref activeHead); + newEdge = nextNewEdge; + } + + RegionBand? previousBand = this.bands.Count > 0 && this.bands[^1].Bottom == y + ? this.bands[^1] + : null; + + RegionBand? rowBand = null; + int matchedIntervalCount = 0; + int winding = 0; + long intervalLeft = 0; + int activeEdge = activeHead; + + while (activeEdge >= 0 && edges[activeEdge].FirstY <= y) + { + // All crossings that round to one integer boundary are one transition. Grouping + // them makes equal-X edge order irrelevant and removes zero-width intermediate spans. + long crossingX = (edges[activeEdge].X + 32768) >> 16; + int windingDelta = 0; + + do + { + int currentEdge = activeEdge; + int nextActiveEdge = edges[currentEdge].Next; + windingDelta += edges[currentEdge].Winding; + + if (edges[currentEdge].LastY == y) + { + int previousActiveEdge = edges[currentEdge].Previous; + + if (previousActiveEdge >= 0) + { + edges[previousActiveEdge].Next = nextActiveEdge; + } + else + { + activeHead = nextActiveEdge; + } + + if (nextActiveEdge >= 0) + { + edges[nextActiveEdge].Previous = previousActiveEdge; + } + } + else + { + edges[currentEdge].X += edges[currentEdge].DxDy; + MoveEdgeBackward(edges, currentEdge, ref activeHead); + } + + activeEdge = nextActiveEdge; + } + while (activeEdge >= 0 && + edges[activeEdge].FirstY <= y && + ((edges[activeEdge].X + 32768) >> 16) == crossingX); + + bool wasInside = (winding & windingMask) != 0; + winding += windingDelta; + bool isInside = (winding & windingMask) != 0; + + if (!wasInside && isInside) + { + intervalLeft = crossingX; + continue; + } + + if (!wasInside || isInside) + { + continue; + } + + // Clamp in the wide type and reject a wholly clipped span before narrowing. + // A surviving endpoint is therefore guaranteed to fit the integer clip. + long clippedLeft = Math.Max(intervalLeft, clipLeft); + long clippedRight = Math.Min(crossingX, clipRight); + + if (clippedLeft >= clippedRight) + { + continue; + } + + int left = (int)clippedLeft; + int right = (int)clippedRight; + + if (!hasBounds) + { + regionLeft = left; + regionTop = y; + regionRight = right; + hasBounds = true; + } + else + { + regionLeft = Math.Min(regionLeft, left); + regionRight = Math.Max(regionRight, right); + } + + regionBottom = y + 1; + + // Delay allocating a row band while its intervals still match the preceding + // band. On the first difference, copy only the already-matched prefix that + // must become part of the new canonical band. + if (rowBand is null && + previousBand is not null && + matchedIntervalCount < previousBand.Intervals.Count && + previousBand.Intervals[matchedIntervalCount].Left == left && + previousBand.Intervals[matchedIntervalCount].Right == right) + { + matchedIntervalCount++; + continue; + } + + if (rowBand is null) + { + rowBand = new RegionBand(y, y + 1); + + if (previousBand is not null && matchedIntervalCount > 0) + { + rowBand.Intervals.EnsureCapacity(previousBand.Intervals.Count); + + for (int i = 0; i < matchedIntervalCount; i++) + { + rowBand.Intervals.Add(previousBand.Intervals[i]); + } + } + } + + rowBand.Intervals.Add(new Interval(left, right)); + } + + firstFutureEdge = activeEdge; + + if (rowBand is not null) + { + this.bands.Add(rowBand); + } + else if (previousBand is not null && matchedIntervalCount == previousBand.Intervals.Count) + { + // Identical consecutive rows share the existing interval storage. + previousBand.Bottom = y + 1; + } + else if (previousBand is not null && matchedIntervalCount > 0) + { + // A shorter row can differ only after its matching prefix has ended. + RegionBand shorterBand = new(y, y + 1); + shorterBand.Intervals.EnsureCapacity(matchedIntervalCount); + + for (int i = 0; i < matchedIntervalCount; i++) + { + shorterBand.Intervals.Add(previousBand.Intervals[i]); + } + + this.bands.Add(shorterBand); + } + + y++; + } + + if (hasBounds) + { + this.bounds = Rectangle.FromLTRB(regionLeft, regionTop, regionRight, regionBottom); + this.rectanglesValid = false; + } + } + /// /// Initializes a new instance of the class containing the same area as the specified region. /// @@ -228,6 +554,80 @@ public bool Contains(int x, int y) return false; } + /// + /// Returns a value indicating whether this region contains the specified region. + /// + /// The region to test. + /// if this region contains all of ; otherwise, . + public bool Contains(Region region) + { + // Empty regions contain no area, while the bounds test rejects containment before scanning the canonical bands. + if (this.IsEmpty || region.IsEmpty || !this.bounds.Contains(region.bounds)) + { + return false; + } + + // Bands are ordered and non-overlapping, so the candidate containing band never needs to move backwards. + int firstContainingBandIndex = 0; + for (int i = 0; i < region.bands.Count; i++) + { + RegionBand requiredBand = region.bands[i]; + while (firstContainingBandIndex < this.bands.Count && this.bands[firstContainingBandIndex].Bottom <= requiredBand.Top) + { + firstContainingBandIndex++; + } + + int containingBandIndex = firstContainingBandIndex; + int coveredTop = requiredBand.Top; + + // Every vertical portion of the required band must be covered without a gap. + while (coveredTop < requiredBand.Bottom) + { + if (containingBandIndex >= this.bands.Count) + { + return false; + } + + RegionBand containingBand = this.bands[containingBandIndex]; + if (containingBand.Top > coveredTop) + { + return false; + } + + // Intervals are also ordered and non-overlapping, allowing a monotonic scan within the overlapping bands. + int containingIntervalIndex = 0; + for (int j = 0; j < requiredBand.Intervals.Count; j++) + { + Interval required = requiredBand.Intervals[j]; + while (containingIntervalIndex < containingBand.Intervals.Count + && containingBand.Intervals[containingIntervalIndex].Right <= required.Left) + { + containingIntervalIndex++; + } + + if (containingIntervalIndex >= containingBand.Intervals.Count) + { + return false; + } + + Interval containing = containingBand.Intervals[containingIntervalIndex]; + + // A required interval is contained only when one interval covers its complete horizontal extent. + if (containing.Left > required.Left || containing.Right < required.Right) + { + return false; + } + } + + // Continue at the first uncovered scanline when the required band spans multiple containing bands. + coveredTop = Math.Min(containingBand.Bottom, requiredBand.Bottom); + containingBandIndex++; + } + } + + return true; + } + /// /// Returns a value indicating whether the region intersects the specified rectangle. /// @@ -279,6 +679,80 @@ public bool Intersects(Rectangle rectangle) return false; } + /// + /// Returns a value indicating whether this region intersects the specified region. + /// + /// The region to test. + /// if the regions have area in common; otherwise, . + public bool Intersects(Region region) + { + // Touching bounds have no shared area, so they can be rejected before scanning the canonical bands. + if (this.IsEmpty || region.IsEmpty + || this.bounds.Right <= region.bounds.Left + || region.bounds.Right <= this.bounds.Left + || this.bounds.Bottom <= region.bounds.Top + || region.bounds.Bottom <= this.bounds.Top) + { + return false; + } + + // Both band lists are ordered and non-overlapping, enabling a linear two-pointer vertical sweep. + int firstBandIndex = 0; + int secondBandIndex = 0; + while (firstBandIndex < this.bands.Count && secondBandIndex < region.bands.Count) + { + RegionBand firstBand = this.bands[firstBandIndex]; + RegionBand secondBand = region.bands[secondBandIndex]; + if (firstBand.Bottom <= secondBand.Top) + { + firstBandIndex++; + continue; + } + + if (secondBand.Bottom <= firstBand.Top) + { + secondBandIndex++; + continue; + } + + // The bands overlap vertically, so scan their ordered intervals for a horizontal overlap. + int firstIntervalIndex = 0; + int secondIntervalIndex = 0; + while (firstIntervalIndex < firstBand.Intervals.Count && secondIntervalIndex < secondBand.Intervals.Count) + { + Interval first = firstBand.Intervals[firstIntervalIndex]; + Interval second = secondBand.Intervals[secondIntervalIndex]; + if (first.Right <= second.Left) + { + firstIntervalIndex++; + continue; + } + + if (second.Right <= first.Left) + { + secondIntervalIndex++; + continue; + } + + return true; + } + + // Advance every band ending at this boundary so the sweep continues beyond the tested vertical overlap. + int overlappingBottom = Math.Min(firstBand.Bottom, secondBand.Bottom); + if (firstBand.Bottom == overlappingBottom) + { + firstBandIndex++; + } + + if (secondBand.Bottom == overlappingBottom) + { + secondBandIndex++; + } + } + + return false; + } + /// /// Intersects this region with the specified rectangle. /// @@ -437,8 +911,8 @@ public IPath ToPath() /// The path describing the region boundary. private IPath BuildBoundaryPath() { - // Match SkRegion's boundary export shape: rectangles are first represented as - // opposing vertical edges, then linked into closed contours around the region + // Rectangles are first represented as opposing vertical edges, then linked into + // closed contours around the region // boundary. Shared internal edges cancel because the rectangle list is already // normalized into non-overlapping bands/intervals. List edges = new(this.rectangles.Count * 2); @@ -813,6 +1287,57 @@ private static bool IntervalsEqual(List first, List second) return true; } + /// + /// Moves an edge backwards through the linked crossing order when its X position precedes its current predecessor. + /// + /// The edge storage containing the linked order. + /// The edge whose X position may have moved backwards. + /// The first edge in crossing order. + private static void MoveEdgeBackward(Span edges, int edgeIndex, ref int head) + { + int previousIndex = edges[edgeIndex].Previous; + + if (previousIndex < 0 || edges[previousIndex].X <= edges[edgeIndex].X) + { + return; + } + + // Unlink the edge before searching backwards through the already-sorted prefix. + int nextIndex = edges[edgeIndex].Next; + edges[previousIndex].Next = nextIndex; + + if (nextIndex >= 0) + { + edges[nextIndex].Previous = previousIndex; + } + + int insertionPredecessor = previousIndex; + + while (insertionPredecessor >= 0 && edges[insertionPredecessor].X > edges[edgeIndex].X) + { + insertionPredecessor = edges[insertionPredecessor].Previous; + } + + if (insertionPredecessor < 0) + { + edges[edgeIndex].Previous = -1; + edges[edgeIndex].Next = head; + edges[head].Previous = edgeIndex; + head = edgeIndex; + return; + } + + int insertionSuccessor = edges[insertionPredecessor].Next; + edges[edgeIndex].Previous = insertionPredecessor; + edges[edgeIndex].Next = insertionSuccessor; + edges[insertionPredecessor].Next = edgeIndex; + + if (insertionSuccessor >= 0) + { + edges[insertionSuccessor].Previous = edgeIndex; + } + } + /// /// Converts one rectangle to its boundary path. /// @@ -848,6 +1373,58 @@ public Interval(int left, int right) public int Right { get; } } + /// + /// Represents one non-horizontal path edge during integer scan conversion. + /// + private struct RegionEdge : IComparable + { + /// + /// Gets or sets the first scanline crossed by the edge. + /// + public int FirstY { get; set; } + + /// + /// Gets or sets the last scanline crossed by the edge. + /// + public int LastY { get; set; } + + /// + /// Gets or sets the current crossing position in signed 16.16 fixed-point units. + /// + public long X { get; set; } + + /// + /// Gets or sets the signed 16.16 X advance for one scanline. + /// + public long DxDy { get; set; } + + /// + /// Gets or sets the winding contribution made when the edge is crossed. + /// + public int Winding { get; set; } + + /// + /// Gets or sets the preceding edge index in active crossing order. + /// + public int Previous { get; set; } + + /// + /// Gets or sets the following edge index in active crossing order. + /// + public int Next { get; set; } + + /// + /// Compares edges by their first scanline and initial crossing position. + /// + /// The edge to compare with this edge. + /// A value indicating the relative scan order of the edges. + public readonly int CompareTo(RegionEdge other) + { + int y = this.FirstY.CompareTo(other.FirstY); + return y != 0 ? y : this.X.CompareTo(other.X); + } + } + /// /// Represents one Y band with common X interval coverage. /// diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/FileProvider.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/FileProvider.cs index 2eee9796..f7c8db0b 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/FileProvider.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/FileProvider.cs @@ -6,9 +6,8 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; -using IOPath = System.IO.Path; - using Xunit.Abstractions; +using IOPath = System.IO.Path; namespace SixLabors.ImageSharp.Drawing.Tests; From 9eedc750f01ee4923a0d087f470b04b88ea82b5b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 22 Aug 2026 22:28:09 +1000 Subject: [PATCH 3/7] Update catalog pages from upstream --- samples/AvaloniaControlCatalog/App.xaml.cs | 11 - .../AvaloniaControlCatalog/MainView.xaml.cs | 174 +----------- .../AvaloniaControlCatalog/MainWindow.xaml | 1 + .../AvaloniaControlCatalog/MainWindow.xaml.cs | 39 --- .../Pages/CalendarDatePickerPage.xaml | 3 + .../Pages/CalendarPage.xaml | 11 + .../Pages/CompositionPage.axaml | 263 ++++++++++++++---- .../Pages/CompositionPage.axaml.cs | 179 +++++++++++- .../Pages/NumericUpDownPage.xaml.cs | 2 +- .../Pages/PlatformSettingsPage.xaml | 136 +++++++++ .../Pages/PlatformSettingsPage.xaml.cs | 30 ++ .../Pages/RefreshContainerPage.axaml | 1 + .../Pages/RefreshContainerPage.axaml.cs | 7 + .../Pages/TextBlockPage.xaml | 23 +- .../Pages/TextBoxPage.xaml | 5 +- .../Pages/WindowCustomizationsPage.xaml | 59 +++- .../Pages/WrapPanelPage.axaml | 120 ++++++++ .../Pages/WrapPanelPage.axaml.cs | 13 + .../AvaloniaControlCatalog/ScrollPage.xaml | 2 +- .../ViewModels/CursorPageViewModel.cs | 3 +- .../ViewModels/DataValidationViewModel.cs | 2 + .../ViewModels/MainWindowViewModel.cs | 52 ++++ .../MainWindowViewModel_PageList.cs | 2 + .../ViewModels/PlatformSettingsViewModel.cs | 80 ++++++ .../ViewModels/RefreshContainerViewModel.cs | 2 - .../ViewModels/WrapPanelPageViewModel.cs | 105 +++++++ 26 files changed, 1010 insertions(+), 315 deletions(-) create mode 100644 samples/AvaloniaControlCatalog/Pages/PlatformSettingsPage.xaml create mode 100644 samples/AvaloniaControlCatalog/Pages/PlatformSettingsPage.xaml.cs create mode 100644 samples/AvaloniaControlCatalog/Pages/WrapPanelPage.axaml create mode 100644 samples/AvaloniaControlCatalog/Pages/WrapPanelPage.axaml.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/PlatformSettingsViewModel.cs create mode 100644 samples/AvaloniaControlCatalog/ViewModels/WrapPanelPageViewModel.cs diff --git a/samples/AvaloniaControlCatalog/App.xaml.cs b/samples/AvaloniaControlCatalog/App.xaml.cs index 159e4ebf..71e4113a 100644 --- a/samples/AvaloniaControlCatalog/App.xaml.cs +++ b/samples/AvaloniaControlCatalog/App.xaml.cs @@ -43,17 +43,6 @@ public override void OnFrameworkInitializationCompleted() if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) { desktopLifetime.MainWindow = new MainWindow { DataContext = new MainWindowViewModel() }; - - if (int.TryParse(Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_EXIT_AFTER_MS"), out int exitDelay) && exitDelay > 0) - { - // Test-only hook: let the real desktop lifetime render normally, then shut it down - // without a human closing the window. - desktopLifetime.MainWindow.Opened += async (_, _) => - { - await Task.Delay(exitDelay); - desktopLifetime.Shutdown(); - }; - } } else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime) { diff --git a/samples/AvaloniaControlCatalog/MainView.xaml.cs b/samples/AvaloniaControlCatalog/MainView.xaml.cs index 0f738320..d4a45a12 100644 --- a/samples/AvaloniaControlCatalog/MainView.xaml.cs +++ b/samples/AvaloniaControlCatalog/MainView.xaml.cs @@ -1,20 +1,12 @@ using System; -using System.Diagnostics; -using System.Globalization; -using System.Linq; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Input.Raw; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Styling; -using Avalonia.Threading; -using Avalonia.VisualTree; using ControlCatalog.Models; using ControlCatalog.ViewModels; -using AvaloniaCalendar = Avalonia.Controls.Calendar; namespace ControlCatalog { @@ -205,171 +197,7 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) }; } - int startupPageIndex = 0; - string? startupPage = Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_PAGE"); - - if (!string.IsNullOrWhiteSpace(startupPage)) - { - for (int i = 0; i < ViewModel.Pages.Count; i++) - { - if (string.Equals(ViewModel.Pages[i].Header, startupPage, StringComparison.OrdinalIgnoreCase)) - { - startupPageIndex = i; - break; - } - } - } - - ViewModel.SelectedPageIndex = startupPageIndex; - - string? navigateToPage = Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_NAVIGATE_TO_PAGE"); - - if (!string.IsNullOrWhiteSpace(navigateToPage)) - { - // Test-only hook: changing selection after layout exercises the same dirty-region - // repaint path as clicking a navigation item in the drawer. - Dispatcher.UIThread.Post(async () => - { - await Task.Delay(350); - - string[] navigationSequence = navigateToPage.Split( - ',', - StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - - foreach (string pageName in navigationSequence) - { - for (int i = 0; i < ViewModel.Pages.Count; i++) - { - if (string.Equals(ViewModel.Pages[i].Header, pageName, StringComparison.OrdinalIgnoreCase)) - { - ListBox? navigationList = this.GetVisualDescendants() - .OfType() - .FirstOrDefault(x => ReferenceEquals(x.ItemsSource, ViewModel.Pages)); - - Console.Error.WriteLine($"ControlCatalog navigation atTicks={Stopwatch.GetTimestamp()} page={pageName}"); - - if (navigationList is not null) - { - navigationList.SelectedIndex = i; - } - else - { - ViewModel.SelectedPageIndex = i; - } - - if (Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_KEEP_DRAWER_OPEN") == "1") - { - ViewModel.IsDrawerOpened = true; - } - - break; - } - } - - // Leave enough time for the selected page's layout and ordered flushes to - // finish before the next selection measures a distinct navigation. - await Task.Delay(1000); - } - }, DispatcherPriority.Loaded); - } - - if (double.TryParse( - Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_SCROLL_Y"), - NumberStyles.Float, - CultureInfo.InvariantCulture, - out double scrollY)) - { - // Test-only hook: scrolling after layout captures the same dirty-region path that a - // user scroll exercises, while keeping the sample's normal startup path untouched. - Dispatcher.UIThread.Post(async () => - { - await Task.Delay(250); - - ScrollViewer? scrollViewer = this.GetVisualDescendants() - .OfType() - .Where(x => x.Extent.Height > x.Viewport.Height && x.Viewport.Height > 0) - .OrderByDescending(x => x.Viewport.Width) - .ThenByDescending(x => x.Extent.Height - x.Viewport.Height) - .FirstOrDefault(); - - if (scrollViewer is not null) - { - int stepCount = int.TryParse( - Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_SCROLL_STEPS"), - out int parsedStepCount) - ? Math.Max(1, parsedStepCount) - : 1; - - for (int i = 1; i <= stepCount; i++) - { - scrollViewer.Offset = new Vector(scrollViewer.Offset.X, scrollY * i / stepCount); - await Task.Delay(80); - } - } - }, DispatcherPriority.Loaded); - } - - if (int.TryParse( - Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_CALENDAR_SCROLL_MONTHS"), - NumberStyles.Integer, - CultureInfo.InvariantCulture, - out int calendarScrollMonths)) - { - // Test-only hook: Calendar navigation invalidates the CalendarItem internals, which - // exercises a different dirty-region path than scrolling the page viewport. - Dispatcher.UIThread.Post(async () => - { - await Task.Delay(250); - - AvaloniaCalendar[] calendars = this.GetVisualDescendants() - .OfType() - .Where(x => x.IsVisible) - .ToArray(); - - int calendarStart = 0; - int calendarEnd = calendars.Length; - - if (int.TryParse( - Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_CALENDAR_SCROLL_INDEX"), - NumberStyles.Integer, - CultureInfo.InvariantCulture, - out int calendarIndex)) - { - calendarStart = calendarIndex; - calendarEnd = calendarIndex + 1; - } - - using Pointer pointer = new(Pointer.GetNextFreeId(), PointerType.Mouse, true); - PointerPointProperties properties = new(RawInputModifiers.None, PointerUpdateKind.Other); - int direction = calendarScrollMonths < 0 ? 1 : -1; - int steps = Math.Abs(calendarScrollMonths); - - for (int i = 0; i < steps; i++) - { - for (int j = calendarStart; j < calendarEnd; j++) - { - AvaloniaCalendar calendar = calendars[j]; - Point position = calendar.TranslatePoint( - new Point(calendar.Bounds.Width / 2, calendar.Bounds.Height / 2), - topLevel) ?? default; - - PointerWheelEventArgs args = new( - calendar, - pointer, - topLevel, - position, - (ulong)Environment.TickCount64, - properties, - KeyModifiers.None, - new Vector(0, direction)); - - calendar.RaiseEvent(args); - } - - await Task.Delay(80); - } - }, DispatcherPriority.Loaded); - } + ViewModel.SelectedPageIndex = 0; } } } diff --git a/samples/AvaloniaControlCatalog/MainWindow.xaml b/samples/AvaloniaControlCatalog/MainWindow.xaml index d839689d..308b29c5 100644 --- a/samples/AvaloniaControlCatalog/MainWindow.xaml +++ b/samples/AvaloniaControlCatalog/MainWindow.xaml @@ -12,6 +12,7 @@ CanResize="{Binding CanResize}" CanMinimize="{Binding CanMinimize}" CanMaximize="{Binding CanMaximize}" + WindowDrawnDecorations.TitleBarDecorations="{Binding TitleBarDecorations}" Win32Properties.WindowCornerPreference="{Binding Win32WindowCornerPreference}" x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" x:DataType="vm:MainWindowViewModel"> diff --git a/samples/AvaloniaControlCatalog/MainWindow.xaml.cs b/samples/AvaloniaControlCatalog/MainWindow.xaml.cs index d43536a0..ca040df5 100644 --- a/samples/AvaloniaControlCatalog/MainWindow.xaml.cs +++ b/samples/AvaloniaControlCatalog/MainWindow.xaml.cs @@ -1,7 +1,5 @@ using System; -using System.Globalization; using System.Runtime.InteropServices; -using Avalonia; using Avalonia.Controls; using Avalonia.Input; @@ -15,12 +13,6 @@ public MainWindow() { InitializeComponent(); - if (TryGetStartupSize(out Size startupSize)) - { - Width = startupSize.Width; - Height = startupSize.Height; - } - _recentMenu = ((NativeMenu.GetMenu(this)?.Items[0] as NativeMenuItem)?.Menu?.Items[2] as NativeMenuItem)?.Menu; } @@ -39,36 +31,5 @@ public void OnCloseClicked(object sender, EventArgs args) { Close(); } - - /// - /// Reads the optional startup window size used by automated renderer captures. - /// - /// The parsed startup size. - /// when a valid startup size was provided. - private static bool TryGetStartupSize(out Size size) - { - size = default; - string? value = Environment.GetEnvironmentVariable("IMAGESHARP_CONTROL_CATALOG_WINDOW_SIZE"); - - if (string.IsNullOrWhiteSpace(value)) - { - return false; - } - - string[] parts = value.Split('x', 'X', ','); - if (parts.Length != 2) - { - return false; - } - - if (!double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out double width) || - !double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double height)) - { - return false; - } - - size = new Size(width, height); - return true; - } } } diff --git a/samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml b/samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml index 769dbc0d..c00af6c8 100644 --- a/samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml +++ b/samples/AvaloniaControlCatalog/Pages/CalendarDatePickerPage.xaml @@ -49,6 +49,9 @@ + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml b/samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml index 99727e91..44ab767d 100644 --- a/samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml +++ b/samples/AvaloniaControlCatalog/Pages/CalendarPage.xaml @@ -48,6 +48,17 @@ + + + + + # + + + diff --git a/samples/AvaloniaControlCatalog/Pages/CompositionPage.axaml b/samples/AvaloniaControlCatalog/Pages/CompositionPage.axaml index a3e9b047..0e4fcf2a 100644 --- a/samples/AvaloniaControlCatalog/Pages/CompositionPage.axaml +++ b/samples/AvaloniaControlCatalog/Pages/CompositionPage.axaml @@ -1,61 +1,204 @@ - - - - - - - - - - - - - - - - - - - - - - - - - Resize me - - - - - - - - - - - - - - - - - - Precise dirty rects - - - - + + + + + + + + + + + + + + + + + + + + + + + + + Resize me + + + + + + + + + + + + + + + + + + + Precise dirty rects + + + + + + + + + + + + + + + + + + + + + + - private HintingMode hintingMode = HintingMode.Standard; + private HintingMode hintingMode = HintingMode.Full; /// /// Initializes a new glyph run wrapper. @@ -78,6 +78,7 @@ public GlyphRunImpl(GlyphTypeface glyphTypeface, double fontRenderingEmSize, IRe new GlyphOptions { Font = font, + HintingMode = HintingMode.Full, TextBaseline = TextBaseline.Alphabetic, ColorFontSupport = ColorFontSupport.ColrV0 | ColorFontSupport.ColrV1 | ColorFontSupport.Svg }); @@ -140,8 +141,8 @@ public RichGlyphOptions CreateGlyphOptions(AvaloniaTextOptions textOptions) { AvaloniaTextHintingMode.None => HintingMode.None, AvaloniaTextHintingMode.Light => HintingMode.Standard, - AvaloniaTextHintingMode.Strong => HintingMode.Standard, - _ => HintingMode.Standard + AvaloniaTextHintingMode.Strong => HintingMode.Full, + _ => HintingMode.Full }; return new RichGlyphOptions From f9b4ef7139d1c6d02dacadb13447869077184d29 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 22 Aug 2026 22:53:53 +1000 Subject: [PATCH 5/7] Update ImageSharp.Drawing.All.sln --- ImageSharp.Drawing.All.sln | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/ImageSharp.Drawing.All.sln b/ImageSharp.Drawing.All.sln index ba011499..bac39415 100644 --- a/ImageSharp.Drawing.All.sln +++ b/ImageSharp.Drawing.All.sln @@ -333,10 +333,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawShapesWithImageSharp", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AvaloniaControlCatalog", "samples\AvaloniaControlCatalog\AvaloniaControlCatalog.csproj", "{333F3D4F-D251-5601-4851-307FF0F13D23}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SixLabors.Fonts", "..\Fonts\src\SixLabors.Fonts\SixLabors.Fonts.csproj", "{4A922B77-34EC-EA6A-8E96-8353C8FA0640}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolygonClipper", "..\PolygonClipper\src\PolygonClipper\PolygonClipper.csproj", "{5EC089DC-69F7-643B-93BB-293D199F5F74}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawingBackendBenchmark", "samples\DrawingBackendBenchmark\DrawingBackendBenchmark.csproj", "{B5CF5531-624F-9F49-46BD-937FF4062F54}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebGPUExternalSurfaceDemo", "samples\WebGPUExternalSurfaceDemo\WebGPUExternalSurfaceDemo.csproj", "{97619DEB-226C-EB67-140D-0C7E67B2F7D0}" @@ -411,30 +407,6 @@ Global {333F3D4F-D251-5601-4851-307FF0F13D23}.Release|x64.Build.0 = Release|Any CPU {333F3D4F-D251-5601-4851-307FF0F13D23}.Release|x86.ActiveCfg = Release|Any CPU {333F3D4F-D251-5601-4851-307FF0F13D23}.Release|x86.Build.0 = Release|Any CPU - {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Debug|x64.ActiveCfg = Debug|Any CPU - {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Debug|x64.Build.0 = Debug|Any CPU - {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Debug|x86.ActiveCfg = Debug|Any CPU - {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Debug|x86.Build.0 = Debug|Any CPU - {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Release|Any CPU.Build.0 = Release|Any CPU - {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Release|x64.ActiveCfg = Release|Any CPU - {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Release|x64.Build.0 = Release|Any CPU - {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Release|x86.ActiveCfg = Release|Any CPU - {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Release|x86.Build.0 = Release|Any CPU - {5EC089DC-69F7-643B-93BB-293D199F5F74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5EC089DC-69F7-643B-93BB-293D199F5F74}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5EC089DC-69F7-643B-93BB-293D199F5F74}.Debug|x64.ActiveCfg = Debug|Any CPU - {5EC089DC-69F7-643B-93BB-293D199F5F74}.Debug|x64.Build.0 = Debug|Any CPU - {5EC089DC-69F7-643B-93BB-293D199F5F74}.Debug|x86.ActiveCfg = Debug|Any CPU - {5EC089DC-69F7-643B-93BB-293D199F5F74}.Debug|x86.Build.0 = Debug|Any CPU - {5EC089DC-69F7-643B-93BB-293D199F5F74}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5EC089DC-69F7-643B-93BB-293D199F5F74}.Release|Any CPU.Build.0 = Release|Any CPU - {5EC089DC-69F7-643B-93BB-293D199F5F74}.Release|x64.ActiveCfg = Release|Any CPU - {5EC089DC-69F7-643B-93BB-293D199F5F74}.Release|x64.Build.0 = Release|Any CPU - {5EC089DC-69F7-643B-93BB-293D199F5F74}.Release|x86.ActiveCfg = Release|Any CPU - {5EC089DC-69F7-643B-93BB-293D199F5F74}.Release|x86.Build.0 = Release|Any CPU {B5CF5531-624F-9F49-46BD-937FF4062F54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B5CF5531-624F-9F49-46BD-937FF4062F54}.Debug|Any CPU.Build.0 = Debug|Any CPU {B5CF5531-624F-9F49-46BD-937FF4062F54}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -558,8 +530,6 @@ Global {C7606104-5D58-4670-912C-3F336606B02D} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} {43A3CA46-2675-4AD4-88FC-DDC2E657F0B7} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070} {333F3D4F-D251-5601-4851-307FF0F13D23} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070} - {4A922B77-34EC-EA6A-8E96-8353C8FA0640} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} - {5EC089DC-69F7-643B-93BB-293D199F5F74} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} {B5CF5531-624F-9F49-46BD-937FF4062F54} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070} {97619DEB-226C-EB67-140D-0C7E67B2F7D0} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070} {4570F31D-D00F-6A80-DB7C-74826CBD15AE} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070} @@ -574,8 +544,6 @@ Global EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{2e33181e-6e28-4662-a801-e2e7dc206029}*SharedItemsImports = 5 - ..\Fonts\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{4a922b77-34ec-ea6a-8e96-8353c8fa0640}*SharedItemsImports = 5 - ..\PolygonClipper\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{5ec089dc-69f7-643b-93bb-293d199f5f74}*SharedItemsImports = 5 shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{638fc2ae-7fc1-6527-e051-c6b34803f08d}*SharedItemsImports = 5 shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13 EndGlobalSection From fb07c1ba8048eb68d51b8cdb008e218982fb34fb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 23 Aug 2026 12:27:48 +1000 Subject: [PATCH 6/7] Add X11 WebGPU surface support to the Avalonia renderer Open our own Xlib Display and pair it with the XID Avalonia exposes, since window ids are valid across connections to the same server. --- .../WebGPURenderTargetImpl.cs | 34 ++++++++++++++++--- .../Xlib.cs | 30 ++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 src/ImageSharp.Drawing.Renderers.Avalonia/Xlib.cs diff --git a/src/ImageSharp.Drawing.Renderers.Avalonia/WebGPURenderTargetImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/WebGPURenderTargetImpl.cs index 4078704a..990730f6 100644 --- a/src/ImageSharp.Drawing.Renderers.Avalonia/WebGPURenderTargetImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/WebGPURenderTargetImpl.cs @@ -18,6 +18,7 @@ internal sealed class WebGPURenderTargetImpl : IRenderTarget { private readonly WebGPUSurfaceHost host; private readonly WebGPUSurfaceSession session; + private readonly nint x11Display; private WebGPUExternalSurface? surface; private WebGPUCompositeAlphaMode alphaMode; private bool isDisposed; @@ -31,9 +32,9 @@ internal sealed class WebGPURenderTargetImpl : IRenderTarget /// when the native surface can be used by WebGPU. public static bool TryCreate(WebGPUSurfaceSession session, INativePlatformHandleSurface nativeSurface, [NotNullWhen(true)] out WebGPURenderTargetImpl? renderTarget) { - if (TryCreateSurfaceHost(nativeSurface, out WebGPUSurfaceHost host)) + if (TryCreateSurfaceHost(nativeSurface, out WebGPUSurfaceHost host, out nint x11Display)) { - renderTarget = new WebGPURenderTargetImpl(session, host); + renderTarget = new WebGPURenderTargetImpl(session, host, x11Display); return true; } @@ -41,14 +42,16 @@ public static bool TryCreate(WebGPUSurfaceSession session, INativePlatformHandle return false; } - private WebGPURenderTargetImpl(WebGPUSurfaceSession session, WebGPUSurfaceHost host) + private WebGPURenderTargetImpl(WebGPUSurfaceSession session, WebGPUSurfaceHost host, nint x11Display) { this.session = session; this.host = host; + this.x11Display = x11Display; } - private static bool TryCreateSurfaceHost(INativePlatformHandleSurface nativeSurface, out WebGPUSurfaceHost host) + private static bool TryCreateSurfaceHost(INativePlatformHandleSurface nativeSurface, out WebGPUSurfaceHost host, out nint x11Display) { + x11Display = 0; switch (nativeSurface.HandleDescriptor) { case "HWND": @@ -56,6 +59,20 @@ private static bool TryCreateSurfaceHost(INativePlatformHandleSurface nativeSurf host = WebGPUSurfaceHost.Win32(nativeSurface.Handle, hinstance); return true; + case "XID": + // Avalonia exposes the window id but keeps its Display* internal. Window ids are valid + // on any connection to the same server, so open our own connection and keep it open for + // the lifetime of the render target; the WebGPU surface is created against it. + x11Display = Xlib.XOpenDisplay(null); + if (x11Display == 0) + { + host = default; + return false; + } + + host = WebGPUSurfaceHost.X11(x11Display, (nuint)nativeSurface.Handle); + return true; + case "SurfaceView": host = WebGPUSurfaceHost.Android(nativeSurface.Handle); return true; @@ -66,7 +83,7 @@ private static bool TryCreateSurfaceHost(INativePlatformHandleSurface nativeSurf default: // Avalonia currently exposes several GPU-capable platform surfaces through GL/Metal/D3D - // abstractions rather than full native window descriptors. X11, Wayland, and NSView require + // abstractions rather than full native window descriptors. Wayland and NSView require // additional upstream handle metadata before they can be mapped to Silk.NET WebGPU hosts here. host = default; return false; @@ -137,6 +154,13 @@ public void Dispose() } this.surface?.Dispose(); + + // The surface is gone, so the X connection it was created on can close. + if (this.x11Display != 0) + { + Xlib.XCloseDisplay(this.x11Display); + } + this.isDisposed = true; } } diff --git a/src/ImageSharp.Drawing.Renderers.Avalonia/Xlib.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/Xlib.cs new file mode 100644 index 00000000..835ac3c3 --- /dev/null +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/Xlib.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; + +/// +/// The Xlib entry points needed to open a connection to the X server that Avalonia's window lives on. +/// Avalonia keeps its own Display* internal, and window ids are valid across connections, so the +/// renderer opens a second connection and pairs it with the window id Avalonia exposes. +/// +internal static partial class Xlib +{ + /// + /// Opens a connection to the X server. Pass to use the DISPLAY environment variable. + /// + /// The display name, or for the default display. + /// The Display*, or when no connection could be made. + [LibraryImport("libX11.so.6", StringMarshalling = StringMarshalling.Utf8)] + public static partial nint XOpenDisplay(string? displayName); + + /// + /// Closes a connection opened by . + /// + /// The Display* to close. + /// Zero on success. + [LibraryImport("libX11.so.6")] + public static partial int XCloseDisplay(nint display); +} From 9a15171522805a480bcf27341bd17e0701a37335 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 23 Aug 2026 13:29:25 +1000 Subject: [PATCH 7/7] Keep CPU output visible on Win32 and use the window's HINSTANCE Select Win32 software rendering from the renderer initializer when the application has not configured Win32PlatformOptions: always in Cpu mode, and in Auto mode when the WebGPU probe fails. Composited windows do not display framebuffer output. Removes the sample-only override. Use GetModuleHandle(null) for the HWND surface, the module Avalonia registers its window class against. --- samples/AvaloniaControlCatalog/Program.cs | 11 ---- ...ageSharp.Drawing.Renderers.Avalonia.csproj | 1 + .../Kernel32.cs | 21 ++++++++ .../PlatformRenderInterface.cs | 51 +++++++++++++++++++ .../WebGPURenderTargetImpl.cs | 6 +-- 5 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 src/ImageSharp.Drawing.Renderers.Avalonia/Kernel32.cs diff --git a/samples/AvaloniaControlCatalog/Program.cs b/samples/AvaloniaControlCatalog/Program.cs index 859a31b2..32d62a86 100644 --- a/samples/AvaloniaControlCatalog/Program.cs +++ b/samples/AvaloniaControlCatalog/Program.cs @@ -33,17 +33,6 @@ private static AppBuilder BuildAvaloniaApp() .UsePlatformDetect() .WithInterFont(); - if (backendMode == DrawingBackendMode.Cpu) - { - // This sample-only override must run before Avalonia creates the backend context; - // otherwise Win32 creates a platform graphics context and the top-level surfaces - // represent the GPU/composited path instead of the framebuffer path we need to test. - builder = builder.With(new Win32PlatformOptions - { - RenderingMode = [Win32RenderingMode.Software] - }); - } - // Temporary backend switch for visual comparison: ISD_BACKEND=skia uses Avalonia's default (Skia). if (!string.Equals(Environment.GetEnvironmentVariable("ISD_BACKEND"), "skia", StringComparison.OrdinalIgnoreCase)) { diff --git a/src/ImageSharp.Drawing.Renderers.Avalonia/ImageSharp.Drawing.Renderers.Avalonia.csproj b/src/ImageSharp.Drawing.Renderers.Avalonia/ImageSharp.Drawing.Renderers.Avalonia.csproj index 4d522eb0..936a521c 100644 --- a/src/ImageSharp.Drawing.Renderers.Avalonia/ImageSharp.Drawing.Renderers.Avalonia.csproj +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/ImageSharp.Drawing.Renderers.Avalonia.csproj @@ -32,6 +32,7 @@ + diff --git a/src/ImageSharp.Drawing.Renderers.Avalonia/Kernel32.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/Kernel32.cs new file mode 100644 index 00000000..e6d9fd17 --- /dev/null +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/Kernel32.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Drawing.Renderers.Avalonia; + +/// +/// The Win32 entry point needed to identify the module that owns Avalonia's windows. +/// +internal static partial class Kernel32 +{ + /// + /// Gets the handle of a loaded module. Pass for the process executable, + /// which is the module Avalonia registers its window classes against. + /// + /// The module name, or for the process executable. + /// The module handle (HINSTANCE). + [LibraryImport("kernel32.dll", EntryPoint = "GetModuleHandleW", StringMarshalling = StringMarshalling.Utf16)] + public static partial nint GetModuleHandle(string? moduleName); +} diff --git a/src/ImageSharp.Drawing.Renderers.Avalonia/PlatformRenderInterface.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/PlatformRenderInterface.cs index 34415261..6f4901fa 100644 --- a/src/ImageSharp.Drawing.Renderers.Avalonia/PlatformRenderInterface.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/PlatformRenderInterface.cs @@ -62,6 +62,11 @@ public static void Initialize() AppendWebGpuLog($"failed to wire wgpu log callback: {ex.Message}"); } + if (OperatingSystem.IsWindows()) + { + EnsureWin32SoftwareRenderingForFramebuffer(); + } + PlatformRenderInterface platform = new(); AvaloniaLocator.CurrentMutable @@ -69,6 +74,52 @@ public static void Initialize() .Bind().ToConstant(new FontManagerImpl()); } + /// + /// Keeps CPU rendering visible on Windows. Win32 creates GPU-composited windows whenever a GPU + /// platform is available, and pixels drawn by the CPU into those windows are never shown. When + /// this renderer will draw with the CPU and the application did not configure + /// itself, select software rendering so the windows keep a + /// redirection surface. Avalonia runs this initializer after the application's options and before + /// the windowing subsystem, which is the only point where both facts are known. + /// + private static void EnsureWin32SoftwareRenderingForFramebuffer() + { + if (AvaloniaLocator.Current.GetService() is not null) + { + return; + } + + bool usesFramebuffer; + switch (DrawingContextImpl.BackendMode) + { + case DrawingBackendMode.Cpu: + usesFramebuffer = true; + break; + + case DrawingBackendMode.Auto: + WebGPUEnvironmentError probe = WebGPUEnvironment.ProbeAvailability(); + usesFramebuffer = probe != WebGPUEnvironmentError.Success; + if (usesFramebuffer) + { + AppendWebGpuLog($"WebGPU unavailable ({probe}); using the CPU framebuffer with Win32 software rendering."); + } + + break; + + default: + usesFramebuffer = false; + break; + } + + if (usesFramebuffer) + { + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Win32PlatformOptions + { + RenderingMode = [Win32RenderingMode.Software] + }); + } + } + private static void AppendWebGpuLog(string message) { string line = $"{DateTime.Now:HH:mm:ss.fff} {message}"; diff --git a/src/ImageSharp.Drawing.Renderers.Avalonia/WebGPURenderTargetImpl.cs b/src/ImageSharp.Drawing.Renderers.Avalonia/WebGPURenderTargetImpl.cs index 990730f6..ad62d60f 100644 --- a/src/ImageSharp.Drawing.Renderers.Avalonia/WebGPURenderTargetImpl.cs +++ b/src/ImageSharp.Drawing.Renderers.Avalonia/WebGPURenderTargetImpl.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; using Avalonia; using Avalonia.Platform; using Avalonia.Rendering.Composition; @@ -55,8 +54,9 @@ private static bool TryCreateSurfaceHost(INativePlatformHandleSurface nativeSurf switch (nativeSurface.HandleDescriptor) { case "HWND": - nint hinstance = Marshal.GetHINSTANCE(typeof(WebGPURenderTargetImpl).Module); - host = WebGPUSurfaceHost.Win32(nativeSurface.Handle, hinstance); + // Avalonia registers its window classes against the process executable, so that is + // the module that owns this window. The managed assembly's module would be the wrong one. + host = WebGPUSurfaceHost.Win32(nativeSurface.Handle, Kernel32.GetModuleHandle(null)); return true; case "XID":