Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/Controls/tests/Core.UnitTests/ImageBrushTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Microsoft.Maui.Graphics;
using Xunit;

namespace Microsoft.Maui.Controls.Core.UnitTests
{
public class ImageBrushTests : BaseTestFixture
{
[Fact]
public void ImageBrushConvertsToImageSourcePaint()
{
var imageSource = ImageSource.FromFile("background.png");

Paint paint = new ImageBrush { ImageSource = imageSource };

var imagePaint = Assert.IsAssignableFrom<IImageSourcePaint>(paint);
Assert.Same(imageSource, imagePaint.ImageSource);
}

[Fact]
public void ImageSourcePaintConvertsBackToImageBrush()
{
var imageSource = ImageSource.FromFile("background.png");

Paint paint = new ImageBrush { ImageSource = imageSource };
Brush brush = (Brush)paint;

var imageBrush = Assert.IsType<ImageBrush>(brush);
Assert.Same(imageSource, imageBrush.ImageSource);
}

[Fact]
public void SolidAndGradientPaintsAreNotImageSourcePaints()
{
Assert.IsNotAssignableFrom<IImageSourcePaint>((Paint)new SolidColorBrush(Colors.Red));
Assert.IsNotAssignableFrom<IImageSourcePaint>((Paint)new LinearGradientBrush());
Assert.IsNotAssignableFrom<IImageSourcePaint>((Paint)new RadialGradientBrush());
}
}
}
68 changes: 68 additions & 0 deletions src/Core/src/ImageSources/IImageSourcePaint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#nullable enable
using Microsoft.Maui.Graphics;

namespace Microsoft.Maui
{
/// <summary>
/// Exposes the <see cref="IImageSource"/> of a <see cref="Paint"/> that .NET MAUI uses to fill an area
/// with an image.
/// </summary>
/// <remarks>
/// <para>
/// This is a <b>consumption-only</b> contract. It exists so that code receiving a <see cref="Paint"/> -
/// most commonly a platform backend handling <see cref="IView.Background"/> - can recognize an image
/// background and read its image source without reflection.
/// </para>
/// <para>
/// <b>Implementing this interface outside of .NET MAUI is not supported.</b> .NET MAUI reserves the right
/// to add members to this interface in future releases, which would be a breaking change for external
/// implementers. Only paints created by .NET MAUI are guaranteed to be recognized and rendered by the
/// built-in handlers; a custom <see cref="Paint"/> implementing this interface is not guaranteed to be
/// honored, because not every built-in handler routes backgrounds through the image-source path.
/// </para>
/// <para>
/// Values implementing this interface are obtained by pattern matching an existing <see cref="Paint"/>,
/// typically from <see cref="IView.Background"/>. In .NET MAUI a paint of this kind is produced when a
/// background is set from an image - for example a <c>Microsoft.Maui.Controls.ImageBrush</c>, or
/// <c>Page.BackgroundImageSource</c>.
/// </para>
/// <para>
/// <see cref="ImageSource"/> can be <see langword="null"/>, which represents an image background with
/// nothing to draw. Treat it the same as having no image background: clear any previously applied image
/// rather than attempting to resolve it. A non-<see langword="null"/> value should be resolved through an
/// <see cref="IImageSourceServiceProvider"/>; note that resolution is asynchronous and may still yield no
/// image.
/// </para>
/// <para>
/// This is distinct from <see cref="ImagePaint"/>. <see cref="ImagePaint"/> carries an already-loaded
/// <see cref="IImage"/> for drawing operations, whereas this contract carries an unresolved
/// <see cref="IImageSource"/> that describes where an image comes from (a file, URI, stream, or font glyph)
/// and must be loaded through an image source service. A paint will not implement both.
/// </para>
/// <example>
/// The following example shows how a platform backend can render an image background:
/// <code language="csharp"><![CDATA[
/// public static void MapBackground(IViewHandler handler, IView view)
/// {
/// if (view.Background is IImageSourcePaint imagePaint)
/// {
/// // May be null, in which case any existing image background is cleared.
/// var provider = handler.GetRequiredService<IImageSourceServiceProvider>();
/// ApplyImageBackgroundAsync(handler.PlatformView, imagePaint.ImageSource, provider);
/// }
/// else
/// {
/// ApplyPaintBackground(handler.PlatformView, view.Background);
/// }
/// }
/// ]]></code>
/// </example>
/// </remarks>
public interface IImageSourcePaint
{
/// <summary>
/// Gets the image source used to fill the area, or <see langword="null"/> when there is no image to draw.
/// </summary>
IImageSource? ImageSource { get; }
}
}
2 changes: 1 addition & 1 deletion src/Core/src/ImageSources/ImageSourcePaint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Microsoft.Maui
{
class ImageSourcePaint : Paint
class ImageSourcePaint : Paint, IImageSourcePaint
{
public ImageSourcePaint()
{
Expand Down
2 changes: 2 additions & 0 deletions src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -363,3 +363,5 @@ virtual Microsoft.Maui.Platform.StackNavigationManager.OnCreateNavigationAnimati
Microsoft.Maui.ISwipeItemMenuItemIconColor
Microsoft.Maui.ISwipeItemMenuItemIconColor.IconColor.get -> Microsoft.Maui.Graphics.Color?
static Microsoft.Maui.Handlers.SwipeItemMenuItemHandler.MapIconColor(Microsoft.Maui.Handlers.ISwipeItemMenuItemHandler! handler, Microsoft.Maui.ISwipeItemMenuItem! view) -> void
Microsoft.Maui.IImageSourcePaint
Microsoft.Maui.IImageSourcePaint.ImageSource.get -> Microsoft.Maui.IImageSource?
2 changes: 2 additions & 0 deletions src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,5 @@ virtual Microsoft.Maui.Animations.PlatformTicker.Dispose(bool disposing) -> void
Microsoft.Maui.ISwipeItemMenuItemIconColor
Microsoft.Maui.ISwipeItemMenuItemIconColor.IconColor.get -> Microsoft.Maui.Graphics.Color?
static Microsoft.Maui.Handlers.SwipeItemMenuItemHandler.MapIconColor(Microsoft.Maui.Handlers.ISwipeItemMenuItemHandler! handler, Microsoft.Maui.ISwipeItemMenuItem! view) -> void
Microsoft.Maui.IImageSourcePaint
Microsoft.Maui.IImageSourcePaint.ImageSource.get -> Microsoft.Maui.IImageSource?
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,5 @@ virtual Microsoft.Maui.Animations.PlatformTicker.Dispose(bool disposing) -> void
Microsoft.Maui.ISwipeItemMenuItemIconColor
Microsoft.Maui.ISwipeItemMenuItemIconColor.IconColor.get -> Microsoft.Maui.Graphics.Color?
static Microsoft.Maui.Handlers.SwipeItemMenuItemHandler.MapIconColor(Microsoft.Maui.Handlers.ISwipeItemMenuItemHandler! handler, Microsoft.Maui.ISwipeItemMenuItem! view) -> void
Microsoft.Maui.IImageSourcePaint
Microsoft.Maui.IImageSourcePaint.ImageSource.get -> Microsoft.Maui.IImageSource?
2 changes: 2 additions & 0 deletions src/Core/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ static Microsoft.Maui.GridLength.implicit operator Microsoft.Maui.GridLength(str
Microsoft.Maui.ISwipeItemMenuItemIconColor
Microsoft.Maui.ISwipeItemMenuItemIconColor.IconColor.get -> Microsoft.Maui.Graphics.Color?
static Microsoft.Maui.Handlers.SwipeItemMenuItemHandler.MapIconColor(Microsoft.Maui.Handlers.ISwipeItemMenuItemHandler! handler, Microsoft.Maui.ISwipeItemMenuItem! view) -> void
Microsoft.Maui.IImageSourcePaint
Microsoft.Maui.IImageSourcePaint.ImageSource.get -> Microsoft.Maui.IImageSource?
2 changes: 2 additions & 0 deletions src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,5 @@ virtual Microsoft.Maui.MauiWinUIApplication.OnAppInstanceActivated(Microsoft.Win
Microsoft.Maui.ISwipeItemMenuItemIconColor
Microsoft.Maui.ISwipeItemMenuItemIconColor.IconColor.get -> Microsoft.Maui.Graphics.Color?
static Microsoft.Maui.Handlers.SwipeItemMenuItemHandler.MapIconColor(Microsoft.Maui.Handlers.ISwipeItemMenuItemHandler! handler, Microsoft.Maui.ISwipeItemMenuItem! view) -> void
Microsoft.Maui.IImageSourcePaint
Microsoft.Maui.IImageSourcePaint.ImageSource.get -> Microsoft.Maui.IImageSource?
2 changes: 2 additions & 0 deletions src/Core/src/PublicAPI/net/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ static Microsoft.Maui.Handlers.SwipeItemMenuItemHandler.MapIconColor(Microsoft.M
static Microsoft.Maui.Handlers.ShapeViewHandler.MapFlowDirection(Microsoft.Maui.Handlers.IShapeViewHandler! handler, Microsoft.Maui.IShapeView! shapeView) -> void
static Microsoft.Maui.Handlers.SearchBarHandler.MapCursorPosition(Microsoft.Maui.Handlers.ISearchBarHandler! handler, Microsoft.Maui.ISearchBar! searchBar) -> void
static Microsoft.Maui.Handlers.SearchBarHandler.MapSelectionLength(Microsoft.Maui.Handlers.ISearchBarHandler! handler, Microsoft.Maui.ISearchBar! searchBar) -> void
Microsoft.Maui.IImageSourcePaint
Microsoft.Maui.IImageSourcePaint.ImageSource.get -> Microsoft.Maui.IImageSource?
2 changes: 2 additions & 0 deletions src/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ static Microsoft.Maui.Handlers.FlyoutViewHandler.MapFlyoutWidth(Microsoft.Maui.H
static Microsoft.Maui.Handlers.FlyoutViewHandler.MapIsGestureEnabled(Microsoft.Maui.Handlers.IFlyoutViewHandler! handler, Microsoft.Maui.IFlyoutView! flyoutView) -> void
static Microsoft.Maui.Handlers.FlyoutViewHandler.MapIsPresented(Microsoft.Maui.Handlers.IFlyoutViewHandler! handler, Microsoft.Maui.IFlyoutView! flyoutView) -> void
static Microsoft.Maui.SafeAreaEdges.Container.get -> Microsoft.Maui.SafeAreaEdges
Microsoft.Maui.IImageSourcePaint
Microsoft.Maui.IImageSourcePaint.ImageSource.get -> Microsoft.Maui.IImageSource?
2 changes: 2 additions & 0 deletions src/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ static Microsoft.Maui.Handlers.FlyoutViewHandler.MapFlyoutWidth(Microsoft.Maui.H
static Microsoft.Maui.Handlers.FlyoutViewHandler.MapIsGestureEnabled(Microsoft.Maui.Handlers.IFlyoutViewHandler! handler, Microsoft.Maui.IFlyoutView! flyoutView) -> void
static Microsoft.Maui.Handlers.FlyoutViewHandler.MapIsPresented(Microsoft.Maui.Handlers.IFlyoutViewHandler! handler, Microsoft.Maui.IFlyoutView! flyoutView) -> void
static Microsoft.Maui.SafeAreaEdges.Container.get -> Microsoft.Maui.SafeAreaEdges
Microsoft.Maui.IImageSourcePaint
Microsoft.Maui.IImageSourcePaint.ImageSource.get -> Microsoft.Maui.IImageSource?
152 changes: 152 additions & 0 deletions src/Core/tests/UnitTests/ImageSource/ImageSourcePaintContractTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using System;
using System.Linq;
using System.Reflection;
using Microsoft.Maui.Graphics;
using Xunit;

namespace Microsoft.Maui.UnitTests.ImageSource
{
/// <summary>
/// Verifies the public <see cref="IImageSourcePaint"/> contract that out-of-tree platform backends
/// consume to detect and read image-source backgrounds. The contract is consumption-only, so these
/// tests never implement it outside of .NET MAUI.
/// </summary>
[Category(TestCategory.Core)]
public class ImageSourcePaintContractTests
{
[Fact]
public void ContractIsVisibleToExternalAssemblies()
{
var contract = typeof(IImageSourcePaint);

Assert.True(contract.IsInterface);
Assert.True(contract.IsPublic, "IImageSourcePaint must be public so external backends can reference it.");
Assert.Equal("Microsoft.Maui.IImageSourcePaint", contract.FullName);
}

[Fact]
public void ContractExposesOnlyTheImageSource()
{
var members = typeof(IImageSourcePaint).GetMembers(BindingFlags.Public | BindingFlags.Instance);

var property = Assert.Single(typeof(IImageSourcePaint).GetProperties());
Assert.Equal(nameof(IImageSourcePaint.ImageSource), property.Name);
Assert.Equal(typeof(IImageSource), property.PropertyType);
Assert.NotNull(property.GetMethod);
Assert.Null(property.SetMethod);

// Only the getter should be projected onto the contract; no other implementation detail leaks out.
Assert.Equal(new[] { property.GetMethod }, members.OfType<MethodInfo>());
}

[Fact]
public void BuiltInImageSourcePaintImplementsTheContractAndStaysInternal()
{
var paintType = typeof(IView).Assembly.GetType("Microsoft.Maui.ImageSourcePaint", throwOnError: true);

Assert.False(paintType.IsVisible, "ImageSourcePaint should remain an implementation detail.");
Assert.True(typeof(IImageSourcePaint).IsAssignableFrom(paintType));
Assert.True(typeof(Paint).IsAssignableFrom(paintType));
}

[Fact]
public void ExternalBackendCanReadImageSourceFromBuiltInPaint()
{
var imageSource = new ImageSourceStub();
var view = new ViewStub { Background = new ImageSourcePaint(imageSource) };

var result = FakeExternalBackend.Describe(view);

Assert.Equal(FakeExternalBackend.PaintKind.Image, result.Kind);
Assert.Same(imageSource, result.ImageSource);
}

[Fact]
public void ExternalBackendDistinguishesSolidPaint()
{
var view = new ViewStub { Background = new SolidPaint(Colors.Red) };

var result = FakeExternalBackend.Describe(view);

Assert.Equal(FakeExternalBackend.PaintKind.Solid, result.Kind);
Assert.Null(result.ImageSource);
}

[Theory]
[InlineData(typeof(LinearGradientPaint))]
[InlineData(typeof(RadialGradientPaint))]
public void ExternalBackendDistinguishesGradientPaint(Type gradientPaintType)
{
var view = new ViewStub { Background = (Paint)Activator.CreateInstance(gradientPaintType) };

var result = FakeExternalBackend.Describe(view);

Assert.Equal(FakeExternalBackend.PaintKind.Gradient, result.Kind);
Assert.Null(result.ImageSource);
}

[Fact]
public void ExternalBackendDistinguishesNoPaint()
{
var result = FakeExternalBackend.Describe(new ViewStub());

Assert.Equal(FakeExternalBackend.PaintKind.None, result.Kind);
Assert.Null(result.ImageSource);
}

[Fact]
public void ExternalBackendReadsNullImageSourceAsAnImagePaint()
{
// A null ImageSource still identifies an image background; it simply has nothing to draw.
var view = new ViewStub { Background = new ImageSourcePaint() };

var result = FakeExternalBackend.Describe(view);

Assert.Equal(FakeExternalBackend.PaintKind.Image, result.Kind);
Assert.Null(result.ImageSource);
}

[Fact]
public void ImagePaintIsNotAnImageSourcePaint()
{
// Graphics.ImagePaint carries an already-loaded IImage and is a distinct concept.
var view = new ViewStub { Background = new ImagePaint() };

var result = FakeExternalBackend.Describe(view);

Assert.NotEqual(FakeExternalBackend.PaintKind.Image, result.Kind);
Assert.IsNotAssignableFrom<IImageSourcePaint>(view.Background);
}

/// <summary>
/// Stands in for an out-of-tree platform backend. It only consumes the contract - it never implements
/// it - and every member it touches is public .NET MAUI API, so the image source is retrieved with no
/// reflection and no internals access.
/// </summary>
static class FakeExternalBackend
{
public enum PaintKind
{
None,
Solid,
Gradient,
Image,
}

public static (PaintKind Kind, IImageSource ImageSource) Describe(IView view)
{
switch (view.Background)
{
case IImageSourcePaint imagePaint:
return (PaintKind.Image, imagePaint.ImageSource);
case GradientPaint:
return (PaintKind.Gradient, null);
case SolidPaint:
return (PaintKind.Solid, null);
default:
return (PaintKind.None, null);
}
}
Comment on lines +136 to +149
}
}
}
Loading