diff --git a/README.md b/README.md index 92180ca76d..38790aa5cc 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,14 @@ Install-Package Svg.Controls.Skia.Avalonia ``` +#### Background + +```XAML + +``` + ### CSS styling ```XAML @@ -265,6 +273,29 @@ Install-Package Svg.Controls.Skia.Avalonia ``` +#### SvgBrush Markup Extension + +Use `SvgBrush` when you want to place an SVG-backed `VisualBrush` in a resource dictionary and reuse it for backgrounds or other brush targets: + +```XAML + + + /Assets/__tiger.svg + + + + +``` + +The optional properties mirror those on `VisualBrush`, so you can tweak layout, tiling, opacity, and transforms directly in XAML while the control takes care of loading and rendering the SVG content. + #### Avalonia Previewer To make controls work with `Avalonia Previewer` please add the following lines to `BuildAvaloniaApp()` method: diff --git a/Svg.Skia.sln b/Svg.Skia.sln index 25d26c8a86..b21e3509c7 100644 --- a/Svg.Skia.sln +++ b/Svg.Skia.sln @@ -77,6 +77,8 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svg.Custom", "src\Svg.Custom\Svg.Custom.csproj", "{CFA46E73-0050-4C57-85CE-6C5868A2483C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svg.Controls.Skia.Avalonia.UnitTests", "tests\Svg.Controls.Skia.Avalonia.UnitTests\Svg.Controls.Skia.Avalonia.UnitTests.csproj", "{D4467DCA-494D-4C32-9525-4A9713221A53}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svg.Controls.Avalonia.UnitTests", "tests\Svg.Controls.Avalonia.UnitTests\Svg.Controls.Avalonia.UnitTests.csproj", "{55A5979A-292D-4A51-A89E-14FCEF5E6792}" +EndProject EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svg.Model", "src\Svg.Model\Svg.Model.csproj", "{4C970B2C-6C96-445B-B80B-4EFBF803FD5F}" EndProject @@ -157,6 +159,10 @@ Global {D4467DCA-494D-4C32-9525-4A9713221A53}.Debug|Any CPU.Build.0 = Debug|Any CPU {D4467DCA-494D-4C32-9525-4A9713221A53}.Release|Any CPU.ActiveCfg = Release|Any CPU {D4467DCA-494D-4C32-9525-4A9713221A53}.Release|Any CPU.Build.0 = Release|Any CPU + {55A5979A-292D-4A51-A89E-14FCEF5E6792}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55A5979A-292D-4A51-A89E-14FCEF5E6792}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55A5979A-292D-4A51-A89E-14FCEF5E6792}.Release|Any CPU.ActiveCfg = Release|Any CPU + {55A5979A-292D-4A51-A89E-14FCEF5E6792}.Release|Any CPU.Build.0 = Release|Any CPU {4C970B2C-6C96-445B-B80B-4EFBF803FD5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4C970B2C-6C96-445B-B80B-4EFBF803FD5F}.Debug|Any CPU.Build.0 = Debug|Any CPU {4C970B2C-6C96-445B-B80B-4EFBF803FD5F}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -241,6 +247,7 @@ Global {81724F00-B7C3-4E25-B473-C7433BABDC81} = {B65D5B3A-77BE-4AFF-B502-A136B9C932F8} {CFA46E73-0050-4C57-85CE-6C5868A2483C} = {C5FFCF4B-86DC-453E-8006-44EE9EEFEE39} {D4467DCA-494D-4C32-9525-4A9713221A53} = {7863AE7D-FF68-45BF-BA68-6FA0E5604CB7} + {55A5979A-292D-4A51-A89E-14FCEF5E6792} = {7863AE7D-FF68-45BF-BA68-6FA0E5604CB7} {4C970B2C-6C96-445B-B80B-4EFBF803FD5F} = {4C42912C-9F8C-43D9-A4B5-4427F7EC8F18} {29F59C87-EAE6-4DD3-8666-B79BFAF6B34D} = {4C42912C-9F8C-43D9-A4B5-4427F7EC8F18} {223B7A5A-E263-4D40-9A6E-FE31EAE92F45} = {4C42912C-9F8C-43D9-A4B5-4427F7EC8F18} diff --git a/samples/AvaloniaSvgSample/MainWindow.axaml b/samples/AvaloniaSvgSample/MainWindow.axaml index 0ad439b3c2..bd01435bc7 100644 --- a/samples/AvaloniaSvgSample/MainWindow.axaml +++ b/samples/AvaloniaSvgSample/MainWindow.axaml @@ -105,6 +105,23 @@ + + + + + + + + + /Assets/__tiger.svg + + + + + + + + + + + + + + + /Assets/__tiger.svg + + + + + + +/// Provides an SVG-backed brush that can be declared in XAML resources. +/// +public class SvgBrush : MarkupExtension +{ + /// + /// Gets or sets the SVG resource or file path. + /// + [Content] + public string? Path { get; set; } + + /// + /// Gets or sets the stretch applied to the resulting brush. + /// + public Stretch? Stretch { get; set; } + + /// + /// Gets or sets the horizontal alignment applied to the resulting brush. + /// + public AlignmentX? AlignmentX { get; set; } + + /// + /// Gets or sets the vertical alignment applied to the resulting brush. + /// + public AlignmentY? AlignmentY { get; set; } + + /// + /// Gets or sets the tile mode applied to the resulting brush. + /// + public TileMode? TileMode { get; set; } + + /// + /// Gets or sets the destination rectangle applied to the resulting brush. + /// + public RelativeRect? DestinationRect { get; set; } + + /// + /// Gets or sets the source rectangle applied to the resulting brush. + /// + public RelativeRect? SourceRect { get; set; } + + /// + /// Gets or sets the opacity applied to the resulting brush. + /// + public double? Opacity { get; set; } + + /// + /// Gets or sets the transform applied to the resulting brush. + /// + public Transform? Transform { get; set; } + + /// + /// Gets or sets the transform origin applied to the resulting brush. + /// + public RelativePoint? TransformOrigin { get; set; } + + /// + /// Creates a configured with the supplied image and optional overrides. + /// + /// The SVG image instance rendered by the brush. + /// Optional stretch applied to the brush. + /// Optional horizontal alignment applied to the brush. + /// Optional vertical alignment applied to the brush. + /// Optional tile mode applied to the brush. + /// Optional destination rectangle for the brush content. + /// Optional source rectangle cropping the brush content. + /// Optional opacity multiplier applied to the brush. + /// Optional transform applied to the brush. + /// Optional transform origin applied when is set. + /// A that renders . + internal static IBrush CreateFromImage( + IImage image, + Stretch? stretch = null, + AlignmentX? alignmentX = null, + AlignmentY? alignmentY = null, + TileMode? tileMode = null, + RelativeRect? destinationRect = null, + RelativeRect? sourceRect = null, + double? opacity = null, + Transform? transform = null, + RelativePoint? transformOrigin = null) + { + var brush = new VisualBrush + { + Visual = new Image + { + Source = image + } + }; + + if (stretch.HasValue) + { + brush.Stretch = stretch.Value; + } + + if (alignmentX.HasValue) + { + brush.AlignmentX = alignmentX.Value; + } + + if (alignmentY.HasValue) + { + brush.AlignmentY = alignmentY.Value; + } + + if (tileMode.HasValue) + { + brush.TileMode = tileMode.Value; + } + + if (destinationRect.HasValue) + { + brush.DestinationRect = destinationRect.Value; + } + + if (sourceRect.HasValue) + { + brush.SourceRect = sourceRect.Value; + } + + if (opacity.HasValue) + { + brush.Opacity = opacity.Value; + } + + if (transform is not null) + { + brush.Transform = transform; + } + + if (transformOrigin.HasValue) + { + brush.TransformOrigin = transformOrigin.Value; + } + + return brush; + } + + /// + public override object ProvideValue(IServiceProvider serviceProvider) + { + if (serviceProvider is null) + { + throw new ArgumentNullException(nameof(serviceProvider)); + } + + if (Path is null) + { + throw new InvalidOperationException("SvgBrush requires a non-null Path."); + } + + var baseUri = serviceProvider.GetContextBaseUri(); + var source = SvgSource.Load(Path, baseUri); + var image = new SvgImage + { + Source = source + }; + + return CreateFromImage( + image, + Stretch, + AlignmentX, + AlignmentY, + TileMode, + DestinationRect, + SourceRect, + Opacity, + Transform, + TransformOrigin); + } +} diff --git a/src/Svg.Controls.Avalonia/SvgImageExtension.cs b/src/Svg.Controls.Avalonia/SvgImageExtension.cs index b0f30401c7..72acdea6a1 100644 --- a/src/Svg.Controls.Avalonia/SvgImageExtension.cs +++ b/src/Svg.Controls.Avalonia/SvgImageExtension.cs @@ -31,14 +31,23 @@ public override object ProvideValue(IServiceProvider serviceProvider) var baseUri = context.BaseUri; var source = SvgSource.Load(path, baseUri); var target = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget))!; - if (target.TargetProperty is AvaloniaProperty property) + var image = new SvgImage { Source = source }; + + if (target.TargetProperty is not AvaloniaProperty property) + { + return image; + } + + if (typeof(IImage).IsAssignableFrom(property.PropertyType)) { - if (property.PropertyType == typeof(IImage)) - { - return new SvgImage { Source = source }; - } - return new Image { Source = new SvgImage { Source = source } }; + return image; } - return new SvgImage { Source = source }; + + if (typeof(IBrush).IsAssignableFrom(property.PropertyType)) + { + return SvgBrush.CreateFromImage(image); + } + + return new Image { Source = image }; } } diff --git a/src/Svg.Controls.Skia.Avalonia/SvgBrush.cs b/src/Svg.Controls.Skia.Avalonia/SvgBrush.cs new file mode 100644 index 0000000000..8e5b7426e2 --- /dev/null +++ b/src/Svg.Controls.Skia.Avalonia/SvgBrush.cs @@ -0,0 +1,218 @@ +// Copyright (c) Wiesław Šoltés. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. +using System; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using Avalonia.Metadata; +using Svg.Model; + +namespace Avalonia.Svg.Skia; + +/// +/// Provides an SVG-backed brush that can be declared in XAML resources. +/// +public class SvgBrush : MarkupExtension +{ + /// + /// Gets or sets the SVG resource or file path. + /// + [Content] + public string? Path { get; set; } + + /// + /// Gets or sets the CSS applied when loading the SVG resource. + /// + public string? Css { get; set; } + + /// + /// Gets or sets the current CSS applied when loading the SVG resource. + /// + public string? CurrentCss { get; set; } + + /// + /// Gets or sets the stretch applied to the resulting brush. + /// + public Stretch? Stretch { get; set; } + + /// + /// Gets or sets the horizontal alignment applied to the resulting brush. + /// + public AlignmentX? AlignmentX { get; set; } + + /// + /// Gets or sets the vertical alignment applied to the resulting brush. + /// + public AlignmentY? AlignmentY { get; set; } + + /// + /// Gets or sets the tile mode applied to the resulting brush. + /// + public TileMode? TileMode { get; set; } + + /// + /// Gets or sets the destination rectangle applied to the resulting brush. + /// + public RelativeRect? DestinationRect { get; set; } + + /// + /// Gets or sets the source rectangle applied to the resulting brush. + /// + public RelativeRect? SourceRect { get; set; } + + /// + /// Gets or sets the opacity applied to the resulting brush. + /// + public double? Opacity { get; set; } + + /// + /// Gets or sets the transform applied to the resulting brush. + /// + public Transform? Transform { get; set; } + + /// + /// Gets or sets the transform origin applied to the resulting brush. + /// + public RelativePoint? TransformOrigin { get; set; } + + /// + /// Creates a configured with the provided image and optional overrides. + /// + /// The SVG image instance rendered by the brush. + /// Optional stretch applied to the brush. + /// Optional horizontal alignment applied to the brush. + /// Optional vertical alignment applied to the brush. + /// Optional tile mode applied to the brush. + /// Optional destination rectangle for the brush content. + /// Optional source rectangle cropping the brush content. + /// Optional opacity multiplier applied to the brush. + /// Optional transform applied to the brush. + /// Optional transform origin applied when is set. + /// A that renders . + internal static IBrush CreateFromImage( + IImage image, + Stretch? stretch = null, + AlignmentX? alignmentX = null, + AlignmentY? alignmentY = null, + TileMode? tileMode = null, + RelativeRect? destinationRect = null, + RelativeRect? sourceRect = null, + double? opacity = null, + Transform? transform = null, + RelativePoint? transformOrigin = null) + { + var brush = new VisualBrush + { + Visual = new Image + { + Source = image + } + }; + + if (stretch.HasValue) + { + brush.Stretch = stretch.Value; + } + + if (alignmentX.HasValue) + { + brush.AlignmentX = alignmentX.Value; + } + + if (alignmentY.HasValue) + { + brush.AlignmentY = alignmentY.Value; + } + + if (tileMode.HasValue) + { + brush.TileMode = tileMode.Value; + } + + if (destinationRect.HasValue) + { + brush.DestinationRect = destinationRect.Value; + } + + if (sourceRect.HasValue) + { + brush.SourceRect = sourceRect.Value; + } + + if (opacity.HasValue) + { + brush.Opacity = opacity.Value; + } + + if (transform is not null) + { + brush.Transform = transform; + } + + if (transformOrigin.HasValue) + { + brush.TransformOrigin = transformOrigin.Value; + } + + return brush; + } + + /// + public override object ProvideValue(IServiceProvider serviceProvider) + { + if (serviceProvider is null) + { + throw new ArgumentNullException(nameof(serviceProvider)); + } + + if (Path is null) + { + throw new InvalidOperationException("SvgBrush requires a non-null Path."); + } + + var baseUri = serviceProvider.GetContextBaseUri(); + var parameters = CreateParameters(Css, CurrentCss); + var source = SvgSource.Load(Path, baseUri, parameters); + var image = new SvgImage + { + Source = source, + Css = Css, + CurrentCss = CurrentCss + }; + + return CreateFromImage( + image, + Stretch, + AlignmentX, + AlignmentY, + TileMode, + DestinationRect, + SourceRect, + Opacity, + Transform, + TransformOrigin); + } + + private static SvgParameters? CreateParameters(string? css, string? currentCss) + { + var combined = CombineCss(css, currentCss); + return string.IsNullOrWhiteSpace(combined) + ? null + : new SvgParameters(null, combined); + } + + private static string? CombineCss(string? css, string? currentCss) + { + if (string.IsNullOrWhiteSpace(css)) + { + return string.IsNullOrWhiteSpace(currentCss) ? null : currentCss; + } + + if (string.IsNullOrWhiteSpace(currentCss)) + { + return css; + } + + return string.Concat(css, ' ', currentCss); + } +} diff --git a/src/Svg.Controls.Skia.Avalonia/SvgImageExtension.cs b/src/Svg.Controls.Skia.Avalonia/SvgImageExtension.cs index 9d3d638324..00ddfa6e76 100644 --- a/src/Svg.Controls.Skia.Avalonia/SvgImageExtension.cs +++ b/src/Svg.Controls.Skia.Avalonia/SvgImageExtension.cs @@ -39,11 +39,16 @@ public override object ProvideValue(IServiceProvider serviceProvider) return image; } - if (property.PropertyType == typeof(IImage)) + if (typeof(IImage).IsAssignableFrom(property.PropertyType)) { return image; } + if (typeof(IBrush).IsAssignableFrom(property.PropertyType)) + { + return SvgBrush.CreateFromImage(image); + } + return new Image { Source = image }; } diff --git a/tests/Svg.Controls.Avalonia.UnitTests/Assets/Icon.svg b/tests/Svg.Controls.Avalonia.UnitTests/Assets/Icon.svg new file mode 100644 index 0000000000..382d97f02f --- /dev/null +++ b/tests/Svg.Controls.Avalonia.UnitTests/Assets/Icon.svg @@ -0,0 +1,31 @@ + + SVG Logo + + + + + + + + + + + + + + + + + + + + + + + SVG + + + + + + diff --git a/tests/Svg.Controls.Avalonia.UnitTests/Svg.Controls.Avalonia.UnitTests.csproj b/tests/Svg.Controls.Avalonia.UnitTests/Svg.Controls.Avalonia.UnitTests.csproj new file mode 100644 index 0000000000..d9696b5e74 --- /dev/null +++ b/tests/Svg.Controls.Avalonia.UnitTests/Svg.Controls.Avalonia.UnitTests.csproj @@ -0,0 +1,27 @@ + + + + net9.0 + Library + False + enable + Avalonia.Svg.UnitTests + + + + + + + + + + + + + + + + + + + diff --git a/tests/Svg.Controls.Avalonia.UnitTests/SvgImageTests.cs b/tests/Svg.Controls.Avalonia.UnitTests/SvgImageTests.cs new file mode 100644 index 0000000000..81f221d279 --- /dev/null +++ b/tests/Svg.Controls.Avalonia.UnitTests/SvgImageTests.cs @@ -0,0 +1,56 @@ +using Avalonia; +using Avalonia.Headless.XUnit; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Svg; +using Avalonia.Svg.UnitTests.Views; +using Xunit; + +namespace Avalonia.Svg.UnitTests; + +public class SvgImageTests +{ + [AvaloniaFact] + public void SvgImageExtension_Returns_VisualBrush_For_Brush_Property() + { + var view = new SvgImageBackgroundView(); + var host = Assert.IsType(view.BackgroundHost); + + var brush = Assert.IsType(host.Background); + var image = Assert.IsType(brush.Visual); + var svgImage = Assert.IsType(image.Source); + Assert.NotNull(svgImage.Source); + } + + [AvaloniaFact] + public void SvgBrushResource_Returns_VisualBrush() + { + var view = new SvgBrushBackgroundView(); + var host = Assert.IsType(view.BackgroundHost); + + var brush = Assert.IsType(host.Background); + var image = Assert.IsType(brush.Visual); + var svgImage = Assert.IsType(image.Source); + Assert.NotNull(svgImage.Source); + Assert.Equal(Stretch.UniformToFill, brush.Stretch); + Assert.Equal(AlignmentX.Center, brush.AlignmentX); + Assert.Equal(AlignmentY.Bottom, brush.AlignmentY); + Assert.Equal(TileMode.Tile, brush.TileMode); + Assert.Equal(RelativeUnit.Absolute, brush.DestinationRect.Unit); + Assert.Equal(RelativeUnit.Absolute, brush.SourceRect.Unit); + Assert.Equal(0.1, brush.DestinationRect.Rect.X, 6); + Assert.Equal(0.2, brush.DestinationRect.Rect.Y, 6); + Assert.Equal(0.6, brush.DestinationRect.Rect.Width, 6); + Assert.Equal(0.7, brush.DestinationRect.Rect.Height, 6); + Assert.Equal(0.05, brush.SourceRect.Rect.X, 6); + Assert.Equal(0.05, brush.SourceRect.Rect.Y, 6); + Assert.Equal(0.9, brush.SourceRect.Rect.Width, 6); + Assert.Equal(0.9, brush.SourceRect.Rect.Height, 6); + Assert.Equal(0.5, brush.Opacity); + var matrixTransform = Assert.IsType(brush.Transform); + Assert.Equal(new Matrix(1, 0, 0, 1, 10, 20), matrixTransform.Matrix); + Assert.Equal(RelativeUnit.Absolute, brush.TransformOrigin.Unit); + Assert.Equal(0.25, brush.TransformOrigin.Point.X, 6); + Assert.Equal(0.75, brush.TransformOrigin.Point.Y, 6); + } +} diff --git a/tests/Svg.Controls.Avalonia.UnitTests/TestApplication.cs b/tests/Svg.Controls.Avalonia.UnitTests/TestApplication.cs new file mode 100644 index 0000000000..cc86a11e7d --- /dev/null +++ b/tests/Svg.Controls.Avalonia.UnitTests/TestApplication.cs @@ -0,0 +1,24 @@ +using Avalonia; +using Avalonia.Headless; +using Avalonia.Platform; + +[assembly: Avalonia.Headless.AvaloniaTestApplication(typeof(Avalonia.Svg.UnitTests.SvgControlsAvaloniaTestsAppBuilder))] + +namespace Avalonia.Svg.UnitTests; + +internal static class SvgControlsAvaloniaTestsAppBuilder +{ + public static AppBuilder BuildAvaloniaApp() => + AppBuilder.Configure() + .UseHeadless(new AvaloniaHeadlessPlatformOptions()) + .LogToTrace(); +} + +internal sealed class SvgControlsAvaloniaTestsApp : Application +{ + public override void OnFrameworkInitializationCompleted() + { + AssetLoader.SetDefaultAssembly(typeof(SvgControlsAvaloniaTestsApp).Assembly); + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/tests/Svg.Controls.Avalonia.UnitTests/Views/SvgBrushBackgroundView.axaml b/tests/Svg.Controls.Avalonia.UnitTests/Views/SvgBrushBackgroundView.axaml new file mode 100644 index 0000000000..e9405f0acb --- /dev/null +++ b/tests/Svg.Controls.Avalonia.UnitTests/Views/SvgBrushBackgroundView.axaml @@ -0,0 +1,19 @@ + + + /Assets/Icon.svg + + + diff --git a/tests/Svg.Controls.Avalonia.UnitTests/Views/SvgBrushBackgroundView.axaml.cs b/tests/Svg.Controls.Avalonia.UnitTests/Views/SvgBrushBackgroundView.axaml.cs new file mode 100644 index 0000000000..8b8e214b28 --- /dev/null +++ b/tests/Svg.Controls.Avalonia.UnitTests/Views/SvgBrushBackgroundView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Svg.UnitTests.Views; + +public partial class SvgBrushBackgroundView : UserControl +{ + public SvgBrushBackgroundView() + { + InitializeComponent(); + } +} diff --git a/tests/Svg.Controls.Avalonia.UnitTests/Views/SvgImageBackgroundView.axaml b/tests/Svg.Controls.Avalonia.UnitTests/Views/SvgImageBackgroundView.axaml new file mode 100644 index 0000000000..644fc099bd --- /dev/null +++ b/tests/Svg.Controls.Avalonia.UnitTests/Views/SvgImageBackgroundView.axaml @@ -0,0 +1,7 @@ + + + diff --git a/tests/Svg.Controls.Avalonia.UnitTests/Views/SvgImageBackgroundView.axaml.cs b/tests/Svg.Controls.Avalonia.UnitTests/Views/SvgImageBackgroundView.axaml.cs new file mode 100644 index 0000000000..8cd6ea7335 --- /dev/null +++ b/tests/Svg.Controls.Avalonia.UnitTests/Views/SvgImageBackgroundView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Svg.UnitTests.Views; + +public partial class SvgImageBackgroundView : UserControl +{ + public SvgImageBackgroundView() + { + InitializeComponent(); + } +} diff --git a/tests/Svg.Controls.Skia.Avalonia.UnitTests/Svg.Controls.Skia.Avalonia.UnitTests.csproj b/tests/Svg.Controls.Skia.Avalonia.UnitTests/Svg.Controls.Skia.Avalonia.UnitTests.csproj index 14dfcc9ff9..307b76f3ac 100644 --- a/tests/Svg.Controls.Skia.Avalonia.UnitTests/Svg.Controls.Skia.Avalonia.UnitTests.csproj +++ b/tests/Svg.Controls.Skia.Avalonia.UnitTests/Svg.Controls.Skia.Avalonia.UnitTests.csproj @@ -23,4 +23,8 @@ + + + + diff --git a/tests/Svg.Controls.Skia.Avalonia.UnitTests/SvgImageTests.cs b/tests/Svg.Controls.Skia.Avalonia.UnitTests/SvgImageTests.cs index 4491ede805..735fa3150f 100644 --- a/tests/Svg.Controls.Skia.Avalonia.UnitTests/SvgImageTests.cs +++ b/tests/Svg.Controls.Skia.Avalonia.UnitTests/SvgImageTests.cs @@ -1,24 +1,66 @@ -using System; -using Avalonia.Platform; +using Avalonia; +using Avalonia.Headless.XUnit; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Svg.Skia; +using Avalonia.Svg.Skia.UnitTests.Views; using Xunit; namespace Avalonia.Svg.Skia.UnitTests; public class SvgImageTests { - [Fact] + [AvaloniaFact] public void SvgImage_Load() { - var uri = new Uri($"avares://Svg.Controls.Skia.Avalonia.UnitTests/Assets/Icon.svg"); - var assetLoader = new StandardAssetLoader(); // AvaloniaLocator.Current.GetService() - - var svgFile = assetLoader.Open(uri); - Assert.NotNull(svgFile); - - var svgSource = SvgSource.LoadFromStream(svgFile); + var uri = new System.Uri($"avares://{typeof(SvgImageTests).Assembly.GetName().Name}/Assets/Icon.svg"); + using var stream = Avalonia.Platform.AssetLoader.Open(uri); + var svgSource = SvgSource.LoadFromStream(stream); Assert.NotNull(svgSource); var svgImage = new SvgImage() { Source = svgSource }; Assert.NotNull(svgImage); } + + [AvaloniaFact] + public void SvgImageExtension_Returns_VisualBrush_For_Brush_Property() + { + var view = new SvgImageBackgroundView(); + var host = Assert.IsType(view.BackgroundHost); + var brush = Assert.IsType(host.Background); + var image = Assert.IsType(brush.Visual); + var svgImage = Assert.IsType(image.Source); + Assert.NotNull(svgImage.Source); + } + + [AvaloniaFact] + public void SvgBrushResource_Returns_VisualBrush() + { + var view = new SvgBrushBackgroundView(); + var host = Assert.IsType(view.BackgroundHost); + var brush = Assert.IsType(host.Background); + var image = Assert.IsType(brush.Visual); + var svgImage = Assert.IsType(image.Source); + Assert.NotNull(svgImage.Source); + Assert.Equal(Stretch.UniformToFill, brush.Stretch); + Assert.Equal(AlignmentX.Center, brush.AlignmentX); + Assert.Equal(AlignmentY.Bottom, brush.AlignmentY); + Assert.Equal(TileMode.Tile, brush.TileMode); + Assert.Equal(RelativeUnit.Absolute, brush.DestinationRect.Unit); + Assert.Equal(RelativeUnit.Absolute, brush.SourceRect.Unit); + Assert.Equal(0.1, brush.DestinationRect.Rect.X, 6); + Assert.Equal(0.2, brush.DestinationRect.Rect.Y, 6); + Assert.Equal(0.6, brush.DestinationRect.Rect.Width, 6); + Assert.Equal(0.7, brush.DestinationRect.Rect.Height, 6); + Assert.Equal(0.05, brush.SourceRect.Rect.X, 6); + Assert.Equal(0.05, brush.SourceRect.Rect.Y, 6); + Assert.Equal(0.9, brush.SourceRect.Rect.Width, 6); + Assert.Equal(0.9, brush.SourceRect.Rect.Height, 6); + Assert.Equal(0.5, brush.Opacity); + var matrixTransform = Assert.IsType(brush.Transform); + Assert.Equal(new Matrix(1, 0, 0, 1, 10, 20), matrixTransform.Matrix); + Assert.Equal(RelativeUnit.Absolute, brush.TransformOrigin.Unit); + Assert.Equal(0.25, brush.TransformOrigin.Point.X, 6); + Assert.Equal(0.75, brush.TransformOrigin.Point.Y, 6); + } } diff --git a/tests/Svg.Controls.Skia.Avalonia.UnitTests/TestApplication.cs b/tests/Svg.Controls.Skia.Avalonia.UnitTests/TestApplication.cs new file mode 100644 index 0000000000..e5fc6fe66b --- /dev/null +++ b/tests/Svg.Controls.Skia.Avalonia.UnitTests/TestApplication.cs @@ -0,0 +1,24 @@ +using Avalonia; +using Avalonia.Headless; +using Avalonia.Platform; + +[assembly: Avalonia.Headless.AvaloniaTestApplication(typeof(Avalonia.Svg.Skia.UnitTests.SvgControlsSkiaAvaloniaTestsAppBuilder))] + +namespace Avalonia.Svg.Skia.UnitTests; + +internal static class SvgControlsSkiaAvaloniaTestsAppBuilder +{ + public static AppBuilder BuildAvaloniaApp() => + AppBuilder.Configure() + .UseHeadless(new AvaloniaHeadlessPlatformOptions()) + .LogToTrace(); +} + +internal sealed class SvgControlsSkiaAvaloniaTestsApp : Application +{ + public override void OnFrameworkInitializationCompleted() + { + AssetLoader.SetDefaultAssembly(typeof(SvgControlsSkiaAvaloniaTestsApp).Assembly); + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/tests/Svg.Controls.Skia.Avalonia.UnitTests/Views/SvgBrushBackgroundView.axaml b/tests/Svg.Controls.Skia.Avalonia.UnitTests/Views/SvgBrushBackgroundView.axaml new file mode 100644 index 0000000000..10d1785ec9 --- /dev/null +++ b/tests/Svg.Controls.Skia.Avalonia.UnitTests/Views/SvgBrushBackgroundView.axaml @@ -0,0 +1,19 @@ + + + /Assets/Icon.svg + + + diff --git a/tests/Svg.Controls.Skia.Avalonia.UnitTests/Views/SvgBrushBackgroundView.axaml.cs b/tests/Svg.Controls.Skia.Avalonia.UnitTests/Views/SvgBrushBackgroundView.axaml.cs new file mode 100644 index 0000000000..1585f2935b --- /dev/null +++ b/tests/Svg.Controls.Skia.Avalonia.UnitTests/Views/SvgBrushBackgroundView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Svg.Skia.UnitTests.Views; + +public partial class SvgBrushBackgroundView : UserControl +{ + public SvgBrushBackgroundView() + { + InitializeComponent(); + } +} diff --git a/tests/Svg.Controls.Skia.Avalonia.UnitTests/Views/SvgImageBackgroundView.axaml b/tests/Svg.Controls.Skia.Avalonia.UnitTests/Views/SvgImageBackgroundView.axaml new file mode 100644 index 0000000000..37d9de1c3b --- /dev/null +++ b/tests/Svg.Controls.Skia.Avalonia.UnitTests/Views/SvgImageBackgroundView.axaml @@ -0,0 +1,7 @@ + + + diff --git a/tests/Svg.Controls.Skia.Avalonia.UnitTests/Views/SvgImageBackgroundView.axaml.cs b/tests/Svg.Controls.Skia.Avalonia.UnitTests/Views/SvgImageBackgroundView.axaml.cs new file mode 100644 index 0000000000..58f6340acf --- /dev/null +++ b/tests/Svg.Controls.Skia.Avalonia.UnitTests/Views/SvgImageBackgroundView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Svg.Skia.UnitTests.Views; + +public partial class SvgImageBackgroundView : UserControl +{ + public SvgImageBackgroundView() + { + InitializeComponent(); + } +}