diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png new file mode 100644 index 000000000000..3cfafd6540d3 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue30403.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue30403.cs new file mode 100644 index 000000000000..d11943ede035 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue30403.cs @@ -0,0 +1,26 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 30403, "Image under WinUI does not respect VerticalOptions and HorizontalOptions with AspectFit", PlatformAffected.UWP)] +public class Issue30403 : TestContentPage +{ + protected override void Init() + { + Title = "Issue 30403"; + + Content = new Grid + { + BackgroundColor = Colors.LightGray, + Children = + { + new Image + { + AutomationId = "TestImage", + Source = "dotnet_bot.png", + Aspect = Aspect.AspectFit, + VerticalOptions = LayoutOptions.Center, + HorizontalOptions = LayoutOptions.Center, + } + } + }; + } +} diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png new file mode 100644 index 000000000000..22aa5071ed34 Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png differ diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30403.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30403.cs new file mode 100644 index 000000000000..ea0d4fb68db1 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30403.cs @@ -0,0 +1,25 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue30403 : _IssuesUITest +{ + public Issue30403(TestDevice testDevice) : base(testDevice) + { + } + + public override string Issue => "Image under WinUI does not respect VerticalOptions and HorizontalOptions with AspectFit"; + + [Test] + [Category(UITestCategories.Image)] + public void ImageRespectsVerticalAndHorizontalOptionsWithAspectFit() + { + App.WaitForElement("TestImage"); + + // Verify that the image is positioned correctly according to VerticalOptions.Center and HorizontalOptions.Center + // with AspectFit aspect ratio. The image should appear in the center of the Grid. + VerifyScreenshot(); + } +} diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png new file mode 100644 index 000000000000..3c1f7e279e48 Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png differ diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyGif.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyGif.png index 3164c2f8d217..0671ac6b814c 100644 Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyGif.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyGif.png differ diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyJpg.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyJpg.png index 5a8e2b372ff9..ab34d56fbb05 100644 Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyJpg.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyJpg.png differ diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyPng.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyPng.png index e2a27d10c753..2c9fc3947288 100644 Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyPng.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyPng.png differ diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifySvg.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifySvg.png index 0be5c5278b05..65b6fa0da96b 100644 Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifySvg.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifySvg.png differ diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/NoScrollbarsTest.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/NoScrollbarsTest.png index 202eb5acc2a8..ec97d75334f0 100644 Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/NoScrollbarsTest.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/NoScrollbarsTest.png differ diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/RadioButtonContentNotRendering.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/RadioButtonContentNotRendering.png index 69726e16d53f..438d1e344a01 100644 Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/RadioButtonContentNotRendering.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/RadioButtonContentNotRendering.png differ diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShadowsDontRespectControlShape.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShadowsDontRespectControlShape.png index 6fd5df1edce0..a2fe0a78c813 100644 Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShadowsDontRespectControlShape.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShadowsDontRespectControlShape.png differ diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/VerifyModelItemsObservableCollectionWhenMultipleModePreSelection.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/VerifyModelItemsObservableCollectionWhenMultipleModePreSelection.png index de6c51458ca7..5c884a59c092 100644 Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/VerifyModelItemsObservableCollectionWhenMultipleModePreSelection.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/VerifyModelItemsObservableCollectionWhenMultipleModePreSelection.png differ diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/VerifyModelItemsObservableCollectionWhenSingleModePreSelection.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/VerifyModelItemsObservableCollectionWhenSingleModePreSelection.png index 30dbf3e2d594..4901f26a8da1 100644 Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/VerifyModelItemsObservableCollectionWhenSingleModePreSelection.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/VerifyModelItemsObservableCollectionWhenSingleModePreSelection.png differ diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png new file mode 100644 index 000000000000..756c5f8f7db6 Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png differ diff --git a/src/Core/src/Handlers/Image/ImageHandler.Windows.cs b/src/Core/src/Handlers/Image/ImageHandler.Windows.cs index ee3a6ddbd533..9cbcc2253d8f 100644 --- a/src/Core/src/Handlers/Image/ImageHandler.Windows.cs +++ b/src/Core/src/Handlers/Image/ImageHandler.Windows.cs @@ -1,7 +1,9 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Imaging; using WImage = Microsoft.UI.Xaml.Controls.Image; namespace Microsoft.Maui.Handlers @@ -55,6 +57,33 @@ protected override void RemoveContainer() UpdateValue(nameof(IView.Width)); } + /// + public override Graphics.Size GetDesiredSize(double widthConstraint, double heightConstraint) + { + // Compute a possible size without mutating platform properties during measure. + var possibleSize = base.GetDesiredSize(widthConstraint, heightConstraint); + + // For AspectFit we want each non-Fill axis (independently) to not exceed intrinsic bitmap size + // so that alignment (Center, Start, End) has space to operate. A Fill axis should remain + // unconstrained here and rely on layout constraints. + if (VirtualView.Aspect == Aspect.AspectFit) + { + var imageSize = GetImageSize(); + double w = possibleSize.Width; + double h = possibleSize.Height; + + if (VirtualView.HorizontalLayoutAlignment != Primitives.LayoutAlignment.Fill && imageSize.Width > 0) + w = Math.Min(w, imageSize.Width); + + if (VirtualView.VerticalLayoutAlignment != Primitives.LayoutAlignment.Fill && imageSize.Height > 0) + h = Math.Min(h, imageSize.Height); + + return new Graphics.Size(w, h); + } + + return possibleSize; + } + /// /// Maps the abstract property to the platform-specific implementations. /// @@ -112,6 +141,9 @@ public static void MapAspect(IImageHandler handler, IImage image) { handler.UpdateValue(nameof(IViewHandler.ContainerView)); handler.PlatformView?.UpdateAspect(image); + // Aspect changes may affect whether we cap to intrinsic size + if (handler is ImageHandler ih) + ih.UpdatePlatformMaxConstraints(); } /// @@ -135,15 +167,75 @@ public static void MapSource(IImageHandler handler, IImage image) => /// /// The associated handler. /// The associated instance. - public static Task MapSourceAsync(IImageHandler handler, IImage image) => - handler.SourceLoader.UpdateImageSourceAsync(); + public static Task MapSourceAsync(IImageHandler handler, IImage image) + { + // Reset platform caps so we don't keep stale values between sources + if (handler is ImageHandler ih && ih.PlatformView is not null) + { + ih.PlatformView.MaxWidth = double.PositiveInfinity; + ih.PlatformView.MaxHeight = double.PositiveInfinity; + } + + return handler.SourceLoader.UpdateImageSourceAsync(); + } void OnImageOpened(object sender, RoutedEventArgs e) { // Because this resolves from a task we should validate that the // handler hasn't been disconnected if (this.IsConnected()) + { UpdateValue(nameof(IImage.IsAnimationPlaying)); + // Apply platform constraints when the decoded size is available + UpdatePlatformMaxConstraints(); + } + } + + /// + /// Updates platform MaxWidth/MaxHeight based on current aspect/alignment and decoded image size. + /// Avoids doing this during GetDesiredSize to prevent side effects across layout passes. + /// + private void UpdatePlatformMaxConstraints() + { + if (PlatformView is null || VirtualView is null) + return; + + if (VirtualView.Aspect == Aspect.AspectFit) + { + var sz = GetImageSize(); + + // Width: cap to intrinsic only if horizontal alignment isn't Fill + if (VirtualView.HorizontalLayoutAlignment != Primitives.LayoutAlignment.Fill && sz.Width > 0) + PlatformView.MaxWidth = Math.Min(sz.Width, VirtualView.MaximumWidth); + else + PlatformView.MaxWidth = VirtualView.MaximumWidth; + + // Height: cap to intrinsic only if vertical alignment isn't Fill + if (VirtualView.VerticalLayoutAlignment != Primitives.LayoutAlignment.Fill && sz.Height > 0) + PlatformView.MaxHeight = Math.Min(sz.Height, VirtualView.MaximumHeight); + else + PlatformView.MaxHeight = VirtualView.MaximumHeight; + + return; + } + + // Non AspectFit: mirror the view's declared maximums + PlatformView.MaxWidth = VirtualView.MaximumWidth; + PlatformView.MaxHeight = VirtualView.MaximumHeight; + } + + private Graphics.Size GetImageSize() + { + if (PlatformView.Source is BitmapSource bitmap) + { + // BitmapSource may not have PixelWidth/PixelHeight set until image is loaded + if (bitmap.PixelWidth > 0 && bitmap.PixelHeight > 0) + { + return new Graphics.Size(bitmap.PixelWidth, bitmap.PixelHeight); + } + // If not available, return zero + } + return Graphics.Size.Zero; } partial class ImageImageSourcePartSetter diff --git a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt index ad4534aa97fd..a060bf128ef1 100644 --- a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -147,6 +147,7 @@ override Microsoft.Maui.Converters.ThicknessTypeConverter.CanConvertFrom(System. override Microsoft.Maui.Converters.ThicknessTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool override Microsoft.Maui.Converters.ThicknessTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? override Microsoft.Maui.Converters.ThicknessTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object? +override Microsoft.Maui.Handlers.ImageHandler.GetDesiredSize(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size override Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.Equals(object? obj) -> bool override Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.GetHashCode() -> int override Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.ToString() -> string!