Skip to content
126 changes: 126 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue30957.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using Microsoft.Maui.Layouts;

namespace Maui.Controls.Sample.Issues
{
[Issue(IssueTracker.Github, 30957, "FlexLayout Wrap Misalignment with Dynamically-Sized Buttons in .NET MAUI", PlatformAffected.All)]
public class Issue30957 : ContentPage
{
FlexLayout _testFlexLayout;
Label _statusLabel;
Button _toggleButton1, _toggleButton2, _toggleButton3;
bool _isToggled = false;

public Issue30957()
{
Title = "Issue30957";

var stackLayout = new StackLayout { Padding = new Thickness(20) };

var instructionsLabel = new Label
{
AutomationId = "Issue30957FirstLabel",
Text = "This reproduces FlexLayout wrapping issue with font family switching. Click 'Toggle Font' to trigger precision issues.",
Margin = new Thickness(0, 0, 0, 20)
};
stackLayout.Children.Add(instructionsLabel);

var toggleAllButton = new Button
{
Text = "Toggle Font Family",
AutomationId = "Issue30957ToggleButton",
BackgroundColor = Colors.LightBlue,
Margin = new Thickness(0, 0, 0, 10)
};
toggleAllButton.Clicked += OnToggleFontClicked;
stackLayout.Children.Add(toggleAllButton);

var border = new Border
{
Stroke = Colors.Black,
StrokeThickness = 1,
BackgroundColor = Colors.White,
HorizontalOptions = LayoutOptions.Start,
Padding = new Thickness(4)
};

_testFlexLayout = new FlexLayout
{
Wrap = FlexWrap.Wrap
};

_toggleButton1 = new Button
{
Text = "Button1",
AutomationId = "Issue30957Button1",
BackgroundColor = Colors.White,
FontFamily = "OpenSansRegular",
CornerRadius = 0,
FontSize = 16,
TextColor = Colors.Black
};

_toggleButton2 = new Button
{
Text = "Button2",
AutomationId = "Issue30957Button2",
BackgroundColor = Colors.White,
FontFamily = "OpenSansRegular",
CornerRadius = 0,
FontSize = 16,
TextColor = Colors.Black
};

_toggleButton3 = new Button
{
Text = "Button3",
AutomationId = "Issue30957Button3",
BackgroundColor = Colors.White,
FontFamily = "OpenSansRegular",
CornerRadius = 0,
FontSize = 16,
TextColor = Colors.Black
};

_testFlexLayout.Children.Add(_toggleButton1);
_testFlexLayout.Children.Add(_toggleButton2);
_testFlexLayout.Children.Add(_toggleButton3);

border.Content = _testFlexLayout;
stackLayout.Children.Add(border);

_statusLabel = new Label
{
AutomationId = "Issue30957StatusLabel",
Text = "Status: Ready to test - Click 'Toggle Font Family' to trigger precision issue",
Margin = new Thickness(0, 10, 0, 0),
TextColor = Colors.DarkGreen
};
stackLayout.Children.Add(_statusLabel);

Content = stackLayout;
}

void OnToggleFontClicked(object sender, EventArgs e)
{
_isToggled = !_isToggled;

string fontFamily = _isToggled ? "OpenSansSemibold" : "OpenSansRegular";
Color backgroundColor = _isToggled ? Colors.Black : Colors.White;
Color textColor = _isToggled ? Colors.White : Colors.Black;

_toggleButton1.FontFamily = fontFamily;
_toggleButton1.BackgroundColor = backgroundColor;
_toggleButton1.TextColor = textColor;

_toggleButton2.FontFamily = fontFamily;
_toggleButton2.BackgroundColor = backgroundColor;
_toggleButton2.TextColor = textColor;

_toggleButton3.FontFamily = fontFamily;
_toggleButton3.BackgroundColor = backgroundColor;
_toggleButton3.TextColor = textColor;

_statusLabel.Text = $"Status: Font toggled to {fontFamily} - This triggers precision issue that tolerance fix addresses";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues
{
public class Issue30957 : _IssuesUITest
{
public Issue30957(TestDevice testDevice) : base(testDevice)
{
}

public override string Issue => "FlexLayout Wrap Misalignment with Dynamically-Sized Buttons in .NET MAUI";

[Test]
[Category(UITestCategories.Layout)]
public void FlexLayoutWrappingWithToleranceWorksCorrectly()
{

App.WaitForElement("Issue30957ToggleButton");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems can be still be reproduced on Android. #31341 (comment)
Could the test verify the rendering with snapshots?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @jsuarezruiz , The issue could not be reproduced on my end. Based on the reported issue on windows, it appears to be related to device density or another rendering factor.
Also, the test case has been updated to capture a screenshot for verification. Please review and confirm if any additional concerns remain.

@DavidIDCI DavidIDCI Oct 31, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it does seem to be related to device density or other device display factor.

I tested with a pixel 5 emulator that has a density of 2.75, and the issue does not reproduce with the provided sample in the linked issue ( #30957 ). I then tested with a pixel 7, density 2.625, and it does reproduce the issue with the provided sample. 2.625 is a pretty common density among Android devices, and all of them with that density reproduce this issue with the sample.

I will note that almost every Android device I have tested will reproduce this issue, but in slightly different cases (different text in the buttons) depending on the device's density. I can confirm that the "Button1/Button2/Button3" text in the buttons used in the UI test doesn't reproduce on any of the Android device's I've tested. I don't know the details of what the UI tests are run on, but if there are details somewhere I can try to find a better repro for the automated test cases. Does the text in the #30957 sample also pass automated tests?

I also will note that because my team was experiencing this issue mostly on Android, we applied the workaround in the code here to our own branch of the FlexLayout and just removed the #if WINDOWS conditionals, and the issue seems to be mitigated on both platforms. I understand you probably can't merge code in without a valid A/B test case, but I do think this fix is best applied to all platforms, not just Windows. Again I am willing to help find better repro steps -- and I recommend testing with the emulator config below (and the text in the original issue) to validate the linked issue repros on Android.

config.ini

disk.dataPartition.size=6442450944
fastboot.forceColdBoot=no
fastboot.forceFastBoot=yes
hw.accelerometer=yes
hw.arc=no
hw.audioInput=yes
hw.battery=yes
hw.camera.back=virtualscene
hw.camera.front=emulated
hw.cpu.ncore=4
hw.dPad=no
hw.gps=yes
hw.gpu.mode=auto
hw.keyboard=yes
hw.lcd.density=420
hw.lcd.height=1200
hw.lcd.width=540
hw.mainKeys=no
hw.ramSize=1536
hw.sdCard=yes
hw.sensors.orientation=yes
hw.sensors.proximity=yes
hw.trackBall=no
sdcard.size=512M
skin.dynamic=yes
skin.name=1080x2400
vm.heapSize=256
hw.device.hash2=MD5:9cfa1e95ac9cd7b57d7aac913d0b34ca17a9d277d6036111b5bd65a8291598a3
hw.device.name=pixel_7
hw.device.manufacturer=Google
showDeviceFrame=no
tag.id=google_apis_playstore
tag.display=Google Play
PlayStore.enabled=true
abi.type=x86_64
hw.cpu.arch=x86_64
hw.gpu.enabled=yes
avd.ini.displayname=Pixel 7 - API 36
image.sysdir.1=system-images\android-36\google_apis_playstore\x86_64\
AvdId=pixel_7_-_api_36


Copilot AI Aug 27, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an empty line after the WaitForElement call that serves no purpose and should be removed to improve code readability.

Suggested change
App.WaitForElement("Issue30957ToggleButton");

Copilot uses AI. Check for mistakes.
var buttonRectsInitial = new[] {
App.WaitForElement("Issue30957Button1").GetRect(),
App.WaitForElement("Issue30957Button2").GetRect(),
App.WaitForElement("Issue30957Button3").GetRect()
};

for (int i = 0; i < buttonRectsInitial.Length - 1; i++)
{
Assert.That(buttonRectsInitial[i+1].X, Is.GreaterThan(buttonRectsInitial[i].X), $"Button{i+2} should be right of Button{i+1} (initial)");
Assert.That(Math.Abs(buttonRectsInitial[i].Y - buttonRectsInitial[i+1].Y), Is.LessThan(10), $"Button{i+1} and Button{i+2} should be on the same row (initial)");
}

App.Tap("Issue30957ToggleButton");

var buttonRectsFinal = new[] {
App.WaitForElement("Issue30957Button1").GetRect(),
App.WaitForElement("Issue30957Button2").GetRect(),
App.WaitForElement("Issue30957Button3").GetRect()
};

for (int i = 0; i < buttonRectsFinal.Length - 1; i++)
{
Assert.That(Math.Abs(buttonRectsFinal[i].Y - buttonRectsFinal[i+1].Y), Is.LessThan(10), $"Button{i+1} and Button{i+2} should remain on the same row after font change");
Assert.That(buttonRectsFinal[i+1].X, Is.GreaterThan(buttonRectsFinal[i].X), $"Button{i+2} should be right of Button{i+1} after font change");
}

var statusText = App.WaitForElement("Issue30957StatusLabel").GetText();
Assert.That(statusText, Does.Contain("Font toggled"), "Status label should indicate font was toggled");
}
}
}
7 changes: 6 additions & 1 deletion src/Core/src/Layouts/Flex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -533,10 +533,15 @@ static void layout_item(Item item, float width, float height, bool inMeasureMode
child.Frame[layout.frame_size_i] = basis - child.MarginThickness(layout.vertical);
}

float flex_tolerance = layout.flex_dim;
#if WINDOWS
// Windows requires tolerance for floating-point precision issues in flex wrapping
flex_tolerance += 0.1f;

Copilot AI Aug 27, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic number 0.1f lacks explanation for why this specific value was chosen. Consider defining this as a named constant with documentation explaining how this tolerance value was determined and whether it relates to specific Windows scaling factors or precision limits.

Suggested change
float flex_tolerance = layout.flex_dim;
#if WINDOWS
// Windows requires tolerance for floating-point precision issues in flex wrapping
flex_tolerance += 0.1f;
flex_tolerance += WindowsFlexTolerance;

Copilot uses AI. Check for mistakes.
#endif
float child_size = child.Frame[layout.frame_size_i];
if (layout.wrap)
{
if (layout.flex_dim < child_size)
if (flex_tolerance < child_size)
{
// Not enough space for this child on this line, layout the
// remaining items and move it to a new line.
Expand Down
Loading