Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion src/Controls/src/Core/Brush/Brush.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public static implicit operator Brush(Paint paint)
}
}

if (paint is ImageSourcePaint imageSourcePaint && imageSourcePaint.ImageSource is ImageSource imageSource)
if (paint is IImageSourcePaint imageSourcePaint && imageSourcePaint.ImageSource is ImageSource imageSource)
return new ImageBrush { ImageSource = imageSource };

return null;
Expand Down
57 changes: 57 additions & 0 deletions src/Controls/tests/Core.UnitTests/ImageBrushTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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 ExternalImageSourcePaintConvertsToImageBrush()
{
var imageSource = ImageSource.FromFile("background.png");

Brush brush = (Brush)new ExternalImageSourcePaint(imageSource);

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());
}

class ExternalImageSourcePaint : Paint, IImageSourcePaint
{
public ExternalImageSourcePaint(IImageSource imageSource) => ImageSource = imageSource;

public IImageSource ImageSource { get; }
}
}
}
2 changes: 1 addition & 1 deletion src/Core/src/Handlers/Editor/EditorHandler.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ public static void MapBackground(IEditorHandler handler, IEditor editor)
if (handler.PlatformView is not MauiTextView platformView)
return;

if (editor.Background is ImageSourcePaint image)
if (editor.Background is IImageSourcePaint image)
{
var provider = handler.GetRequiredService<IImageSourceServiceProvider>();
platformView.UpdateBackgroundImageSourceAsync(image.ImageSource, provider)
Expand Down
2 changes: 1 addition & 1 deletion src/Core/src/Handlers/Entry/EntryHandler.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public static void MapBackground(IEntryHandler handler, IEntry entry)
if (handler.PlatformView is not MauiTextField platformView)
return;

if (entry.Background is ImageSourcePaint image)
if (entry.Background is IImageSourcePaint image)
{
var provider = handler.GetRequiredService<IImageSourceServiceProvider>();
platformView.UpdateBackgroundImageSourceAsync(image.ImageSource, provider)
Expand Down
2 changes: 1 addition & 1 deletion src/Core/src/Handlers/View/ViewHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ public static void MapBackground(IViewHandler handler, IView view)
if (handler.PlatformView is not PlatformView platformView)
return;

if (view.Background is ImageSourcePaint image)
if (view.Background is IImageSourcePaint image)
{
var provider = handler.GetRequiredService<IImageSourceServiceProvider>();

Expand Down
46 changes: 46 additions & 0 deletions src/Core/src/ImageSources/IImageSourcePaint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#nullable enable
using Microsoft.Maui.Graphics;

namespace Microsoft.Maui
{
/// <summary>
/// Represents a <see cref="Paint"/> that fills an area with the contents of an <see cref="IImageSource"/>.
/// </summary>
/// <remarks>
/// <para>
/// Platform backends receive backgrounds as a <see cref="Paint"/> through <see cref="IView.Background"/>.
/// Pattern matching that paint against this interface is the supported way to detect an image-source
/// background and to obtain the <see cref="ImageSource"/> so it can be resolved with an
/// <see cref="IImageSourceServiceProvider"/>.
/// </para>
/// <para>
/// This interface is implemented by the paint that .NET MAUI produces for an image background, and it may
/// also be implemented by custom <see cref="Paint"/> types so that they are treated as image backgrounds by
/// the built-in handlers.
/// </para>
/// <example>
/// The following example shows how an out-of-tree handler can render an image background:
/// <code language="csharp"><![CDATA[
/// public static void MapBackground(IViewHandler handler, IView view)
/// {
/// if (view.Background is IImageSourcePaint imagePaint)
/// {
/// 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 that is used to fill the area.
/// </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: 1 addition & 1 deletion src/Core/src/Platform/Tizen/ViewExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public static void UpdateBackground(this ContentViewGroup platformView, IBorderV

public static void UpdateBackground(this NView platformView, IView view)
{
if (view.Background is ImageSourcePaint image)
if (view.Background is IImageSourcePaint image)
{
var provider = view.Handler?.GetRequiredService<IImageSourceServiceProvider>();
platformView.UpdateBackgroundImageSourceAsync(image.ImageSource, provider)
Expand Down
2 changes: 1 addition & 1 deletion src/Core/src/Platform/iOS/PageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public static void UpdateTitle(this UIViewController viewController, IContentVie

public static void UpdateBackground(this UIView platformView, IContentView page, IImageSourceServiceProvider? provider)
{
if (page.Background is ImageSourcePaint image)
if (page.Background is IImageSourcePaint image)
platformView.UpdateBackgroundImageSourceAsync(image.ImageSource, provider).FireAndForget();
else
platformView.UpdateBackground(page);
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?
154 changes: 154 additions & 0 deletions src/Core/tests/UnitTests/ImageSource/ImageSourcePaintContractTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
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
/// rely on to detect and render image-source backgrounds.
/// </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 ExternalBackendCanReadImageSourceFromExternalPaint()
{
var imageSource = new ExternalImageSource();
var view = new ViewStub { Background = new ExternalImageSourcePaint(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);
}

/// <summary>
/// A paint authored entirely outside of .NET MAUI. It only depends on public API, which proves a
/// third-party backend can both produce and consume image-source backgrounds.
/// </summary>
class ExternalImageSourcePaint : Paint, IImageSourcePaint
{
public ExternalImageSourcePaint(IImageSource imageSource) => ImageSource = imageSource;

public IImageSource ImageSource { get; }
}

class ExternalImageSource : IImageSource
{
public bool IsEmpty => false;
}

/// <summary>
/// Stands in for an out-of-tree platform backend. Every member it touches is public .NET MAUI API and
/// the image source is retrieved through the contract - 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