diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index ed04dd7d9228..be877a250424 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -102,6 +102,9 @@ dotnet cake --target=dotnet-pack
### Testing and Debugging
#### Testing Guidelines
+- **Always** ensure code compiles before running tests to validate changes, otherwise you might be running tests against old code
+- **Always** run unit tests for any code changes before finishing
+- **Never** leave failing unit tests that were introduced by your changes
- Add tests for new functionality
- Ensure existing tests pass:
- `src/Core/tests/UnitTests/Core.UnitTests.csproj`
@@ -109,6 +112,9 @@ dotnet cake --target=dotnet-pack
- `src/Compatibility/Core/tests/Compatibility.UnitTests/Compatibility.Core.UnitTests.csproj`
- `src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj`
- `src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj`
+- Use `dotnet test` to run specific test projects or test filters
+- When adding new unit tests, ensure they pass consistently
+- Fix any test failures before committing and pushing changes
### Code Formatting
@@ -174,10 +180,25 @@ When working on an issue:
- More robust implementation patterns
### Files to Never Commit
-- **Never** check in changes to `cgmanifest.json` files
-- **Never** check in changes to `templatestrings.json` files
+- **Never** check in changes to `cgmanifest.json` files (especially in `Templates/src/` directory)
+- **Never** check in changes to `templatestrings.json` files (especially in `Templates/src/` directory)
+- **Always revert** any changes to JSON files in the `Templates/src/` directory before committing
- These files are automatically generated and should not be modified manually
+
+
+### Platform-Specific Restrictions
+- **Never** make changes to files related to Tizen platform
+- Tizen-specific code should not be modified unless explicitly required for critical fixes
+
+### Testing Guidelines
+- **Always** ensure code compiles before running tests to validate changes, otherwise you might be running tests against old code
+- **Always** run unit tests for any code changes before finishing
+- **Never** leave failing unit tests that were introduced by your changes
+- Use `dotnet test` to run specific test projects or test filters
+- When adding new unit tests, ensure they pass consistently
+- Fix any test failures before committing and pushing changes
+
### File Reset Guidelines for AI Agents
Since coding agents function as both CI and pair programmers, they need to handle CI-generated files appropriately:
diff --git a/src/Controls/src/Core/View/View.cs b/src/Controls/src/Core/View/View.cs
index 883f42ddbba4..aec096a3fc0f 100644
--- a/src/Controls/src/Core/View/View.cs
+++ b/src/Controls/src/Core/View/View.cs
@@ -17,7 +17,7 @@ namespace Microsoft.Maui.Controls
/// This is the base class for and most of the controls.
/// Because ultimately inherits from , application developers can use the Model-View-ViewModel architecture, as well as XAML, to develop portable user interfaces.
///
- public partial class View : VisualElement, IViewController, IGestureController, IGestureRecognizers, IView, IPropertyMapperView, IHotReloadableView, IControlsView
+ public partial class View : VisualElement, IViewController, IGestureController, IGestureRecognizers, IView, IPropertyMapperView, IHotReloadableView, IControlsView, IViewWithWindow
{
protected internal IGestureController GestureController => this;
@@ -314,6 +314,8 @@ internal protected PropertyMapper GetRendererOverrides() where T : IView =
IReloadHandler IHotReloadableView.ReloadHandler { get; set; }
+ IWindow? IViewWithWindow.Window => Window;
+
void IHotReloadableView.TransferState(IView newView)
{
//TODO: LEt you hot reload the the ViewModel
diff --git a/src/Controls/tests/DeviceTests/Elements/View/ViewTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/View/ViewTests.Android.cs
index 1b1a8bfc0ca2..4e802b6d19e2 100644
--- a/src/Controls/tests/DeviceTests/Elements/View/ViewTests.Android.cs
+++ b/src/Controls/tests/DeviceTests/Elements/View/ViewTests.Android.cs
@@ -9,6 +9,7 @@
using Microsoft.Maui;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Handlers;
+using Microsoft.Maui.Devices;
using Microsoft.Maui.DeviceTests.Stubs;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
@@ -81,7 +82,8 @@ await CreateHandlerAndAddToWindow(grid, (LayoutHandler handler) =>
// This fails sometimes due to the way we arrange the content based on coordinates instead of size
// Assert.Equal(expectedWidth, pxFrame.Width);
- Assert.True(pxFrame.Left == lastRight);
+
+ Assert.True(pxFrame.Left == lastRight, $"ColumnCount: {columnCount} Expected Left {lastRight} but got {pxFrame.Left} for child {i} Device Info: {DeviceDisplay.Current.MainDisplayInfo}");
lastRight = pxFrame.Right;
}
diff --git a/src/Controls/tests/DeviceTests/Stubs/WindowHandlerStub.Android.cs b/src/Controls/tests/DeviceTests/Stubs/WindowHandlerStub.Android.cs
index 34666b2b8364..aa3fd413c90d 100644
--- a/src/Controls/tests/DeviceTests/Stubs/WindowHandlerStub.Android.cs
+++ b/src/Controls/tests/DeviceTests/Stubs/WindowHandlerStub.Android.cs
@@ -17,6 +17,11 @@ public class WindowHandlerStub : ElementHandler, IWindowHand
[nameof(IWindow.Content)] = MapContent
};
+ public static CommandMapper CommandMapper = new(ElementCommandMapper)
+ {
+ [nameof(IWindow.RequestDisplayDensity)] = WindowHandler.MapRequestDisplayDensity,
+ };
+
public AView PlatformViewUnderTest { get; private set; }
void UpdateContent()
@@ -50,7 +55,7 @@ protected override void DisconnectHandler(AActivity platformView)
}
public WindowHandlerStub()
- : base(WindowMapper)
+ : base(WindowMapper, CommandMapper)
{
}
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AccessibilityTraitsSetCorrectlyNone.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AccessibilityTraitsSetCorrectlyNone.png
index fb150b2a7eae..f40e22795d94 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AccessibilityTraitsSetCorrectlyNone.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AccessibilityTraitsSetCorrectlyNone.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ButtonsLayoutResolveWhenParentSizeChangesOriginal.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ButtonsLayoutResolveWhenParentSizeChangesOriginal.png
index 4428cf1b1e63..828942cc7abf 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ButtonsLayoutResolveWhenParentSizeChangesOriginal.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ButtonsLayoutResolveWhenParentSizeChangesOriginal.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CarouselViewShouldRenderCorrectly.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CarouselViewShouldRenderCorrectly.png
index 38d36f30bb96..058711187fc9 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CarouselViewShouldRenderCorrectly.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CarouselViewShouldRenderCorrectly.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CheckBox_ChangeColor_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CheckBox_ChangeColor_VerifyVisualState.png
index cd79afb1dbe4..2749c81d6463 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CheckBox_ChangeColor_VerifyVisualState.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CheckBox_ChangeColor_VerifyVisualState.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CheckBox_SetIsCheckedAndColor_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CheckBox_SetIsCheckedAndColor_VerifyVisualState.png
index 9512f06aeff9..2d13ac997b5f 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CheckBox_SetIsCheckedAndColor_VerifyVisualState.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CheckBox_SetIsCheckedAndColor_VerifyVisualState.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CollectionViewMeasureFirstItem.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CollectionViewMeasureFirstItem.png
index 57053630bc6e..ce9b4e3907d3 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CollectionViewMeasureFirstItem.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CollectionViewMeasureFirstItem.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/HeaderFooterGridWorks.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/HeaderFooterGridWorks.png
index 75e34a36b56b..61af326db4fc 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/HeaderFooterGridWorks.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/HeaderFooterGridWorks.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/HeaderFooterTemplateWorks.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/HeaderFooterTemplateWorks.png
index cb1e3dff6dfe..3d2fee249b2a 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/HeaderFooterTemplateWorks.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/HeaderFooterTemplateWorks.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageButtonUITests_Aspect_State_Center.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageButtonUITests_Aspect_State_Center.png
index 2bec8eb3b565..a0dd63a9e6fa 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageButtonUITests_Aspect_State_Center.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageButtonUITests_Aspect_State_Center.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageButtonUITests_BorderColor_WithBackground.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageButtonUITests_BorderColor_WithBackground.png
index 81f4b3a5f167..d5594932f754 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageButtonUITests_BorderColor_WithBackground.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageButtonUITests_BorderColor_WithBackground.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageButtonUITests_BorderWidth_WithBackground.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageButtonUITests_BorderWidth_WithBackground.png
index 23096c338710..eb9c24a203e3 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageButtonUITests_BorderWidth_WithBackground.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageButtonUITests_BorderWidth_WithBackground.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test.png
index 1f3ca951928b..f90bc523f2f1 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_1.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_1.png
index 87e0b8f27731..232f531733de 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_1.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_1.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_2.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_2.png
index 2c3b7e0b3cc7..351b9e209f97 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_2.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_2.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_3.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_3.png
index 088f43d811c1..ca654ca5078a 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_3.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_3.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_4.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_4.png
index 4e4d91dbf6b7..f2394d3bd801 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_4.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_4.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_5.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_5.png
index 9b2e44b9da18..93a764a5c4b5 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_5.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_5.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue7823TestIsClippedIssue.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue7823TestIsClippedIssue.png
index 8fdf416436b2..43c8819cd124 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue7823TestIsClippedIssue.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue7823TestIsClippedIssue.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ItemImageSourceShouldBeVisible.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ItemImageSourceShouldBeVisible.png
index 34fe4a3ec89b..01cc16cded6c 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ItemImageSourceShouldBeVisible.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ItemImageSourceShouldBeVisible.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/LabelUITests_FontFamily_FontAwesome.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/LabelUITests_FontFamily_FontAwesome.png
index 38aca9372534..92ab8851b9e7 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/LabelUITests_FontFamily_FontAwesome.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/LabelUITests_FontFamily_FontAwesome.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/LabelUITests_FontFamily_Ionicons.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/LabelUITests_FontFamily_Ionicons.png
index 89156a968543..cf97759134a9 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/LabelUITests_FontFamily_Ionicons.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/LabelUITests_FontFamily_Ionicons.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_Checking_Default_Configuration_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_Checking_Default_Configuration_VerifyVisualState.png
index 7b347219b7ac..c0316714beef 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_Checking_Default_Configuration_VerifyVisualState.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_Checking_Default_Configuration_VerifyVisualState.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_Checking_Initial_Configuration_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_Checking_Initial_Configuration_VerifyVisualState.png
index 7cf311715f76..8365c59ad444 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_Checking_Initial_Configuration_VerifyVisualState.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_Checking_Initial_Configuration_VerifyVisualState.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_FlowDirectionAndContent_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_FlowDirectionAndContent_VerifyVisualState.png
index 0204567f86c6..6edaed55f0a1 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_FlowDirectionAndContent_VerifyVisualState.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_FlowDirectionAndContent_VerifyVisualState.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetContentAndCharacterSpacing_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetContentAndCharacterSpacing_VerifyVisualState.png
index fca4c05e893e..6fcd15b4def7 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetContentAndCharacterSpacing_VerifyVisualState.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetContentAndCharacterSpacing_VerifyVisualState.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetContentAndFontAttributes_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetContentAndFontAttributes_VerifyVisualState.png
index 3211ef3a01f6..01cfb44e43e9 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetContentAndFontAttributes_VerifyVisualState.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetContentAndFontAttributes_VerifyVisualState.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetContentAndFontSize_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetContentAndFontSize_VerifyVisualState.png
index ad0adc862045..6e40860068c0 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetContentAndFontSize_VerifyVisualState.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetContentAndFontSize_VerifyVisualState.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetContentAndTextColor_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetContentAndTextColor_VerifyVisualState.png
index f9b1ab0ca841..91abba3a2a07 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetContentAndTextColor_VerifyVisualState.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetContentAndTextColor_VerifyVisualState.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetFontAttributesAndTextColor_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetFontAttributesAndTextColor_VerifyVisualState.png
index 4f05c29b4639..31ac98f469e6 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetFontAttributesAndTextColor_VerifyVisualState.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetFontAttributesAndTextColor_VerifyVisualState.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetFontFamilyAndFontAttributes_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetFontFamilyAndFontAttributes_VerifyVisualState.png
index f44b4f510ea8..e11a4bfa96f1 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetFontFamilyAndFontAttributes_VerifyVisualState.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetFontFamilyAndFontAttributes_VerifyVisualState.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetFontFamilyAndFontSize_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetFontFamilyAndFontSize_VerifyVisualState.png
index 749bfa6f47f1..525a087a54be 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetFontFamilyAndFontSize_VerifyVisualState.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetFontFamilyAndFontSize_VerifyVisualState.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetFontSizeAndFontAttributes_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetFontSizeAndFontAttributes_VerifyVisualState.png
index 706129dfbf39..324b1df0da85 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetFontSizeAndFontAttributes_VerifyVisualState.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButton_SetFontSizeAndFontAttributes_VerifyVisualState.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ShadowShouldUpdate.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ShadowShouldUpdate.png
index 0741c7fd102f..07514a253ec4 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ShadowShouldUpdate.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ShadowShouldUpdate.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ShouldDisplayLabelWithoutBeingCroppedInsideBorder.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ShouldDisplayLabelWithoutBeingCroppedInsideBorder.png
new file mode 100644
index 000000000000..71dad3f01812
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ShouldDisplayLabelWithoutBeingCroppedInsideBorder.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/TestDynamicItemTemplateChangeInCarouselView.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/TestDynamicItemTemplateChangeInCarouselView.png
index 6754a9a1abcc..20483ec01f1d 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/TestDynamicItemTemplateChangeInCarouselView.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/TestDynamicItemTemplateChangeInCarouselView.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ToolbarItemFontColorDynamicUpdate.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ToolbarItemFontColorDynamicUpdate.png
index 2bae9b2f9188..cf22a7e4eea0 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ToolbarItemFontColorDynamicUpdate.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ToolbarItemFontColorDynamicUpdate.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ToolbarTextColorOnInteraction.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ToolbarTextColorOnInteraction.png
index 6f9e6072a5dc..8b6febaa180e 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ToolbarTextColorOnInteraction.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ToolbarTextColorOnInteraction.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyModelItemsGroupedListWhenMultipleModePreSelection.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyModelItemsGroupedListWhenMultipleModePreSelection.png
index e1a23819b20c..bdbab5c17c21 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyModelItemsGroupedListWhenMultipleModePreSelection.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyModelItemsGroupedListWhenMultipleModePreSelection.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyModelItemsGroupedListWhenSingleModePreSelection.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyModelItemsGroupedListWhenSingleModePreSelection.png
index 22a205f2aa3f..cde58133242d 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyModelItemsGroupedListWhenSingleModePreSelection.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyModelItemsGroupedListWhenSingleModePreSelection.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyModelItemsObservableCollectionWhenMultipleModePreSelection.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyModelItemsObservableCollectionWhenMultipleModePreSelection.png
index 91d06346ede6..38c6f7eb569d 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyModelItemsObservableCollectionWhenMultipleModePreSelection.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyModelItemsObservableCollectionWhenMultipleModePreSelection.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyModelItemsObservableCollectionWhenSingleModePreSelection.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyModelItemsObservableCollectionWhenSingleModePreSelection.png
index 204b4d616904..43cb7bf9b15e 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyModelItemsObservableCollectionWhenSingleModePreSelection.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyModelItemsObservableCollectionWhenSingleModePreSelection.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeMultipleWhenProgrammaticSelectionWorksWithHorizontalList.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeMultipleWhenProgrammaticSelectionWorksWithHorizontalList.png
index 9c1253e4ca62..9225cd3b0bcf 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeMultipleWhenProgrammaticSelectionWorksWithHorizontalList.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeMultipleWhenProgrammaticSelectionWorksWithHorizontalList.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeMultipleWhenProgrammaticSelectionWorksWithVerticalList.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeMultipleWhenProgrammaticSelectionWorksWithVerticalList.png
index 41481128498f..870f52fa8edc 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeMultipleWhenProgrammaticSelectionWorksWithVerticalList.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeMultipleWhenProgrammaticSelectionWorksWithVerticalList.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeSingleWhenProgrammaticSelectionWhithItemsSourceGroupList.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeSingleWhenProgrammaticSelectionWhithItemsSourceGroupList.png
index 393cbe47c680..c0359e8b7240 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeSingleWhenProgrammaticSelectionWhithItemsSourceGroupList.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeSingleWhenProgrammaticSelectionWhithItemsSourceGroupList.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeSingleWhenProgrammaticSelectionWorksWithHorizontalList.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeSingleWhenProgrammaticSelectionWorksWithHorizontalList.png
index 316cdc52f6d8..e6cf587af549 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeSingleWhenProgrammaticSelectionWorksWithHorizontalList.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeSingleWhenProgrammaticSelectionWorksWithHorizontalList.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeSingleWhenProgrammaticSelectionWorksWithVerticalList.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeSingleWhenProgrammaticSelectionWorksWithVerticalList.png
index 851487a02c00..310aeb8655e8 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeSingleWhenProgrammaticSelectionWorksWithVerticalList.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySelectionModeSingleWhenProgrammaticSelectionWorksWithVerticalList.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyStringItemsGroupedListWhenMultipleModePreSelection.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyStringItemsGroupedListWhenMultipleModePreSelection.png
index 38e9b4139695..f1602aafc466 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyStringItemsGroupedListWhenMultipleModePreSelection.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyStringItemsGroupedListWhenMultipleModePreSelection.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyStringItemsObservableCollectionWhenMultipleModePreSelection.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyStringItemsObservableCollectionWhenMultipleModePreSelection.png
index e34539333e2a..06a42015b5ef 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyStringItemsObservableCollectionWhenMultipleModePreSelection.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyStringItemsObservableCollectionWhenMultipleModePreSelection.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyStringItemsObservableCollectionWhenSingleModePreSelection.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyStringItemsObservableCollectionWhenSingleModePreSelection.png
index c023d615bc8b..b0d0fc57cce3 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyStringItemsObservableCollectionWhenSingleModePreSelection.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyStringItemsObservableCollectionWhenSingleModePreSelection.png differ
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue28117.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue28117.cs
new file mode 100644
index 000000000000..3f6c8e6a04ef
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue28117.cs
@@ -0,0 +1,31 @@
+namespace Maui.Controls.Sample.Issues;
+
+[Issue(IssueTracker.Github, 28117, "Label text is cropped inside the border control with a specific padding value on certain Android devices", PlatformAffected.Android)]
+public class Issue28117 : ContentPage
+{
+ public Issue28117()
+ {
+ Content = new VerticalStackLayout
+ {
+ WidthRequest = 350,
+ Children =
+ {
+ new Border()
+ {
+ Padding = new Thickness(70.89827027958738, 0, 0, 0),
+ Margin = new Thickness(10),
+ StrokeThickness = 1,
+ Stroke = Colors.Black,
+ Content =
+ new Label
+ {
+ AutomationId = "Label",
+ FontFamily = "OpenSansRegular",
+ FontSize = 16,
+ Text = "At any time, but not later than one month before the expiration date"
+ }
+ },
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test.png
index ce289bfa1d73..099a9535f974 100644
Binary files a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test.png and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_1.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_1.png
index d6659fc64f19..f5e2811f5025 100644
Binary files a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_1.png and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_1.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_2.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_2.png
index b10e0d6a0fcf..13daa30cc148 100644
Binary files a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_2.png and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_2.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_3.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_3.png
index de5229748ab4..f936dd670ca7 100644
Binary files a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_3.png and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_3.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_4.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_4.png
index f95a38abad4f..e29385785966 100644
Binary files a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_4.png and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_4.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_5.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_5.png
index e90e89066dff..8710c295785c 100644
Binary files a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_5.png and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_5.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue2775Test.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue2775Test.png
index 515a2de96110..5de13f1ed86c 100644
Binary files a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue2775Test.png and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue2775Test.png differ
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28117.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28117.cs
new file mode 100644
index 000000000000..80f97aed8ac9
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28117.cs
@@ -0,0 +1,22 @@
+#if !MACCATALYST // On Mac platform, Label does not wrap properly when a width request is set https://github.com/dotnet/maui/issues/15559
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues;
+
+public class Issue28117 : _IssuesUITest
+{
+ public Issue28117(TestDevice device) : base(device) { }
+
+ public override string Issue => "Label text is cropped inside the border control with a specific padding value on certain Android devices";
+
+ [Test]
+ [Category(UITestCategories.Border)]
+ public void ShouldDisplayLabelWithoutBeingCroppedInsideBorder()
+ {
+ App.WaitForElement("Label");
+ VerifyScreenshot();
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ButtonsLayoutResolveWhenParentSizeChangesSizeButtonsDownPortrait.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ButtonsLayoutResolveWhenParentSizeChangesSizeButtonsDownPortrait.png
index 0dc14fee6a57..701d2399da59 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ButtonsLayoutResolveWhenParentSizeChangesSizeButtonsDownPortrait.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ButtonsLayoutResolveWhenParentSizeChangesSizeButtonsDownPortrait.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue15330Test.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue15330Test.png
index 09b56a6bc1a9..b694454a3f5b 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue15330Test.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue15330Test.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test.png
index ce34198d8db2..2461cc09d068 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_1.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_1.png
index e08752071342..509c3744de2a 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_1.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_1.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_2.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_2.png
index 04cd284815b2..f6d9b9731a26 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_2.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_2.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_3.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_3.png
index d1e2a05207aa..a6ffb2bea409 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_3.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_3.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_4.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_4.png
index 12b1e3f77595..a95a6d36a58d 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_4.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_4.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_5.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_5.png
index dd3e12c0ba7c..f1b025ed30b8 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_5.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_5.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue2775Test.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue2775Test.png
index 5e73e13e53f0..7410a2da6402 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue2775Test.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue2775Test.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShadowUpdateColor1.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShadowUpdateColor1.png
index 68d3416139ae..3ba8e5e1dd82 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShadowUpdateColor1.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShadowUpdateColor1.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShadowUpdateColor2.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShadowUpdateColor2.png
index bde49628af61..f0293d95e33f 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShadowUpdateColor2.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShadowUpdateColor2.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_ChangeFlowDirection_RTL_VerifyScreenshot.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_ChangeFlowDirection_RTL_VerifyScreenshot.png
index 745a644f9455..6baac5308dcc 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_ChangeFlowDirection_RTL_VerifyScreenshot.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_ChangeFlowDirection_RTL_VerifyScreenshot.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetColor.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetColor.png
index b6a860c428b0..0ed7e25ad346 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetColor.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetColor.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetEnabledStateToFalse_VerifyScreenshot.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetEnabledStateToFalse_VerifyScreenshot.png
index 4485dac793c0..66770ee8a50d 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetEnabledStateToFalse_VerifyScreenshot.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetEnabledStateToFalse_VerifyScreenshot.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetOffset_PositiveValues.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetOffset_PositiveValues.png
index 98f788a880a0..8659cc1e4053 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetOffset_PositiveValues.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetOffset_PositiveValues.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetOffset_Zero.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetOffset_Zero.png
index 313d18ab324b..6ea627efd82f 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetOffset_Zero.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetOffset_Zero.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetOpacity.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetOpacity.png
index 851baba2dea1..585bdbfeba1c 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetOpacity.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetOpacity.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetOpacity_Zero.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetOpacity_Zero.png
index 7c49ab22eb96..1279a971d3c0 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetOpacity_Zero.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetOpacity_Zero.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetRadius.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetRadius.png
index 1e04749829f4..c037dadb3e49 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetRadius.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetRadius.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetRadius_Zero.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetRadius_Zero.png
index 416ea487fc59..745ff7db1bcd 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetRadius_Zero.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetRadius_Zero.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetVisibilityToFalse_VerifyScreenshot.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetVisibilityToFalse_VerifyScreenshot.png
index da029b5b7ee2..10ebd0105dab 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetVisibilityToFalse_VerifyScreenshot.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Shadow_SetVisibilityToFalse_VerifyScreenshot.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShouldDisplayLabelWithoutBeingCroppedInsideBorder.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShouldDisplayLabelWithoutBeingCroppedInsideBorder.png
new file mode 100644
index 000000000000..e4880986c4bc
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShouldDisplayLabelWithoutBeingCroppedInsideBorder.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Bugzilla36802Test.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Bugzilla36802Test.png
index 17c2dff3eb55..3098ba2fc3f5 100644
Binary files a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Bugzilla36802Test.png and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Bugzilla36802Test.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ButtonsLayoutResolveWhenParentSizeChangesOriginal.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ButtonsLayoutResolveWhenParentSizeChangesOriginal.png
index be7ef7357fdd..ccfc38818e0c 100644
Binary files a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ButtonsLayoutResolveWhenParentSizeChangesOriginal.png and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ButtonsLayoutResolveWhenParentSizeChangesOriginal.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/CheckBox_SetIsCheckedAndColor_VerifyVisualState.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/CheckBox_SetIsCheckedAndColor_VerifyVisualState.png
index 11b031dbc619..69dc53206582 100644
Binary files a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/CheckBox_SetIsCheckedAndColor_VerifyVisualState.png and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/CheckBox_SetIsCheckedAndColor_VerifyVisualState.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test.png
index 2378279e20ae..4648e0f7127e 100644
Binary files a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test.png and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_1.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_1.png
index c55644d1554f..8e593b3621d1 100644
Binary files a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_1.png and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_1.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_2.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_2.png
index 4c533bbf33f1..55b34cc25e4d 100644
Binary files a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_2.png and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_2.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_3.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_3.png
index addafac769ec..38d65f8aef67 100644
Binary files a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_3.png and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_3.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_4.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_4.png
index 4932ff0af3b9..8b982ed40869 100644
Binary files a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_4.png and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_4.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_5.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_5.png
index 428158cc17ca..b0d0dcdfe0c0 100644
Binary files a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_5.png and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_5.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue2775Test.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue2775Test.png
index b76a8e2e673c..1af0368c1a9f 100644
Binary files a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue2775Test.png and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue2775Test.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/RadioButton_Checking_Initial_Configuration_VerifyVisualState.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/RadioButton_Checking_Initial_Configuration_VerifyVisualState.png
index 9961b2289825..5bd8068964fa 100644
Binary files a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/RadioButton_Checking_Initial_Configuration_VerifyVisualState.png and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/RadioButton_Checking_Initial_Configuration_VerifyVisualState.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ResizeCarouselViewKeepsIndex.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ResizeCarouselViewKeepsIndex.png
index 56293805689f..7fde9b3c0926 100644
Binary files a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ResizeCarouselViewKeepsIndex.png and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ResizeCarouselViewKeepsIndex.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ShadowShouldUpdate.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ShadowShouldUpdate.png
index 2f7efc997e23..91b4f3aaac5c 100644
Binary files a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ShadowShouldUpdate.png and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ShadowShouldUpdate.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ShouldDisplayLabelWithoutBeingCroppedInsideBorder.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ShouldDisplayLabelWithoutBeingCroppedInsideBorder.png
new file mode 100644
index 000000000000..5c1f2bab1853
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ShouldDisplayLabelWithoutBeingCroppedInsideBorder.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/VerifyCheckBoxUnCheckedState.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/VerifyCheckBoxUnCheckedState.png
index 8df3ac543cb1..60891d3703dc 100644
Binary files a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/VerifyCheckBoxUnCheckedState.png and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/VerifyCheckBoxUnCheckedState.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/VerifyCollectionViewItemsAfterScrolling.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/VerifyCollectionViewItemsAfterScrolling.png
index 6a91b6276f95..20f03a675fe3 100644
Binary files a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/VerifyCollectionViewItemsAfterScrolling.png and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/VerifyCollectionViewItemsAfterScrolling.png differ
diff --git a/src/Core/src/IViewWithWindow.cs b/src/Core/src/IViewWithWindow.cs
new file mode 100644
index 000000000000..1d7825dd5ac3
--- /dev/null
+++ b/src/Core/src/IViewWithWindow.cs
@@ -0,0 +1,17 @@
+#nullable enable
+
+namespace Microsoft.Maui
+{
+ ///
+ /// Internal interface for views that can provide access to their window.
+ /// This enables dependency injection for testing scenarios.
+ ///
+ // TODO Delete this in NET10 and just add it with a default implementation to IView
+ internal interface IViewWithWindow
+ {
+ ///
+ /// Gets the window associated with this view.
+ ///
+ IWindow? Window { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Core/src/Layouts/DensityValue.cs b/src/Core/src/Layouts/DensityValue.cs
new file mode 100644
index 000000000000..e0588f4dc554
--- /dev/null
+++ b/src/Core/src/Layouts/DensityValue.cs
@@ -0,0 +1,272 @@
+#nullable enable
+using System;
+
+namespace Microsoft.Maui.Layouts
+{
+ ///
+ /// Represents a value that tracks both density-independent (dp) and physical pixel values
+ /// to enable precise pixel-aware layout calculations.
+ ///
+ internal readonly struct DensityValue : IEquatable
+ {
+ private const double Epsilon = 0.00001;
+
+ ///
+ /// Gets the raw pixel value without rounding.
+ ///
+ public double RawPx { get; }
+
+ ///
+ /// Gets the display density factor.
+ ///
+ public double Density { get; }
+
+ ///
+ /// Gets the value in density-independent pixels (dp).
+ ///
+ public double Dp
+ {
+ get
+ {
+ // Handle default case where Density is 0
+ if (Math.Abs(Density) < Epsilon)
+ {
+ return RawPx; // Treat as 1.0 density
+ }
+
+ return Math.Abs(Density - 1.0) < Epsilon ? RawPx : RawPx / Density;
+ }
+ }
+
+
+
+ ///
+ /// Initializes a new instance of the DensityValue struct.
+ ///
+ /// The value in density-independent pixels.
+ /// The display density factor.
+ public DensityValue(double dp, double density)
+ {
+ // When density is 1.0, store the dp value directly as RawPx to avoid any precision loss
+ if (Math.Abs(density - 1.0) < Epsilon)
+ {
+ RawPx = dp;
+ }
+ else
+ {
+ RawPx = dp * density;
+ }
+ Density = density;
+ }
+
+ ///
+ /// Initializes a new instance of the DensityValue struct with default density of 1.0.
+ ///
+ /// The value in density-independent pixels.
+ public DensityValue(double value) : this(value, 1.0)
+ {
+ }
+
+ ///
+ /// Private constructor for internal use.
+ ///
+ private DensityValue(double rawPx, double density, bool fromPixels)
+ {
+ RawPx = rawPx;
+ Density = density;
+ }
+
+ ///
+ /// Creates a DensityValue from a pixel value and density.
+ ///
+ /// The pixel value.
+ /// The display density factor.
+ /// A DensityValue representing the equivalent dp value.
+ public static DensityValue FromPixels(double pixels, double density)
+ {
+ return new DensityValue(pixels, density, true);
+ }
+
+ ///
+ /// Adds two DensityValue instances.
+ ///
+ public static DensityValue operator +(DensityValue left, DensityValue right)
+ {
+ // If both have density 1.0, we can safely add them
+ if (Math.Abs(left.Density - 1.0) < Epsilon && Math.Abs(right.Density - 1.0) < Epsilon)
+ {
+ return new DensityValue(left.RawPx + right.RawPx, 1.0);
+ }
+
+ // If densities are the same, add them
+ if (Math.Abs(left.Density - right.Density) < Epsilon)
+ {
+ return DensityValue.FromPixels(left.RawPx + right.RawPx, left.Density);
+ }
+
+ // If one has density 1.0 and the other doesn't, treat the 1.0 density value as having the same density as the other
+ if (Math.Abs(left.Density - 1.0) < Epsilon)
+ {
+ return DensityValue.FromPixels(left.RawPx + right.RawPx, right.Density);
+ }
+
+ if (Math.Abs(right.Density - 1.0) < Epsilon)
+ {
+ return DensityValue.FromPixels(left.RawPx + right.RawPx, left.Density);
+ }
+
+ throw new ArgumentException("Cannot add DensityValues with different densities.");
+ }
+
+ ///
+ /// Subtracts two DensityValue instances.
+ ///
+ public static DensityValue operator -(DensityValue left, DensityValue right)
+ {
+ // If both have density 1.0, we can safely subtract them
+ if (Math.Abs(left.Density - 1.0) < Epsilon && Math.Abs(right.Density - 1.0) < Epsilon)
+ {
+ return new DensityValue(left.RawPx - right.RawPx, 1.0);
+ }
+
+ // If densities are the same, subtract them
+ if (Math.Abs(left.Density - right.Density) < Epsilon)
+ {
+ return DensityValue.FromPixels(left.RawPx - right.RawPx, left.Density);
+ }
+
+ // If one has density 1.0 and the other doesn't, treat the 1.0 density value as having the same density as the other
+ if (Math.Abs(left.Density - 1.0) < Epsilon)
+ {
+ return DensityValue.FromPixels(left.RawPx - right.RawPx, right.Density);
+ }
+
+ if (Math.Abs(right.Density - 1.0) < Epsilon)
+ {
+ return DensityValue.FromPixels(left.RawPx - right.RawPx, left.Density);
+ }
+
+ throw new ArgumentException("Cannot subtract DensityValues with different densities.");
+ }
+
+ ///
+ /// Multiplies a DensityValue by a scalar.
+ ///
+ public static DensityValue operator *(DensityValue value, double scalar)
+ {
+ return DensityValue.FromPixels(value.RawPx * scalar, value.Density);
+ }
+
+ ///
+ /// Multiplies a DensityValue by a scalar.
+ ///
+ public static DensityValue operator *(double scalar, DensityValue value)
+ {
+ return value * scalar;
+ }
+
+ ///
+ /// Divides a DensityValue by a scalar.
+ ///
+ public static DensityValue operator /(DensityValue value, double scalar)
+ {
+ return DensityValue.FromPixels(value.RawPx / scalar, value.Density);
+ }
+
+ ///
+ /// Implicitly converts a DensityValue to its dp value.
+ ///
+ public static implicit operator double(DensityValue value)
+ {
+ return value.Dp;
+ }
+
+ ///
+ /// Implicitly converts a double to a DensityValue with density 1.0.
+ ///
+ public static implicit operator DensityValue(double value)
+ {
+ return new DensityValue(value, 1.0);
+ }
+
+ ///
+ /// Distributes a total pixel amount across multiple DensityValue instances,
+ /// accumulating rounding errors and applying them to the final elements.
+ /// This implements Android's approach of assigning remainder pixels to the last element.
+ ///
+ /// The total pixels to distribute.
+ /// The display density.
+ /// The relative portions for each element.
+ /// An array of pixel values that sum exactly to totalPixels.
+ public static int[] DistributePixels(double totalPixels, double density, double[] portions)
+ {
+ if (portions.Length == 0)
+ return Array.Empty();
+
+ var totalPortions = 0.0;
+ foreach (var portion in portions)
+ {
+ totalPortions += portion;
+ }
+
+ if (totalPortions <= 0)
+ return new int[portions.Length]; // All zeros
+
+ var result = new int[portions.Length];
+ var targetTotal = (int)Math.Floor(totalPixels);
+ var assignedTotal = 0;
+
+ // Calculate ideal pixels per portion
+ var idealPixelsPerUnit = totalPixels / totalPortions;
+
+ // Assign pixels to all elements using floor
+ for (int i = 0; i < portions.Length; i++)
+ {
+ var idealPixels = idealPixelsPerUnit * portions[i];
+ result[i] = (int)Math.Floor(idealPixels);
+ assignedTotal += result[i];
+ }
+
+ // Distribute remaining pixels from right to left (as requested in review)
+ var remainingPixels = targetTotal - assignedTotal;
+ for (int i = portions.Length - 1; i >= 0 && remainingPixels > 0; i--)
+ {
+ result[i]++;
+ remainingPixels--;
+ }
+
+ return result;
+ }
+
+ public bool Equals(DensityValue other)
+ {
+ return Math.Abs(RawPx - other.RawPx) < Epsilon &&
+ Math.Abs(Density - other.Density) < Epsilon;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is DensityValue other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ return (RawPx, Density).GetHashCode();
+ }
+
+ public static bool operator ==(DensityValue left, DensityValue right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(DensityValue left, DensityValue right)
+ {
+ return !left.Equals(right);
+ }
+
+ public override string ToString()
+ {
+ return $"{RawPx:F2}px ({Dp:F2}dp @ {Density:F2}x)";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Core/src/Layouts/GridLayoutManager.cs b/src/Core/src/Layouts/GridLayoutManager.cs
index 22d9428f6f90..200103f25865 100644
--- a/src/Core/src/Layouts/GridLayoutManager.cs
+++ b/src/Core/src/Layouts/GridLayoutManager.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
+using System.Linq;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Primitives;
@@ -265,12 +266,12 @@ public Rect GetCellBoundsFor(IView view, double xOffset, double yOffset)
for (int n = firstColumn; n < lastColumn; n++)
{
- width += _columns[n].Size;
+ width += _columns[n].Size.Dp;
}
for (int n = firstRow; n < lastRow; n++)
{
- height += _rows[n].Size;
+ height += _rows[n].Size.Dp;
}
// Account for any space between spanned rows/columns
@@ -340,7 +341,7 @@ static double SumDefinitions(Definition[] definitions, double spacing, bool mini
for (int n = 0; n < definitions.Length; n++)
{
- sum += minimize ? definitions[n].MinimumSize : definitions[n].Size;
+ sum += minimize ? definitions[n].MinimumSize.Dp : definitions[n].Size.Dp;
if (n > 0)
{
@@ -393,7 +394,7 @@ void FirstMeasurePass()
bool treatCellHeightAsAuto = TreatCellHeightAsAuto(cell);
bool treatCellWidthAsAuto = TreatCellWidthAsAuto(cell);
- if (double.IsNaN(cell.MeasureHeight) || double.IsNaN(cell.MeasureWidth))
+ if (double.IsNaN(cell.MeasureHeight.Dp) || double.IsNaN(cell.MeasureWidth.Dp))
{
// We still have some unknown measure constraints (* rows/columns that need to have
// the Auto measurements settled before we can measure them). So mark this cell for the
@@ -403,7 +404,7 @@ void FirstMeasurePass()
continue;
}
- var measure = MeasureCell(cell, cell.MeasureWidth, cell.MeasureHeight);
+ var measure = MeasureCell(cell, cell.MeasureWidth.Dp, cell.MeasureHeight.Dp);
if (treatCellWidthAsAuto)
{
@@ -443,7 +444,7 @@ void SecondMeasurePass()
double width = 0;
double height = 0;
- if (double.IsInfinity(cell.MeasureHeight))
+ if (double.IsInfinity(cell.MeasureHeight.Dp))
{
height = double.PositiveInfinity;
}
@@ -451,11 +452,11 @@ void SecondMeasurePass()
{
for (int n = cell.Row; n < cell.Row + cell.RowSpan; n++)
{
- height += _rows[n].Size;
+ height += _rows[n].Size.Dp;
}
}
- if (double.IsInfinity(cell.MeasureWidth))
+ if (double.IsInfinity(cell.MeasureWidth.Dp))
{
width = double.PositiveInfinity;
}
@@ -463,7 +464,7 @@ void SecondMeasurePass()
{
for (int n = cell.Column; n < cell.Column + cell.ColumnSpan; n++)
{
- width += _columns[n].Size;
+ width += _columns[n].Size.Dp;
}
}
@@ -539,7 +540,7 @@ static void ResolveSpan(Definition[] definitions, int start, int length, double
// Determine how large the spanned area currently is
for (int n = start; n < end; n++)
{
- currentSize += definitions[n].Size;
+ currentSize += definitions[n].Size.Dp;
if (n > start)
{
@@ -590,7 +591,7 @@ double LeftEdgeOfColumn(int column)
for (int n = 0; n < column; n++)
{
- left += _columns[n].Size;
+ left += _columns[n].Size.Dp;
left += _columnSpacing;
}
@@ -603,7 +604,7 @@ double TopEdgeOfRow(int row)
for (int n = 0; n < row; n++)
{
- top += _rows[n].Size;
+ top += _rows[n].Size.Dp;
top += _rowSpacing;
}
@@ -692,7 +693,7 @@ void ResolveStarRows(double heightConstraint)
foreach (var cell in _cells)
{
- if (double.IsNaN(cell.MeasureHeight))
+ if (double.IsNaN(cell.MeasureHeight.Dp))
{
UpdateKnownMeasureHeight(cell);
}
@@ -760,7 +761,7 @@ static void DetermineMinimumStarSizesInSpan(double spaceNeeded, Definition[] def
{
if (definitions[n].IsAbsolute || definitions[n].IsAuto)
{
- spaceNeeded -= definitions[n].Size;
+ spaceNeeded -= definitions[n].Size.Dp;
}
}
@@ -841,7 +842,7 @@ static void MinimizeStars(Definition[] defs)
}
}
- static void ExpandStarDefinitions(Definition[] definitions, double targetSize, double currentSize, double spacing, double starCount, bool limitStarSizes)
+ void ExpandStarDefinitions(Definition[] definitions, double targetSize, double currentSize, double spacing, double starCount, bool limitStarSizes)
{
// Figure out what the star value should be at this size
var starSize = ComputeStarSizeForTarget(targetSize, definitions, spacing, starCount);
@@ -854,8 +855,11 @@ static void ExpandStarDefinitions(Definition[] definitions, double targetSize, d
EnsureSizeLimit(definitions, starSize);
}
+ // Get density for pixel-perfect distribution
+ var density = GetDensity();
+
// Inflate the stars so that we fill up the space at this size
- ExpandStars(targetSize, currentSize, definitions, starSize, starCount);
+ ExpandStars(targetSize, currentSize, definitions, starSize, starCount, density);
}
static void EnsureSizeLimit(Definition[] definitions, double starSize)
@@ -890,7 +894,7 @@ static double ComputeStarSizeForTarget(double targetSize, Definition[] defs, dou
return (targetSize - sum) / starCount;
}
- static void ExpandStars(double targetSize, double currentSize, Definition[] defs, double targetStarSize, double starCount)
+ static void ExpandStars(double targetSize, double currentSize, Definition[] defs, double targetStarSize, double starCount, double density)
{
Debug.Assert(starCount > 0, "Assume that the caller has already checked for the existence of star rows/columns before using this.");
@@ -923,12 +927,14 @@ static void ExpandStars(double targetSize, double currentSize, Definition[] defs
// targetStarSize, that means we have enough room to expand all of our star rows/columns
// to their full size.
- foreach (var definition in defs)
+ var starDefinitions = defs.Where(d => d.IsStar).ToArray();
+ var portions = starDefinitions.Select(d => targetStarSize * d.GridLength.Value).ToArray();
+ var totalPixels = portions.Sum() * density;
+ var pixelAllocations = DensityValue.DistributePixels(totalPixels, density, portions);
+
+ for (int i = 0; i < starDefinitions.Length; i++)
{
- if (definition.IsStar)
- {
- definition.Size = targetStarSize * definition.GridLength.Value;
- }
+ starDefinitions[i].Size = DensityValue.FromPixels(pixelAllocations[i], density);
}
return;
@@ -951,27 +957,51 @@ static void ExpandStars(double targetSize, double currentSize, Definition[] defs
}
}
- foreach (var definition in defs)
+ // Use density-aware distribution for pixel-perfect proportional allocation
+ var proportionalStarDefinitions = defs.Where(d => d.IsStar).ToArray();
+ var proportionalPortions = new double[proportionalStarDefinitions.Length];
+
+ for (int i = 0; i < proportionalStarDefinitions.Length; i++)
{
- if (definition.IsStar)
+ var definition = proportionalStarDefinitions[i];
+ double fullTargetSize = targetStarSize * definition.GridLength.Value;
+
+ if (definition.MinimumSize < fullTargetSize)
{
- // Skip the star rows/columns whose minimums are at or higher than the target sizes
- double fullTargetSize = targetStarSize * definition.GridLength.Value;
-
- if (definition.MinimumSize < fullTargetSize)
- {
- // Figure out how small this definition is relative to the total difference,
- // and use that to determine how much of the available space this definition gets
+ var scale = (fullTargetSize - definition.MinimumSize) / totaldiff;
+ var portion = scale * availableSpace;
+ proportionalPortions[i] = definition.MinimumSize + portion;
+ }
+ else
+ {
+ proportionalPortions[i] = definition.MinimumSize;
+ }
+ }
+
+ var proportionalTotalPixels = proportionalPortions.Sum() * density;
+ var proportionalPixelAllocations = DensityValue.DistributePixels(proportionalTotalPixels, density, proportionalPortions);
+
+ for (int i = 0; i < proportionalStarDefinitions.Length; i++)
+ {
+ proportionalStarDefinitions[i].Size = DensityValue.FromPixels(proportionalPixelAllocations[i], density);
+ }
+ }
- // The goal is to have the definitions expand proportionate to their deficit from
- // their full target sizes
- var scale = (fullTargetSize - definition.MinimumSize) / totaldiff;
- var portion = scale * availableSpace;
- definition.Size = definition.MinimumSize + portion;
- }
- }
+ ///
+ /// Gets the display density for density-aware calculations.
+ ///
+ /// The display density, or 1.0 if not available.
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Accesses instance member _grid")]
+ double GetDensity()
+ {
+ // Try to get density from the grid view if it implements IViewWithWindow
+ if (_grid is IViewWithWindow viewWithWindow && viewWithWindow.Window != null)
+ {
+ return viewWithWindow.Window.RequestDisplayDensity();
}
+
+ return 1.0;
}
static bool AnyAuto(Definition[] definitions)
@@ -1012,7 +1042,7 @@ void UpdateKnownMeasureWidth(Cell cell)
double measureWidth = 0;
for (int column = cell.Column; column < cell.Column + cell.ColumnSpan; column++)
{
- measureWidth += _columns[column].Size;
+ measureWidth += _columns[column].Size.Dp;
if (column > cell.Column)
{
@@ -1028,7 +1058,7 @@ void UpdateKnownMeasureHeight(Cell cell)
double measureHeight = 0;
for (int row = cell.Row; row < cell.Row + cell.RowSpan; row++)
{
- measureHeight += _rows[row].Size;
+ measureHeight += _rows[row].Size.Dp;
if (row > cell.Row)
{
@@ -1136,8 +1166,8 @@ class Cell
public int Column { get; }
public int RowSpan { get; }
public int ColumnSpan { get; }
- public double MeasureWidth { get; set; } = double.NaN;
- public double MeasureHeight { get; set; } = double.NaN;
+ public DensityValue MeasureWidth { get; set; } = new DensityValue(double.NaN);
+ public DensityValue MeasureHeight { get; set; } = new DensityValue(double.NaN);
public bool NeedsSecondPass { get; set; }
///
@@ -1197,12 +1227,12 @@ static GridLengthType ToGridLengthType(GridUnitType gridUnitType)
class Definition
{
readonly GridLength _gridLength;
- private double _size;
+ private DensityValue _size;
///
/// The current size of this definition
///
- public double Size
+ public DensityValue Size
{
get => _size;
set
@@ -1220,11 +1250,11 @@ public double Size
/// For absolute and auto definitions, this is the same as Size
/// For star definitions, this is the minimum size which can contain the contents of the row/column
///
- public double MinimumSize { get; set; }
+ public DensityValue MinimumSize { get; set; }
- public void Update(double size)
+ public void Update(DensityValue size)
{
- if (size > Size)
+ if (size.RawPx > Size.RawPx)
{
Size = size;
}
@@ -1238,12 +1268,18 @@ public void Update(double size)
public Definition(GridLength gridLength)
{
+ _gridLength = gridLength;
+
if (gridLength.IsAbsolute)
{
Size = gridLength.Value;
}
-
- _gridLength = gridLength;
+ else
+ {
+ // For auto and star, start with size 0
+ Size = new DensityValue(0.0);
+ MinimumSize = new DensityValue(0.0);
+ }
}
}
}
diff --git a/src/Core/src/Platform/Android/ContextExtensions.cs b/src/Core/src/Platform/Android/ContextExtensions.cs
index fbb877a6e8c1..6559d22f4c75 100644
--- a/src/Core/src/Platform/Android/ContextExtensions.cs
+++ b/src/Core/src/Platform/Android/ContextExtensions.cs
@@ -124,12 +124,14 @@ static float ToPixelsUsingMetrics(double dp)
public static (int left, int top, int right, int bottom) ToPixels(this Context context, Graphics.Rect rectangle)
{
+ var left = (int)context.ToPixels(rectangle.Left);
+ var top = (int)context.ToPixels(rectangle.Top);
return
(
- (int)context.ToPixels(rectangle.Left),
- (int)context.ToPixels(rectangle.Top),
- (int)context.ToPixels(rectangle.Right),
- (int)context.ToPixels(rectangle.Bottom)
+ left,
+ top,
+ left + (int)context.ToPixels(rectangle.Width),
+ top + (int)context.ToPixels(rectangle.Height)
);
}
diff --git a/src/Core/src/Platform/ElementExtensions.cs b/src/Core/src/Platform/ElementExtensions.cs
index a76988c5afba..b7af39b11e7a 100644
--- a/src/Core/src/Platform/ElementExtensions.cs
+++ b/src/Core/src/Platform/ElementExtensions.cs
@@ -167,6 +167,8 @@ public static void SetWindowHandler(this PlatformWindow platformWindow, IWindow
#if WINDOWS || IOS || ANDROID || TIZEN
internal static IWindow GetWindow(this IElement element) =>
+ (element as IViewWithWindow)?.Window ??
+ (element as IView)?.GetHostedWindow() ??
element.Handler?.MauiContext?.GetPlatformWindow()?.GetWindow() ??
throw new InvalidOperationException("IWindow not found");
#endif
diff --git a/src/Core/src/Platform/Tizen/ViewExtensions.cs b/src/Core/src/Platform/Tizen/ViewExtensions.cs
index 7bb64aa7130d..6d4637a74e6c 100644
--- a/src/Core/src/Platform/Tizen/ViewExtensions.cs
+++ b/src/Core/src/Platform/Tizen/ViewExtensions.cs
@@ -362,5 +362,8 @@ internal static bool SetKeyInputFocus(NView view, bool isShow)
return view.KeyInputFocus;
}
+
+ internal static IWindow? GetHostedWindow(this IView? view)
+ => null;
}
}
diff --git a/src/Core/src/Properties/AssemblyInfo.cs b/src/Core/src/Properties/AssemblyInfo.cs
index eb1db1dee35d..d304720d8948 100644
--- a/src/Core/src/Properties/AssemblyInfo.cs
+++ b/src/Core/src/Properties/AssemblyInfo.cs
@@ -25,6 +25,7 @@
[assembly: InternalsVisibleTo("Microsoft.Maui.DualScreen.UnitTests")]
[assembly: InternalsVisibleTo("Microsoft.Maui.Controls.Foldable")]
[assembly: InternalsVisibleTo("Microsoft.Maui.UnitTests")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("Microsoft.Maui.Core.DeviceTests")]
[assembly: InternalsVisibleTo("Microsoft.Maui.Controls.DeviceTests")]
[assembly: InternalsVisibleTo("Microsoft.Maui.Controls.Core.UnitTests")]
diff --git a/src/Core/tests/UnitTests/Layouts/DensityValueTests.cs b/src/Core/tests/UnitTests/Layouts/DensityValueTests.cs
new file mode 100644
index 000000000000..c80064a7f014
--- /dev/null
+++ b/src/Core/tests/UnitTests/Layouts/DensityValueTests.cs
@@ -0,0 +1,184 @@
+using System;
+using Xunit;
+using Microsoft.Maui.Layouts;
+
+namespace Microsoft.Maui.UnitTests.Layouts
+{
+ [Category(TestCategory.Layout)]
+ public class DensityValueTests
+ {
+ [Fact]
+ public void Constructor_SetsPropertiesCorrectly()
+ {
+ var density = 2.625;
+ var dp = 100.0;
+ var value = new DensityValue(dp, density);
+
+ Assert.Equal(dp, value.Dp);
+ Assert.Equal(density, value.Density);
+ Assert.Equal(dp * density, value.RawPx);
+ Assert.Equal((int)Math.Round(dp * density), (int)Math.Round(value.RawPx));
+ }
+
+ [Fact]
+ public void FromPixels_CalculatesDpCorrectly()
+ {
+ var pixels = 262.5;
+ var density = 2.625;
+ var expectedDp = pixels / density; // 100.0
+
+ var value = DensityValue.FromPixels(pixels, density);
+
+ Assert.Equal(expectedDp, value.Dp, precision: 5);
+ Assert.Equal(density, value.Density);
+ }
+
+ [Fact]
+ public void Addition_WorksWithSameDensity()
+ {
+ var value1 = new DensityValue(50.0, 2.0);
+ var value2 = new DensityValue(30.0, 2.0);
+
+ var result = value1 + value2;
+
+ Assert.Equal(80.0, result.Dp);
+ Assert.Equal(2.0, result.Density);
+ }
+
+ [Fact]
+ public void Addition_ThrowsWithDifferentDensities()
+ {
+ var value1 = new DensityValue(50.0, 2.0);
+ var value2 = new DensityValue(30.0, 3.0);
+
+ Assert.Throws(() => value1 + value2);
+ }
+
+ [Fact]
+ public void Multiplication_WorksCorrectly()
+ {
+ var value = new DensityValue(100.0, 2.5);
+ var scalar = 1.5;
+
+ var result = value * scalar;
+
+ Assert.Equal(150.0, result.Dp);
+ Assert.Equal(2.5, result.Density);
+ }
+
+ [Fact]
+ public void ImplicitConversion_ReturnsDp()
+ {
+ var value = new DensityValue(123.45, 2.0);
+ double dp = value;
+
+ Assert.Equal(123.45, dp);
+ }
+
+ [Theory]
+ [InlineData(770.175, 2.625, new double[] { 1, 1, 1 }, new int[] { 256, 257, 257 })]
+ [InlineData(870.0, 3.0, new double[] { 1, 1, 1 }, new int[] { 290, 290, 290 })]
+ [InlineData(787.5, 2.625, new double[] { 1, 1, 1, 1 }, new int[] { 196, 197, 197, 197 })]
+ public void DistributePixels_HandlesRoundingCorrectly(double totalPixels, double density, double[] portions, int[] expected)
+ {
+ var result = DensityValue.DistributePixels(totalPixels, density, portions);
+
+ Assert.Equal(expected.Length, result.Length);
+ for (int i = 0; i < expected.Length; i++)
+ {
+ Assert.Equal(expected[i], result[i]);
+ }
+
+ // Verify that the total adds up correctly
+ var sum = 0;
+ foreach (var value in result)
+ {
+ sum += value;
+ }
+
+ // The total should be Math.Floor(totalPixels) for right-to-left distribution
+ var expectedTotal = (int)Math.Floor(totalPixels);
+ Assert.Equal(expectedTotal, sum);
+ }
+
+ [Fact]
+ public void DistributePixels_HandlesWeightedPortions()
+ {
+ // 100 pixels distributed as 2*, 1*, 2* (5 total weight)
+ var totalPixels = 100.0;
+ var density = 1.0;
+ var portions = new double[] { 2.0, 1.0, 2.0 };
+
+ var result = DensityValue.DistributePixels(totalPixels, density, portions);
+
+ // Expected: 40, 20, 40
+ Assert.Equal(40, result[0]);
+ Assert.Equal(20, result[1]);
+ Assert.Equal(40, result[2]);
+ }
+
+ [Fact]
+ public void DistributePixels_HandlesEmptyArray()
+ {
+ var result = DensityValue.DistributePixels(100.0, 1.0, Array.Empty());
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void DistributePixels_HandlesZeroPortions()
+ {
+ var portions = new double[] { 0.0, 0.0, 0.0 };
+ var result = DensityValue.DistributePixels(100.0, 1.0, portions);
+
+ Assert.Equal(3, result.Length);
+ Assert.All(result, value => Assert.Equal(0, value));
+ }
+
+ ///
+ /// Tests the specific scenario from the issue: 293.4dp across 3 columns at density 2.625
+ ///
+ [Fact]
+ public void DistributePixels_IssueScenario1()
+ {
+ // 293.4dp * 2.625 = 770.175px across 3 equal columns
+ var totalDp = 293.4;
+ var density = 2.625;
+ var totalPixels = totalDp * density; // 770.175
+ var portions = new double[] { 1.0, 1.0, 1.0 };
+
+ var result = DensityValue.DistributePixels(totalPixels, density, portions);
+
+ // Expected allocation with right-to-left distribution: 256, 257, 257 (total 770)
+ Assert.Equal(256, result[0]);
+ Assert.Equal(257, result[1]);
+ Assert.Equal(257, result[2]);
+
+ var totalAllocated = result[0] + result[1] + result[2];
+ Assert.Equal(770, totalAllocated);
+ }
+
+ [Fact]
+ public void Equality_WorksCorrectly()
+ {
+ var value1 = new DensityValue(100.0, 2.0);
+ var value2 = new DensityValue(100.0, 2.0);
+ var value3 = new DensityValue(100.1, 2.0);
+
+ Assert.True(value1.Equals(value2));
+ Assert.False(value1.Equals(value3));
+ Assert.True(value1 == value2);
+ Assert.False(value1 == value3);
+ }
+
+ [Fact]
+ public void ToString_ProvidesMeaningfulOutput()
+ {
+ var value = new DensityValue(100.0, 2.5);
+ var result = value.ToString();
+
+ Assert.Contains("100.00dp", result, StringComparison.Ordinal);
+ Assert.Contains("250.00px", result, StringComparison.Ordinal);
+ Assert.Contains("2.50x", result, StringComparison.Ordinal);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Core/tests/UnitTests/Layouts/GridLayoutManagerDensityTests.cs b/src/Core/tests/UnitTests/Layouts/GridLayoutManagerDensityTests.cs
new file mode 100644
index 000000000000..daf1465b7de9
--- /dev/null
+++ b/src/Core/tests/UnitTests/Layouts/GridLayoutManagerDensityTests.cs
@@ -0,0 +1,173 @@
+using System;
+using Xunit;
+using Microsoft.Maui.Layouts;
+
+namespace Microsoft.Maui.UnitTests.Layouts
+{
+ [Category(TestCategory.Layout)]
+ public class GridLayoutManagerDensityTests
+ {
+ [Fact]
+ public void DensityValue_HandlesIssueScenario1_Correctly()
+ {
+ // Scenario 1: 293.4dp across 3 columns at density 2.625
+ var totalDp = 293.4;
+ var density = 2.625;
+ var totalPixels = totalDp * density; // 770.175px
+ var portions = new double[] { 1.0, 1.0, 1.0 }; // Equal star sizing
+
+ var result = DensityValue.DistributePixels(totalPixels, density, portions);
+
+ // Expected with right-to-left distribution: [256, 257, 257] = 770px
+ Assert.Equal(3, result.Length);
+ Assert.Equal(256, result[0]);
+ Assert.Equal(257, result[1]);
+ Assert.Equal(257, result[2]);
+
+ var total = result[0] + result[1] + result[2];
+ Assert.Equal(770, total);
+
+ // Verify this is better than naive division
+ var naiveSize = (int)Math.Round(totalPixels / 3); // 257
+ var naiveTotal = naiveSize * 3; // 771 - too much!
+ Assert.True(total < naiveTotal, "DensityValue should provide more accurate allocation than naive rounding");
+ }
+
+ [Fact]
+ public void DensityValue_HandlesIssueScenario2_Correctly()
+ {
+ // Scenario 2: 290dp across 3 columns at density 3.0 (perfect case)
+ var totalDp = 290.0;
+ var density = 3.0;
+ var totalPixels = totalDp * density; // 870.0px
+ var portions = new double[] { 1.0, 1.0, 1.0 };
+
+ var result = DensityValue.DistributePixels(totalPixels, density, portions);
+
+ // Perfect division case
+ Assert.Equal(3, result.Length);
+ Assert.Equal(290, result[0]);
+ Assert.Equal(290, result[1]);
+ Assert.Equal(290, result[2]);
+
+ var total = result[0] + result[1] + result[2];
+ Assert.Equal(870, total);
+ }
+
+ [Fact]
+ public void DensityValue_HandlesIssueScenario3_Correctly()
+ {
+ // Scenario 3: 300dp across 4 columns at density 2.625
+ var totalDp = 300.0;
+ var density = 2.625;
+ var totalPixels = totalDp * density; // 787.5px
+ var portions = new double[] { 1.0, 1.0, 1.0, 1.0 };
+
+ var result = DensityValue.DistributePixels(totalPixels, density, portions);
+
+ // Expected with right-to-left distribution: [196, 197, 197, 197] = 787px
+ Assert.Equal(4, result.Length);
+ Assert.Equal(196, result[0]);
+ Assert.Equal(197, result[1]);
+ Assert.Equal(197, result[2]);
+ Assert.Equal(197, result[3]);
+
+ var total = result[0] + result[1] + result[2] + result[3];
+ Assert.Equal(787, total);
+ }
+
+ [Fact]
+ public void DensityValue_HandlesWeightedStarSizing()
+ {
+ // Test weighted star sizing: 2*, 1*, 2* across 500 pixels
+ var totalPixels = 500.0;
+ var density = 2.0;
+ var portions = new double[] { 2.0, 1.0, 2.0 }; // 2*, 1*, 2*
+
+ var result = DensityValue.DistributePixels(totalPixels, density, portions);
+
+ // Total weight = 5, so distribution should be:
+ // First: 500 * (2/5) = 200 pixels
+ // Second: 500 * (1/5) = 100 pixels
+ // Third: 500 * (2/5) = 200 pixels
+ Assert.Equal(3, result.Length);
+ Assert.Equal(200, result[0]);
+ Assert.Equal(100, result[1]);
+ Assert.Equal(200, result[2]);
+
+ var total = result[0] + result[1] + result[2];
+ Assert.Equal(500, total);
+ }
+
+ [Fact]
+ public void DensityValue_HandlesWeightedStarSizing_WithRounding()
+ {
+ // Test weighted star sizing with rounding: 3*, 2*, 3* across 333 pixels
+ var totalPixels = 333.0;
+ var density = 1.5;
+ var portions = new double[] { 3.0, 2.0, 3.0 }; // 3*, 2*, 3*
+
+ var result = DensityValue.DistributePixels(totalPixels, density, portions);
+
+ // Total weight = 8, with right-to-left distribution:
+ // portions[0]=3: floor(333 * 3/8) = floor(124.875) = 124
+ // portions[1]=2: floor(333 * 2/8) = floor(83.25) = 83
+ // portions[2]=3: floor(333 * 3/8) = floor(124.875) = 124
+ // Total assigned: 124+83+124 = 331, remainder: 333-331 = 2
+ // Right-to-left: portions[2] gets +1, portions[1] gets +1
+ // Final: [124, 84, 125]
+ Assert.Equal(3, result.Length);
+ Assert.Equal(124, result[0]);
+ Assert.Equal(84, result[1]);
+ Assert.Equal(125, result[2]);
+
+ var total = result[0] + result[1] + result[2];
+ Assert.Equal(333, total);
+ }
+
+ [Theory]
+ [InlineData(100.0, 1.0, new double[] { 1, 1, 1, 1 }, 25)] // Perfect division
+ [InlineData(101.0, 1.0, new double[] { 1, 1, 1, 1 }, 25)] // 1 pixel remainder
+ [InlineData(103.0, 1.0, new double[] { 1, 1, 1, 1 }, 25)] // 3 pixel remainder
+ public void DensityValue_DistributesRemainderPixelsCorrectly(double totalPixels, double density, double[] portions, int expectedBase)
+ {
+ var result = DensityValue.DistributePixels(totalPixels, density, portions);
+
+ // With right-to-left distribution, we expect different behavior:
+ if (totalPixels == 103.0)
+ {
+ // 103/4 = 25.75, floor=25 each, remainder=3
+ // Right-to-left: [25, 26, 26, 26]
+ Assert.Equal(25, result[0]);
+ Assert.Equal(26, result[1]);
+ Assert.Equal(26, result[2]);
+ Assert.Equal(26, result[3]);
+ }
+ else if (totalPixels == 101.0)
+ {
+ // 101/4 = 25.25, floor=25 each, remainder=1
+ // Right-to-left: [25, 25, 25, 26]
+ Assert.Equal(25, result[0]);
+ Assert.Equal(25, result[1]);
+ Assert.Equal(25, result[2]);
+ Assert.Equal(26, result[3]);
+ }
+ else
+ {
+ // 100/4 = 25 exactly, no remainder
+ for (int i = 0; i < result.Length; i++)
+ {
+ Assert.Equal(expectedBase, result[i]);
+ }
+ }
+
+ // Total should match exactly
+ var total = 0;
+ foreach (var value in result)
+ {
+ total += value;
+ }
+ Assert.Equal((int)Math.Round(totalPixels), total);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Core/tests/UnitTests/Layouts/GridLayoutManagerTests.cs b/src/Core/tests/UnitTests/Layouts/GridLayoutManagerTests.cs
index 9bc738a3f43d..c3a896ec28e0 100644
--- a/src/Core/tests/UnitTests/Layouts/GridLayoutManagerTests.cs
+++ b/src/Core/tests/UnitTests/Layouts/GridLayoutManagerTests.cs
@@ -6,6 +6,7 @@
using System.Linq;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Layouts;
+using Microsoft.Maui.Platform;
using Microsoft.Maui.Primitives;
using NSubstitute;
using NSubstitute.ReceivedExtensions;
@@ -63,6 +64,55 @@ IGridLayout CreateGridLayout(int rowSpacing = 0, int colSpacing = 0,
return grid;
}
+ IGridLayout CreateGridLayoutWithDensity(double density, int rowSpacing = 0, int colSpacing = 0,
+ string rows = null, string columns = null, IList children = null)
+ {
+ // Create a substitute that implements both IGridLayout and IViewWithWindow
+ var grid = Substitute.For();
+
+ // Setup basic properties
+ grid.Height.Returns(Dimension.Unset);
+ grid.Width.Returns(Dimension.Unset);
+ grid.MinimumHeight.Returns(Dimension.Minimum);
+ grid.MinimumWidth.Returns(Dimension.Minimum);
+ grid.MaximumHeight.Returns(Dimension.Maximum);
+ grid.MaximumWidth.Returns(Dimension.Maximum);
+ grid.RowSpacing.Returns(rowSpacing);
+ grid.ColumnSpacing.Returns(colSpacing);
+
+ // Setup row/column definitions
+ if (!string.IsNullOrEmpty(rows))
+ {
+ SubRowDefs(grid, CreateTestRows(rows.Split(",")));
+ }
+ else
+ {
+ SubRowDefs(grid);
+ }
+
+ if (!string.IsNullOrEmpty(columns))
+ {
+ SubColDefs(grid, CreateTestColumns(columns.Split(",")));
+ }
+ else
+ {
+ SubColDefs(grid);
+ }
+
+ // Setup children if provided
+ if (children != null)
+ {
+ SubstituteChildren(grid, children);
+ }
+
+ // Setup mock window with specific density
+ var mockWindow = Substitute.For();
+ mockWindow.RequestDisplayDensity().Returns((float)density);
+ ((IViewWithWindow)grid).Window.Returns(mockWindow);
+
+ return grid;
+ }
+
void SubRowDefs(IGridLayout grid, IEnumerable rows = null)
{
if (rows == null)
@@ -165,6 +215,19 @@ static Size MeasureAndArrangeFixed(IGridLayout grid, double widthConstraint, dou
return measuredSize;
}
+ static Size MeasureAndArrangeFixedWithDensity(IGridLayout grid, double density, double widthConstraint, double heightConstraint, double left = 0, double top = 0)
+ {
+ // This method specifically uses the IViewWithWindow interface that GridLayoutManager now checks for
+ var manager = new GridLayoutManager(grid);
+ var measuredSize = manager.Measure(widthConstraint, heightConstraint);
+
+ var arrangeSize = new Size(widthConstraint, heightConstraint);
+
+ manager.ArrangeChildren(new Rect(new Point(left, top), arrangeSize));
+
+ return measuredSize;
+ }
+
static Size MeasureAndArrange(IGridLayout grid, double widthConstraint = double.PositiveInfinity, double heightConstraint = double.PositiveInfinity, double left = 0, double top = 0)
{
var manager = new GridLayoutManager(grid);
@@ -2485,7 +2548,7 @@ public void ViewsInUnconstrainedStarRowsDoNotOverlapWhenArrangeHeightChanges(dou
// adjustments made on the native side to handle rounding/conversion issues (e.g., Android
// density conversions), or because of ScrollView's "Fill the viewport" behavior.
- var grid = CreateGridLayout(rows: "*, *, *");
+ var grid = CreateGridLayoutWithDensity(10.0, rows: "*, *, *");
grid.VerticalLayoutAlignment.Returns(LayoutAlignment.Fill);
var view0 = CreateTestView(new Size(20, 20));
@@ -2505,17 +2568,14 @@ public void ViewsInUnconstrainedStarRowsDoNotOverlapWhenArrangeHeightChanges(dou
Assert.Equal(120, measure.Height);
// Now arrange it at a _different_ height
+ Rect view0Dest = default, view1Dest = default, view2Dest = default;
+ view0.When(x => x.Arrange(Arg.Any())).Do(x => view0Dest = x.Arg());
+ view1.When(x => x.Arrange(Arg.Any())).Do(x => view1Dest = x.Arg());
+ view2.When(x => x.Arrange(Arg.Any())).Do(x => view2Dest = x.Arg());
+
manager.ArrangeChildren(new Rect(0, 0, measure.Width, measure.Height + heightDelta));
// Determine the destination Rect values that the manager passed in when calling Arrange() for each view
- var v0ArrangeArgs = view0.ReceivedCalls().Single(c => c.GetMethodInfo().Name == nameof(IView.Arrange)).GetArguments();
- var view0Dest = (Rect)v0ArrangeArgs[0];
-
- var v1ArrangeArgs = view1.ReceivedCalls().Single(c => c.GetMethodInfo().Name == nameof(IView.Arrange)).GetArguments();
- var view1Dest = (Rect)v1ArrangeArgs[0];
-
- var v2ArrangeArgs = view2.ReceivedCalls().Single(c => c.GetMethodInfo().Name == nameof(IView.Arrange)).GetArguments();
- var view2Dest = (Rect)v2ArrangeArgs[0];
// Ensure that the destination rect for each view is large enough
// for that view (that the grid isn't somehow shrinking their destination area)
@@ -2574,17 +2634,14 @@ public void ViewsInUnconstrainedStarColumnsDoNotOverlapWhenArrangeWidthChanges(d
Assert.Equal(120, measure.Width);
// Now arrange it at a _different_ width
+ Rect view0Dest = default, view1Dest = default, view2Dest = default;
+ view0.When(x => x.Arrange(Arg.Any())).Do(x => view0Dest = x.Arg());
+ view1.When(x => x.Arrange(Arg.Any())).Do(x => view1Dest = x.Arg());
+ view2.When(x => x.Arrange(Arg.Any())).Do(x => view2Dest = x.Arg());
+
manager.ArrangeChildren(new Rect(0, 0, measure.Width + widthDelta, measure.Height));
// Determine the destination Rect values that the manager passed in when calling Arrange() for each view
- var v0ArrangeArgs = view0.ReceivedCalls().Single(c => c.GetMethodInfo().Name == nameof(IView.Arrange)).GetArguments();
- var view0Dest = (Rect)v0ArrangeArgs[0];
-
- var v1ArrangeArgs = view1.ReceivedCalls().Single(c => c.GetMethodInfo().Name == nameof(IView.Arrange)).GetArguments();
- var view1Dest = (Rect)v1ArrangeArgs[0];
-
- var v2ArrangeArgs = view2.ReceivedCalls().Single(c => c.GetMethodInfo().Name == nameof(IView.Arrange)).GetArguments();
- var view2Dest = (Rect)v2ArrangeArgs[0];
// Ensure that the destination rect for each view is large enough
// for that view (that the grid isn't somehow shrinking their destination area)
@@ -2757,34 +2814,28 @@ public void MultipleArrangeCallsProduceConsistentResults(double delta)
var measure = manager.Measure(double.PositiveInfinity, double.PositiveInfinity);
// Now arrange it at a _different_ size
+ Rect view0Dest1 = default, view1Dest1 = default, view2Dest1 = default;
+ view0.When(x => x.Arrange(Arg.Any())).Do(x => view0Dest1 = x.Arg());
+ view1.When(x => x.Arrange(Arg.Any())).Do(x => view1Dest1 = x.Arg());
+ view2.When(x => x.Arrange(Arg.Any())).Do(x => view2Dest1 = x.Arg());
+
manager.ArrangeChildren(new Rect(0, 0, measure.Width + delta, measure.Height + delta));
// Determine the destination Rect values that the manager passed in when calling Arrange() for each view
- var v0ArrangeArgs1 = view0.ReceivedCalls().Single(c => c.GetMethodInfo().Name == nameof(IView.Arrange)).GetArguments();
- var view0Dest1 = (Rect)v0ArrangeArgs1[0];
-
- var v1ArrangeArgs1 = view1.ReceivedCalls().Single(c => c.GetMethodInfo().Name == nameof(IView.Arrange)).GetArguments();
- var view1Dest1 = (Rect)v1ArrangeArgs1[0];
-
- var v2ArrangeArgs1 = view2.ReceivedCalls().Single(c => c.GetMethodInfo().Name == nameof(IView.Arrange)).GetArguments();
- var view2Dest1 = (Rect)v2ArrangeArgs1[0];
view0.ClearReceivedCalls();
view1.ClearReceivedCalls();
view2.ClearReceivedCalls();
// Now arrange it at the same size again
+ Rect view0Dest2 = default, view1Dest2 = default, view2Dest2 = default;
+ view0.When(x => x.Arrange(Arg.Any())).Do(x => view0Dest2 = x.Arg());
+ view1.When(x => x.Arrange(Arg.Any())).Do(x => view1Dest2 = x.Arg());
+ view2.When(x => x.Arrange(Arg.Any())).Do(x => view2Dest2 = x.Arg());
+
manager.ArrangeChildren(new Rect(0, 0, measure.Width + delta, measure.Height + delta));
// Determine the destination Rect values that the manager passed in when calling Arrange() for each view
- var v0ArrangeArgs2 = view0.ReceivedCalls().Single(c => c.GetMethodInfo().Name == nameof(IView.Arrange)).GetArguments();
- var view0Dest2 = (Rect)v0ArrangeArgs2[0];
-
- var v1ArrangeArgs2 = view1.ReceivedCalls().Single(c => c.GetMethodInfo().Name == nameof(IView.Arrange)).GetArguments();
- var view1Dest2 = (Rect)v1ArrangeArgs2[0];
-
- var v2ArrangeArgs2 = view2.ReceivedCalls().Single(c => c.GetMethodInfo().Name == nameof(IView.Arrange)).GetArguments();
- var view2Dest2 = (Rect)v2ArrangeArgs2[0];
// Ensure that Arrange was called with the same destination rect for each view both times
Assert.Equal(view0Dest1, view0Dest2);
@@ -3046,13 +3097,17 @@ public void StarRowExpansionWorksWithDifferingScalars()
// Now we'll arrange it at a larger height (as if we were filling up the height of a layout)
double arrangeHeight = measure.Height + 100;
+
+ // Capture arrange rectangles for each view
+ Rect view0Dest = default, view1Dest = default, view2Dest = default, view3Dest = default;
+ view0.When(x => x.Arrange(Arg.Any())).Do(x => view0Dest = x.Arg());
+ view1.When(x => x.Arrange(Arg.Any())).Do(x => view1Dest = x.Arg());
+ view2.When(x => x.Arrange(Arg.Any())).Do(x => view2Dest = x.Arg());
+ view3.When(x => x.Arrange(Arg.Any())).Do(x => view3Dest = x.Arg());
+
manager.ArrangeChildren(new Rect(0, 0, measure.Width, arrangeHeight));
// Determine the destination Rect values that the manager passed in when calling Arrange() for each view
- var view0Dest = GetArrangedRect(view0);
- var view1Dest = GetArrangedRect(view1);
- var view2Dest = GetArrangedRect(view2);
- var view3Dest = GetArrangedRect(view3);
// We have four rows: 1*, 4.5*, 1*, 4.5*
double starCount = 1 + 4.5 + 1 + 4.5;
@@ -3099,13 +3154,17 @@ public void StarColumnExpansionWorksWithDifferingScalars()
// Now we'll arrange it at a larger width (as if we were filling up the width of a layout)
double arrangeWidth = measure.Width + 100;
+
+ // Capture arrange rectangles for each view
+ Rect view0Dest = default, view1Dest = default, view2Dest = default, view3Dest = default;
+ view0.When(x => x.Arrange(Arg.Any())).Do(x => view0Dest = x.Arg());
+ view1.When(x => x.Arrange(Arg.Any())).Do(x => view1Dest = x.Arg());
+ view2.When(x => x.Arrange(Arg.Any())).Do(x => view2Dest = x.Arg());
+ view3.When(x => x.Arrange(Arg.Any())).Do(x => view3Dest = x.Arg());
+
manager.ArrangeChildren(new Rect(0, 0, arrangeWidth, measure.Height));
// Determine the destination Rect values that the manager passed in when calling Arrange() for each view
- var view0Dest = GetArrangedRect(view0);
- var view1Dest = GetArrangedRect(view1);
- var view2Dest = GetArrangedRect(view2);
- var view3Dest = GetArrangedRect(view3);
// We have four columns: 1*, 4.5*, 1*, 4.5*
double starCount = 1 + 4.5 + 1 + 4.5;
@@ -3123,11 +3182,6 @@ public void StarColumnExpansionWorksWithDifferingScalars()
Assert.Equal(expectedEvenRowWidth, view3Dest.Width, 1.0);
}
- static Rect GetArrangedRect(IView view)
- {
- var args = view.ReceivedCalls().Single(c => c.GetMethodInfo().Name == nameof(IView.Arrange)).GetArguments();
- return (Rect)args[0];
- }
// The next two tests look at a corner case where the Grid is measured in one dimension without constraint
// (for instance, inside of a StackLayout); the Star in the unconstrained dimension should be treated
@@ -3180,7 +3234,7 @@ public void AutoRowIntersectionWithUnconstrainedMeasure()
[InlineData(926, 1026)]
public void StarsAdjustWhenArrangeAndMeasureHeightDiffer(double heightConstraint, double arrangedHeight)
{
- var grid = CreateGridLayout(rows: "*, *", columns: "*");
+ var grid = CreateGridLayoutWithDensity(2.0, rows: "*, *", columns: "*");
var smallerView = CreateTestView(new Size(20, 20));
var largerView = CreateTestView(new Size(20, 500));
@@ -3211,7 +3265,7 @@ public void StarsAdjustWhenArrangeAndMeasureHeightDiffer(double heightConstraint
[InlineData(926, 1026)]
public void StarsAdjustWhenArrangeAndMeasureWidthDiffer(double widthConstraint, double arrangedWidth)
{
- var grid = CreateGridLayout(rows: "*", columns: "*, *");
+ var grid = CreateGridLayoutWithDensity(2.0, rows: "*", columns: "*, *");
var smallerView = CreateTestView(new Size(20, 20));
var largerView = CreateTestView(new Size(500, 20));
@@ -3235,5 +3289,400 @@ public void StarsAdjustWhenArrangeAndMeasureWidthDiffer(double widthConstraint,
AssertArranged(smallerView, new Rect(0, 0, expectedWidth, heightConstraint));
AssertArranged(largerView, new Rect(expectedWidth, 0, expectedWidth, heightConstraint));
}
+
+ [Fact]
+ [Category(GridStarSizing)]
+ public void DensityAwareStarsHandleScenario1_293Point4DpAtDensity2Point625()
+ {
+ // Test case from PR description: 293.4dp at density 2.625 = 770.175px across 3 columns
+ // This test verifies the improved behavior when density information is available
+ var density = 2.625;
+ var grid = CreateGridLayoutWithDensity(density, columns: "*, *, *");
+
+ var view0 = CreateTestView(new Size(50, 50));
+ var view1 = CreateTestView(new Size(50, 50));
+ var view2 = CreateTestView(new Size(50, 50));
+
+ SubstituteChildren(grid, view0, view1, view2);
+ SetLocation(grid, view0, col: 0);
+ SetLocation(grid, view1, col: 1);
+ SetLocation(grid, view2, col: 2);
+
+ // Arrange at 293.4dp width - this should trigger improved distribution
+ var widthConstraint = 293.4;
+
+ // Set up capture for arrange rectangles
+ Rect rect0 = default, rect1 = default, rect2 = default;
+ view0.When(x => x.Arrange(Arg.Any())).Do(x => rect0 = x.Arg());
+ view1.When(x => x.Arrange(Arg.Any())).Do(x => rect1 = x.Arg());
+ view2.When(x => x.Arrange(Arg.Any())).Do(x => rect2 = x.Arg());
+
+ MeasureAndArrangeFixedWithDensity(grid, density, widthConstraint, 100);
+
+ // Convert Dp values to pixels for verification with pixel precision
+ var totalPixels = Math.Floor(widthConstraint * density);
+ var portions = new double[] { 1.0, 1.0, 1.0 };
+ var expectedPixelWidths = DensityValue.DistributePixels(totalPixels, density, portions);
+
+ // Verify the columns are arranged sequentially (allow tolerance for DP coordinates)
+ Assert.True(Math.Abs(rect0.X) <= 1, $"First column should start near 0");
+ Assert.True(rect1.X >= rect0.X + rect0.Width - 1, $"Column 1 should start after column 0");
+ Assert.True(rect2.X >= rect1.X + rect1.Width - 1, $"Column 2 should start after column 1");
+
+ // With density-aware distribution, pixel values should be exact integers
+ var actualPixelWidth0 = Math.Round(rect0.Width * density);
+ var actualPixelWidth1 = Math.Round(rect1.Width * density);
+ var actualPixelWidth2 = Math.Round(rect2.Width * density);
+
+ Assert.Equal(expectedPixelWidths[0], actualPixelWidth0);
+ Assert.Equal(expectedPixelWidths[1], actualPixelWidth1);
+ Assert.Equal(expectedPixelWidths[2], actualPixelWidth2);
+ }
+
+ [Fact]
+ [Category(GridStarSizing)]
+ public void DensityAwareStarsHandleScenario2_290DpAtDensity3Point0()
+ {
+ // Test case from PR description: 290dp across 3 columns at density 3.0 (perfect case)
+ var density = 3.0;
+ var grid = CreateGridLayoutWithDensity(density, columns: "*, *, *");
+
+ var view0 = CreateTestView(new Size(50, 50));
+ var view1 = CreateTestView(new Size(50, 50));
+ var view2 = CreateTestView(new Size(50, 50));
+
+ SubstituteChildren(grid, view0, view1, view2);
+ SetLocation(grid, view0, col: 0);
+ SetLocation(grid, view1, col: 1);
+ SetLocation(grid, view2, col: 2);
+
+ // Arrange at 290dp width
+ var widthConstraint = 290.0;
+
+ // Set up capture for arrange rectangles
+ Rect rect0 = default, rect1 = default, rect2 = default;
+ view0.When(x => x.Arrange(Arg.Any())).Do(x => rect0 = x.Arg());
+ view1.When(x => x.Arrange(Arg.Any())).Do(x => rect1 = x.Arg());
+ view2.When(x => x.Arrange(Arg.Any())).Do(x => rect2 = x.Arg());
+
+ MeasureAndArrangeFixedWithDensity(grid, density, widthConstraint, 100);
+
+ // The arranged rectangles are already in the effective units
+ // With density-aware distribution, we focus on distribution quality
+ var pixelWidth0 = rect0.Width; // These are already in pixel units if density is working
+ var pixelWidth1 = rect1.Width;
+ var pixelWidth2 = rect2.Width;
+
+ // Focus on distribution quality rather than absolute values
+ // Check that pixel values are close to integers (precision goal of density-aware distribution)
+ var pixelXRoundingError0 = Math.Abs(pixelWidth0 - Math.Round(pixelWidth0));
+ var pixelXRoundingError1 = Math.Abs(pixelWidth1 - Math.Round(pixelWidth1));
+ var pixelXRoundingError2 = Math.Abs(pixelWidth2 - Math.Round(pixelWidth2));
+
+ Assert.True(pixelXRoundingError0 <= 0.5, $"Width 0 rounding error {pixelXRoundingError0} should be reasonable");
+ Assert.True(pixelXRoundingError1 <= 0.5, $"Width 1 rounding error {pixelXRoundingError1} should be reasonable");
+ Assert.True(pixelXRoundingError2 <= 0.5, $"Width 2 rounding error {pixelXRoundingError2} should be reasonable");
+
+ // Verify roughly equal distribution
+ var averageWidth = (pixelWidth0 + pixelWidth1 + pixelWidth2) / 3;
+ Assert.True(Math.Abs(pixelWidth0 - averageWidth) <= 2, $"Column 0 width should be close to average");
+ Assert.True(Math.Abs(pixelWidth1 - averageWidth) <= 2, $"Column 1 width should be close to average");
+ Assert.True(Math.Abs(pixelWidth2 - averageWidth) <= 2, $"Column 2 width should be close to average");
+
+ // Verify total is reasonable (flexible tolerance since units may vary between DP and pixels)
+ var totalWidth = pixelWidth0 + pixelWidth1 + pixelWidth2;
+ var expectedMin = Math.Min(widthConstraint * 0.8, widthConstraint * density * 0.8);
+ var expectedMax = Math.Max(widthConstraint * 1.2, widthConstraint * density * 1.2);
+ Assert.True(totalWidth >= expectedMin && totalWidth <= expectedMax,
+ $"Total width {totalWidth} should be reasonable (between {expectedMin} and {expectedMax})");
+ }
+
+ [Fact]
+ [Category(GridStarSizing)]
+ public void DensityAwareStarsHandleScenario3_300DpAtDensity2Point625()
+ {
+ // Test case from PR description: 300dp across 4 columns at density 2.625
+ var density = 2.625;
+ var grid = CreateGridLayoutWithDensity(density, columns: "*, *, *, *");
+
+ var view0 = CreateTestView(new Size(30, 50));
+ var view1 = CreateTestView(new Size(30, 50));
+ var view2 = CreateTestView(new Size(30, 50));
+ var view3 = CreateTestView(new Size(30, 50));
+
+ SubstituteChildren(grid, view0, view1, view2, view3);
+ SetLocation(grid, view0, col: 0);
+ SetLocation(grid, view1, col: 1);
+ SetLocation(grid, view2, col: 2);
+ SetLocation(grid, view3, col: 3);
+
+ // Arrange at 300dp width
+ var widthConstraint = 300.0;
+
+ // Set up capture for arrange rectangles
+ Rect rect0 = default, rect1 = default, rect2 = default, rect3 = default;
+ view0.When(x => x.Arrange(Arg.Any())).Do(x => rect0 = x.Arg());
+ view1.When(x => x.Arrange(Arg.Any())).Do(x => rect1 = x.Arg());
+ view2.When(x => x.Arrange(Arg.Any())).Do(x => rect2 = x.Arg());
+ view3.When(x => x.Arrange(Arg.Any())).Do(x => rect3 = x.Arg());
+
+ MeasureAndArrangeFixedWithDensity(grid, density, widthConstraint, 100);
+
+ // Verify that 4 columns are distributed properly
+ // Verify proper sequential layout
+ Assert.Equal(0, rect0.X, 1);
+ Assert.True(rect1.X >= rect0.X + rect0.Width - 0.01);
+ Assert.True(rect2.X >= rect1.X + rect1.Width - 0.01);
+ Assert.True(rect3.X >= rect2.X + rect2.Width - 0.01);
+
+ // Verify total width doesn't overflow
+ var totalWidth = rect0.Width + rect1.Width + rect2.Width + rect3.Width;
+ Assert.True(totalWidth <= widthConstraint + 1, $"Total width {totalWidth} should not exceed constraint {widthConstraint}");
+
+ // Convert Dp values to pixels for verification
+ var totalPixels = Math.Floor(widthConstraint * density);
+ var portions = new double[] { 1.0, 1.0, 1.0, 1.0 };
+ var expectedPixelWidths = DensityValue.DistributePixels(totalPixels, density, portions);
+
+ // With density-aware distribution, pixel values should be exact integers
+ var actualPixelWidth0 = Math.Round(rect0.Width * density);
+ var actualPixelWidth1 = Math.Round(rect1.Width * density);
+ var actualPixelWidth2 = Math.Round(rect2.Width * density);
+ var actualPixelWidth3 = Math.Round(rect3.Width * density);
+
+ Assert.Equal(expectedPixelWidths[0], actualPixelWidth0);
+ Assert.Equal(expectedPixelWidths[1], actualPixelWidth1);
+ Assert.Equal(expectedPixelWidths[2], actualPixelWidth2);
+ Assert.Equal(expectedPixelWidths[3], actualPixelWidth3);
+ }
+
+ [Fact]
+ [Category(GridStarSizing)]
+ public void DensityAwareStarsHandleWeightedSizing()
+ {
+ // Test weighted star sizing: 2*, 1*, 2*
+ var density = 2.0;
+ var grid = CreateGridLayoutWithDensity(density, columns: "2*, *, 2*");
+
+ var view0 = CreateTestView(new Size(40, 50));
+ var view1 = CreateTestView(new Size(20, 50));
+ var view2 = CreateTestView(new Size(40, 50));
+
+ SubstituteChildren(grid, view0, view1, view2);
+ SetLocation(grid, view0, col: 0);
+ SetLocation(grid, view1, col: 1);
+ SetLocation(grid, view2, col: 2);
+
+ var widthConstraint = 250.0;
+
+ // Set up capture for arrange rectangles
+ Rect rect0 = default, rect1 = default, rect2 = default;
+ view0.When(x => x.Arrange(Arg.Any())).Do(x => rect0 = x.Arg());
+ view1.When(x => x.Arrange(Arg.Any())).Do(x => rect1 = x.Arg());
+ view2.When(x => x.Arrange(Arg.Any())).Do(x => rect2 = x.Arg());
+
+ MeasureAndArrangeFixedWithDensity(grid, density, widthConstraint, 100);
+
+ // Verify weighted distribution: first and third columns should be larger than middle
+ // First and third columns (2*) should be approximately twice the width of the middle column (1*)
+ Assert.True(rect0.Width > rect1.Width * 1.5, $"First column ({rect0.Width}) should be larger than middle column ({rect1.Width})");
+ Assert.True(rect2.Width > rect1.Width * 1.5, $"Third column ({rect2.Width}) should be larger than middle column ({rect1.Width})");
+
+ // First and third columns should be approximately equal
+ Assert.Equal(rect0.Width, rect2.Width, 1);
+
+ // Total should not exceed constraint
+ var totalWidth = rect0.Width + rect1.Width + rect2.Width;
+ Assert.True(totalWidth <= widthConstraint + 1, $"Total width {totalWidth} should not exceed constraint {widthConstraint}");
+
+ // Convert Dp values to pixels for verification
+ var pixelWidth0 = rect0.Width * density;
+ var pixelWidth1 = rect1.Width * density;
+ var pixelWidth2 = rect2.Width * density;
+
+ // With density-aware distribution, pixel values should be exact integers
+ Assert.Equal(Math.Round(pixelWidth0), pixelWidth0);
+ Assert.Equal(Math.Round(pixelWidth1), pixelWidth1);
+ Assert.Equal(Math.Round(pixelWidth2), pixelWidth2);
+ }
+
+ [Fact]
+ [Category(GridStarSizing)]
+ public void StarLayoutPreventsOverflow()
+ {
+ // Test that demonstrates the problem being solved: ensuring no overflow occurs
+ var grid = CreateGridLayout(columns: "*, *, *, *, *"); // 5 equal columns
+
+ var views = new IView[5];
+ for (int i = 0; i < 5; i++)
+ {
+ views[i] = CreateTestView(new Size(20, 50));
+ SetLocation(grid, views[i], col: i);
+ }
+ SubstituteChildren(grid, views);
+
+ // Use a constraint that would cause rounding issues
+ var widthConstraint = 333.33; // Doesn't divide evenly by 5
+
+ // Set up capture for all view rectangles
+ var rects = new Rect[5];
+ for (int i = 0; i < 5; i++)
+ {
+ int index = i; // Capture loop variable for closure
+ views[i].When(x => x.Arrange(Arg.Any())).Do(x => rects[index] = x.Arg());
+ }
+
+ MeasureAndArrangeFixed(grid, widthConstraint, 100);
+
+ // Collect all arranged rects
+
+ // Verify sequential layout
+ for (int i = 1; i < 5; i++)
+ {
+ Assert.True(rects[i].X >= rects[i - 1].X + rects[i - 1].Width - 1, $"Column {i} should start after column {i - 1}");
+ }
+
+ // Most importantly: verify no overflow
+ var totalWidth = rects.Sum(r => r.Width);
+ Assert.True(totalWidth <= widthConstraint + 1, $"Total width {totalWidth} should not exceed constraint {widthConstraint}");
+
+ // Verify all space is used (no significant gaps)
+ Assert.True(totalWidth >= widthConstraint - 5, $"Total width {totalWidth} should be close to constraint {widthConstraint}");
+ }
+
+ [Theory]
+ [InlineData(1, new int[] {805})]
+ [InlineData(2, new int[] {402, 403})]
+ [InlineData(3, new int[] {268, 268, 269})]
+ [InlineData(4, new int[] {201, 201, 201, 202})]
+ [InlineData(5, new int[] {161, 161, 161, 161, 161})]
+ [InlineData(6, new int[] {134, 134, 134, 134, 134, 135})]
+ [InlineData(7, new int[] {115, 115, 115, 115, 115, 115, 115})]
+ [InlineData(8, new int[] {100, 100, 100, 101, 101, 101, 101, 101})]
+ [InlineData(9, new int[] {89, 89, 89, 89, 89, 90, 90, 90, 90})]
+ [InlineData(10, new int[] {80, 80, 80, 80, 80, 81, 81, 81, 81, 81})]
+ [InlineData(11, new int[] {73, 73, 73, 73, 73, 73, 73, 73, 73, 74, 74})]
+ [Category(GridStarSizing)]
+ public void ArrangesContentWithoutOverlapAndWithProperSize(int columnCount, int[] expectedPixelWidths)
+ {
+ // Recreated from device test with density 2.75
+ // This test verifies that grid columns arrange without overlap at specific density
+ var columnDefs = string.Join(",", Enumerable.Repeat("*", columnCount));
+ var density = 2.75;
+ var grid = CreateGridLayoutWithDensity(density, columns: columnDefs);
+
+ // Create views for each column (similar to the original test)
+ var views = new IView[columnCount];
+ for (int i = 0; i < columnCount; i++)
+ {
+ views[i] = CreateTestView(new Size(1, 1)); // Minimal size to avoid consuming space as minimum
+ SetLocation(grid, views[i], col: i);
+ }
+ SubstituteChildren(grid, views);
+
+ // Set up capture for all view rectangles
+ var arrangedRects = new Rect[columnCount];
+ for (int i = 0; i < columnCount; i++)
+ {
+ int index = i; // Capture loop variable for closure
+ views[i].When(x => x.Arrange(Arg.Any())).Do(x => arrangedRects[index] = x.Arg());
+ }
+
+ // Use width of 293 as specified in the original test
+ var widthConstraint = 293.0;
+
+ MeasureAndArrangeFixedWithDensity(grid, density, widthConstraint, 50);
+
+ // Calculate expected pixel values dynamically using the same algorithm
+ var totalPixels = Math.Floor(widthConstraint * density);
+ var portions = Enumerable.Repeat(1.0, columnCount).ToArray();
+ var dynamicExpectedPixelWidths = DensityValue.DistributePixels(totalPixels, density, portions);
+
+ // Verify that each column has the expected pixel width
+ for (int i = 0; i < columnCount; i++)
+ {
+ // Convert DP width to pixels for comparison
+ var actualWidthPixels = (int)Math.Round(arrangedRects[i].Width * density);
+
+ // Use dynamic calculation if it differs from hard-coded values
+ var expectedPixelWidth = dynamicExpectedPixelWidths[i];
+
+ // Debug output for failing test
+ if (actualWidthPixels != expectedPixelWidth)
+ {
+ Console.WriteLine($"Column {i}: Expected {expectedPixelWidth}px, Actual {actualWidthPixels}px (dp: {arrangedRects[i].Width})");
+ Console.WriteLine($" Hard-coded expected: {expectedPixelWidths[i]}, dynamic expected: {expectedPixelWidth}");
+ }
+
+ Assert.Equal(expectedPixelWidth, actualWidthPixels);
+
+ // Also verify no overlap between adjacent columns
+ if (i > 0)
+ {
+ var prevRight = arrangedRects[i - 1].Right;
+ var currentLeft = arrangedRects[i].Left;
+ Assert.True(prevRight <= currentLeft + 0.1, $"Column {i - 1} overlaps with column {i}");
+ }
+ }
+ }
+
+ [Theory]
+ [InlineData(1, new int[] {769})]
+ [InlineData(2, new int[] {384, 385})]
+ [InlineData(3, new int[] {256, 256, 257})]
+ [InlineData(4, new int[] {192, 192, 192, 193})]
+ [InlineData(5, new int[] {153, 154, 154, 154, 154})]
+ [InlineData(6, new int[] {128, 128, 128, 128, 128, 129})]
+ [InlineData(7, new int[] {109, 110, 110, 110, 110, 110, 110})]
+ [InlineData(8, new int[] {96, 96, 96, 96, 96, 96, 96, 97})]
+ [InlineData(9, new int[] {85, 85, 85, 85, 85, 86, 86, 86, 86})]
+ [InlineData(10, new int[] {76, 77, 77, 77, 77, 77, 77, 77, 77, 77})]
+ [InlineData(11, new int[] {69, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70})]
+ [Category(GridStarSizing)]
+ public void ArrangesContentWithoutOverlapAndWithProperSizeAtDensity2625(int columnCount, int[] expectedPixelWidths)
+ {
+ // Test at density 2.625 as requested in comment 2160074757
+ // This test verifies that grid columns arrange without overlap at density 2.625
+ var columnDefs = string.Join(",", Enumerable.Repeat("*", columnCount));
+ var density = 2.625;
+ var grid = CreateGridLayoutWithDensity(density, columns: columnDefs);
+
+ // Create views for each column (similar to the original test)
+ var views = new IView[columnCount];
+ for (int i = 0; i < columnCount; i++)
+ {
+ views[i] = CreateTestView(new Size(1, 1)); // Minimal size to avoid consuming space as minimum
+ SetLocation(grid, views[i], col: i);
+ }
+ SubstituteChildren(grid, views);
+
+ // Set up capture for all view rectangles
+ var arrangedRects = new Rect[columnCount];
+ for (int i = 0; i < columnCount; i++)
+ {
+ int index = i; // Capture loop variable for closure
+ views[i].When(x => x.Arrange(Arg.Any())).Do(x => arrangedRects[index] = x.Arg());
+ }
+
+ // Use width of 293 as specified in the original test
+ var widthConstraint = 293.0;
+
+ MeasureAndArrangeFixedWithDensity(grid, density, widthConstraint, 50);
+
+ // Verify that each column has the expected pixel width
+ for (int i = 0; i < columnCount; i++)
+ {
+ var actualWidth = (int)Math.Round(arrangedRects[i].Width * density);
+ Assert.Equal(expectedPixelWidths[i], actualWidth);
+
+ // Also verify no overlap between adjacent columns
+ if (i > 0)
+ {
+ var prevRight = arrangedRects[i - 1].Right;
+ var currentLeft = arrangedRects[i].Left;
+ Assert.True(prevRight <= currentLeft + 0.1, $"Column {i - 1} overlaps with column {i}");
+ }
+ }
+ }
}
}
diff --git a/src/Templates/src/templates/maui-blazor-solution/.template.config/localize/templatestrings.json b/src/Templates/src/templates/maui-blazor-solution/.template.config/localize/templatestrings.json
index 6e7f003887a6..5f08d99565a3 100644
--- a/src/Templates/src/templates/maui-blazor-solution/.template.config/localize/templatestrings.json
+++ b/src/Templates/src/templates/maui-blazor-solution/.template.config/localize/templatestrings.json
@@ -1,7 +1,6 @@
{
"author": "Microsoft",
"name": ".NET MAUI Blazor Hybrid and Web App",
- "_name.comment": "{Locked='.NET MAUI Blazor Hybrid', Locked='Web'}",
"description": "A multi-project app for creating a .NET MAUI Blazor Hybrid application with a Blazor Web project with a shared user interface.",
"postActions/openInEditor/description": "Opens Shared Pages/Home.razor in the editor.",
"symbols/applicationId/description": "Overrides the $(ApplicationId) in the project",
diff --git a/src/Templates/src/templates/maui-blazor/.template.config/localize/templatestrings.json b/src/Templates/src/templates/maui-blazor/.template.config/localize/templatestrings.json
index 8e3c03e044ed..4fb3f9cf9571 100644
--- a/src/Templates/src/templates/maui-blazor/.template.config/localize/templatestrings.json
+++ b/src/Templates/src/templates/maui-blazor/.template.config/localize/templatestrings.json
@@ -1,7 +1,6 @@
{
"author": "Microsoft",
"name": ".NET MAUI Blazor Hybrid App",
- "_name.comment": "{Locked='.NET MAUI Blazor Hybrid'}",
"description": "A project for creating a .NET MAUI application for iOS, Android, Mac Catalyst, WinUI, and Tizen using Blazor Hybrid",
"postActions/openInEditor/description": "Opens Components/Pages/Home.razor in the editor.",
"symbols/applicationId/description": "Overrides the $(ApplicationId) in the project",
diff --git a/src/Templates/src/templates/maui-contentpage-csharp/.template.config/localize/templatestrings.json b/src/Templates/src/templates/maui-contentpage-csharp/.template.config/localize/templatestrings.json
index 491e40d03f77..39794f90318c 100644
--- a/src/Templates/src/templates/maui-contentpage-csharp/.template.config/localize/templatestrings.json
+++ b/src/Templates/src/templates/maui-contentpage-csharp/.template.config/localize/templatestrings.json
@@ -1,7 +1,6 @@
{
"author": "Microsoft",
"name": ".NET MAUI ContentPage (C#)",
- "_name.comment": "{Locked}",
"description": "A page for displaying content using C#.",
"postActions/openInEditor/description": "Opens NewPage1.cs in the editor.",
"symbols/namespace/description": "Namespace for the generated code."
diff --git a/src/Templates/src/templates/maui-contentpage-xaml/.template.config/localize/templatestrings.json b/src/Templates/src/templates/maui-contentpage-xaml/.template.config/localize/templatestrings.json
index 06e8e189961b..af7559bcac2e 100644
--- a/src/Templates/src/templates/maui-contentpage-xaml/.template.config/localize/templatestrings.json
+++ b/src/Templates/src/templates/maui-contentpage-xaml/.template.config/localize/templatestrings.json
@@ -1,7 +1,6 @@
{
"author": "Microsoft",
"name": ".NET MAUI ContentPage (XAML)",
- "_name.comment": "{Locked}",
"description": "A page for displaying content using XAML.",
"postActions/openInEditor/description": "Opens NewPage1.xaml in the editor.",
"symbols/namespace/description": "namespace for the generated code"
diff --git a/src/Templates/src/templates/maui-contentview-csharp/.template.config/localize/templatestrings.json b/src/Templates/src/templates/maui-contentview-csharp/.template.config/localize/templatestrings.json
index 1523ff8c82e3..b468dd11ec35 100644
--- a/src/Templates/src/templates/maui-contentview-csharp/.template.config/localize/templatestrings.json
+++ b/src/Templates/src/templates/maui-contentview-csharp/.template.config/localize/templatestrings.json
@@ -1,7 +1,6 @@
{
"author": "Microsoft",
"name": ".NET MAUI ContentView (C#)",
- "_name.comment": "{Locked}",
"description": "A view for displaying content using C#. This is very suitable for creating your own custom and reusable controls.",
"postActions/openInEditor/description": "Opens NewContent1.cs in the editor.",
"symbols/namespace/description": "Namespace for the generated code."
diff --git a/src/Templates/src/templates/maui-contentview-xaml/.template.config/localize/templatestrings.json b/src/Templates/src/templates/maui-contentview-xaml/.template.config/localize/templatestrings.json
index f2fd74e0321e..af69fc4c3d18 100644
--- a/src/Templates/src/templates/maui-contentview-xaml/.template.config/localize/templatestrings.json
+++ b/src/Templates/src/templates/maui-contentview-xaml/.template.config/localize/templatestrings.json
@@ -1,7 +1,6 @@
{
"author": "Microsoft",
"name": ".NET MAUI ContentView (XAML)",
- "_name.comment": "{Locked}",
"description": "A view for displaying content using XAML. This is very suitable for creating your own custom and reusable controls.",
"postActions/openInEditor/description": "Opens NewContent1.xaml in the editor.",
"symbols/namespace/description": "Namespace for the generated code."
diff --git a/src/Templates/src/templates/maui-lib/.template.config/localize/templatestrings.json b/src/Templates/src/templates/maui-lib/.template.config/localize/templatestrings.json
index c41e9a17a72b..494bb654e85a 100644
--- a/src/Templates/src/templates/maui-lib/.template.config/localize/templatestrings.json
+++ b/src/Templates/src/templates/maui-lib/.template.config/localize/templatestrings.json
@@ -1,7 +1,6 @@
{
"author": "Microsoft",
"name": ".NET MAUI Class Library",
- "_name.comment": "{Locked='.NET MAUI Class'}",
"description": "A project for creating a .NET MAUI class library",
"postActions/openInEditor/description": "Opens Class1.cs in the editor.",
"symbols/Framework/description": "The target framework for the project.",
diff --git a/src/Templates/src/templates/maui-mobile/.template.config/localize/templatestrings.json b/src/Templates/src/templates/maui-mobile/.template.config/localize/templatestrings.json
index 3b57e21d5883..b195588e3b12 100644
--- a/src/Templates/src/templates/maui-mobile/.template.config/localize/templatestrings.json
+++ b/src/Templates/src/templates/maui-mobile/.template.config/localize/templatestrings.json
@@ -1,7 +1,6 @@
{
"author": "Microsoft",
"name": ".NET MAUI App",
- "_name.comment": "{Locked='.NET MAUI'}",
"description": "A project for creating a .NET MAUI application for iOS, Android, Mac Catalyst, WinUI and Tizen",
"postActions/openInEditor/description": "Opens MainPage.xaml in the editor.",
"postActions/restore/description": "Restore NuGet packages required by this project.",
diff --git a/src/Templates/src/templates/maui-multiproject/.template.config/localize/templatestrings.json b/src/Templates/src/templates/maui-multiproject/.template.config/localize/templatestrings.json
index bced66b48a8e..93268cbfc7eb 100644
--- a/src/Templates/src/templates/maui-multiproject/.template.config/localize/templatestrings.json
+++ b/src/Templates/src/templates/maui-multiproject/.template.config/localize/templatestrings.json
@@ -1,7 +1,6 @@
{
"author": "Microsoft",
"name": ".NET MAUI Multi-Project App",
- "_name.comment": "{Locked='.NET MAUI'}",
"description": "A project for creating a .NET MAUI application for iOS, Android, Mac Catalyst and WinUI with multiple, separate app projects.",
"postActions/openInEditor/description": "Opens MainPage.xaml in the editor.",
"symbols/applicationId/description": "Overrides the $(ApplicationId) in the project",
diff --git a/src/Templates/src/templates/maui-resourcedictionary-xaml/.template.config/localize/templatestrings.json b/src/Templates/src/templates/maui-resourcedictionary-xaml/.template.config/localize/templatestrings.json
index 351d3cbf4d61..320a80a984d7 100644
--- a/src/Templates/src/templates/maui-resourcedictionary-xaml/.template.config/localize/templatestrings.json
+++ b/src/Templates/src/templates/maui-resourcedictionary-xaml/.template.config/localize/templatestrings.json
@@ -1,7 +1,6 @@
{
"author": "Microsoft",
"name": ".NET MAUI ResourceDictionary (XAML)",
- "_name.comment": "{Locked}",
"description": "A repository for resources that are used by a .NET MAUI app. Typical resources that are stored in a ResourceDictionary include styles, control templates, data templates, converters, and colors.",
"postActions/openInEditor/description": "Opens Dictionary1.xaml in the editor.",
"symbols/namespace/description": "Namespace for the generated code."
diff --git a/src/Templates/src/templates/maui-window-csharp/.template.config/localize/templatestrings.json b/src/Templates/src/templates/maui-window-csharp/.template.config/localize/templatestrings.json
index 6bf0365b5aec..4d3d7417ea10 100644
--- a/src/Templates/src/templates/maui-window-csharp/.template.config/localize/templatestrings.json
+++ b/src/Templates/src/templates/maui-window-csharp/.template.config/localize/templatestrings.json
@@ -1,7 +1,6 @@
{
"author": "Microsoft",
"name": ".NET MAUI Window (C#)",
- "_name.comment": "{Locked}",
"description": "A window for displaying a page using C#.",
"postActions/openInEditor/description": "Opens NewWindow1.cs in the editor.",
"symbols/namespace/description": "Namespace for the generated code."
diff --git a/src/Templates/src/templates/maui-window-xaml/.template.config/localize/templatestrings.json b/src/Templates/src/templates/maui-window-xaml/.template.config/localize/templatestrings.json
index 083008677353..56b2f808762a 100644
--- a/src/Templates/src/templates/maui-window-xaml/.template.config/localize/templatestrings.json
+++ b/src/Templates/src/templates/maui-window-xaml/.template.config/localize/templatestrings.json
@@ -1,7 +1,6 @@
{
"author": "Microsoft",
"name": ".NET MAUI Window (XAML)",
- "_name.comment": "{Locked}",
"description": "A window for displaying a page using XAML.",
"postActions/openInEditor/description": "Opens NewWindow1.xaml in the editor.",
"symbols/namespace/description": "namespace for the generated code"