diff --git a/maui/src/Button/SfButton.Methods.cs b/maui/src/Button/SfButton.Methods.cs index 32d48a4c..1b03c0f6 100644 --- a/maui/src/Button/SfButton.Methods.cs +++ b/maui/src/Button/SfButton.Methods.cs @@ -538,20 +538,41 @@ void DrawButtonOutline(ICanvas canvas, RectF dirtyRect) /// Calculated width. double CalculateWidth(double widthConstraint) { - if (widthConstraint == double.PositiveInfinity || widthConstraint < 0 || WidthRequest < 0) + // If WidthRequest is explicitly set, use it + if (WidthRequest > 0) { - if (ShowIcon && ImageSource != null) - { - return ImageAlignment == Alignment.Top || ImageAlignment == Alignment.Bottom - ? Math.Max(ImageSize, TextSize.Width) + Padding.Left + Padding.Right + StrokeThickness + (_leftPadding * 2) + (_rightPadding * 2) - : ImageSize + TextSize.Width + StrokeThickness + Padding.Left + Padding.Right + (_leftPadding * 2) + (_rightPadding * 2); - } - else - { - return TextSize.Width + Padding.Left + Padding.Right + StrokeThickness + (_leftPadding * 2) + (_rightPadding * 2); - } + return WidthRequest; + } + + // If HorizontalOptions is Fill, use the constraint width when available + if (HorizontalOptions.Alignment == LayoutAlignment.Fill && + widthConstraint != double.PositiveInfinity && widthConstraint > 0) + { + return widthConstraint; + } + + // For HorizontalOptions Start, Center, End, calculate natural width based on content + // but ensure it doesn't exceed available width constraint to prevent overflow + double naturalWidth; + if (ShowIcon && ImageSource != null) + { + naturalWidth = ImageAlignment == Alignment.Top || ImageAlignment == Alignment.Bottom + ? Math.Max(ImageSize, TextSize.Width) + Padding.Left + Padding.Right + StrokeThickness + (_leftPadding * 2) + (_rightPadding * 2) + : ImageSize + TextSize.Width + StrokeThickness + Padding.Left + Padding.Right + (_leftPadding * 2) + (_rightPadding * 2); + } + else + { + naturalWidth = TextSize.Width + Padding.Left + Padding.Right + StrokeThickness + (_leftPadding * 2) + (_rightPadding * 2); + } + + // If we have a finite width constraint and the natural width would exceed it, + // constrain the width to prevent overflow (especially important on Android) + if (widthConstraint != double.PositiveInfinity && widthConstraint > 0 && naturalWidth > widthConstraint) + { + return widthConstraint; } - return widthConstraint; + + return naturalWidth; } /// @@ -570,6 +591,7 @@ double CalculateHeight(double heightConstraint, double width) } else { + // For truncation modes (Head, Middle, Tail) and NoWrap, text should always be on a single line _numberOfLines = 1; } if (ShowIcon && ImageSource != null) @@ -690,7 +712,7 @@ protected override Size MeasureContent(double widthConstraint, double heightCons base.MeasureContent(widthConstraint, heightConstraint); double width = CalculateWidth(widthConstraint); - double height = CalculateHeight(heightConstraint, WidthRequest > 0 ? WidthRequest : width); + double height = CalculateHeight(heightConstraint, width); if (Children.Count > 0 && IsItemTemplate) { diff --git a/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Buttons/SfButtonUnitTests.cs b/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Buttons/SfButtonUnitTests.cs index ef4a8e03..0b45c018 100644 --- a/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Buttons/SfButtonUnitTests.cs +++ b/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Buttons/SfButtonUnitTests.cs @@ -776,6 +776,169 @@ private void AttachVisualStates(SfButton button) #endregion + #region HorizontalOptions Width Tests + + [Theory] + [InlineData(LayoutAlignment.Start)] + [InlineData(LayoutAlignment.Center)] + [InlineData(LayoutAlignment.End)] + public void CalculateWidth_WithNonFillHorizontalOptions_ShouldUseContentWidth(LayoutAlignment alignment) + { + var button = new SfButton(); + button.Text = "Sample Text"; + button.HorizontalOptions = new LayoutOptions(alignment, false); + + // Test with available width constraint larger than content + double widthConstraint = 300; + var actualWidth = (double)InvokePrivateMethod(button, "CalculateWidth", widthConstraint); + + // Should not use the full constraint width, but rather content-based width + Assert.True(actualWidth < widthConstraint, + $"Button with HorizontalOptions.{alignment} should not fill constraint width {widthConstraint}, but got {actualWidth}"); + } + + [Fact] + public void CalculateWidth_WithFillHorizontalOptions_ShouldUseConstraintWidth() + { + var button = new SfButton(); + button.Text = "Sample Text"; + button.HorizontalOptions = LayoutOptions.Fill; + + // Test with available width constraint + double widthConstraint = 300; + var actualWidth = (double)InvokePrivateMethod(button, "CalculateWidth", widthConstraint); + + // Should use the constraint width when HorizontalOptions is Fill + Assert.Equal(widthConstraint, actualWidth); + } + + [Fact] + public void CalculateWidth_WithWidthRequest_ShouldAlwaysUseWidthRequest() + { + var button = new SfButton(); + button.Text = "Sample Text"; + button.WidthRequest = 150; + button.HorizontalOptions = LayoutOptions.Fill; + + // Test with larger width constraint + double widthConstraint = 300; + var actualWidth = (double)InvokePrivateMethod(button, "CalculateWidth", widthConstraint); + + // Should use WidthRequest regardless of HorizontalOptions + Assert.Equal(150, actualWidth); + } + + [Fact] + public void CalculateWidth_WithInfiniteConstraint_ShouldUseContentWidth() + { + var button = new SfButton(); + button.Text = "Sample Text"; + button.HorizontalOptions = LayoutOptions.Fill; + + // Test with infinite width constraint + double widthConstraint = double.PositiveInfinity; + var actualWidth = (double)InvokePrivateMethod(button, "CalculateWidth", widthConstraint); + + // Should fall back to content width even with Fill when constraint is infinite + Assert.True(actualWidth > 0 && actualWidth != double.PositiveInfinity, + $"Button should calculate content width when constraint is infinite, but got {actualWidth}"); + } + + #endregion + + #region Text Wrapping Tests + + [Fact] + public void TextWrapping_ShouldWrapWithoutWidthRequest() + { + var button = new SfButton(); + button.Text = "This is a very long text that should automatically wrap into multiple lines and resize the button height accordingly"; + button.LineBreakMode = LineBreakMode.WordWrap; + button.HorizontalOptions = LayoutOptions.Start; + button.VerticalOptions = LayoutOptions.Start; + + // Measure with width constraint but no WidthRequest + var size = button.MeasureContent(200, double.PositiveInfinity); + + // Calculate expected single line height for comparison + var singleLineButton = new SfButton(); + singleLineButton.Text = "Short text"; + singleLineButton.LineBreakMode = LineBreakMode.NoWrap; + var singleLineSize = singleLineButton.MeasureContent(200, double.PositiveInfinity); + + // Height should be greater than single line due to text wrapping + Assert.True(size.Height > singleLineSize.Height, + $"Button height {size.Height} should be greater than single line height {singleLineSize.Height} when text wraps"); + + // Width should not exceed the constraint + Assert.True(size.Width <= 200, + $"Button width {size.Width} should not exceed width constraint of 200"); + } + + [Fact] + public void TextWrapping_ShouldRespectWidthRequest() + { + var button = new SfButton(); + button.Text = "This is a very long text that should automatically wrap into multiple lines and resize the button height accordingly"; + button.LineBreakMode = LineBreakMode.WordWrap; + button.WidthRequest = 150; + + // Measure with larger width constraint, but WidthRequest should take precedence + var size = button.MeasureContent(300, double.PositiveInfinity); + + // Width should be close to WidthRequest (accounting for padding) + Assert.True(size.Width >= 150, + $"Button width {size.Width} should respect WidthRequest of 150"); + } + + [Fact] + public void TextWrapping_WithIcon_ShouldAccountForIconSpace() + { + var button = new SfButton(); + button.Text = "This is a very long text that should automatically wrap into multiple lines"; + button.LineBreakMode = LineBreakMode.WordWrap; + button.ShowIcon = true; + button.ImageAlignment = Alignment.Start; // Icon on left side + button.ImageSize = 20; + + // Measure with width constraint + var sizeWithIcon = button.MeasureContent(200, double.PositiveInfinity); + + // Compare with button without icon + var buttonNoIcon = new SfButton(); + buttonNoIcon.Text = button.Text; + buttonNoIcon.LineBreakMode = LineBreakMode.WordWrap; + var sizeNoIcon = buttonNoIcon.MeasureContent(200, double.PositiveInfinity); + + // Button with icon should potentially wrap more (higher height) due to less available text width + Assert.True(sizeWithIcon.Height >= sizeNoIcon.Height, + $"Button with icon height {sizeWithIcon.Height} should be >= button without icon height {sizeNoIcon.Height}"); + } + + [Fact] + public void TextWrapping_AndroidOverflowPrevention_ShouldConstrainWidth() + { + var button = new SfButton(); + button.Text = "This is a very long text that should automatically wrap into multiple lines and resize the button height accordingly without overflowing the screen bounds on Android"; + button.LineBreakMode = LineBreakMode.WordWrap; + button.HorizontalOptions = LayoutOptions.Start; // Non-Fill alignment + button.VerticalOptions = LayoutOptions.Start; + + // Simulate Android screen constraint (smaller width) + var size = button.MeasureContent(250, double.PositiveInfinity); + + // Button width should be constrained to prevent overflow + Assert.True(size.Width <= 250, + $"Button width {size.Width} should be constrained to prevent overflow on Android (max 250)"); + + // Height should be greater than single line height due to wrapping + var singleLineHeight = button.MeasureContent(double.PositiveInfinity, double.PositiveInfinity).Height; + Assert.True(size.Height >= singleLineHeight, + $"Button height {size.Height} should accommodate wrapped text (>= {singleLineHeight})"); + } + + #endregion + #region AutomationScenario [Theory]