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,35 @@
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");
App.Tap("Issue30957ToggleButton");

var button1Rect = App.WaitForElement("Issue30957Button1").GetRect();
var button2Rect = App.WaitForElement("Issue30957Button2").GetRect();
var button3Rect = App.WaitForElement("Issue30957Button3").GetRect();

// All three buttons should be on the same row (same Y position)
Assert.That(button2Rect.Y, Is.EqualTo(button1Rect.Y), "Button1 and Button2 should have the same Y position (same row)");
Assert.That(button3Rect.Y, Is.EqualTo(button1Rect.Y), "Button1 and Button3 should have the same Y position (same row)");

// Buttons should be laid out horizontally: each subsequent button starts after the previous one
Assert.That(button2Rect.X, Is.GreaterThan(button1Rect.X), "Button2 should be to the right of Button1");
Assert.That(button3Rect.X, Is.GreaterThan(button2Rect.X), "Button3 should be to the right of Button2");
}
}
}
21 changes: 20 additions & 1 deletion src/Core/src/Layouts/Flex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -533,10 +533,29 @@ static void layout_item(Item item, float width, float height, bool inMeasureMode
child.Frame[layout.frame_size_i] = basis - child.MarginThickness(layout.vertical);
}

#if ANDROID || WINDOWS
// A small tolerance value used to account for floating-point precision errors
// when determining whether a child item fits on the current line during flex wrapping.
// Without this tolerance, items that should fit may be incorrectly wrapped to a new line
// due to minor rounding discrepancies in size calculations.
// 0.1f accounts for sub-pixel rounding errors introduced by DPI scaling factors
// (e.g., 1.25x on Windows, 2.625x density on Android). Values below 1.0f have
// no meaningful effect on intentional layout gaps which are always >= 1dp.
const float FlexWrapTolerance = 0.1f;

// The issue was originally reported on Windows, related to device density or
// scaling factor. It has also been reproduced on Android due to density variations.
// The tolerance is applied unconditionally across all platforms as a conservative fix;
// it has no adverse effects on layout behavior.
float flex_tolerance = layout.flex_dim + FlexWrapTolerance;
#else
// On other platforms, we haven't observed the same precision issues, so we can use the actual available space as the tolerance.
float flex_tolerance = layout.flex_dim;
#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