diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml index 4987e8979ea..3adffed5f70 100644 --- a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml @@ -72,6 +72,14 @@ + + + + + + + @@ -82,6 +90,14 @@ + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs index 243bc5868b1..0a81133bae2 100644 --- a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs @@ -1,8 +1,10 @@ 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 @@ -164,7 +166,7 @@ private void OnShowHeaderToggled(object? sender, RoutedEventArgs e) if (!_isLoaded) return; if (ShowHeaderCheck.IsChecked == true) - DemoDrawer.DrawerHeader = DrawerHeaderBorder; + DemoDrawer.DrawerHeader = HeaderTemplateCombo.SelectedIndex == 0 ? DrawerHeaderBorder : (object)"My Application"; else DemoDrawer.DrawerHeader = null; } @@ -174,9 +176,128 @@ private void OnShowFooterToggled(object? sender, RoutedEventArgs e) if (!_isLoaded) return; if (ShowFooterCheck.IsChecked == true) - DemoDrawer.DrawerFooter = DrawerFooterBorder; + { + 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) diff --git a/src/Avalonia.Controls/Page/DrawerPage.cs b/src/Avalonia.Controls/Page/DrawerPage.cs index f8e039f8cd6..e2cefeb5d91 100644 --- a/src/Avalonia.Controls/Page/DrawerPage.cs +++ b/src/Avalonia.Controls/Page/DrawerPage.cs @@ -124,6 +124,18 @@ public class DrawerPage : Page public static readonly StyledProperty DrawerFooterProperty = AvaloniaProperty.Register(nameof(DrawerFooter)); + /// + /// Defines the property. + /// + public static readonly StyledProperty DrawerHeaderTemplateProperty = + AvaloniaProperty.Register(nameof(DrawerHeaderTemplate)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty DrawerFooterTemplateProperty = + AvaloniaProperty.Register(nameof(DrawerFooterTemplate)); + /// /// Defines the property. /// @@ -403,6 +415,7 @@ public DrawerPlacement DrawerPlacement /// /// Gets or sets the header content displayed at the top of the drawer pane. /// + [DependsOn(nameof(DrawerHeaderTemplate))] public object? DrawerHeader { get => GetValue(DrawerHeaderProperty); @@ -412,12 +425,31 @@ public object? DrawerHeader /// /// Gets or sets the footer content displayed at the bottom of the drawer pane. /// + [DependsOn(nameof(DrawerFooterTemplate))] public object? DrawerFooter { get => GetValue(DrawerFooterProperty); set => SetValue(DrawerFooterProperty, value); } + /// + /// Gets or sets the data template used to display . + /// + public IDataTemplate? DrawerHeaderTemplate + { + get => GetValue(DrawerHeaderTemplateProperty); + set => SetValue(DrawerHeaderTemplateProperty, value); + } + + /// + /// Gets or sets the data template used to display . + /// + public IDataTemplate? DrawerFooterTemplate + { + get => GetValue(DrawerFooterTemplateProperty); + set => SetValue(DrawerFooterTemplateProperty, value); + } + /// /// Gets or sets the icon displayed in the drawer toggle button. /// diff --git a/src/Avalonia.Themes.Fluent/Controls/DrawerPage.xaml b/src/Avalonia.Themes.Fluent/Controls/DrawerPage.xaml index 985814c9670..2a5a672d6b1 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DrawerPage.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DrawerPage.xaml @@ -57,11 +57,13 @@ + new FuncControlTemplate((dp, scope) => + { + var header = new ContentPresenter { Name = "PART_DrawerHeader" }.RegisterInNameScope(scope); + header.Bind(ContentPresenter.ContentProperty, dp.GetObservable(DrawerPage.DrawerHeaderProperty)); + header.Bind(ContentPresenter.ContentTemplateProperty, dp.GetObservable(DrawerPage.DrawerHeaderTemplateProperty)); + + var footer = new ContentPresenter { Name = "PART_DrawerFooter" }.RegisterInNameScope(scope); + footer.Bind(ContentPresenter.ContentProperty, dp.GetObservable(DrawerPage.DrawerFooterProperty)); + footer.Bind(ContentPresenter.ContentTemplateProperty, dp.GetObservable(DrawerPage.DrawerFooterTemplateProperty)); + + return new StackPanel { Children = { header, footer } }; + }); + + private static (DrawerPage dp, ContentPresenter header, ContentPresenter footer, TestRoot root) Create( + object? drawerHeader = null, + IDataTemplate? headerTemplate = null, + object? drawerFooter = null, + IDataTemplate? footerTemplate = null) + { + var dp = new DrawerPage + { + Template = MinimalPaneTemplate(), + DrawerHeader = drawerHeader, + DrawerHeaderTemplate = headerTemplate, + DrawerFooter = drawerFooter, + DrawerFooterTemplate = footerTemplate, + }; + var root = new TestRoot { Child = dp }; + dp.ApplyTemplate(); + + var header = dp.GetVisualDescendants().OfType().First(x => x.Name == "PART_DrawerHeader"); + var footer = dp.GetVisualDescendants().OfType().First(x => x.Name == "PART_DrawerFooter"); + + return (dp, header, footer, root); + } + + [Fact] + public void DrawerHeaderTemplate_IsForwardedToContentPresenter() + { + var template = new FuncDataTemplate((_, _) => new TextBlock()); + var (_, header, _, _) = Create(drawerHeader: "App", headerTemplate: template); + + Assert.Same(template, header.ContentTemplate); + } + + [Fact] + public void DrawerFooterTemplate_IsForwardedToContentPresenter() + { + var template = new FuncDataTemplate((_, _) => new TextBlock()); + var (_, _, footer, _) = Create(drawerFooter: "v1.0", footerTemplate: template); + + Assert.Same(template, footer.ContentTemplate); + } + + [Fact] + public void DrawerHeaderTemplate_RendersControlProducedByFactory() + { + var (_, header, _, _) = Create( + drawerHeader: "App", + headerTemplate: new FuncDataTemplate((_, _) => new Canvas())); + + header.UpdateChild(); + + Assert.IsType(header.Child); + } + + [Fact] + public void DrawerFooterTemplate_RendersControlProducedByFactory() + { + var (_, _, footer, _) = Create( + drawerFooter: "v1.0", + footerTemplate: new FuncDataTemplate((_, _) => new Canvas())); + + footer.UpdateChild(); + + Assert.IsType(footer.Child); + } + + [Fact] + public void DrawerHeaderTemplate_ReceivesDrawerHeaderAsData() + { + object? receivedData = null; + var (_, header, _, _) = Create( + drawerHeader: "MyTitle", + headerTemplate: new FuncDataTemplate((data, _) => + { + receivedData = data; + return new TextBlock { Text = data }; + })); + + header.UpdateChild(); + + Assert.Equal("MyTitle", receivedData); + } + + [Fact] + public void DrawerFooterTemplate_ReceivesDrawerFooterAsData() + { + object? receivedData = null; + var (_, _, footer, _) = Create( + drawerFooter: "v2.0", + footerTemplate: new FuncDataTemplate((data, _) => + { + receivedData = data; + return new TextBlock { Text = data }; + })); + + footer.UpdateChild(); + + Assert.Equal("v2.0", receivedData); + } + + [Fact] + public void DrawerHeaderTemplate_SwapTemplate_UpdatesContentPresenter() + { + var second = new FuncDataTemplate((_, _) => new Border()); + var (dp, header, _, _) = Create( + drawerHeader: "App", + headerTemplate: new FuncDataTemplate((_, _) => new Canvas())); + + header.UpdateChild(); + Assert.IsType(header.Child); + + dp.DrawerHeaderTemplate = second; + header.UpdateChild(); + + Assert.IsType(header.Child); + } + + [Fact] + public void DrawerFooterTemplate_SwapTemplate_UpdatesContentPresenter() + { + var second = new FuncDataTemplate((_, _) => new Border()); + var (dp, _, footer, _) = Create( + drawerFooter: "v1.0", + footerTemplate: new FuncDataTemplate((_, _) => new Canvas())); + + footer.UpdateChild(); + Assert.IsType(footer.Child); + + dp.DrawerFooterTemplate = second; + footer.UpdateChild(); + + Assert.IsType(footer.Child); + } + + [Fact] + public void DrawerHeaderTemplate_ClearingTemplate_FallsBackToDirectContent() + { + var directControl = new TextBlock { Text = "Direct" }; + var (dp, header, _, _) = Create( + drawerHeader: directControl, + headerTemplate: new FuncDataTemplate((_, _) => new Canvas())); + + header.UpdateChild(); + Assert.IsType(header.Child); + + dp.DrawerHeaderTemplate = null; + header.UpdateChild(); + + Assert.Same(directControl, header.Child); + } + + [Fact] + public void DrawerFooterTemplate_ClearingTemplate_FallsBackToDirectContent() + { + var directControl = new TextBlock { Text = "Direct" }; + var (dp, _, footer, _) = Create( + drawerFooter: directControl, + footerTemplate: new FuncDataTemplate((_, _) => new Canvas())); + + footer.UpdateChild(); + Assert.IsType(footer.Child); + + dp.DrawerFooterTemplate = null; + footer.UpdateChild(); + + Assert.Same(directControl, footer.Child); + } + } + public class SwipeGestureTests : ScopedTestBase { [Fact]