Add support for background images with ImageBrush - #36329
Add support for background images with ImageBrush#36329HarishwaranVijayakumar wants to merge 17 commits into
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 36329Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 36329" |
|
Hey there @@HarishwaranVijayakumar! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
This comment has been minimized.
This comment has been minimized.
kubaflo
left a comment
There was a problem hiding this comment.
Could you please check the ai's suggestions?
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 8 findings
See inline comments for details.
|
|
||
| Drawable[] layers = [imageDrawable, strokeDrawable]; | ||
| var layerDrawable = new LayerDrawable(layers); | ||
| layerDrawable.SetId(0, MauiBackgroundDrawableId); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Handler Mapper and Property Patterns / Logic and Correctness — UpdateMauiRippleDrawableImageBackground (new in this PR) stores the loaded imageDrawable under MauiBackgroundDrawableId inside the LayerDrawable. But TryGetMauiBackground (same file, line 241) only recognizes a MAUI background when layerDrawable.FindDrawableByLayerId(MauiBackgroundDrawableId) is GradientDrawable — an arbitrary loaded image Drawable (typically a BitmapDrawable) never satisfies that cast. Concrete repro: set Button.Background/ImageButton.Background to an ImageSourcePaint, then change StrokeColor, StrokeThickness, or CornerRadius. UpdateButtonStroke (ButtonExtensions.cs:45-60, unchanged by this PR) calls UpdateMauiRippleDrawableStroke → TryGetMauiBackground, which now always returns false for an image background, so the code silently falls back to setting platformView.StrokeColor/StrokeWidth/CornerRadius directly on the native MaterialButton/ShapeableImageView — properties with no visual effect because Background is now the custom RippleDrawable built by this method. Net effect: stroke/corner-radius changes are silently ignored for any Button/ImageButton with an image background.
|
|
||
| if (background is ImageSourcePaint sourcePaint) | ||
| { | ||
| platformView.UpdateBackgroundImageSource(sourcePaint.ImageSource, view.Handler); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Logic and Correctness / Regression Prevention — When view.Background is an ImageSourcePaint, this new branch routes to UpdateBackgroundImageSource, whose async completion (UpdateBackgroundImageSourceAsync, line 498, unchanged) assigns the loaded image as a raw Drawable directly to platformView.Background — not wrapped in a MauiDrawable/MauiLayerDrawable. When the background later changes away from the image (e.g., back to null), the else branch calls UpdateBackground(background, treatTransparentAsNull) (line 335), which only clears backgrounds it recognizes as MauiDrawable (line 342) or, for empty paint, only for platformView is LayoutViewGroup or ContentViewGroup (line 366). A plain TextView-based control (e.g., Label, whose LabelHandler.Android.cs MapBackground calls this exact path) is neither, so setting Label.Background = ImageSourcePaint(...) then Label.Background = null leaves the stale image drawable visibly persisting on the label. Switching to a SolidPaint/GradientPaint does work (those branches unconditionally overwrite platformView.Background), so only the null/empty-paint case is broken.
| if (platformView is Control control) | ||
| { | ||
| control.Resources.SetValueForAllKey(resourceKeys, imageBrush); | ||
| control.Background = imageBrush; |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Logic and Correctness / Cross-Platform Consistency — UpdateBackgroundImageForAllStatesAsync (new in this PR) sets control.Background = imageBrush; as a direct local value, in addition to the Resources dictionary entries used for the PointerOver/Pressed/Disabled visual states. None of its callers (ButtonExtensions.UpdateBackground, DatePickerExtensions.UpdateBackground, PickerExtensions.UpdateBackground, TextBoxExtensions.UpdateBackground, TimePickerExtensions.UpdateBackground, RadioButtonExtensions.UpdateBackground, SliderExtensions.UpdateBackgroundColor, SearchBarExtensions.UpdateBackground — all modified in this PR) ever reset control.Background back to null/default when the paint later becomes null or a SolidPaint; they only call Resources.RemoveKeys(...)/SetValueForAllKey(...). Because a directly-assigned local value on a DependencyProperty takes precedence over template/VisualState ThemeResource Setters, the control's Normal visual state keeps showing the stale image after the background is changed away from an ImageSourcePaint, even though hover/pressed/disabled states correctly revert via the resource keys. Concrete repro: set Button.Background to an ImageSource, then set it back to null — the button keeps rendering the old image in its Normal state.
|
|
||
| if (background is ImageSourcePaint sourcePaint) | ||
| { | ||
| layoutPanel?.UpdateBackgroundImageSource(sourcePaint.ImageSource, layout.Handler); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Layout Measure-Arrange Correctness / Logic and Correctness — UpdatePlatformViewBackground(this LayoutPanel layoutPanel, ILayout layout) has a doc comment directly above (lines 362-365) stating "Background and InputTransparent for Windows layouts are heavily intertwined, so setting one usually requires setting the other at the same time." The new ImageSourcePaint branch (added in this PR) calls layoutPanel?.UpdateBackgroundImageSource(...) directly, completely bypassing UpdateInputTransparent(layout.InputTransparent, ...) that the else branch still calls. Concrete repro: a Layout (e.g. Grid/VerticalStackLayout) with InputTransparent=true and an ImageSourcePaint background will not get the input-transparency handling applied, so it can become hit-testable and block touch/pointer input to elements beneath it, unlike the same layout with any other background type.
| { | ||
| [ContentProperty(nameof(ImageSource))] | ||
| class ImageBrush : Brush | ||
| public class ImageBrush : Brush |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[moderate] Public API Surface Design / Logic and Correctness — This line makes ImageBrush public (previously internal), and the accompanying PublicAPI.Unshipped.txt entries in this PR ship Equals/GetHashCode as public API. Equals (line 28-29) performs value comparison on ImageSource, while GetHashCode() (line 31) returns base.GetHashCode() — reference-identity, since neither Brush nor Element overrides it. This violates the .NET contract that equal objects must produce equal hash codes: two distinct ImageBrush instances referencing the same ImageSource compare Equal but hash differently, breaking correctness for Dictionary<ImageBrush,_>/HashSet<ImageBrush> usage. Since this is now public/shipped API, the defect can no longer be fixed without a breaking change.
| break; | ||
|
|
||
| case ImageSourcePaint image: | ||
| uiSearchBar.UpdateBackgroundImageSource(image.ImageSource, searchBar.Handler); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[moderate] Cross-Platform Behavioral Consistency / Logic and Correctness — The new case ImageSourcePaint image: branch only calls UpdateBackgroundImageSource, unlike the sibling null (line 42-43) and SolidPaint (line 51 onward) branches, which both explicitly reset BarTintColor (to UISearchBar.Appearance.BarTintColor or the solid color, respectively). Concrete repro: set SearchBar.Background to a SolidPaint (setting BarTintColor to that color), then change it to an ImageSourcePaint. BarTintColor is never reset, so the previously-applied opaque tint color remains active on the search bar chrome and can visually obscure the newly set background image layer, which is inserted underneath via InsertBackgroundLayer(imageLayer, 0).
| } | ||
| //If this is a Mac optimized interface | ||
| if (OperatingSystem.IsIOSVersionAtLeast(15) && UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Mac) | ||
| else if (OperatingSystem.IsIOSVersionAtLeast(15) && UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Mac) |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Cross-Platform Behavioral Consistency / Regression Prevention — On MacCatalyst, the pre-existing if at this line was changed to else if, making it mutually exclusive with the new ImageSourcePaint branch above (lines 54-57). When running on Mac idiom + iOS 15+, switching Button.Background from an ImageSourcePaint to a SolidPaint/other paint takes this else if branch, which only sets UIButtonConfiguration.BaseBackgroundColor — it never calls RemoveBackgroundLayer() (unlike the non-Mac-idiom else branch at line 80, which reaches UpdateBackground(Paint?, ...) → RemoveBackgroundLayer() in ViewExtensions.cs). The custom background image CALayer previously inserted via UpdateBackgroundImageSource/InsertBackgroundLayer is never removed, so the stale image remains visible underneath/alongside the new configuration-based background on Mac-idiom Catalyst apps.
| return; | ||
| } | ||
|
|
||
| platformView.UpdateMauiRippleDrawableImageBackground(backgroundImageDrawable, stroke, getDefaultRippleColor, beforeSet); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[moderate] Async and Threading Safety — UpdateButtonBackgroundImageSourceAsync (new in this PR, plus the sibling UpdateBorderImageBackgroundAsync/UpdateBackgroundImageSourceAsync additions in this file and the iOS/Windows equivalents) is fire-and-forget with no staleness/generation check: once the async image load completes, it unconditionally calls UpdateMauiRippleDrawableImageBackground/assigns the platform background, without verifying the requested imageSource is still the one currently desired on VirtualView. Rapidly toggling Background between two different ImageSourcePaints, or from an image to a solid color/null and back, can let a stale, out-of-order completion overwrite an already-applied newer background, leaving the wrong image/color visible.
|
/azp run maui-pr-uitests |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 6 findings
See inline comments for details.
| { | ||
| [ContentProperty(nameof(ImageSource))] | ||
| class ImageBrush : Brush | ||
| public class ImageBrush : Brush |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Public API Surface — Making ImageBrush public exposes an equality/hash-code contract violation: Equals treats two brushes with the same ImageSource as equal, but GetHashCode() still delegates to base.GetHashCode(), so equal public instances can hash differently in Dictionary/HashSet. Please make the hash code use the same ImageSource state (or change equality) before shipping this API.
|
|
||
| Drawable[] layers = [imageDrawable, strokeDrawable]; | ||
| var layerDrawable = new LayerDrawable(layers); | ||
| layerDrawable.SetId(0, MauiBackgroundDrawableId); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Android Platform Specifics — This IDs the image drawable as MauiBackgroundDrawableId, but TryGetMauiBackground only recognizes that layer when it is a GradientDrawable. After an ImageBrush background is applied, later stroke/corner/ripple updates fall back to the platform properties instead of updating this custom ripple, so the visible image-backed stroke can stay stale. Please teach TryGetMauiBackground about image-backed layers or keep a recognizable MAUI wrapper layer.
| internal static void UpdateBackgroundImageSource(this AView platformView, IImageSource? imageSource, IElementHandler? handler) | ||
| { | ||
| var provider = handler?.GetRequiredService<IImageSourceServiceProvider>(); | ||
| platformView.UpdateBackgroundImageSourceAsync(imageSource, provider).FireAndForget(handler); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Async and Threading Safety — The new image-background loaders are fire-and-forget but do not verify after await that the handler/view still wants the same ImageSource. A slow URI/file load can finish after Background was changed to a solid/null brush (or after reconnect) and reapply the old image. The same pattern exists in the added iOS/Windows helpers; please capture a generation/current background check or cancellation token before assigning the loaded image.
|
|
||
| if (background is ImageSourcePaint sourcePaint) | ||
| { | ||
| layoutPanel?.UpdateBackgroundImageSource(sourcePaint.ImageSource, layout.Handler); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Windows Platform Specifics — The ImageBrush branch bypasses LayoutPanel.UpdateInputTransparent, but layouts on Windows rely on that method to place non-null backgrounds on the non-hit-test background layer when InputTransparent=true. With an image background this sets Panel.Background directly, so an input-transparent layout can start consuming hits. Please route the loaded image brush through the same UpdateInputTransparent(layout.InputTransparent, brush) path.
| } | ||
| //If this is a Mac optimized interface | ||
| if (OperatingSystem.IsIOSVersionAtLeast(15) && UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Mac) | ||
| else if (OperatingSystem.IsIOSVersionAtLeast(15) && UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Mac) |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] iOS/MacCatalyst Platform Specifics — On the optimized MacCatalyst button path, switching from an ImageBrush to a solid/null background now enters this else if branch and only updates UIButtonConfiguration; it never calls UpdateBackground(...)/RemoveBackgroundLayer(). The background layer inserted by the ImageBrush path can remain visible under the new color/default background. Please clear the image background layer before applying the optimized configuration.
| platformView.Background = null; | ||
| previousDrawable.Dispose(); | ||
| previousDrawable = null; | ||
| ((AView)platformView).UpdateBackgroundImageSource(sourcePaint.ImageSource, view.Handler); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Android Platform Specifics — For EditText (Entry/Editor), the ImageBrush branch installs a plain image drawable. When Background is later cleared, the non-image branch sees that drawable as user-owned (not MauiDrawable/MauiLayerDrawable) and leaves it in place, so the old image background remains and the default material line is not restored. Please wrap/mark the image background as MAUI-owned or explicitly clear it on null backgrounds.
|
/azp run |
|
Azure Pipelines: Successfully started running 3 pipeline(s). |
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
@HarishwaranVijayakumar — new AI review results are available based on this last commit:
8aca808.
🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix
Gate Result: ✅ PASSED
Platform: WINDOWS · Base: net11.0 · Merge base: c887d052
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
🖥️ Issue12928 Issue12928 |
✅ FAIL — 801s | ✅ PASS — 346s |
🔴 Without fix — 🖥️ Issue12928: FAIL ✅ · 801s
Error-relevant lines (filtered from the build log):
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 485
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 retryTimeout, Int32 cropLeft, Int32 cropRight, Int32 cropTop, Int32 cropBottom, Double tolerance, Boolean includeTitleBar) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 309
at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnLabelAndButton() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 20
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnEntryAndEditor() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 28
at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnSearchBarAndPicker() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 36
at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnDatePickerAndTimePicker() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 44
at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnSwitchAndStepper() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 52
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnSliderAndProgressBar() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 60
at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnCheckBoxAndRadioButton() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 68
at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnImageAndImageButton() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 76
at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack sig, BOOL isConstructor, ObjectHandleOnStack result)
at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnContentViewAndBorder() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 84
at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnCollectionViewAndSwipeView() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 92
at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnGridAndFlexLayout() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 108
at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnAbsoluteLayoutAndScrollView() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 116
at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnCarouselViewAndStackLayout() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 100
🟢 With fix — 🖥️ Issue12928: PASS ✅ · 346s
(no coded error found; showing last 1200 chars)
wAndSwipeView Stop
Passed ImageBrushOnCollectionViewAndSwipeView [4 s]
>>>>> 7/24/2026 8:44:50 AM ImageBrushOnGridAndFlexLayout Start
>>>>> 7/24/2026 8:44:55 AM ImageBrushOnGridAndFlexLayout Stop
Passed ImageBrushOnGridAndFlexLayout [4 s]
>>>>> 7/24/2026 8:44:55 AM ImageBrushOnAbsoluteLayoutAndScrollView Start
>>>>> 7/24/2026 8:44:59 AM ImageBrushOnAbsoluteLayoutAndScrollView Stop
Passed ImageBrushOnAbsoluteLayoutAndScrollView [4 s]
>>>>> 7/24/2026 8:44:59 AM ImageBrushOnCarouselViewAndStackLayout Start
>>>>> 7/24/2026 8:45:04 AM ImageBrushOnCarouselViewAndStackLayout Stop
Passed ImageBrushOnCarouselViewAndStackLayout [5 s]
NUnit Adapter 4.5.0.0: Test execution complete
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 11.0.0-preview.7.26365.101)
[xUnit.net 00:00:00.11] Discovering: Controls.TestCases.WinUI.Tests
[xUnit.net 00:00:00.32] Discovered: Controls.TestCases.WinUI.Tests
Results File: D:\a\1\s\CustomAgentLogsTmp\UITests\TestResults\Issue12928.trx
Test Run Successful.
Total tests: 13
Passed: 13
Total time: 1.3561 Minutes
>>> TRX_RESULT_FILE: D:\a\1\s\CustomAgentLogsTmp\UITests\TestResults\Issue12928.trx
📁 Fix files reverted (30 files)
src/Controls/src/Core/ImageBrush.cssrc/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txtsrc/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txtsrc/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txtsrc/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txtsrc/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txtsrc/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txtsrc/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txtsrc/Core/src/Handlers/Button/ButtonHandler.iOS.cssrc/Core/src/Platform/Android/ButtonExtensions.cssrc/Core/src/Platform/Android/ImageButtonExtensions.cssrc/Core/src/Platform/Android/MauiRippleDrawableExtensions.cssrc/Core/src/Platform/Android/RadioButtonExtensions.cssrc/Core/src/Platform/Android/StrokeExtensions.cssrc/Core/src/Platform/Android/ViewExtensions.cssrc/Core/src/Platform/Windows/ButtonExtensions.cssrc/Core/src/Platform/Windows/ContentPanel.cssrc/Core/src/Platform/Windows/DatePickerExtensions.cssrc/Core/src/Platform/Windows/PickerExtensions.cssrc/Core/src/Platform/Windows/RadioButtonExtensions.cssrc/Core/src/Platform/Windows/SearchBarExtensions.cssrc/Core/src/Platform/Windows/SliderExtensions.cssrc/Core/src/Platform/Windows/StepperExtensions.cssrc/Core/src/Platform/Windows/TextBoxExtensions.cssrc/Core/src/Platform/Windows/TimePickerExtensions.cssrc/Core/src/Platform/Windows/ViewExtensions.cssrc/Core/src/Platform/iOS/MauiCALayer.cssrc/Core/src/Platform/iOS/SearchBarExtensions.cssrc/Core/src/Platform/iOS/StrokeExtensions.cssrc/Core/src/Platform/iOS/ViewExtensions.cs
📱 UI Tests — Brush,ViewBaseTests
Detected UI test categories: Brush,ViewBaseTests
✅ Deep UI tests — 163 passed, 0 failed across 2 categories on platform-pool agent (replaces in-process counts above).
🧪 UI Test Execution Results (deep, platform pool)
| Category | Tests | Snapshot diffs |
|---|---|---|
Brush |
48/48 ✓ | — |
ViewBaseTests |
115/115 ✓ | — |
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs) |
📋 Pre-Flight — Context & Validation
Issue: #12928 - ImageBrush background support for controls
PR: #36329 - ImageBrush background support for controls
Platforms Affected: Android, iOS, MacCatalyst, Windows, Tizen
Files Changed: 34 implementation/API, 57 test/snapshot
Key Findings
- GitHub CLI is unauthenticated in this environment, so PR metadata/comments could not be fetched live; context was gathered from the checked-out PR commit and local test files.
- The PR exposes
ImageBrushpublicly and adds platform rendering support for image-backedBackgroundacross many controls. - Regression coverage is UI screenshot based (
Issue12928) with platform snapshots; platform selected for candidate testing: Windows. - Gate result was provided by the caller and was not re-run: tests fail without fix and pass with the PR fix.
Code Review Summary
Verdict: NEEDS_CHANGES
Confidence: low
Errors: 4 | Warnings: 1 | Suggestions: 0
Key code review findings:
- ✗
src/Controls/src/Core/ImageBrush.cs:28-31publicImageBrushcomparesImageSourceinEqualsbut uses identity hash code. - ✗
src/Core/src/Platform/Android/ViewExtensions.csimage background paths can leave stale backgrounds or bypass existing wrappers. - ✗
src/Core/src/Platform/Windows/ViewExtensions.cs:372image layout background bypassesUpdateInputTransparent(...)handling. - ✗
src/Core/src/Handlers/Button/ButtonHandler.iOS.cs:89image background path does not account for iOS button stroke/corner shaping. - ⚠ Async image background helpers lack stale-source validation after awaiting image load.
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #36329 | Make ImageBrush public and add platform-specific ImageBrush rendering paths for control backgrounds, plus public API entries and screenshot UI coverage. |
✅ PASSED (Gate) | ImageBrush.cs, platform background extensions, public API files, UI tests/snapshots |
Original PR fix; gate result supplied by caller. |
🔬 Code Review — Deep Analysis
Code Review — PR #36329
Independent Assessment
What this changes: Makes ImageBrush public and adds platform-specific support for image-backed Background on many controls, plus screenshot UI coverage.
Inferred motivation: Fix #12928 by allowing controls to render image backgrounds consistently.
Reconciliation with PR Narrative
Author claims: Adds ImageBrush background support across Android/iOS/MacCatalyst/Windows/Tizen, with known unsupported controls documented.
Agreement/disagreement: The intent matches the code, but several platform paths bypass existing background-reset, stroke, hit-test, and async-staleness safeguards.
Prior Review Reconciliation
| Prior ❌ Error Finding | Source | Status | Evidence |
|---|---|---|---|
Android image ripple layer not recognized by TryGetMauiBackground |
MauiBot inline | ❌ Unresolved | MauiRippleDrawableExtensions.cs:293 still IDs raw image as MauiBackgroundDrawableId; recognizer still expects GradientDrawable. |
| Android async image loaders can stale-write after background changes | MauiBot inline | ❌ Unresolved | ViewExtensions.cs:505, 531, 571 still assign after await without validating current Background/source. |
Windows layout ImageBrush bypasses InputTransparent workaround |
MauiBot inline | ❌ Unresolved | ViewExtensions.cs:372 still calls UpdateBackgroundImageSource instead of UpdateInputTransparent(...). |
| MacCatalyst optimized button path leaves image layer when switching away | MauiBot inline | ❌ Unresolved | ButtonHandler.iOS.cs:59 optimized branch does not remove the layer inserted by image path. |
Public ImageBrush equality/hash-code contract violation |
MauiBot inline | ❌ Unresolved | ImageBrush.cs:28-31 still compares ImageSource but hashes with base.GetHashCode(). |
Blast Radius Assessment
- Runs for all instances: Yes, mapper/platform background paths are shared for many controls.
- Startup impact: No direct startup initialization, but runs during handler/property mapping.
- Static/shared state: No new static mutable state, but public API surface is expanded.
CI Status
- Required-check result:
gh pr checks --requiredunavailable (gh auth loginrequired). REST check-runs for head8aca808...show failures:maui-pr,Build Analysis,maui-pr-devicetests, and onemaui-pr-uitestsleg. - Classification: Failing / undetermined; not fully classifiable without authenticated
gh/test details. - Action taken: Invoked
azdo-build-investigator; confidence capped low.
Findings
❌ Error — Public ImageBrush exposes broken equality semantics
src/Controls/src/Core/ImageBrush.cs:4, :28-31 makes the type public while Equals compares ImageSource and GetHashCode() uses object identity. Equal public instances can behave incorrectly in hash collections.
❌ Error — Android null/reset paths leave stale image backgrounds
src/Core/src/Platform/Android/ViewExtensions.cs:233 routes ImageSourcePaint through the async image setter, but clearing back to null falls through existing logic that does not clear non-layout TextView backgrounds. Entry/Editor also install a raw drawable at :260, bypassing the existing EditText wrapper that preserves Material underline/padding and restore behavior.
❌ Error — Windows ImageBrush layouts can break InputTransparent
src/Core/src/Platform/Windows/ViewExtensions.cs:372 bypasses the documented UpdateInputTransparent(...) coupling for LayoutPanel. A layout with InputTransparent=true and image background can start consuming pointer input.
❌ Error — iOS/MacCatalyst button ImageBrush ignores stroke/corner shaping
src/Core/src/Handlers/Button/ButtonHandler.iOS.cs:89 uses generic image background insertion without passing IButtonStroke; rounded/stroked buttons can render square image layers.
⚠️ Warning — Async image background helpers lack stale-source validation
New Android/iOS/Windows fire-and-forget loaders assign loaded images after await without verifying the view still wants that image source.
Failure-Mode Probing
- Clear ImageBrush to null: stale drawable can remain on Android text/edit controls.
- Handler/property update after slow image load: old image can overwrite newer solid/null background.
- Windows input-transparent layout: image background uses hit-testable panel background.
- Rounded/stroked iOS button: image layer is not clipped/shaped with button radius.
Verdict: NEEDS_CHANGES
Confidence: low, capped by failing/undetermined CI and broad platform-handler blast radius.
Summary: The feature direction is valid, but current code has unresolved correctness regressions in public API semantics and multiple platform background paths. CI is also failing, so this should not merge as-is.
🛠️ Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix | Centralized Windows ImageSourcePaint -> WinUI ImageBrush conversion in PaintExtensions.ToPlatform(), plus ImageBrush.GetHashCode() fix. |
❌ FAIL | 4 files | Build passed; 11/13 Windows Issue12928 screenshots passed; 2 visual-state controls missed baselines. |
| 2 | try-fix | Windows-only guarded async image brush loader with resource-key apply callbacks, LayoutPanel input transparency preservation, and ImageBrush.GetHashCode() fix. |
✅ PASS | 19 files | 13/13 Windows tests passed; self-review found a moderate image→solid async stale-write race. |
| 3 | try-fix | Add centralized generation-token invalidation to candidate 2's Windows async image pipeline to prevent image→solid/null stale writes. | ❌ FAIL | 18 files | Self-review clean, but test command ran in copied worktree with net10 TFM and failed before tests. |
| 4 | try-fix | Guarded centralized Windows image background pipeline with per-FrameworkElement token invalidation on image and non-image updates, resource-key/DP application, LayoutPanel input transparency preservation, and ImageBrush.GetHashCode() fix. |
✅ PASS | 12 files | 13/13 Windows tests passed from repo root; self-review clean ([]). Selected Windows alternative. |
| PR | PR #36329 | Public ImageBrush plus platform-specific background rendering branches and screenshot coverage. |
✅ PASSED (Gate) | 90+ files | Original PR; gate result supplied by caller. |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| claude-opus-4.6 | 1 | Yes | Centralize Windows conversion in PaintExtensions.ToPlatform(); failed visual-state baselines. |
| claude-opus-4.7 | 1 | Yes | Centralize async image loading with resource-key apply callbacks; passed tests but found stale-write race. |
| gpt-5.3-codex | 1 | Yes | Add generation-token invalidation to close stale-write race; self-review clean but test ran from wrong copied worktree and failed before tests. |
| gpt-5.5 | 1 | Yes | Reapply guarded generation-token approach in the main repo root; passed all Windows tests with clean self-review. |
Exhausted: Yes — meaningful Windows alternatives explored: fully centralized conversion, centralized async/resource-key helper, race-invalidation refinement, and validated race-safe refinement.
Selected Fix: Candidate #4 — passes the Windows regression suite, keeps themed-control fidelity, preserves LayoutPanel input transparency, fixes ImageBrush.GetHashCode(), and prevents stale async image continuations from overwriting newer backgrounds. This is the strongest Windows alternative found; broader non-Windows parity would still require separate platform validation.
📝 Recommended PR Title & Description
Assessment: ✏️ Recommend updating — the current metadata is detailed and mostly accurate for the submitted PR, but the winning fix adds Windows stale-async invalidation/LayoutPanel input-transparency preservation and fixes the ImageBrush hash/equality contract, which the current title and description do not capture.
Recommended title
[Controls] ImageBrush: Support image backgrounds on controls
Recommended description
## Description
This pull request introduces support for using image backgrounds on controls by making `ImageBrush` public and updating platform-specific background mapping logic to handle `ImageSourcePaint` backgrounds consistently across Android, iOS, Windows, MacCatalyst, and Tizen.
## Changes
### Image background support and API changes
- Made the `ImageBrush` class public so developers can use image backgrounds in controls.
- Added the `ImageBrush` constructors, `ImageSource` bindable property, `IsEmpty`, `Equals`, and `GetHashCode` entries to the public API files for Android, iOS, MacCatalyst, Tizen, Windows, .NET, and netstandard.
- Updated `ImageBrush.GetHashCode()` to hash `ImageSource`, matching the existing `Equals` comparison and preserving the equality/hash-code contract for the newly public type.
### Platform-specific background handling
**Android:**
- Updated background mapping for buttons, image buttons, radio buttons, strokes, ripple/background drawables, and general views to recognize `ImageSourcePaint` before falling back to existing background logic.
- Added shared image-source background helper logic for Android views.
**iOS/MacCatalyst:**
- Updated button, search bar, stroke, layer, and view background paths to recognize and render image-backed backgrounds.
- Added shared `UIView` image-source background helper logic.
**Windows:**
- Updated background mapping for Button, DatePicker, Picker, RadioButton, SearchBar, Slider, Stepper, TextBox, TimePicker, layouts, borders, and general views to support `ImageSourcePaint` backgrounds.
- Uses themed-control resource keys and the relevant WinUI background dependency properties so normal, pointer-over, pressed, disabled, and focused visual states continue to render image backgrounds correctly.
- Adds guarded async image loading with a per-`FrameworkElement` generation token so stale image-load continuations cannot overwrite newer solid, null, or non-image background updates.
- Preserves `LayoutPanel.UpdateInputTransparent` behavior when a layout background is an image brush.
### Tests
- Added `Issue12928` UI coverage and platform snapshots for Android, iOS, MacCatalyst, and Windows covering image backgrounds across labels, buttons, entries, editors, search bars, pickers, date/time pickers, switches, steppers, sliders, progress bars, check boxes, radio buttons, images, image buttons, content views, borders, collection views, swipe views, grids, flex layouts, absolute layouts, scroll views, carousel views, and stack layouts.
## Output
| iOS | Mac | Android | Windows |
|----------|----------|----------|----------|
| <img src="https://github.com/user-attachments/assets/cb56d391-bfde-4547-a900-927ef27e7f0a"> | <img src="https://github.com/user-attachments/assets/322ddc61-9c0f-42d6-a5de-0203a8436511"> | <img src="https://github.com/user-attachments/assets/9c14ad79-94e6-42ef-ab5d-643e25c34921"> | <img src="https://github.com/user-attachments/assets/54eb634f-8f23-48d3-97be-7a0b516ac237"> |
## Issues Fixed
Fixes #12928
## Issues / Limitations
- BoxView and GraphicsView are not supported because their underlying drawing pipelines do not implement image paint background rendering.
- RefreshView only works on Windows because iOS, MacCatalyst, and Android do not render `Background` for that control path.
🏁 Report — Final Recommendation
Comparative Fix Report — PR #36329
Candidates compared
| Candidate | Regression result | Reviewer/self-review status | Ranking notes |
|---|---|---|---|
try-fix-4 |
✅ PASS | Clean self-review ([]) |
Winner. Keeps the PR's broad ImageBrush support while improving the validated Windows path with guarded async image-loading, invalidation for non-image updates, LayoutPanel input-transparency preservation, and ImageBrush.GetHashCode() consistency. |
pr |
✅ PASS | Expert reviewer returned 0 inline findings | Good cross-platform implementation with validated Windows regression coverage, but its Windows image-loading paths remain broad fire-and-forget helpers and it leaves the ImageBrush.Equals/GetHashCode contract inconsistent. |
pr-plus-reviewer |
✅ PASS (same evidence as pr) |
No actionable reviewer feedback | Equivalent to pr because the expert reviewer produced no actionable findings to apply. |
try-fix-2 |
✅ PASS | Self-review found a moderate image-to-solid/null stale async write race plus minor cleanup concerns | Passing, but ranked below try-fix-4 because candidate 4 closes the race identified in candidate 2. |
try-fix-1 |
❌ FAIL | One moderate self-review finding | Failed 2/13 Windows screenshot baselines because centralized synchronous PaintExtensions.ToPlatform() did not preserve themed visual-state/resource-key behavior. Per rule, failed candidates rank below passing candidates. |
try-fix-3 |
❌ FAIL | Clean self-review, but validation did not reach test execution | Design addressed the candidate 2 race, but the prescribed test command failed before test execution in the copied worktree. Per rule, failed candidates rank below passing candidates. |
Decision
try-fix-4 is the single winning candidate. It is the strongest validated option for the requested Windows testing platform: it passes all 13 Issue12928 Windows screenshot tests from the repository root, has clean self-review, preserves the PR's visual behavior for themed WinUI controls, and fixes concrete robustness gaps in the raw PR's Windows async image background path.
The raw PR and pr-plus-reviewer remain viable because the gate passed and the expert reviewer returned no inline findings, but try-fix-4 is preferred because it retains the PR behavior while adding generation-token invalidation for stale async image continuations and preserving LayoutPanel.UpdateInputTransparent semantics. try-fix-2 is also passing but is superseded by try-fix-4; try-fix-1 and try-fix-3 are ranked lower because they failed validation.
🧭 Next Steps — alternative fix proposed (try-fix-4)
Automated review — alternative fix proposed
The expert-reviewer evaluation compared the PR fix against automatically generated candidates and selected try-fix-4 as the strongest fix.
Why: try-fix-4 wins because it passes the Windows Issue12928 regression suite, has clean self-review, preserves the PR image-background behavior, and adds guarded async invalidation plus LayoutPanel input-transparency preservation. Failed candidates are ranked lower, and pr-plus-reviewer is identical to the PR because the expert reviewer produced no actionable findings.
Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.
Candidate diff (try-fix-4)
diff --git a/src/Controls/src/Core/ImageBrush.cs b/src/Controls/src/Core/ImageBrush.cs
index cbb8bf001a..b456ced403 100644
--- a/src/Controls/src/Core/ImageBrush.cs
+++ b/src/Controls/src/Core/ImageBrush.cs
@@ -1,7 +1,7 @@
namespace Microsoft.Maui.Controls
{
[ContentProperty(nameof(ImageSource))]
- class ImageBrush : Brush
+ public class ImageBrush : Brush
{
public ImageBrush()
{
@@ -28,6 +28,6 @@ namespace Microsoft.Maui.Controls
public override bool Equals(object? obj) =>
obj is ImageBrush dest && ImageSource == dest.ImageSource;
- public override int GetHashCode() => base.GetHashCode();
+ public override int GetHashCode() => ImageSource?.GetHashCode() ?? 0;
}
}
\ No newline at end of file
diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
index f36d7da71f..fe05e3fa90 100644
--- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
@@ -5,6 +5,15 @@
~Microsoft.Maui.Controls.MultiBinding.ConverterCulture.set -> void
~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.get -> System.Globalization.CultureInfo
~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.set -> void
+Microsoft.Maui.Controls.ImageBrush
+Microsoft.Maui.Controls.ImageBrush.ImageBrush() -> void
+Microsoft.Maui.Controls.ImageBrush.ImageBrush(Microsoft.Maui.Controls.ImageSource! imageSource) -> void
+override Microsoft.Maui.Controls.ImageBrush.Equals(object? obj) -> bool
+override Microsoft.Maui.Controls.ImageBrush.GetHashCode() -> int
+override Microsoft.Maui.Controls.ImageBrush.IsEmpty.get -> bool
+static readonly Microsoft.Maui.Controls.ImageBrush.ImageSourceProperty -> Microsoft.Maui.Controls.BindableProperty!
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.set -> void
*REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList<Microsoft.Maui.Controls.VisualStateGroup>
Microsoft.Maui.Controls.HybridWebView.Invoker.get -> Microsoft.Maui.HybridWebViewInvoker!
Microsoft.Maui.Controls.HybridWebView.Invoker.set -> void
diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt
index 262c62ed3c..4fad9a57f3 100644
--- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt
@@ -1,4 +1,13 @@
#nullable enable
+Microsoft.Maui.Controls.ImageBrush
+Microsoft.Maui.Controls.ImageBrush.ImageBrush() -> void
+Microsoft.Maui.Controls.ImageBrush.ImageBrush(Microsoft.Maui.Controls.ImageSource! imageSource) -> void
+override Microsoft.Maui.Controls.ImageBrush.Equals(object? obj) -> bool
+override Microsoft.Maui.Controls.ImageBrush.GetHashCode() -> int
+override Microsoft.Maui.Controls.ImageBrush.IsEmpty.get -> bool
+static readonly Microsoft.Maui.Controls.ImageBrush.ImageSourceProperty -> Microsoft.Maui.Controls.BindableProperty!
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.set -> void
*REMOVED*~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRootRenderer.TraitCollectionDidChange(UIKit.UITraitCollection previousTraitCollection) -> void
*REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList<Microsoft.Maui.Controls.VisualStateGroup>
~Microsoft.Maui.Controls.Binding.ConverterCulture.get -> System.Globalization.CultureInfo
diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
index 262c62ed3c..4fad9a57f3 100644
--- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
@@ -1,4 +1,13 @@
#nullable enable
+Microsoft.Maui.Controls.ImageBrush
+Microsoft.Maui.Controls.ImageBrush.ImageBrush() -> void
+Microsoft.Maui.Controls.ImageBrush.ImageBrush(Microsoft.Maui.Controls.ImageSource! imageSource) -> void
+override Microsoft.Maui.Controls.ImageBrush.Equals(object? obj) -> bool
+override Microsoft.Maui.Controls.ImageBrush.GetHashCode() -> int
+override Microsoft.Maui.Controls.ImageBrush.IsEmpty.get -> bool
+static readonly Microsoft.Maui.Controls.ImageBrush.ImageSourceProperty -> Microsoft.Maui.Controls.BindableProperty!
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.set -> void
*REMOVED*~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRootRenderer.TraitCollectionDidChange(UIKit.UITraitCollection previousTraitCollection) -> void
*REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList<Microsoft.Maui.Controls.VisualStateGroup>
~Microsoft.Maui.Controls.Binding.ConverterCulture.get -> System.Globalization.CultureInfo
diff --git a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
index abd5f531e0..014891caa6 100644
--- a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
@@ -1,4 +1,13 @@
#nullable enable
+Microsoft.Maui.Controls.ImageBrush
+Microsoft.Maui.Controls.ImageBrush.ImageBrush() -> void
+Microsoft.Maui.Controls.ImageBrush.ImageBrush(Microsoft.Maui.Controls.ImageSource! imageSource) -> void
+override Microsoft.Maui.Controls.ImageBrush.Equals(object? obj) -> bool
+override Microsoft.Maui.Controls.ImageBrush.GetHashCode() -> int
+override Microsoft.Maui.Controls.ImageBrush.IsEmpty.get -> bool
+static readonly Microsoft.Maui.Controls.ImageBrush.ImageSourceProperty -> Microsoft.Maui.Controls.BindableProperty!
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.set -> void
Microsoft.Maui.Controls.HybridWebView.Invoker.get -> Microsoft.Maui.HybridWebViewInvoker!
Microsoft.Maui.Controls.HybridWebView.Invoker.set -> void
Microsoft.Maui.Controls.HybridWebView.SetInvokeJavaScriptTarget<T>(T! target, System.Text.Json.Serialization.JsonSerializerContext! jsonSerializerContext) -> void
diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt
index b4af1371f9..0646306f75 100644
--- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt
@@ -1,4 +1,13 @@
#nullable enable
+Microsoft.Maui.Controls.ImageBrush
+Microsoft.Maui.Controls.ImageBrush.ImageBrush() -> void
+Microsoft.Maui.Controls.ImageBrush.ImageBrush(Microsoft.Maui.Controls.ImageSource! imageSource) -> void
+override Microsoft.Maui.Controls.ImageBrush.Equals(object? obj) -> bool
+override Microsoft.Maui.Controls.ImageBrush.GetHashCode() -> int
+override Microsoft.Maui.Controls.ImageBrush.IsEmpty.get -> bool
+static readonly Microsoft.Maui.Controls.ImageBrush.ImageSourceProperty -> Microsoft.Maui.Controls.BindableProperty!
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.set -> void
Microsoft.Maui.Controls.HybridWebView.Invoker.get -> Microsoft.Maui.HybridWebViewInvoker!
Microsoft.Maui.Controls.HybridWebView.Invoker.set -> void
Microsoft.Maui.Controls.HybridWebView.SetInvokeJavaScriptTarget<T>(T! target, System.Text.Json.Serialization.JsonSerializerContext! jsonSerializerContext) -> void
diff --git a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt
index 83ef6f0b60..adf29fb32e 100644
--- a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt
@@ -33,6 +33,15 @@ Microsoft.Maui.Controls.ImageSource.InvalidateStyle() -> void
~Microsoft.Maui.Controls.Internals.ResourcesChangedEventArgs.ResourcesChangedEventArgs(System.Collections.Generic.IEnumerable<string> keys, System.Func<string, object> resolver) -> void
~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.get -> System.Globalization.CultureInfo
~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.set -> void
+Microsoft.Maui.Controls.ImageBrush
+Microsoft.Maui.Controls.ImageBrush.ImageBrush() -> void
+Microsoft.Maui.Controls.ImageBrush.ImageBrush(Microsoft.Maui.Controls.ImageSource! imageSource) -> void
+override Microsoft.Maui.Controls.ImageBrush.Equals(object? obj) -> bool
+override Microsoft.Maui.Controls.ImageBrush.GetHashCode() -> int
+override Microsoft.Maui.Controls.ImageBrush.IsEmpty.get -> bool
+static readonly Microsoft.Maui.Controls.ImageBrush.ImageSourceProperty -> Microsoft.Maui.Controls.BindableProperty!
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.set -> void
Microsoft.Maui.Controls.LongPressGestureRecognizer
Microsoft.Maui.Controls.LongPressGestureRecognizer.AllowableMovement.get -> double
Microsoft.Maui.Controls.LongPressGestureRecognizer.AllowableMovement.set -> void
diff --git a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
index d3aa29d723..fce90697d4 100644
--- a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
@@ -25,6 +25,15 @@ Microsoft.Maui.Controls.ImageSource.InvalidateStyle() -> void
~Microsoft.Maui.Controls.Internals.ResourcesChangedEventArgs.ResourcesChangedEventArgs(System.Collections.Generic.IEnumerable<string> keys, System.Func<string, object> resolver) -> void
~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.get -> System.Globalization.CultureInfo
~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.set -> void
+Microsoft.Maui.Controls.ImageBrush
+Microsoft.Maui.Controls.ImageBrush.ImageBrush() -> void
+Microsoft.Maui.Controls.ImageBrush.ImageBrush(Microsoft.Maui.Controls.ImageSource! imageSource) -> void
+override Microsoft.Maui.Controls.ImageBrush.Equals(object? obj) -> bool
+override Microsoft.Maui.Controls.ImageBrush.GetHashCode() -> int
+override Microsoft.Maui.Controls.ImageBrush.IsEmpty.get -> bool
+static readonly Microsoft.Maui.Controls.ImageBrush.ImageSourceProperty -> Microsoft.Maui.Controls.BindableProperty!
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.set -> void
Microsoft.Maui.Controls.LongPressGestureRecognizer
Microsoft.Maui.Controls.LongPressGestureRecognizer.AllowableMovement.get -> double
Microsoft.Maui.Controls.LongPressGestureRecognizer.AllowableMovement.set -> void
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnAbsoluteLayoutAndScrollView.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnAbsoluteLayoutAndScrollView.png
new file mode 100644
index 0000000000..61f32d36cb
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnAbsoluteLayoutAndScrollView.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCarouselViewAndStackLayout.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCarouselViewAndStackLayout.png
new file mode 100644
index 0000000000..f8385edcb6
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCarouselViewAndStackLayout.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCheckBoxAndRadioButton.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCheckBoxAndRadioButton.png
new file mode 100644
index 0000000000..05084feca1
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCheckBoxAndRadioButton.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCollectionViewAndSwipeView.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCollectionViewAndSwipeView.png
new file mode 100644
index 0000000000..2529347e95
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCollectionViewAndSwipeView.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnContentViewAndBorder.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnContentViewAndBorder.png
new file mode 100644
index 0000000000..97d7ecbcfc
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnContentViewAndBorder.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnDatePickerAndTimePicker.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnDatePickerAndTimePicker.png
new file mode 100644
index 0000000000..3466735839
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnDatePickerAndTimePicker.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnEntryAndEditor.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnEntryAndEditor.png
new file mode 100644
index 0000000000..38f79e61bf
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnEntryAndEditor.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnGridAndFlexLayout.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnGridAndFlexLayout.png
new file mode 100644
index 0000000000..3cc2a1f7cb
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnGridAndFlexLayout.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnImageAndImageButton.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnImageAndImageButton.png
new file mode 100644
index 0000000000..f095a928fd
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnImageAndImageButton.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnLabelAndButton.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnLabelAndButton.png
new file mode 100644
index 0000000000..06747f98fa
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnLabelAndButton.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSearchBarAndPicker.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSearchBarAndPicker.png
new file mode 100644
index 0000000000..d581758d77
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSearchBarAndPicker.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSliderAndProgressBar.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSliderAndProgressBar.png
new file mode 100644
index 0000000000..d99c64d614
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSliderAndProgressBar.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSwitchAndStepper.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSwitchAndStepper.png
new file mode 100644
index 0000000000..e539da53c2
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSwitchAndStepper.png differ
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue12928.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue12928.xaml
new file mode 100644
index 0000000000..c5bae8c6ec
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue12928.xaml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ x:Class="Maui.Controls.Sample.Issues.Issue12928"
+ Title="ImageBrush Background">
+
+ <Grid RowDefinitions="Auto, *, *, Auto, *, *, Auto"
+ Padding="15"
+ RowSpacing="8">
+
+ <Label x:Name="Control1Label"
+ Text="Label"
+ FontAttributes="Bold"
+ AutomationId="Control1Label"/>
+
+ <ContentView x:Name="Control1ColorContainer"
+ Grid.Row="1"
+ AutomationId="Control1ColorContainer"/>
+
+ <ContentView x:Name="Control1ImageContainer"
+ Grid.Row="2"
+ AutomationId="Control1Container"/>
+
+ <Label x:Name="Control2Label"
+ Grid.Row="3"
+ Text="Button"
+ FontAttributes="Bold"
+ AutomationId="Control2Label"/>
+
+ <ContentView x:Name="Control2ColorContainer"
+ Grid.Row="4"
+ AutomationId="Control2ColorContainer"/>
+
+ <ContentView x:Name="Control2ImageContainer"
+ Grid.Row="5"
+ AutomationId="Control2Container"/>
+
+ <Button Grid.Row="6"
+ Text="Select Controls"
+ Clicked="OnOptionsClicked"
+ AutomationId="OptionsButton"/>
+ </Grid>
+</ContentPage>
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue12928.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue12928.xaml.cs
new file mode 100644
index 0000000000..da4f75e539
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue12928.xaml.cs
@@ -0,0 +1,412 @@
+using Microsoft.Maui.Controls.Shapes;
+using Microsoft.Maui.Graphics;
+
+namespace Maui.Controls.Sample.Issues
+{
+ [Issue(IssueTracker.Github, 12928, "ImageBrush background support for controls", PlatformAffected.All)]
+ public partial class Issue12928 : ContentPage
+ {
+ readonly Dictionary<string, Func<View>> _controlFactories;
+
+ public Issue12928()
+ {
+ InitializeComponent();
+ _controlFactories = CreateControlFactories();
+ ShowControls("Label", "Button");
+ }
+
+ ImageBrush CreateImageBrush()
+ {
+ return new ImageBrush { ImageSource = "groceries.png" };
+ }
+
+ Dictionary<string, Func<View>> CreateControlFactories()
+ {
+ return new Dictionary<string, Func<View>>
+ {
+ ["Label"] = CreateLabel,
+ ["Button"] = CreateButton,
+ ["Entry"] = CreateEntry,
+ ["Editor"] = CreateEditor,
+ ["SearchBar"] = CreateSearchBar,
+ ["Picker"] = CreatePicker,
+ ["DatePicker"] = CreateDatePicker,
+ ["TimePicker"] = CreateTimePicker,
+ ["Switch"] = CreateSwitch,
+ ["Stepper"] = CreateStepper,
+ ["Slider"] = CreateSlider,
+ ["ProgressBar"] = CreateProgressBar,
+ ["CheckBox"] = CreateCheckBox,
+ ["RadioButton"] = CreateRadioButton,
+ ["Image"] = CreateImage,
+ ["ImageButton"] = CreateImageButton,
+ ["ContentView"] = CreateContentView,
+ ["Border"] = CreateBorder,
+ ["CollectionView"] = CreateCollectionView,
+ ["SwipeView"] = CreateSwipeView,
+ ["CarouselView"] = CreateCarouselView,
+ ["StackLayout"] = CreateStackLayout,
+ ["Grid"] = CreateGrid,
+ ["FlexLayout"] = CreateFlexLayout,
+ ["AbsoluteLayout"] = CreateAbsoluteLayout,
+ ["ScrollView"] = CreateScrollView,
+ };
+ }
+
+ public IReadOnlyList<string> ControlNames => _controlFactories.Keys.ToList();
+
+ public void ShowControls(string control1, string control2)
+ {
+ if (_controlFactories.TryGetValue(control1, out var factory1))
+ {
+ Control1Label.Text = control1;
+
+ var colorView1 = factory1();
+ colorView1.Background = new SolidColorBrush(Colors.CornflowerBlue);
+ colorView1.AutomationId = "ColorControl1";
+ Control1ColorContainer.Content = colorView1;
+
+ var imageView1 = factory1();
+ imageView1.Background = CreateImageBrush();
+ Control1ImageContainer.Content = imageView1;
+ }
+
+ if (_controlFactories.TryGetValue(control2, out var factory2))
+ {
+ Control2Label.Text = control2;
+
+ var colorView2 = factory2();
+ colorView2.Background = new SolidColorBrush(Colors.CornflowerBlue);
+ colorView2.AutomationId = "ColorControl2";
+ Control2ColorContainer.Content = colorView2;
+
+ var imageView2 = factory2();
+ imageView2.Background = CreateImageBrush();
+ Control2ImageContainer.Content = imageView2;
+ }
+ }
+
+ async void OnOptionsClicked(object sender, EventArgs e)
+ {
+ await Navigation.PushModalAsync(new NavigationPage(new Issue12928OptionsPage(this)));
+ }
+
+ Label CreateLabel() => new Label
+ {
+ Text = "Label",
+ TextColor = Colors.Purple,
+ FontSize = 18,
+ HeightRequest = 60,
+ VerticalTextAlignment = TextAlignment.Center,
+ HorizontalTextAlignment = TextAlignment.Center,
+ };
+
+ Button CreateButton() => new Button
+ {
+ Text = "Button",
+ HeightRequest = 60,
+ };
+
+ Entry CreateEntry() => new Entry
+ {
+ Placeholder = "Entry",
+ HeightRequest = 50,
+ };
+
+ Editor CreateEditor() => new Editor
+ {
+ Placeholder = "Editor",
+ HeightRequest = 80,
+ };
+
+ SearchBar CreateSearchBar() => new SearchBar
+ {
+ Placeholder = "SearchBar",
+ HeightRequest = 50,
+ };
+
+ Picker CreatePicker()
+ {
+ var picker = new Picker
+ {
+ Title = "Picker",
+ HeightRequest = 50,
+ };
+ picker.Items.Add("Option 1");
+ picker.Items.Add("Option 2");
+ return picker;
+ }
+
+ DatePicker CreateDatePicker() => new DatePicker
+ {
+ Date = new DateTime(2025, 1, 1),
+ HeightRequest = 50,
+ };
+
+ TimePicker CreateTimePicker() => new TimePicker
+ {
+ Time = new TimeSpan(10, 30, 0),
+ HeightRequest = 50,
+ };
+
+ Switch CreateSwitch() => new Switch
+ {
+ IsToggled = false,
+ HeightRequest = 60,
+ };
+
+ Stepper CreateStepper() => new Stepper
+ {
+ HeightRequest = 60,
+ };
+
+ Slider CreateSlider() => new Slider
+ {
+ Minimum = 0,
+ Maximum = 100,
+ Value = 50,
+ HeightRequest = 60,
+ };
+
+ ProgressBar CreateProgressBar() => new ProgressBar
+ {
+ Progress = 0.6,
+ HeightRequest = 40,
+ };
+
+ CheckBox CreateCheckBox() => new CheckBox
+ {
+ IsChecked = false,
+ HeightRequest = 60,
+ };
+
+ RadioButton CreateRadioButton() => new RadioButton
+ {
+ Content = "RadioButton",
+ HeightRequest = 60,
+ };
+
+ Image CreateImage() => new Image
+ {
+ Source = "dotnet_bot.png",
+ HeightRequest = 100,
+ Aspect = Aspect.AspectFit,
+ };
+
+ ImageButton CreateImageButton() => new ImageButton
+ {
+ Source = "dotnet_bot.png",
+ HeightRequest = 80,
+ WidthRequest = 80,
+ };
+
+ ContentView CreateContentView() => new ContentView
+ {
+ HeightRequest = 80,
+ Content = new Label
+ {
+ Text = "ContentView",
+ HorizontalOptions = LayoutOptions.Center,
+ VerticalOptions = LayoutOptions.Center
+ },
+ };
+
+ Border CreateBorder() => new Border
+ {
+ HeightRequest = 120,
+ StrokeThickness = 2,
+ Stroke = Colors.DarkGray,
+ StrokeShape = new RoundRectangle { CornerRadius = 10 },
+ Content = new Label
+ {
+ Text = "Border",
+ HorizontalOptions = LayoutOptions.Center,
+ VerticalOptions = LayoutOptions.Center
+ },
+ };
+
+ CollectionView CreateCollectionView() => new CollectionView
+ {
+ HeightRequest = 120,
+ ItemsSource = new[] { "Item 1", "Item 2", "Item 3" },
+ };
+
+ SwipeView CreateSwipeView()
+ {
+ var swipeView = new SwipeView
+ {
+ HeightRequest = 80,
+ };
+ swipeView.Content = new Label
+ {
+ Text = "SwipeView",
+ HorizontalOptions = LayoutOptions.Center,
+ VerticalOptions = LayoutOptions.Center
+ };
+ return swipeView;
+ }
+
+ CarouselView CreateCarouselView() => new CarouselView
+ {
+ HeightRequest = 120,
+ ItemsSource = new[] { "Slide 1", "Slide 2", "Slide 3" },
+ ItemTemplate = new DataTemplate(() =>
+ {
+ var label = new Label
+ {
+ HorizontalOptions = LayoutOptions.Center,
+ VerticalOptions = LayoutOptions.Center
+ };
+ label.SetBinding(Label.TextProperty, ".");
+ return label;
+ }),
+ };
+
+ StackLayout CreateStackLayout() => new StackLayout
+ {
+ HeightRequest = 120,
+ Children =
+ {
+ new Label { Text = "StackLayout", HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center }
+ }
+ };
+
+ Grid CreateGrid() => new Grid
+ {
+ HeightRequest = 120,
+ Children =
+ {
+ new Label { Text = "Grid", HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center }
+ }
+ };
+
+ FlexLayout CreateFlexLayout() => new FlexLayout
+ {
+ HeightRequest = 120,
+ Children =
+ {
+ new Label { Text = "FlexLayout", HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center }
+ }
+ };
+
+ AbsoluteLayout CreateAbsoluteLayout()
+ {
+ var layout = new AbsoluteLayout
+ {
+ HeightRequest = 120,
+ };
+ var label = new Label { Text = "AbsoluteLayout" };
+ AbsoluteLayout.SetLayoutBounds(label, new Rect(0.5, 0.5, -1, -1));
+ AbsoluteLayout.SetLayoutFlags(label, Microsoft.Maui.Layouts.AbsoluteLayoutFlags.PositionProportional);
+ layout.Children.Add(label);
+ return layout;
+ }
+
+ ScrollView CreateScrollView() => new ScrollView
+ {
+ HeightRequest = 120,
+ Content = new Label
+ {
+ Text = "ScrollView",
+ HorizontalOptions = LayoutOptions.Center,
+ VerticalOptions = LayoutOptions.Center
+ }
+ };
+
+ }
+
+ public class Issue12928OptionsPage : ContentPage
+ {
+ readonly Issue12928 _mainPage;
+ readonly List<string> _selectedControls = new();
+
+ public Issue12928OptionsPage(Issue12928 mainPage)
+ {
+ _mainPage = mainPage;
+ Title = "Select 2 Controls";
+
+ var applyButton = new Button
+ {
+ Text = "Apply",
+ AutomationId = "ApplyButton"
+ };
+ applyButton.Clicked += OnApplyClicked;
+
+ var scrollView = new ScrollView
+ {
+ Content = CreateCheckBoxList()
+ };
+
+ Content = new Grid
+ {
+ RowDefinitions = { new RowDefinition(GridLength.Star), new RowDefinition(GridLength.Auto) },
+ Children = { scrollView, applyButton }
+ };
+ Grid.SetRow(applyButton, 1);
+ }
+
+ Grid CreateCheckBoxList()
+ {
+ var grid = new Grid
+ {
+ Padding = 10,
+ ColumnSpacing = 5,
+ RowSpacing = 0,
+ ColumnDefinitions = { new ColumnDefinition(GridLength.Star), new ColumnDefinition(GridLength.Star) }
+ };
+
+ var controlNames = _mainPage.ControlNames;
+ int rows = (controlNames.Count + 1) / 2;
+ for (int i = 0; i < rows; i++)
+ grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
+
+ for (int i = 0; i < controlNames.Count; i++)
+ {
+ var controlName = controlNames[i];
+ int row = i / 2;
+ int col = i % 2;
+
+ var cell = new HorizontalStackLayout { Spacing = 4 };
+ var checkBox = new CheckBox
+ {
+ AutomationId = $"Check{controlName}",
+ ScaleX = 0.8,
+ ScaleY = 0.8
+ };
+ checkBox.CheckedChanged += (s, e) =>
+ {
+ if (e.Value)
+ _selectedControls.Add(controlName);
+ else
+ _selectedControls.Remove(controlName);
+ };
+ var label = new Label
+ {
+ Text = controlName,
+ FontSize = 12,
+ VerticalOptions = LayoutOptions.Center
+ };
+
+ cell.Children.Add(checkBox);
+ cell.Children.Add(label);
+ grid.Add(cell, col, row);
+ }
+
+ return grid;
+ }
+
+ async void OnApplyClicked(object sender, EventArgs e)
+ {
+ if (_selectedControls.Count >= 2)
+ {
+ _mainPage.ShowControls(_selectedControls[0], _selectedControls[1]);
+ }
+ else if (_selectedControls.Count == 1)
+ {
+ _mainPage.ShowControls(_selectedControls[0], _selectedControls[0]);
+ }
+
+ await Navigation.PopModalAsync();
+ }
+ }
+}
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnAbsoluteLayoutAndScrollView.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnAbsoluteLayoutAndScrollView.png
new file mode 100644
index 0000000000..88b3891499
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnAbsoluteLayoutAndScrollView.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCarouselViewAndStackLayout.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCarouselViewAndStackLayout.png
new file mode 100644
index 0000000000..dee4cb1ce3
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCarouselViewAndStackLayout.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCheckBoxAndRadioButton.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCheckBoxAndRadioButton.png
new file mode 100644
index 0000000000..f11bda65b0
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCheckBoxAndRadioButton.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCollectionViewAndSwipeView.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCollectionViewAndSwipeView.png
new file mode 100644
index 0000000000..fad31b8d02
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCollectionViewAndSwipeView.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnContentViewAndBorder.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnContentViewAndBorder.png
new file mode 100644
index 0000000000..0d881d612f
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnContentViewAndBorder.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnDatePickerAndTimePicker.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnDatePickerAndTimePicker.png
new file mode 100644
index 0000000000..c29188ec46
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnDatePickerAndTimePicker.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnEntryAndEditor.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnEntryAndEditor.png
new file mode 100644
index 0000000000..b220e19e71
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnEntryAndEditor.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnGridAndFlexLayout.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnGridAndFlexLayout.png
new file mode 100644
index 0000000000..b0b1a013fe
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnGridAndFlexLayout.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnImageAndImageButton.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnImageAndImageButton.png
new file mode 100644
index 0000000000..d40398c134
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnImageAndImageButton.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnLabelAndButton.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnLabelAndButton.png
new file mode 100644
index 0000000000..9e5ae8f726
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnLabelAndButton.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSearchBarAndPicker.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSearchBarAndPicker.png
new file mode 100644
index 0000000000..dba87168a9
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSearchBarAndPicker.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSliderAndProgressBar.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSliderAndProgressBar.png
new file mode 100644
index 0000000000..c66244344a
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSliderAndProgressBar.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSwitchAndStepper.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSwitchAndStepper.png
new file mode 100644
index 0000000000..a5154aba1d
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSwitchAndStepper.png differ
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs
new file mode 100644
index 0000000000..b4e13de495
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs
@@ -0,0 +1,134 @@
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues
+{
+ public class Issue12928 : _IssuesUITest
+ {
+ public Issue12928(TestDevice testDevice) : base(testDevice)
+ {
+ }
+
+ public override string Issue => "ImageBrush background support for controls";
+
+ [Test, Order(1)]
+ [Category(UITestCategories.Brush)]
+ public void ImageBrushOnLabelAndButton()
+ {
+ SelectControls("Label", "Button");
+ VerifyScreenshot();
+ }
+
+ [Test, Order(2)]
+ [Category(UITestCategories.Brush)]
+ public void ImageBrushOnEntryAndEditor()
+ {
+ SelectControls("Entry", "Editor");
+ VerifyScreenshot();
+ }
+
+ [Test, Order(3)]
+ [Category(UITestCategories.Brush)]
+ public void ImageBrushOnSearchBarAndPicker()
+ {
+ SelectControls("SearchBar", "Picker");
+ VerifyScreenshot();
+ }
+
+ [Test, Order(4)]
+ [Category(UITestCategories.Brush)]
+ public void ImageBrushOnDatePickerAndTimePicker()
+ {
+ SelectControls("DatePicker", "TimePicker");
+ VerifyScreenshot();
+ }
+
+ [Test, Order(5)]
+ [Category(UITestCategories.Brush)]
+ public void ImageBrushOnSwitchAndStepper()
+ {
+ SelectControls("Switch", "Stepper");
+ VerifyScreenshot();
+ }
+
+ [Test, Order(6)]
+ [Category(UITestCategories.Brush)]
+ public void ImageBrushOnSliderAndProgressBar()
+ {
+ SelectControls("Slider", "ProgressBar");
+ VerifyScreenshot();
+ }
+
+ [Test, Order(7)]
+ [Category(UITestCategories.Brush)]
+ public void ImageBrushOnCheckBoxAndRadioButton()
+ {
+ SelectControls("CheckBox", "RadioButton");
+ VerifyScreenshot();
+ }
+
+ [Test, Order(8)]
+ [Category(UITestCategories.Brush)]
+ public void ImageBrushOnImageAndImageButton()
+ {
+ SelectControls("Image", "ImageButton");
+ VerifyScreenshot();
+ }
+
+ [Test, Order(9)]
+ [Category(UITestCategories.Brush)]
+ public void ImageBrushOnContentViewAndBorder()
+ {
+ SelectControls("ContentView", "Border");
+ VerifyScreenshot();
+ }
+
+ [Test, Order(10)]
+ [Category(UITestCategories.Brush)]
+ public void ImageBrushOnCollectionViewAndSwipeView()
+ {
+ SelectControls("CollectionView", "SwipeView");
+ VerifyScreenshot();
+ }
+
+ [Test, Order(13)]
+ [Category(UITestCategories.Brush)]
+ public void ImageBrushOnCarouselViewAndStackLayout()
+ {
+ SelectControls("CarouselView", "StackLayout");
+ VerifyScreenshot();
+ }
+
+ [Test, Order(11)]
+ [Category(UITestCategories.Brush)]
+ public void ImageBrushOnGridAndFlexLayout()
+ {
+ SelectControls("Grid", "FlexLayout");
+ VerifyScreenshot();
+ }
+
+ [Test, Order(12)]
+ [Category(UITestCategories.Brush)]
+ public void ImageBrushOnAbsoluteLayoutAndScrollView()
+ {
+ SelectControls("AbsoluteLayout", "ScrollView");
+ VerifyScreenshot();
+ }
+
+
+ void SelectControls(string control1, string control2)
+ {
+ App.WaitForElement("OptionsButton");
+ App.Click("OptionsButton");
+
+ App.WaitForElement($"Check{control1}");
+ App.Click($"Check{control1}");
+ App.Click($"Check{control2}");
+
+ App.WaitForElement("ApplyButton");
+ App.Click("ApplyButton");
+ App.WaitForElement("OptionsButton");
+ }
+ }
+}
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnAbsoluteLayoutAndScrollView.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnAbsoluteLayoutAndScrollView.png
new file mode 100644
index 0000000000..49cca8fb79
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnAbsoluteLayoutAndScrollView.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCarouselViewAndStackLayout.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCarouselViewAndStackLayout.png
new file mode 100644
index 0000000000..797857d352
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCarouselViewAndStackLayout.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCheckBoxAndRadioButton.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCheckBoxAndRadioButton.png
new file mode 100644
index 0000000000..89e4d68d7b
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCheckBoxAndRadioButton.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCollectionViewAndSwipeView.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCollectionViewAndSwipeView.png
new file mode 100644
index 0000000000..74ee6e9bf8
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCollectionViewAndSwipeView.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnContentViewAndBorder.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnContentViewAndBorder.png
new file mode 100644
index 0000000000..ff258fa1eb
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnContentViewAndBorder.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnDatePickerAndTimePicker.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnDatePickerAndTimePicker.png
new file mode 100644
index 0000000000..b5ad5c6251
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnDatePickerAndTimePicker.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnEntryAndEditor.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnEntryAndEditor.png
new file mode 100644
index 0000000000..1e7d0cadad
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnEntryAndEditor.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnGridAndFlexLayout.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnGridAndFlexLayout.png
new file mode 100644
index 0000000000..cdc81e6850
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnGridAndFlexLayout.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnImageAndImageButton.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnImageAndImageButton.png
new file mode 100644
index 0000000000..6e2ca85eed
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnImageAndImageButton.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnLabelAndButton.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnLabelAndButton.png
new file mode 100644
index 0000000000..f0ec957371
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnLabelAndButton.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSearchBarAndPicker.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSearchBarAndPicker.png
new file mode 100644
index 0000000000..a33a717c56
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSearchBarAndPicker.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSliderAndProgressBar.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSliderAndProgressBar.png
new file mode 100644
index 0000000000..6c9ab22386
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSliderAndProgressBar.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSwitchAndStepper.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSwitchAndStepper.png
new file mode 100644
index 0000000000..0ba827fe1d
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSwitchAndStepper.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnAbsoluteLayoutAndScrollView.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnAbsoluteLayoutAndScrollView.png
new file mode 100644
index 0000000000..d99e182e85
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnAbsoluteLayoutAndScrollView.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCarouselViewAndStackLayout.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCarouselViewAndStackLayout.png
new file mode 100644
index 0000000000..b78fb80315
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCarouselViewAndStackLayout.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCheckBoxAndRadioButton.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCheckBoxAndRadioButton.png
new file mode 100644
index 0000000000..a7cb953b60
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCheckBoxAndRadioButton.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCollectionViewAndSwipeView.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCollectionViewAndSwipeView.png
new file mode 100644
index 0000000000..065cbd2dde
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCollectionViewAndSwipeView.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnContentViewAndBorder.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnContentViewAndBorder.png
new file mode 100644
index 0000000000..f2b8691c2a
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnContentViewAndBorder.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnDatePickerAndTimePicker.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnDatePickerAndTimePicker.png
new file mode 100644
index 0000000000..ef84984773
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnDatePickerAndTimePicker.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnEntryAndEditor.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnEntryAndEditor.png
new file mode 100644
index 0000000000..a5085cc239
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnEntryAndEditor.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnGridAndFlexLayout.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnGridAndFlexLayout.png
new file mode 100644
index 0000000000..535d7fc7f9
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnGridAndFlexLayout.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnImageAndImageButton.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnImageAndImageButton.png
new file mode 100644
index 0000000000..c82d69f533
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnImageAndImageButton.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnLabelAndButton.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnLabelAndButton.png
new file mode 100644
index 0000000000..a6ad5d0800
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnLabelAndButton.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSearchBarAndPicker.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSearchBarAndPicker.png
new file mode 100644
index 0000000000..6d75120a70
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSearchBarAndPicker.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSliderAndProgressBar.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSliderAndProgressBar.png
new file mode 100644
index 0000000000..7488b026af
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSliderAndProgressBar.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSwitchAndStepper.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSwitchAndStepper.png
new file mode 100644
index 0000000000..5dc566055d
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSwitchAndStepper.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnAbsoluteLayoutAndScrollView.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnAbsoluteLayoutAndScrollView.png
new file mode 100644
index 0000000000..639c98bc51
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnAbsoluteLayoutAndScrollView.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCarouselViewAndStackLayout.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCarouselViewAndStackLayout.png
new file mode 100644
index 0000000000..47f5a1658e
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCarouselViewAndStackLayout.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCheckBoxAndRadioButton.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCheckBoxAndRadioButton.png
new file mode 100644
index 0000000000..4321aae97a
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCheckBoxAndRadioButton.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCollectionViewAndSwipeView.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCollectionViewAndSwipeView.png
new file mode 100644
index 0000000000..abe9e503e3
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCollectionViewAndSwipeView.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnContentViewAndBorder.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnContentViewAndBorder.png
new file mode 100644
index 0000000000..a38864a449
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnContentViewAndBorder.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnDatePickerAndTimePicker.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnDatePickerAndTimePicker.png
new file mode 100644
index 0000000000..d5dd9c0b2c
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnDatePickerAndTimePicker.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnEntryAndEditor.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnEntryAndEditor.png
new file mode 100644
index 0000000000..2cb4d5621d
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnEntryAndEditor.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnGridAndFlexLayout.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnGridAndFlexLayout.png
new file mode 100644
index 0000000000..f1acb6b05b
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnGridAndFlexLayout.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnImageAndImageButton.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnImageAndImageButton.png
new file mode 100644
index 0000000000..919ba5b307
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnImageAndImageButton.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnLabelAndButton.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnLabelAndButton.png
new file mode 100644
index 0000000000..bf6b258d29
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnLabelAndButton.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSearchBarAndPicker.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSearchBarAndPicker.png
new file mode 100644
index 0000000000..a1cab5216e
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSearchBarAndPicker.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSliderAndProgressBar.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSliderAndProgressBar.png
new file mode 100644
index 0000000000..f289ff82f9
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSliderAndProgressBar.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSwitchAndStepper.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSwitchAndStepper.png
new file mode 100644
index 0000000000..50444613b6
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSwitchAndStepper.png differ
diff --git a/src/Core/src/Handlers/Button/ButtonHandler.iOS.cs b/src/Core/src/Handlers/Button/ButtonHandler.iOS.cs
index 678e953c4a..68c4b24656 100644
--- a/src/Core/src/Handlers/Button/ButtonHandler.iOS.cs
+++ b/src/Core/src/Handlers/Button/ButtonHandler.iOS.cs
@@ -51,8 +51,12 @@ namespace Microsoft.Maui.Handlers
#if MACCATALYST
public static void MapBackground(IButtonHandler handler, IButton button)
{
+ if (button.Background is ImageSourcePaint sourcePaint)
+ {
+ handler.PlatformView?.UpdateBackgroundImageSource(sourcePaint.ImageSource, handler);
+ }
//If this is a Mac optimized interface
- if (OperatingSystem.IsIOSVersionAtLeast(15) && UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Mac)
+ else if (OperatingSystem.IsIOSVersionAtLeast(15) && UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Mac)
{
var config = handler.PlatformView?.Configuration ?? UIButtonConfiguration.BorderedButtonConfiguration;
if (button?.Background is Paint paint)
@@ -80,7 +84,14 @@ namespace Microsoft.Maui.Handlers
// TODO: Make this public in .NET 11
internal static void MapBackground(IButtonHandler handler, IButton button)
{
- handler.PlatformView?.UpdateBackground(button.Background);
+ if (button.Background is ImageSourcePaint sourcePaint)
+ {
+ handler.PlatformView?.UpdateBackgroundImageSource(sourcePaint.ImageSource, handler);
+ }
+ else
+ {
+ handler.PlatformView?.UpdateBackground(button.Background);
+ }
}
#endif
diff --git a/src/Core/src/Platform/Android/ButtonExtensions.cs b/src/Core/src/Platform/Android/ButtonExtensions.cs
index fc96087a32..a65e7ffec9 100644
--- a/src/Core/src/Platform/Android/ButtonExtensions.cs
+++ b/src/Core/src/Platform/Android/ButtonExtensions.cs
@@ -2,6 +2,7 @@
using Google.Android.Material.Button;
using Microsoft.Maui.Graphics;
using AColor = Android.Graphics.Color;
+using AView = Android.Views.View;
using R = Android.Resource;
namespace Microsoft.Maui.Platform
@@ -60,33 +61,45 @@ namespace Microsoft.Maui.Platform
internal static void UpdateButtonBackground(this MaterialButton platformView, IButton button)
{
- platformView.UpdateMauiRippleDrawableBackground(
- button.Background,
- button,
- () =>
- {
- // Copy the tints from a temporary button.
- // TODO: optimize this to avoid creating a new button every time.
-
- var context = platformView.Context!;
- using var btn = new MaterialButton(context);
- var defaultTintList = btn.BackgroundTintList;
- var defaultColor = defaultTintList?.GetColorForState([R.Attribute.StateEnabled], AColor.Black);
-
- return defaultColor ?? AColor.Black;
- },
- () =>
- {
- // If some theme or user value has been set, we can override the default, white
- // ripple color using this button property.
- return platformView.RippleColor;
- },
- () =>
- {
- // We have a background, so we need to null out the tint list to avoid the tint
- // overriding the background.
- platformView.BackgroundTintList = null;
- });
+ if (button.Background is ImageSourcePaint sourcePaint)
+ {
+ ((AView)platformView).UpdateButtonBackgroundImageSource(
+ sourcePaint.ImageSource,
+ button.Handler,
+ button,
+ () => platformView.RippleColor,
+ () => { platformView.BackgroundTintList = null; });
+ }
+ else
+ {
+ platformView.UpdateMauiRippleDrawableBackground(
+ button.Background,
+ button,
+ () =>
+ {
+ // Copy the tints from a temporary button.
+ // TODO: optimize this to avoid creating a new button every time.
+
+ var context = platformView.Context!;
+ using var btn = new MaterialButton(context);
+ var defaultTintList = btn.BackgroundTintList;
+ var defaultColor = defaultTintList?.GetColorForState([R.Attribute.StateEnabled], AColor.Black);
+
+ return defaultColor ?? AColor.Black;
+ },
+ () =>
+ {
+ // If some theme or user value has been set, we can override the default, white
+ // ripple color using this button property.
+
... [truncated]The diff was truncated to fit GitHub's review body limit.
kubaflo
left a comment
There was a problem hiding this comment.
Could you please resolve conflicts and check the ai's suggestions?
8aca808 to
3b8027b
Compare
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Description
This pull request introduces support for using image backgrounds on controls by adding and exposing the
ImageBrushclass, and updates platform-specific background mapping logic to handle image backgrounds consistently across Android, iOS, Windows, MacCatalyst, and Tizen.The main changes include making
ImageBrushpublic, updating public API files for all target platforms, and modifying background mapping functions to check for and apply image backgrounds before falling back to default logic.Changes
Image background support and API changes
ImageBrushclass public inImageBrush.csand exposed its properties and methods in the public API files for all platforms, enabling developers to use image backgrounds in their controls.ImageBrush(constructors, property accessors, overrides) to thePublicAPI.Unshipped.txtfiles for Android, iOS, MacCatalyst, Tizen, Windows, .NET, and netstandard.Platform-specific background handling improvements
Android:
iOS/MacCatalyst:
Windows:
Shared helpers to reduce duplication
Issues Fixed
Fixes #12928
Output
Issues / Limitations