Skip to content

Commit daafcf9

Browse files
RedthCopilot
andcommitted
Add public IImageSourcePaint contract for external platform backends
MAUI represents an image background as the internal `ImageSourcePaint` type. Out-of-tree platform backends receive backgrounds as a `Paint` via `IView.Background`, so they cannot detect an image background or read its `IImageSource` without reflection or `InternalsVisibleTo`. Adds a minimal, additive public contract, `Microsoft.Maui.IImageSourcePaint`, exposing a single read-only `ImageSource` property. The internal `ImageSourcePaint` now implements it, and every in-tree consumer pattern matches on the interface instead of the concrete type. This is purely additive: - `ImageSourcePaint` stays internal, so no implementation detail is exposed. - No brush/paint API, XAML, or serialization surface changes; `ImageBrush` round-trips exactly as before. - Interface pattern matching is trim/AOT safe and adds no reflection. - Equality and built-in platform behavior are unchanged. Because in-tree handlers now match the interface, a backend (or app) can also supply its own `Paint` implementing `IImageSourcePaint` and have the built-in handlers render it as an image background. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ec0bf62-da2c-49fb-af27-a0a79cbbd6bb
1 parent bedd1b1 commit daafcf9

18 files changed

Lines changed: 280 additions & 7 deletions

File tree

src/Controls/src/Core/Brush/Brush.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public static implicit operator Brush(Paint paint)
5252
}
5353
}
5454

55-
if (paint is ImageSourcePaint imageSourcePaint && imageSourcePaint.ImageSource is ImageSource imageSource)
55+
if (paint is IImageSourcePaint imageSourcePaint && imageSourcePaint.ImageSource is ImageSource imageSource)
5656
return new ImageBrush { ImageSource = imageSource };
5757

5858
return null;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using Microsoft.Maui.Graphics;
2+
using Xunit;
3+
4+
namespace Microsoft.Maui.Controls.Core.UnitTests
5+
{
6+
public class ImageBrushTests : BaseTestFixture
7+
{
8+
[Fact]
9+
public void ImageBrushConvertsToImageSourcePaint()
10+
{
11+
var imageSource = ImageSource.FromFile("background.png");
12+
13+
Paint paint = new ImageBrush { ImageSource = imageSource };
14+
15+
var imagePaint = Assert.IsAssignableFrom<IImageSourcePaint>(paint);
16+
Assert.Same(imageSource, imagePaint.ImageSource);
17+
}
18+
19+
[Fact]
20+
public void ImageSourcePaintConvertsBackToImageBrush()
21+
{
22+
var imageSource = ImageSource.FromFile("background.png");
23+
24+
Paint paint = new ImageBrush { ImageSource = imageSource };
25+
Brush brush = (Brush)paint;
26+
27+
var imageBrush = Assert.IsType<ImageBrush>(brush);
28+
Assert.Same(imageSource, imageBrush.ImageSource);
29+
}
30+
31+
[Fact]
32+
public void ExternalImageSourcePaintConvertsToImageBrush()
33+
{
34+
var imageSource = ImageSource.FromFile("background.png");
35+
36+
Brush brush = (Brush)new ExternalImageSourcePaint(imageSource);
37+
38+
var imageBrush = Assert.IsType<ImageBrush>(brush);
39+
Assert.Same(imageSource, imageBrush.ImageSource);
40+
}
41+
42+
[Fact]
43+
public void SolidAndGradientPaintsAreNotImageSourcePaints()
44+
{
45+
Assert.IsNotAssignableFrom<IImageSourcePaint>((Paint)new SolidColorBrush(Colors.Red));
46+
Assert.IsNotAssignableFrom<IImageSourcePaint>((Paint)new LinearGradientBrush());
47+
Assert.IsNotAssignableFrom<IImageSourcePaint>((Paint)new RadialGradientBrush());
48+
}
49+
50+
class ExternalImageSourcePaint : Paint, IImageSourcePaint
51+
{
52+
public ExternalImageSourcePaint(IImageSource imageSource) => ImageSource = imageSource;
53+
54+
public IImageSource ImageSource { get; }
55+
}
56+
}
57+
}

src/Core/src/Handlers/Editor/EditorHandler.iOS.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ public static void MapBackground(IEditorHandler handler, IEditor editor)
144144
if (handler.PlatformView is not MauiTextView platformView)
145145
return;
146146

147-
if (editor.Background is ImageSourcePaint image)
147+
if (editor.Background is IImageSourcePaint image)
148148
{
149149
var provider = handler.GetRequiredService<IImageSourceServiceProvider>();
150150
platformView.UpdateBackgroundImageSourceAsync(image.ImageSource, provider)

src/Core/src/Handlers/Entry/EntryHandler.iOS.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public static void MapBackground(IEntryHandler handler, IEntry entry)
5353
if (handler.PlatformView is not MauiTextField platformView)
5454
return;
5555

56-
if (entry.Background is ImageSourcePaint image)
56+
if (entry.Background is IImageSourcePaint image)
5757
{
5858
var provider = handler.GetRequiredService<IImageSourceServiceProvider>();
5959
platformView.UpdateBackgroundImageSourceAsync(image.ImageSource, provider)

src/Core/src/Handlers/View/ViewHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ public static void MapBackground(IViewHandler handler, IView view)
386386
if (handler.PlatformView is not PlatformView platformView)
387387
return;
388388

389-
if (view.Background is ImageSourcePaint image)
389+
if (view.Background is IImageSourcePaint image)
390390
{
391391
var provider = handler.GetRequiredService<IImageSourceServiceProvider>();
392392

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#nullable enable
2+
using Microsoft.Maui.Graphics;
3+
4+
namespace Microsoft.Maui
5+
{
6+
/// <summary>
7+
/// Represents a <see cref="Paint"/> that fills an area with the contents of an <see cref="IImageSource"/>.
8+
/// </summary>
9+
/// <remarks>
10+
/// <para>
11+
/// Platform backends receive backgrounds as a <see cref="Paint"/> through <see cref="IView.Background"/>.
12+
/// Pattern matching that paint against this interface is the supported way to detect an image-source
13+
/// background and to obtain the <see cref="ImageSource"/> so it can be resolved with an
14+
/// <see cref="IImageSourceServiceProvider"/>.
15+
/// </para>
16+
/// <para>
17+
/// This interface is implemented by the paint that .NET MAUI produces for an image background, and it may
18+
/// also be implemented by custom <see cref="Paint"/> types so that they are treated as image backgrounds by
19+
/// the built-in handlers.
20+
/// </para>
21+
/// <example>
22+
/// The following example shows how an out-of-tree handler can render an image background:
23+
/// <code language="csharp"><![CDATA[
24+
/// public static void MapBackground(IViewHandler handler, IView view)
25+
/// {
26+
/// if (view.Background is IImageSourcePaint imagePaint)
27+
/// {
28+
/// var provider = handler.GetRequiredService<IImageSourceServiceProvider>();
29+
/// ApplyImageBackgroundAsync(handler.PlatformView, imagePaint.ImageSource, provider);
30+
/// }
31+
/// else
32+
/// {
33+
/// ApplyPaintBackground(handler.PlatformView, view.Background);
34+
/// }
35+
/// }
36+
/// ]]></code>
37+
/// </example>
38+
/// </remarks>
39+
public interface IImageSourcePaint
40+
{
41+
/// <summary>
42+
/// Gets the image source that is used to fill the area.
43+
/// </summary>
44+
IImageSource? ImageSource { get; }
45+
}
46+
}

src/Core/src/ImageSources/ImageSourcePaint.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
namespace Microsoft.Maui
55
{
6-
class ImageSourcePaint : Paint
6+
class ImageSourcePaint : Paint, IImageSourcePaint
77
{
88
public ImageSourcePaint()
99
{

src/Core/src/Platform/Tizen/ViewExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public static void UpdateBackground(this ContentViewGroup platformView, IBorderV
6060

6161
public static void UpdateBackground(this NView platformView, IView view)
6262
{
63-
if (view.Background is ImageSourcePaint image)
63+
if (view.Background is IImageSourcePaint image)
6464
{
6565
var provider = view.Handler?.GetRequiredService<IImageSourceServiceProvider>();
6666
platformView.UpdateBackgroundImageSourceAsync(image.ImageSource, provider)

src/Core/src/Platform/iOS/PageExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public static void UpdateTitle(this UIViewController viewController, IContentVie
1414

1515
public static void UpdateBackground(this UIView platformView, IContentView page, IImageSourceServiceProvider? provider)
1616
{
17-
if (page.Background is ImageSourcePaint image)
17+
if (page.Background is IImageSourcePaint image)
1818
platformView.UpdateBackgroundImageSourceAsync(image.ImageSource, provider).FireAndForget();
1919
else
2020
platformView.UpdateBackground(page);

src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,3 +363,5 @@ virtual Microsoft.Maui.Platform.StackNavigationManager.OnCreateNavigationAnimati
363363
Microsoft.Maui.ISwipeItemMenuItemIconColor
364364
Microsoft.Maui.ISwipeItemMenuItemIconColor.IconColor.get -> Microsoft.Maui.Graphics.Color?
365365
static Microsoft.Maui.Handlers.SwipeItemMenuItemHandler.MapIconColor(Microsoft.Maui.Handlers.ISwipeItemMenuItemHandler! handler, Microsoft.Maui.ISwipeItemMenuItem! view) -> void
366+
Microsoft.Maui.IImageSourcePaint
367+
Microsoft.Maui.IImageSourcePaint.ImageSource.get -> Microsoft.Maui.IImageSource?

0 commit comments

Comments
 (0)