From ca5d6003b44564ab4e9c82aebb84cc079595b1e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Su=C3=A1rez?= Date: Thu, 19 Mar 2026 13:56:54 +0100 Subject: [PATCH] Add IconTemplate to Page and DrawerPage (#20946) * Fix TabItem.Icon type and add IconTemplate * Update API suppressions * Added tests * Add IconTemplate to Page and DrawerPage * Updated tests * More changes --------- Co-authored-by: Julien Lebosquain --- .../DrawerPageCustomizationPage.xaml | 5 + .../Pages/DrawerPage/EcoTrackerAppPage.xaml | 8 +- .../TabbedPageCustomTabBarPage.xaml.cs | 16 +-- .../TabbedPageCustomizationPage.xaml.cs | 11 +- .../TabbedPage/TabbedPageFabPage.xaml.cs | 8 +- src/Avalonia.Controls/Page/DrawerPage.cs | 52 +++----- src/Avalonia.Controls/Page/Page.cs | 16 +++ src/Avalonia.Controls/Page/TabbedPage.cs | 58 ++------- .../Controls/DrawerPage.xaml | 6 + .../Controls/DrawerPage.xaml | 6 + .../DrawerPageTests.cs | 111 ++---------------- .../TabControlTests.cs | 89 +++++++++++--- .../TabbedPageTests.cs | 108 ++++------------- 13 files changed, 191 insertions(+), 303 deletions(-) diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml index cba7837314..4987e8979e 100644 --- a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml @@ -118,6 +118,11 @@ Header="Customization" DrawerLength="260" DrawerHeaderBackground="{DynamicResource SystemControlHighlightAccentBrush}"> + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml index 22320fbc8d..1e9106ccfe 100644 --- a/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml +++ b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml @@ -52,9 +52,13 @@ - + 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 + + + + + diff --git a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageCustomTabBarPage.xaml.cs b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageCustomTabBarPage.xaml.cs index bb7302c1cd..b1a692b85a 100644 --- a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageCustomTabBarPage.xaml.cs +++ b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageCustomTabBarPage.xaml.cs @@ -5,7 +5,6 @@ namespace ControlCatalog.Pages { public partial class TabbedPageCustomTabBarPage : UserControl { - // Fluent UI icon geometries (24x24 viewbox) private static readonly StreamGeometry HomeGeometry = 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 WalletGeometry = @@ -25,16 +24,11 @@ namespace ControlCatalog.Pages private void SetupIcons() { - SetIcon(HomePage, HomeGeometry); - SetIcon(WalletPage, WalletGeometry); - SetIcon(SendPage, SendGeometry); - SetIcon(ActivityPage, ActivityGeometry); - SetIcon(ProfilePage, ProfileGeometry); - } - - private static void SetIcon(ContentPage page, StreamGeometry geometry) - { - page.Icon = geometry; + HomePage.Icon = new PathIcon { Data = HomeGeometry }; + WalletPage.Icon = new PathIcon { Data = WalletGeometry }; + SendPage.Icon = new PathIcon { Data = SendGeometry }; + ActivityPage.Icon = new PathIcon { Data = ActivityGeometry }; + ProfilePage.Icon = new PathIcon { Data = ProfileGeometry }; } } } diff --git a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageCustomizationPage.xaml.cs b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageCustomizationPage.xaml.cs index dc72759c5e..b4eb6d9b49 100644 --- a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageCustomizationPage.xaml.cs +++ b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageCustomizationPage.xaml.cs @@ -109,14 +109,9 @@ namespace ControlCatalog.Pages private void OnShowIconsChanged(object? sender, RoutedEventArgs e) { bool show = ShowIconsCheck.IsChecked == true; - SetIcon(HomePage, show ? HomeGeometry : null); - SetIcon(SearchPage, show ? SearchGeometry : null); - SetIcon(SettingsPage, show ? SettingsGeometry : null); - } - - private static void SetIcon(ContentPage page, StreamGeometry? geometry) - { - page.Icon = geometry; + HomePage.Icon = show ? new PathIcon { Data = HomeGeometry } : null; + SearchPage.Icon = show ? new PathIcon { Data = SearchGeometry } : null; + SettingsPage.Icon = show ? new PathIcon { Data = SettingsGeometry } : null; } private void OnTabEnabledChanged(object? sender, RoutedEventArgs e) diff --git a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs index 5c10a50df7..b52bfd4d8a 100644 --- a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs +++ b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs @@ -28,10 +28,10 @@ namespace ControlCatalog.Pages private void SetupIcons() { - FeedPage.Icon = FeedGeometry; - DiscoverPage.Icon = DiscoverGeometry; - AlertsPage.Icon = AlertsGeometry; - ProfilePage.Icon = ProfileGeometry; + 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) diff --git a/src/Avalonia.Controls/Page/DrawerPage.cs b/src/Avalonia.Controls/Page/DrawerPage.cs index 69b1a41842..954ec9c918 100644 --- a/src/Avalonia.Controls/Page/DrawerPage.cs +++ b/src/Avalonia.Controls/Page/DrawerPage.cs @@ -29,9 +29,6 @@ namespace Avalonia.Controls [TemplatePart("PART_PaneButton", typeof(ToggleButton))] [TemplatePart("PART_CompactPaneToggle", typeof(ToggleButton))] [TemplatePart("PART_Backdrop", typeof(Border))] - [TemplatePart("PART_CompactPaneIconPresenter", typeof(ContentPresenter))] - [TemplatePart("PART_PaneIconPresenter", typeof(ContentPresenter))] - [TemplatePart("PART_BottomPaneIconPresenter", typeof(ContentPresenter))] [PseudoClasses(":placement-right", ":placement-top", ":placement-bottom", ":detail-is-navpage")] public class DrawerPage : Page { @@ -133,6 +130,12 @@ namespace Avalonia.Controls public static readonly StyledProperty DrawerIconProperty = AvaloniaProperty.Register(nameof(DrawerIcon)); + /// + /// Defines the property. + /// + public static readonly StyledProperty DrawerIconTemplateProperty = + AvaloniaProperty.Register(nameof(DrawerIconTemplate)); + private static readonly DefaultPageDataTemplate s_defaultPageDataTemplate = new DefaultPageDataTemplate(); /// @@ -206,9 +209,6 @@ namespace Avalonia.Controls private ContentPresenter? _drawerPresenter; private ContentPresenter? _drawerHeaderPresenter; private ContentPresenter? _drawerFooterPresenter; - private ContentPresenter? _compactPaneIconPresenter; - private ContentPresenter? _paneIconPresenter; - private ContentPresenter? _bottomPaneIconPresenter; private SplitView? _splitView; private Border? _topBar; private ToggleButton? _paneButton; @@ -427,6 +427,15 @@ namespace Avalonia.Controls set => SetValue(DrawerIconProperty, value); } + /// + /// Gets or sets the data template used to display the drawer icon. + /// + public IDataTemplate? DrawerIconTemplate + { + get => GetValue(DrawerIconTemplateProperty); + set => SetValue(DrawerIconTemplateProperty, value); + } + /// /// Gets or sets the data template used to display content. /// @@ -536,16 +545,11 @@ namespace Avalonia.Controls _drawerPresenter = e.NameScope.Find("PART_DrawerPresenter"); _drawerHeaderPresenter = e.NameScope.Find("PART_DrawerHeader"); _drawerFooterPresenter = e.NameScope.Find("PART_DrawerFooter"); - _compactPaneIconPresenter = e.NameScope.Find("PART_CompactPaneIconPresenter"); - _paneIconPresenter = e.NameScope.Find("PART_PaneIconPresenter"); - _bottomPaneIconPresenter = e.NameScope.Find("PART_BottomPaneIconPresenter"); _splitView = e.NameScope.Find("PART_SplitView"); _topBar = e.NameScope.Find("PART_TopBar"); _paneButton = e.NameScope.Find("PART_PaneButton"); _backdrop = e.NameScope.Find("PART_Backdrop"); - UpdateIconPresenters(); - if (_backdrop != null) { if (IsAttachedToVisualTree) @@ -568,11 +572,7 @@ namespace Avalonia.Controls { base.OnPropertyChanged(change); - if (change.Property == DrawerIconProperty) - { - UpdateIconPresenters(); - } - else if (change.Property == DrawerProperty || change.Property == ContentProperty) + if (change.Property == DrawerProperty || change.Property == ContentProperty) { if (change.OldValue is ILogical oldLogical) LogicalChildren.Remove(oldLogical); @@ -1006,26 +1006,6 @@ namespace Avalonia.Controls e.Handled = true; } - private void UpdateIconPresenters() - { - if (_compactPaneIconPresenter != null) - _compactPaneIconPresenter.Content = CreateIconContent(DrawerIcon); - if (_paneIconPresenter != null) - _paneIconPresenter.Content = CreateIconContent(DrawerIcon); - if (_bottomPaneIconPresenter != null) - _bottomPaneIconPresenter.Content = CreateIconContent(DrawerIcon); - } - - internal static object? CreateIconContent(object? icon) => icon switch - { - ITemplate template => template.Build(), - Geometry g => new PathIcon { Data = g }, - PathIcon pi => new PathIcon { Data = pi.Data }, - DrawingImage { Drawing: GeometryDrawing { Geometry: { } gd } } => new PathIcon { Data = gd }, - IImage image => new Image { Source = image }, - _ => null - }; - private void ApplyDrawerBackground() { if (_splitView == null) diff --git a/src/Avalonia.Controls/Page/Page.cs b/src/Avalonia.Controls/Page/Page.cs index a894a9b86f..a12ce76ddf 100644 --- a/src/Avalonia.Controls/Page/Page.cs +++ b/src/Avalonia.Controls/Page/Page.cs @@ -2,6 +2,7 @@ using System; using System.Threading.Tasks; using Avalonia.Automation; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Interactivity; namespace Avalonia.Controls @@ -31,6 +32,12 @@ namespace Avalonia.Controls public static readonly StyledProperty IconProperty = AvaloniaProperty.Register(nameof(Icon)); + /// + /// Defines the property. + /// + public static readonly StyledProperty IconTemplateProperty = + AvaloniaProperty.Register(nameof(IconTemplate)); + /// /// Defines the property. /// @@ -94,6 +101,15 @@ namespace Avalonia.Controls set => SetValue(IconProperty, value); } + /// + /// Gets or sets the data template used to display the icon. + /// + public IDataTemplate? IconTemplate + { + get => GetValue(IconTemplateProperty); + set => SetValue(IconTemplateProperty, value); + } + /// /// Gets or sets the safe-area padding applied to this page's content. /// diff --git a/src/Avalonia.Controls/Page/TabbedPage.cs b/src/Avalonia.Controls/Page/TabbedPage.cs index 76bdeaa560..69815eb56a 100644 --- a/src/Avalonia.Controls/Page/TabbedPage.cs +++ b/src/Avalonia.Controls/Page/TabbedPage.cs @@ -6,13 +6,10 @@ using Avalonia.Automation.Peers; using Avalonia.Collections; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Input.GestureRecognizers; -using Avalonia.Layout; using Avalonia.LogicalTree; -using Avalonia.Media; using Avalonia.Threading; namespace Avalonia.Controls @@ -323,7 +320,8 @@ namespace Avalonia.Controls tabItem.IsEnabled = GetIsTabEnabled(page); tabItem.Header = page.Header; - tabItem.Icon = CreateIconContent(page.Icon); + tabItem.Icon = page.Icon; + tabItem.IconTemplate = page.IconTemplate; if (e.Index == (_tabControl?.SelectedIndex ?? -1)) UpdateActivePage(); @@ -351,7 +349,8 @@ namespace Avalonia.Controls tabItem.IsEnabled = GetIsTabEnabled(page); tabItem.Header = page.Header; - tabItem.Icon = CreateIconContent(page.Icon); + tabItem.Icon = page.Icon; + tabItem.IconTemplate = page.IconTemplate; } UpdateActivePage(); @@ -365,7 +364,12 @@ namespace Avalonia.Controls if (e.Property == Page.IconProperty) { if (_pageContainerMap.TryGetValue(page, out var tabItem)) - tabItem.Icon = CreateIconContent(page.Icon); + tabItem.Icon = page.Icon; + } + else if (e.Property == Page.IconTemplateProperty) + { + if (_pageContainerMap.TryGetValue(page, out var tabItem)) + tabItem.IconTemplate = page.IconTemplate; } else if (e.Property == Page.HeaderProperty) { @@ -378,44 +382,6 @@ namespace Avalonia.Controls } } - /// - /// Creates a visual control from a page icon value. - /// - internal static Control? CreateIconContent(object? icon) - { - if (icon is ITemplate template) - return template.Build(); - - Geometry? geometry = icon switch - { - Geometry g => g, - PathIcon pi => pi.Data, - DrawingImage { Drawing: GeometryDrawing { Geometry: { } gd } } => gd, - _ => null - }; - - if (geometry != null) - { - var path = new Path - { - Data = geometry, - Stretch = Stretch.Uniform, - HorizontalAlignment = HorizontalAlignment.Center, - }; - - path.Bind( - Path.FillProperty, - path.GetObservable(Documents.TextElement.ForegroundProperty)); - - return path; - } - - if (icon is IImage image) - return new Image { Source = image }; - - return null; - } - private int FindNearestEnabledTab(int disabledIndex) { int count = GetTabCount(); @@ -687,7 +653,7 @@ namespace Avalonia.Controls var placement = ResolveTabPlacement(); bool isHorizontal = placement == TabPlacement.Top || placement == TabPlacement.Bottom; - bool isRtl = FlowDirection == FlowDirection.RightToLeft; + bool isRtl = FlowDirection == Media.FlowDirection.RightToLeft; int delta = (e.SwipeDirection, isHorizontal, isRtl) switch { @@ -721,7 +687,7 @@ namespace Avalonia.Controls var resolved = ResolveTabPlacement(); bool isHorizontal = resolved == TabPlacement.Top || resolved == TabPlacement.Bottom; - bool isRtl = FlowDirection == FlowDirection.RightToLeft; + bool isRtl = FlowDirection == Media.FlowDirection.RightToLeft; bool next = isHorizontal ? (isRtl ? e.Key == Key.Left : e.Key == Key.Right) : e.Key == Key.Down; bool prev = isHorizontal ? (isRtl ? e.Key == Key.Right : e.Key == Key.Left) : e.Key == Key.Up; diff --git a/src/Avalonia.Themes.Fluent/Controls/DrawerPage.xaml b/src/Avalonia.Themes.Fluent/Controls/DrawerPage.xaml index 4f892153b1..985814c967 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DrawerPage.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DrawerPage.xaml @@ -47,6 +47,8 @@ VerticalAlignment="Center"/> @@ -101,6 +103,8 @@ VerticalAlignment="Center"/> @@ -146,6 +150,8 @@ VerticalAlignment="Center"/> diff --git a/src/Avalonia.Themes.Simple/Controls/DrawerPage.xaml b/src/Avalonia.Themes.Simple/Controls/DrawerPage.xaml index 9a3e6d37cf..9acf4c4d43 100644 --- a/src/Avalonia.Themes.Simple/Controls/DrawerPage.xaml +++ b/src/Avalonia.Themes.Simple/Controls/DrawerPage.xaml @@ -46,6 +46,8 @@ VerticalAlignment="Center"/> @@ -92,6 +94,8 @@ VerticalAlignment="Center"/> @@ -129,6 +133,8 @@ VerticalAlignment="Center"/> diff --git a/tests/Avalonia.Controls.UnitTests/DrawerPageTests.cs b/tests/Avalonia.Controls.UnitTests/DrawerPageTests.cs index f4afe81e19..87fb421a72 100644 --- a/tests/Avalonia.Controls.UnitTests/DrawerPageTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DrawerPageTests.cs @@ -1160,113 +1160,26 @@ public class DrawerPageTests public class IconTests : ScopedTestBase { [Fact] - public void Geometry_ReturnsPathIcon() + public void DrawerIconTemplate_RoundTrips() { - var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; - var result = DrawerPage.CreateIconContent(geometry); - Assert.IsType(result); - Assert.Same(geometry, ((PathIcon)result!).Data); - } - - [Fact] - public void PathIcon_ReturnsPathIcon() - { - var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; - var pathIcon = new PathIcon { Data = geometry }; - var result = DrawerPage.CreateIconContent(pathIcon); - Assert.IsType(result); - Assert.Same(geometry, ((PathIcon)result!).Data); - } - - [Fact] - public void DrawingImage_WithGeometryDrawing_ReturnsPathIcon() - { - var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; - var drawing = new GeometryDrawing { Geometry = geometry }; - var drawingImage = new DrawingImage(drawing); - var result = DrawerPage.CreateIconContent(drawingImage); - Assert.IsType(result); - Assert.Same(geometry, ((PathIcon)result!).Data); - } - - [Fact] - public void Image_ReturnsImage() - { - var image = new TestImage(); - var result = DrawerPage.CreateIconContent(image); - Assert.IsType(result); - Assert.Same(image, ((Image)result!).Source); - } - - private sealed class TestImage : IImage - { - public Size Size => new Size(1, 1); - public void Draw(DrawingContext context, Rect sourceRect, Rect destRect) { } - } - - [Fact] - public void EmptyString_ReturnsNull() - { - var result = DrawerPage.CreateIconContent(""); - Assert.Null(result); - } - - [Fact] - public void NullString_ReturnsNull() - { - var result = DrawerPage.CreateIconContent((string?)null); - Assert.Null(result); - } - - [Fact] - public void Null_ReturnsNull() - { - var result = DrawerPage.CreateIconContent(null); - Assert.Null(result); - } - - [Fact] - public void Template_BuildsControl() - { - var template = new FuncTemplate(() => new Border()); - var result = DrawerPage.CreateIconContent(template); - Assert.IsType(result); - } - - [Fact] - public void Template_BuildsSeparateInstances() - { - var template = new FuncTemplate(() => new Border()); - var first = DrawerPage.CreateIconContent(template); - var second = DrawerPage.CreateIconContent(template); - Assert.NotSame(first, second); + var template = new FuncDataTemplate((_, _) => new PathIcon()); + var dp = new DrawerPage { DrawerIconTemplate = template }; + Assert.Same(template, dp.DrawerIconTemplate); } [Fact] - public void NonEmptyString_ReturnsNull() - { - var result = DrawerPage.CreateIconContent("M10 20v-6h4v6"); - Assert.Null(result); - } - - [Fact] - public void UnsupportedType_ReturnsNull() - { - var result = DrawerPage.CreateIconContent(42); - Assert.Null(result); - } - - [Fact] - public void ChangingDrawerIcon_AfterTemplateApplied_UpdatesPresenters() + public void DrawerIcon_With_Geometry_Does_Not_Throw() { var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; - var dp = new DrawerPage { DrawerIcon = new PathIcon { Data = geometry } }; + var dp = new DrawerPage + { + DrawerIcon = geometry, + DrawerIconTemplate = new FuncDataTemplate((_, _) => new PathIcon()), + }; var root = new TestRoot { Child = dp }; - var geometry2 = new EllipseGeometry { Rect = new Rect(0, 0, 20, 20) }; - dp.DrawerIcon = new PathIcon { Data = geometry2 }; - - Assert.Same(geometry2, dp.DrawerIcon is PathIcon pi ? pi.Data : null); + dp.DrawerIcon = new EllipseGeometry { Rect = new Rect(0, 0, 20, 20) }; + Assert.NotNull(dp.DrawerIcon); } } diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 8bad019c2b..42643c5560 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -955,6 +955,30 @@ namespace Avalonia.Controls.UnitTests }.RegisterInNameScope(scope)); } + private static IControlTemplate TabItemWithIconTemplate() + { + return new FuncControlTemplate((parent, scope) => + new StackPanel + { + Children = + { + new ContentPresenter + { + Name = "PART_IconPresenter", + [~ContentPresenter.ContentProperty] = new TemplateBinding(TabItem.IconProperty), + [~ContentPresenter.ContentTemplateProperty] = new TemplateBinding(TabItem.IconTemplateProperty), + }.RegisterInNameScope(scope), + new ContentPresenter + { + Name = "PART_ContentPresenter", + [~ContentPresenter.ContentProperty] = new TemplateBinding(TabItem.HeaderProperty), + [~ContentPresenter.ContentTemplateProperty] = new TemplateBinding(TabItem.HeaderTemplateProperty), + RecognizesAccessKey = true, + }.RegisterInNameScope(scope), + } + }); + } + private static ControlTheme CreateTabControlControlTheme() { return new ControlTheme(typeof(TabControl)) @@ -1495,35 +1519,72 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void TabItem_Icon_DefaultIsNull() + public void TabItem_IconTemplate_Creates_Content_From_NonControl_Icon() { - var tabItem = new TabItem(); - Assert.Null(tabItem.Icon); + var tabItem = new TabItem + { + Icon = "home", + IconTemplate = new FuncDataTemplate((val, _) => + new TextBlock { Text = (string)val }), + Template = TabItemWithIconTemplate(), + }; + + var root = new TestRoot { Child = tabItem }; + tabItem.ApplyTemplate(); + tabItem.Presenter!.UpdateChild(); + + var iconPresenter = tabItem.GetTemplateChildren().OfType().First(x => x.Name == "PART_IconPresenter"); + Assert.NotNull(iconPresenter); + Assert.Equal("home", iconPresenter!.Content); + Assert.NotNull(iconPresenter.ContentTemplate); + + iconPresenter.UpdateChild(); + var textBlock = iconPresenter.Child as TextBlock; + Assert.NotNull(textBlock); + Assert.Equal("home", textBlock!.Text); } [Fact] - public void TabItem_Icon_RoundTrips() + public void TabItem_Icon_Without_Template_Renders_Control_Directly() { - var tabItem = new TabItem(); var icon = new Avalonia.Controls.Shapes.Path { Data = new Avalonia.Media.EllipseGeometry { Rect = new Rect(0, 0, 10, 10) } }; - tabItem.Icon = icon; - Assert.Same(icon, tabItem.Icon); + var tabItem = new TabItem + { + Icon = icon, + Template = TabItemWithIconTemplate(), + }; + + var root = new TestRoot { Child = tabItem }; + tabItem.ApplyTemplate(); + tabItem.Presenter!.UpdateChild(); + + var iconPresenter = tabItem.GetTemplateChildren().OfType().First(x => x.Name == "PART_IconPresenter"); + Assert.NotNull(iconPresenter); + Assert.Same(icon, iconPresenter!.Content); + Assert.Null(iconPresenter.ContentTemplate); } [Fact] - public void TabItem_Icon_CanBeSetToNull() + public void TabItem_Icon_Change_Updates_Presenter_Content() { - var tabItem = new TabItem(); - var icon = new Avalonia.Controls.Shapes.Path + var tabItem = new TabItem { - Data = new Avalonia.Media.EllipseGeometry { Rect = new Rect(0, 0, 10, 10) } + Icon = "first", + Template = TabItemWithIconTemplate(), }; - tabItem.Icon = icon; - tabItem.Icon = null; - Assert.Null(tabItem.Icon); + + var root = new TestRoot { Child = tabItem }; + tabItem.ApplyTemplate(); + tabItem.Presenter!.UpdateChild(); + + var iconPresenter = tabItem.GetTemplateChildren().OfType().First(x => x.Name == "PART_IconPresenter"); + Assert.Equal("first", iconPresenter!.Content); + + tabItem.Icon = "second"; + Assert.Equal("second", iconPresenter.Content); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/TabbedPageTests.cs b/tests/Avalonia.Controls.UnitTests/TabbedPageTests.cs index 3fa34e0f2b..a27f398a17 100644 --- a/tests/Avalonia.Controls.UnitTests/TabbedPageTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabbedPageTests.cs @@ -913,111 +913,53 @@ public class TabbedPageTests } } - public class IconTests : ScopedTestBase + public class PageIconTemplateTests : ScopedTestBase { [Fact] - public void Geometry_ReturnsPath() + public void Page_Icon_AcceptsControlValue() { - var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; - var result = TabbedPage.CreateIconContent(geometry); - Assert.IsType(result); - Assert.Same(geometry, ((Path)result!).Data); + var icon = new PathIcon { Data = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) } }; + var page = new ContentPage { Icon = icon }; + Assert.Same(icon, page.Icon); } [Fact] - public void PathIcon_ReturnsPath() + public void Page_Icon_AcceptsNonControlValue() { var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; - var pathIcon = new PathIcon { Data = geometry }; - var result = TabbedPage.CreateIconContent(pathIcon); - Assert.IsType(result); - Assert.Same(geometry, ((Path)result!).Data); - } - - [Fact] - public void EmptyString_ReturnsNull() - { - var result = TabbedPage.CreateIconContent(""); - Assert.Null(result); - } - - [Fact] - public void NullString_ReturnsNull() - { - var result = TabbedPage.CreateIconContent((string?)null); - Assert.Null(result); + var page = new ContentPage { Icon = geometry }; + Assert.Same(geometry, page.Icon); } [Fact] - public void Null_ReturnsNull() + public void Page_IconTemplate_RoundTrips() { - var result = TabbedPage.CreateIconContent(null); - Assert.Null(result); + var template = new FuncDataTemplate((_, _) => new Border()); + var page = new ContentPage { IconTemplate = template }; + Assert.Same(template, page.IconTemplate); } [Fact] - public void DrawingImage_WithGeometryDrawing_ReturnsPath() + public void DrawerPage_DrawerIconTemplate_RoundTrips() { - var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; - var drawing = new GeometryDrawing { Geometry = geometry }; - var drawingImage = new DrawingImage(drawing); - var result = TabbedPage.CreateIconContent(drawingImage); - Assert.IsType(result); - Assert.Same(geometry, ((Path)result!).Data); + var template = new FuncDataTemplate((_, _) => new Border()); + var dp = new DrawerPage { DrawerIconTemplate = template }; + Assert.Same(template, dp.DrawerIconTemplate); } [Fact] - public void Path_HasStretchUniform() + public void DrawerPage_DrawerIcon_With_Geometry_Does_Not_Throw() { var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; - var result = TabbedPage.CreateIconContent(geometry); - Assert.Equal(Stretch.Uniform, ((Path)result!).Stretch); - } - - [Fact] - public void Image_ReturnsImage() - { - var image = new TestImage(); - var result = TabbedPage.CreateIconContent(image); - Assert.IsType(result); - Assert.Same(image, ((Image)result!).Source); - } - - private sealed class TestImage : IImage - { - public Size Size => new Size(1, 1); - public void Draw(DrawingContext context, Rect sourceRect, Rect destRect) { } - } - - [Fact] - public void Template_BuildsControl() - { - var template = new FuncTemplate(() => new Border()); - var result = TabbedPage.CreateIconContent(template); - Assert.IsType(result); - } - - [Fact] - public void Template_BuildsSeparateInstances() - { - var template = new FuncTemplate(() => new Border()); - var first = TabbedPage.CreateIconContent(template); - var second = TabbedPage.CreateIconContent(template); - Assert.NotSame(first, second); - } - - [Fact] - public void NonEmptyString_ReturnsNull() - { - var result = TabbedPage.CreateIconContent("M10 20v-6h4v6"); - Assert.Null(result); - } + var dp = new DrawerPage + { + DrawerIcon = geometry, + DrawerIconTemplate = new FuncDataTemplate((_, _) => new PathIcon()), + }; + var root = new TestRoot { Child = dp }; - [Fact] - public void UnsupportedType_ReturnsNull() - { - var result = TabbedPage.CreateIconContent(42); - Assert.Null(result); + dp.DrawerIcon = new EllipseGeometry { Rect = new Rect(0, 0, 20, 20) }; + Assert.NotNull(dp.DrawerIcon); } }