Browse Source

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 <julien@lebosquain.net>
pull/20953/head
Javier Suárez 4 days ago
committed by GitHub
parent
commit
ca5d6003b4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml
  2. 8
      samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml
  3. 16
      samples/ControlCatalog/Pages/TabbedPage/TabbedPageCustomTabBarPage.xaml.cs
  4. 11
      samples/ControlCatalog/Pages/TabbedPage/TabbedPageCustomizationPage.xaml.cs
  5. 8
      samples/ControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs
  6. 52
      src/Avalonia.Controls/Page/DrawerPage.cs
  7. 16
      src/Avalonia.Controls/Page/Page.cs
  8. 58
      src/Avalonia.Controls/Page/TabbedPage.cs
  9. 6
      src/Avalonia.Themes.Fluent/Controls/DrawerPage.xaml
  10. 6
      src/Avalonia.Themes.Simple/Controls/DrawerPage.xaml
  11. 111
      tests/Avalonia.Controls.UnitTests/DrawerPageTests.cs
  12. 89
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  13. 108
      tests/Avalonia.Controls.UnitTests/TabbedPageTests.cs

5
samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml

@ -118,6 +118,11 @@
Header="Customization"
DrawerLength="260"
DrawerHeaderBackground="{DynamicResource SystemControlHighlightAccentBrush}">
<DrawerPage.DrawerIconTemplate>
<DataTemplate DataType="Geometry">
<PathIcon Data="{Binding}" />
</DataTemplate>
</DrawerPage.DrawerIconTemplate>
<DrawerPage.DrawerHeader>
<Border x:Name="DrawerHeaderBorder" Padding="16">
<StackPanel Spacing="4">

8
samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml

@ -52,9 +52,13 @@
</DrawerPage.Resources>
<DrawerPage.DrawerIcon>
<PathIcon Width="22" Height="22"
Data="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" />
<StreamGeometry>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</StreamGeometry>
</DrawerPage.DrawerIcon>
<DrawerPage.DrawerIconTemplate>
<DataTemplate DataType="Geometry">
<PathIcon Width="22" Height="22" Data="{Binding}" />
</DataTemplate>
</DrawerPage.DrawerIconTemplate>
<DrawerPage.DrawerHeader>
<StackPanel Background="{StaticResource EcoDrawerBg}" Margin="0,0,0,8">

16
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 };
}
}
}

11
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)

8
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)

52
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<object?> DrawerIconProperty =
AvaloniaProperty.Register<DrawerPage, object?>(nameof(DrawerIcon));
/// <summary>
/// Defines the <see cref="DrawerIconTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate?> DrawerIconTemplateProperty =
AvaloniaProperty.Register<DrawerPage, IDataTemplate?>(nameof(DrawerIconTemplate));
private static readonly DefaultPageDataTemplate s_defaultPageDataTemplate = new DefaultPageDataTemplate();
/// <summary>
@ -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);
}
/// <summary>
/// Gets or sets the data template used to display the drawer icon.
/// </summary>
public IDataTemplate? DrawerIconTemplate
{
get => GetValue(DrawerIconTemplateProperty);
set => SetValue(DrawerIconTemplateProperty, value);
}
/// <summary>
/// Gets or sets the data template used to display <see cref="Drawer"/> content.
/// </summary>
@ -536,16 +545,11 @@ namespace Avalonia.Controls
_drawerPresenter = e.NameScope.Find<ContentPresenter>("PART_DrawerPresenter");
_drawerHeaderPresenter = e.NameScope.Find<ContentPresenter>("PART_DrawerHeader");
_drawerFooterPresenter = e.NameScope.Find<ContentPresenter>("PART_DrawerFooter");
_compactPaneIconPresenter = e.NameScope.Find<ContentPresenter>("PART_CompactPaneIconPresenter");
_paneIconPresenter = e.NameScope.Find<ContentPresenter>("PART_PaneIconPresenter");
_bottomPaneIconPresenter = e.NameScope.Find<ContentPresenter>("PART_BottomPaneIconPresenter");
_splitView = e.NameScope.Find<SplitView>("PART_SplitView");
_topBar = e.NameScope.Find<Border>("PART_TopBar");
_paneButton = e.NameScope.Find<ToggleButton>("PART_PaneButton");
_backdrop = e.NameScope.Find<Border>("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<Control> template => template.Build(),
Geometry g => new PathIcon { Data = g },
PathIcon pi => new PathIcon { Data = pi.Data },
DrawingImage { Drawing: GeometryDrawing { Geometry: { } gd } } => new PathIcon { Data = gd },
IImage image => new Image { Source = image },
_ => null
};
private void ApplyDrawerBackground()
{
if (_splitView == null)

16
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<object?> IconProperty =
AvaloniaProperty.Register<Page, object?>(nameof(Icon));
/// <summary>
/// Defines the <see cref="IconTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate?> IconTemplateProperty =
AvaloniaProperty.Register<Page, IDataTemplate?>(nameof(IconTemplate));
/// <summary>
/// Defines the <see cref="CurrentPage"/> property.
/// </summary>
@ -94,6 +101,15 @@ namespace Avalonia.Controls
set => SetValue(IconProperty, value);
}
/// <summary>
/// Gets or sets the data template used to display the icon.
/// </summary>
public IDataTemplate? IconTemplate
{
get => GetValue(IconTemplateProperty);
set => SetValue(IconTemplateProperty, value);
}
/// <summary>
/// Gets or sets the safe-area padding applied to this page's content.
/// </summary>

58
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
}
}
/// <summary>
/// Creates a visual control from a page icon value.
/// </summary>
internal static Control? CreateIconContent(object? icon)
{
if (icon is ITemplate<Control> template)
return template.Build();
Geometry? geometry = icon switch
{
Geometry g => g,
PathIcon pi => pi.Data,
DrawingImage { Drawing: GeometryDrawing { Geometry: { } gd } } => gd,
_ => 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;

6
src/Avalonia.Themes.Fluent/Controls/DrawerPage.xaml

@ -47,6 +47,8 @@
VerticalAlignment="Center"/>
<ContentPresenter
Name="PART_CompactPaneIconPresenter"
Content="{TemplateBinding DrawerIcon}"
ContentTemplate="{TemplateBinding DrawerIconTemplate}"
IsVisible="{Binding DrawerIcon, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static ObjectConverters.IsNotNull}}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
@ -101,6 +103,8 @@
VerticalAlignment="Center"/>
<ContentPresenter
Name="PART_PaneIconPresenter"
Content="{TemplateBinding DrawerIcon}"
ContentTemplate="{TemplateBinding DrawerIconTemplate}"
IsVisible="{Binding DrawerIcon, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static ObjectConverters.IsNotNull}}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
@ -146,6 +150,8 @@
VerticalAlignment="Center"/>
<ContentPresenter
Name="PART_BottomPaneIconPresenter"
Content="{TemplateBinding DrawerIcon}"
ContentTemplate="{TemplateBinding DrawerIconTemplate}"
IsVisible="{Binding DrawerIcon, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static ObjectConverters.IsNotNull}}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>

6
src/Avalonia.Themes.Simple/Controls/DrawerPage.xaml

@ -46,6 +46,8 @@
VerticalAlignment="Center"/>
<ContentPresenter
Name="PART_CompactPaneIconPresenter"
Content="{TemplateBinding DrawerIcon}"
ContentTemplate="{TemplateBinding DrawerIconTemplate}"
IsVisible="{Binding DrawerIcon, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static ObjectConverters.IsNotNull}}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
@ -92,6 +94,8 @@
VerticalAlignment="Center"/>
<ContentPresenter
Name="PART_PaneIconPresenter"
Content="{TemplateBinding DrawerIcon}"
ContentTemplate="{TemplateBinding DrawerIconTemplate}"
IsVisible="{Binding DrawerIcon, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static ObjectConverters.IsNotNull}}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
@ -129,6 +133,8 @@
VerticalAlignment="Center"/>
<ContentPresenter
Name="PART_BottomPaneIconPresenter"
Content="{TemplateBinding DrawerIcon}"
ContentTemplate="{TemplateBinding DrawerIconTemplate}"
IsVisible="{Binding DrawerIcon, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static ObjectConverters.IsNotNull}}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>

111
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<PathIcon>(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<PathIcon>(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<PathIcon>(result);
Assert.Same(geometry, ((PathIcon)result!).Data);
}
[Fact]
public void Image_ReturnsImage()
{
var image = new TestImage();
var result = DrawerPage.CreateIconContent(image);
Assert.IsType<Image>(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<Control>(() => new Border());
var result = DrawerPage.CreateIconContent(template);
Assert.IsType<Border>(result);
}
[Fact]
public void Template_BuildsSeparateInstances()
{
var template = new FuncTemplate<Control>(() => new Border());
var first = DrawerPage.CreateIconContent(template);
var second = DrawerPage.CreateIconContent(template);
Assert.NotSame(first, second);
var template = new FuncDataTemplate<object>((_, _) => 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<object>((_, _) => 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);
}
}

89
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -955,6 +955,30 @@ namespace Avalonia.Controls.UnitTests
}.RegisterInNameScope(scope));
}
private static IControlTemplate TabItemWithIconTemplate()
{
return new FuncControlTemplate<TabItem>((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<object>((val, _) =>
new TextBlock { Text = (string)val }),
Template = TabItemWithIconTemplate(),
};
var root = new TestRoot { Child = tabItem };
tabItem.ApplyTemplate();
tabItem.Presenter!.UpdateChild();
var iconPresenter = tabItem.GetTemplateChildren().OfType<ContentPresenter>().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<ContentPresenter>().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<ContentPresenter>().First(x => x.Name == "PART_IconPresenter");
Assert.Equal("first", iconPresenter!.Content);
tabItem.Icon = "second";
Assert.Equal("second", iconPresenter.Content);
}
[Fact]

108
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<Path>(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<Path>(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<object>((_, _) => 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<Path>(result);
Assert.Same(geometry, ((Path)result!).Data);
var template = new FuncDataTemplate<object>((_, _) => 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<Image>(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<Control>(() => new Border());
var result = TabbedPage.CreateIconContent(template);
Assert.IsType<Border>(result);
}
[Fact]
public void Template_BuildsSeparateInstances()
{
var template = new FuncTemplate<Control>(() => 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<object>((_, _) => 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);
}
}

Loading…
Cancel
Save