diff --git a/src/Controls/src/Core/Platform/iOS/Extensions/FormattedStringExtensions.cs b/src/Controls/src/Core/Platform/iOS/Extensions/FormattedStringExtensions.cs index d622371f9598..55b2eeb4cd30 100644 --- a/src/Controls/src/Core/Platform/iOS/Extensions/FormattedStringExtensions.cs +++ b/src/Controls/src/Core/Platform/iOS/Extensions/FormattedStringExtensions.cs @@ -82,14 +82,22 @@ public static NSAttributedString ToNSAttributedString( { style.LineHeightMultiple = new nfloat(lineHeight); } - - style.Alignment = defaultHorizontalAlignment switch + + if (span.Parent is FormattedString formattedString && formattedString.Parent is Label parentLabel) + { + var flowDirection = parentLabel.FlowDirection; + style.Alignment = defaultHorizontalAlignment.ToPlatformHorizontal(flowDirection.ToUIUserInterfaceLayoutDirection()); + } + else { - TextAlignment.Start => UITextAlignment.Left, - TextAlignment.Center => UITextAlignment.Center, - TextAlignment.End => UITextAlignment.Right, - _ => UITextAlignment.Left - }; + style.Alignment = defaultHorizontalAlignment switch + { + TextAlignment.Start => UITextAlignment.Left, + TextAlignment.Center => UITextAlignment.Center, + TextAlignment.End => UITextAlignment.Right, + _ => UITextAlignment.Left + }; + } var font = span.ToFont(defaultFontSize); if (font.IsDefault && defaultFont.HasValue) diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue81_InitialLTR.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue81_InitialLTR.png new file mode 100644 index 000000000000..961c1355836e Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue81_InitialLTR.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue81_ToggledBackLTR.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue81_ToggledBackLTR.png new file mode 100644 index 000000000000..20b8746e55eb Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue81_ToggledBackLTR.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue81_ToggledRTL.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue81_ToggledRTL.png new file mode 100644 index 000000000000..f30ac6d4b05b Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue81_ToggledRTL.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue31480.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue31480.cs new file mode 100644 index 000000000000..4fa0c3df0ea3 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue31480.cs @@ -0,0 +1,143 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 31480, "RightToLeft does not apply for FormattedText", PlatformAffected.iOS | PlatformAffected.macOS)] +public class Issue31480 : TestContentPage +{ + const string ToggleButton = "ToggleFlowDirection"; + const string FormattedTextLabel = "FormattedTextLabel"; + + Label _formattedTextLabel; + bool _isRtl = false; + + protected override void Init() + { + var layout = new StackLayout { Padding = new Thickness(20), Spacing = 20 }; + + var instructions = new Label + { + Text = "This test demonstrates FormattedText alignment with FlowDirection. " + + "Tap the button to toggle between LTR and RTL. " + + "The text should align properly based on the FlowDirection.", + FontSize = 14 + }; + + layout.Children.Add(instructions); + + // Create FormattedText label + var formattedString = new FormattedString(); + formattedString.Spans.Add(new Span + { + Text = "This is RTL formatted text that should align correctly", FontSize = 16 + }); + + _formattedTextLabel = new Label + { + AutomationId = FormattedTextLabel, + FormattedText = formattedString, + FlowDirection = FlowDirection.LeftToRight, + HorizontalTextAlignment = TextAlignment.Start, + BackgroundColor = Colors.LightGray, + Padding = new Thickness(10), + Margin = new Thickness(0, 10) + }; + + layout.Children.Add(_formattedTextLabel); + + // Add a status label to show current flow direction + var statusLabel = new Label + { + Text = "Current FlowDirection: LeftToRight", FontSize = 12, TextColor = Colors.Blue + }; + + layout.Children.Add(statusLabel); + + // Add toggle button + var toggleButton = new Button + { + AutomationId = ToggleButton, + Text = "Toggle FlowDirection", + BackgroundColor = Colors.Blue, + TextColor = Colors.White + }; + + toggleButton.Clicked += (sender, e) => + { + _isRtl = !_isRtl; + _formattedTextLabel.FlowDirection = _isRtl ? FlowDirection.RightToLeft : FlowDirection.LeftToRight; + statusLabel.Text = $"Current FlowDirection: {_formattedTextLabel.FlowDirection}"; + }; + + layout.Children.Add(toggleButton); + + // Add additional test labels for different scenarios + AddTestScenarios(layout); + + Content = layout; + } + + private void AddTestScenarios(StackLayout layout) + { + // Test scenario 1: RTL with Start alignment + var formattedStringRtlStart = new FormattedString(); + formattedStringRtlStart.Spans.Add(new Span { Text = "RTL Start alignment test", FontSize = 14 }); + + var rtlStartLabel = new Label + { + AutomationId = "FormattedTextStartRTL", + FormattedText = formattedStringRtlStart, + FlowDirection = FlowDirection.RightToLeft, + HorizontalTextAlignment = TextAlignment.Start, + BackgroundColor = Colors.LightYellow, + Padding = new Thickness(10), + Margin = new Thickness(0, 5) + }; + + layout.Children.Add(new Label + { + Text = "RTL with Start alignment:", FontSize = 12, FontAttributes = FontAttributes.Bold + }); + layout.Children.Add(rtlStartLabel); + + // Test scenario 2: LTR with Start alignment + var formattedStringLtrStart = new FormattedString(); + formattedStringLtrStart.Spans.Add(new Span { Text = "LTR Start alignment test", FontSize = 14 }); + + var ltrStartLabel = new Label + { + AutomationId = "FormattedTextStartLTR", + FormattedText = formattedStringLtrStart, + FlowDirection = FlowDirection.LeftToRight, + HorizontalTextAlignment = TextAlignment.Start, + BackgroundColor = Colors.LightGreen, + Padding = new Thickness(10), + Margin = new Thickness(0, 5) + }; + + layout.Children.Add(new Label + { + Text = "LTR with Start alignment:", FontSize = 12, FontAttributes = FontAttributes.Bold + }); + layout.Children.Add(ltrStartLabel); + + // Test scenario 3: RTL label for main test + var formattedStringRtl = new FormattedString(); + formattedStringRtl.Spans.Add(new Span { Text = "Fixed RTL FormattedText alignment", FontSize = 14 }); + + var rtlLabel = new Label + { + AutomationId = "FormattedTextRTLLabel", + FormattedText = formattedStringRtl, + FlowDirection = FlowDirection.RightToLeft, + HorizontalTextAlignment = TextAlignment.Start, + BackgroundColor = Colors.LightPink, + Padding = new Thickness(10), + Margin = new Thickness(0, 5) + }; + + layout.Children.Add(new Label + { + Text = "RTL FormattedText (should align right):", FontSize = 12, FontAttributes = FontAttributes.Bold + }); + layout.Children.Add(rtlLabel); + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue81_InitialLTR.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue81_InitialLTR.png new file mode 100644 index 000000000000..bde52448d392 Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue81_InitialLTR.png differ diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue81_ToggledRTL.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue81_ToggledRTL.png new file mode 100644 index 000000000000..77d519ef4738 Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue81_ToggledRTL.png differ diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31480.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31480.cs new file mode 100644 index 000000000000..e1e63bf169e6 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31480.cs @@ -0,0 +1,39 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue31480 : _IssuesUITest +{ + public Issue31480(TestDevice testDevice) : base(testDevice) + { + } + + public override string Issue => "RightToLeft does not apply for FormattedText"; + + [Test] + [Category(UITestCategories.Label)] + public void FormattedTextToggleFlowDirectionTest() + { + Exception? exception = null; + + // Verify initial state (LTR) + App.WaitForElement("FormattedTextLabel"); + App.WaitForElement("ToggleFlowDirection"); + VerifyScreenshotOrSetException(ref exception, "Issue81_InitialLTR"); + + // Toggle to RTL + App.Tap("ToggleFlowDirection"); + VerifyScreenshotOrSetException(ref exception, "Issue81_ToggledRTL"); + + // Toggle back to LTR + App.Tap("ToggleFlowDirection"); + VerifyScreenshotOrSetException(ref exception, "Issue81_ToggledBackLTR"); + + if (exception != null) + { + throw exception; + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue81_ToggledBackLTR.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue81_ToggledBackLTR.png new file mode 100644 index 000000000000..fa36fb51dbf5 Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue81_ToggledBackLTR.png differ diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue81_ToggledRTL.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue81_ToggledRTL.png new file mode 100644 index 000000000000..2d7eb03f835e Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue81_ToggledRTL.png differ diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue81_InitialLTR.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue81_InitialLTR.png new file mode 100644 index 000000000000..aa8f2086701f Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue81_InitialLTR.png differ diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue81_ToggledRTL.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue81_ToggledRTL.png new file mode 100644 index 000000000000..20fd37a068e9 Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue81_ToggledRTL.png differ diff --git a/src/Core/src/Platform/iOS/FlowDirectionExtensions.cs b/src/Core/src/Platform/iOS/FlowDirectionExtensions.cs index a49517e466a9..356a8b49a119 100644 --- a/src/Core/src/Platform/iOS/FlowDirectionExtensions.cs +++ b/src/Core/src/Platform/iOS/FlowDirectionExtensions.cs @@ -18,5 +18,19 @@ public static FlowDirection ToFlowDirection(this UIUserInterfaceLayoutDirection throw new NotSupportedException($"ToFlowDirection: {direction}"); } } + + // TODO: Make it public in .NET 10 + internal static UIUserInterfaceLayoutDirection ToUIUserInterfaceLayoutDirection(this FlowDirection direction) + { + switch (direction) + { + case FlowDirection.LeftToRight: + return UIUserInterfaceLayoutDirection.LeftToRight; + case FlowDirection.RightToLeft: + return UIUserInterfaceLayoutDirection.RightToLeft; + default: + throw new NotSupportedException($"ToUIUserInterfaceLayoutDirection: {direction}"); + } + } } } \ No newline at end of file diff --git a/src/Core/tests/DeviceTests/Handlers/Label/LabelHandlerTests.iOS.cs b/src/Core/tests/DeviceTests/Handlers/Label/LabelHandlerTests.iOS.cs index a84680ee70f0..53e736a2877b 100644 --- a/src/Core/tests/DeviceTests/Handlers/Label/LabelHandlerTests.iOS.cs +++ b/src/Core/tests/DeviceTests/Handlers/Label/LabelHandlerTests.iOS.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Foundation; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Maui.Controls; using Microsoft.Maui.DeviceTests.Stubs; using Microsoft.Maui.Graphics; using Microsoft.Maui.Handlers; @@ -14,6 +15,41 @@ namespace Microsoft.Maui.DeviceTests { public partial class LabelHandlerTests { + [Theory(DisplayName = "FormattedText HorizontalTextAlignment adjusts for FlowDirection")] + [InlineData(TextAlignment.Start, FlowDirection.LeftToRight, UITextAlignment.Left)] + [InlineData(TextAlignment.End, FlowDirection.LeftToRight, UITextAlignment.Right)] + [InlineData(TextAlignment.Start, FlowDirection.RightToLeft, UITextAlignment.Right)] + [InlineData(TextAlignment.End, FlowDirection.RightToLeft, UITextAlignment.Left)] + public async Task FormattedTextHorizontalTextAlignmentAdjustsForFlowDirection(TextAlignment alignment, FlowDirection flowDirection, UITextAlignment expected) + { + var formattedString = new FormattedString(); + formattedString.Spans.Add(new Span { Text = "This is formatted TEXT!" }); + + var label = new LabelStub + { + FormattedText = formattedString, + HorizontalTextAlignment = alignment, + FlowDirection = flowDirection + }; + + var handler = await CreateHandlerAsync(label); + var platformLabel = GetPlatformLabel(handler); + + var actualAlignment = await InvokeOnMainThreadAsync(() => + { + // Get the alignment from the attributed text's paragraph style + var attributedText = platformLabel.AttributedText; + if (attributedText != null && attributedText.Length > 0) + { + var paragraphStyle = (NSParagraphStyle)attributedText.GetAttribute(UIStringAttributeKey.ParagraphStyle, 0, out _); + return paragraphStyle?.Alignment ?? platformLabel.TextAlignment; + } + return platformLabel.TextAlignment; + }); + + Assert.Equal(expected, actualAlignment); + } + [Fact(DisplayName = "Horizontal TextAlignment Updates Correctly")] public async Task HorizontalTextAlignmentInitializesCorrectly() { diff --git a/src/Core/tests/DeviceTests/Stubs/LabelStub.cs b/src/Core/tests/DeviceTests/Stubs/LabelStub.cs index c524eb538970..2a3cf78a052d 100644 --- a/src/Core/tests/DeviceTests/Stubs/LabelStub.cs +++ b/src/Core/tests/DeviceTests/Stubs/LabelStub.cs @@ -1,9 +1,12 @@ +using Microsoft.Maui.Controls; using Microsoft.Maui.Graphics; namespace Microsoft.Maui.DeviceTests.Stubs { public partial class LabelStub : StubBase, ILabel { + public FormattedString FormattedText { get; set; } + public string Text { get; set; } public TextType TextType { get; set; } = TextType.Text;