diff --git a/src/Controls/src/Core/Handlers/Shell/ShellHandler.Windows.cs b/src/Controls/src/Core/Handlers/Shell/ShellHandler.Windows.cs index 22b3572a6a28..743dc9eb74d2 100644 --- a/src/Controls/src/Core/Handlers/Shell/ShellHandler.Windows.cs +++ b/src/Controls/src/Core/Handlers/Shell/ShellHandler.Windows.cs @@ -1,5 +1,7 @@ #nullable disable using System; +using System.Runtime.InteropServices; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Maui.Controls.Platform; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -12,6 +14,14 @@ namespace Microsoft.Maui.Controls.Handlers { public partial class ShellHandler : ViewHandler { + [DllImport("user32.dll", SetLastError = true)] + static extern int GetWindowLong(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll", SetLastError = true)] + static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); + + const int GWL_EXSTYLE = -20; + const int WS_EX_LAYOUTRTL = 0x00400000; ScrollViewer _scrollViewer; double? _topAreaHeight = null; double? _headerHeight = null; @@ -164,6 +174,23 @@ public static void MapFlyout(ShellHandler handler, IFlyoutView flyoutView) internal static void MapFlowDirection(ShellHandler handler, Shell view) { handler.PlatformView.UpdateFlowDirection(view); + var windowRootView = handler.MauiContext?.GetNavigationRootManager()?.RootView as WindowRootView; + var window = handler.MauiContext?.Services?.GetService(); + + if (window == null) + return; + + var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(window); + var isRtl = view.FlowDirection == FlowDirection.RightToLeft; + var exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); + + // Apply or remove WS_EX_LAYOUTRTL to mirror the window including title bar buttons + var newExStyle = isRtl ? (exStyle | WS_EX_LAYOUTRTL) : (exStyle & ~WS_EX_LAYOUTRTL); + if (exStyle != newExStyle) + SetWindowLong(hwnd, GWL_EXSTYLE, newExStyle); + + if (windowRootView != null) + windowRootView.FlowDirection = isRtl ? Microsoft.UI.Xaml.FlowDirection.RightToLeft : Microsoft.UI.Xaml.FlowDirection.LeftToRight; } public static void MapIsPresented(ShellHandler handler, IFlyoutView flyoutView) diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue32476.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue32476.cs new file mode 100644 index 000000000000..da68df1b47d0 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue32476.cs @@ -0,0 +1,59 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 32476, "Binding RTL FlowDirection in Shell causes Flyout MenuIcon and native window controls to overlap", PlatformAffected.UWP)] +public class Issue32476 : Shell +{ + public Issue32476() + { + FlyoutBehavior = FlyoutBehavior.Flyout; + var button = new Button + { + Text = "Toggle FlowDirection to RTL", + AutomationId = "ToggleButton" + }; + + var buttonTapLabel = new Label + { + Text = "Tap the button to toggle FlowDirection to RTL", + HorizontalOptions = LayoutOptions.Center + }; + + button.Clicked += (s, e) => + { + buttonTapLabel.Text = "Toggling FlowDirection..."; + FlowDirection = FlowDirection.RightToLeft; + buttonTapLabel.Text = "FlowDirection is now RTL"; + }; + + var contentPage = new ContentPage + { + Content = new VerticalStackLayout + { + Padding = 20, + Spacing = 10, + Children = + { + new Label + { + Text = "Tap the button to toggle FlowDirection to RTL. The Flyout MenuIcon must not overlap with native window controls.", + LineBreakMode = LineBreakMode.WordWrap + }, + button, + buttonTapLabel + } + } + }; + Items.Add(new FlyoutItem + { + Title = "Home", + Items = + { + new ShellContent + { + Title = "Main", + Content = contentPage + } + } + }); + } +} diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ShellRTLFlowDirectionShouldNotCauseOverlap.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ShellRTLFlowDirectionShouldNotCauseOverlap.png new file mode 100644 index 000000000000..cbf7d45d972e Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ShellRTLFlowDirectionShouldNotCauseOverlap.png differ diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32476.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32476.cs new file mode 100644 index 000000000000..6d50ccdee34f --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32476.cs @@ -0,0 +1,26 @@ +#if MACCATALYST || WINDOWS // Native window controls (minimize, maximize, close) are applicable only on Windows and MacCatalyst +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue32476 : _IssuesUITest +{ + public Issue32476(TestDevice testDevice) : base(testDevice) + { + } + public override string Issue => "Binding RTL FlowDirection in Shell causes Flyout MenuIcon and native window controls to overlap"; + [Test] + [Category(UITestCategories.Shell)] + public void ShellRTLFlowDirectionShouldNotCauseOverlap() + { + App.WaitForElement("ToggleButton"); + App.Tap("ToggleButton"); + App.Tap("ToggleButton"); + + // wait for window layout to update after flow direction change. + Task.Delay(500).Wait(); + VerifyScreenshot(includeTitleBar: true); + } +} +#endif diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShellRTLFlowDirectionShouldNotCauseOverlap.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShellRTLFlowDirectionShouldNotCauseOverlap.png new file mode 100644 index 000000000000..270645e1c4d6 Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShellRTLFlowDirectionShouldNotCauseOverlap.png differ