[Windows] Fix Shell title bar overlap with window controls in RTL mode#33109
[Windows] Fix Shell title bar overlap with window controls in RTL mode#33109Shalini-Ashokan wants to merge 8 commits intodotnet:mainfrom
Conversation
|
Hey there @@Shalini-Ashokan! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
🚦 Gate — Test Verification📊 Expand Full Gate —
|
| # | Type | Test Name | Filter |
|---|---|---|---|
| 1 | UITest | Issue32476 | Issue32476 |
Verification
| Step | Expected | Actual | Result |
|---|---|---|---|
| Without fix | FAIL | FAIL | ✅ |
| With fix | PASS | PASS | ✅ |
Test Execution
Without fix (expect FAIL):
- ✅ Issue32476: FAIL (1 total, 0 passed, 1 failed) — 575 s
- Failed:
ShellRTLFlowDirectionShouldNotCauseOverlap [5 s] - Error:
VisualTestUtils.VisualTestFailedException : Snapshot different than baseline: ShellRTLFlowDirectionShouldNotCauseOverlap.png (0.72% difference) If the correct baseline has changed (this isn't a a b...
- Failed:
With fix (expect PASS):
- ✅ Issue32476: PASS (1 total, 1 passed, failed) — 476 s
Fix Files Reverted
eng/pipelines/ci-copilot.ymlsrc/Controls/src/Core/Handlers/Shell/ShellHandler.Windows.cs
Base Branch: main | Merge Base: 720a9d4
🤖 AI Summary📊 Expand Full Review —
|
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #33109 | Apply WS_EX_LAYOUTRTL via P/Invoke + set WindowRootView.FlowDirection |
✅ PASSED (Gate) | ShellHandler.Windows.cs |
Original PR; P/Invoke-based; may have side effects on full window layout |
🔧 Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix (claude-opus-4.6) | AppWindowTitleBar inset swap — WindowRootView.FlowDirection=RTL + swap LeftInset/RightInset |
❌ FAIL (0.72% diff) | ShellHandler.Windows.cs |
Caption buttons stay on right; reference expects them on left |
| 2 | try-fix (claude-sonnet-4.6) | WS_EX_LAYOUTRTL via GetWindowLongPtrW/SetWindowLongPtrW in nested NativeMethods class; window resolved via view.Window MAUI API |
✅ PASS | ShellHandler.Windows.cs |
64-bit safe; cleaner window resolution; no new file needed |
| 3 | try-fix (gpt-5.3-codex) | XAML-only WindowTitleBarContainerMargin approach — no P/Invoke |
❌ FAIL (0.78% diff) | ShellHandler.Windows.cs |
Caption buttons remain on right; XAML cannot physically reposition them |
| 4 | try-fix (gpt-5.4, gemini fallback) | WS_EX_LAYOUTRTL via SetWindowLongPtr; window from view.Window; P/Invoke in nested NativeMethods class |
✅ PASS | ShellHandler.Windows.cs |
64-bit safe; MAUI window API; single-file change |
| 5 | try-fix (claude-sonnet-4.6, cross-poll) | DwmSetWindowAttribute(DWMWA_NONCLIENT_RTL_LAYOUT) — surgical DWM attribute |
❌ FAIL (0.72% diff) | ShellHandler.Windows.cs |
Mirrors glyphs only, does not reposition caption buttons physically |
| PR | PR #33109 | WS_EX_LAYOUTRTL via SetWindowLong (32-bit) + Services.GetService<Window>() |
✅ PASSED (Gate) | ShellHandler.Windows.cs |
Works but 32-bit P/Invoke + fragile window resolution |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| claude-opus-4.6 | 2 | No | WS_EX_LAYOUTRTL is the only mechanism; solution space exhausted |
| claude-sonnet-4.6 | 2 | Yes | DWMWA_NONCLIENT_RTL_LAYOUT (tested as attempt 5 → ❌ FAIL) |
| gpt-5.3-codex | 2 | No | No new ideas |
| gpt-5.4 | 2 | Yes (low value) | Full custom title bar — too invasive, not worth testing |
| claude-opus-4.6 | 3 | No | Exhausted |
| gpt-5.3-codex | 3 | Yes (same mechanism) | Apply at HWND creation time + SWP_FRAMECHANGED — variant of WS_EX_LAYOUTRTL, not meaningfully different since existing fixes already pass |
Exhausted: Yes
Selected Fix: Attempt 2/4 (both passing, both better than PR) — Reason: WS_EX_LAYOUTRTL is the only mechanism that physically repositions caption buttons. Attempt 4's diff (nested NativeMethods, SetWindowLongPtr, view.Window resolution) is the cleanest single-file implementation. PR's fix has 32-bit P/Invoke and fragile DI window resolution that should be improved.
📋 Report — Final Recommendation
⚠️ Final Recommendation: REQUEST CHANGES
Phase Status
| Phase | Status | Notes |
|---|---|---|
| Pre-Flight | ✅ COMPLETE | Issue #32476, Windows Shell RTL overlap |
| Gate | ✅ PASSED | Windows — tests fail without fix, pass with fix |
| Try-Fix | ✅ COMPLETE | 5 attempts, 2 independent passes (attempts 2 & 4) |
| Report | ✅ COMPLETE |
Summary
The PR correctly identifies WS_EX_LAYOUTRTL as the required Win32 mechanism to physically reposition caption buttons (minimize/maximize/close) to the left side in RTL mode. The approach is validated — 5 attempts across multiple models confirmed there is no WinUI3-native (non-P/Invoke) alternative that achieves the same visual result. However, the PR's implementation has addressable quality issues that should be resolved before merge.
Root Cause
When Shell.FlowDirection = RightToLeft on Windows, the WinUI/XAML content mirrors correctly but the native non-client area (caption buttons) is controlled by DWM and does not respond to XAML FlowDirection. The WS_EX_LAYOUTRTL extended window style is the only Win32 API that instructs DWM to physically move caption buttons from the right to the left side.
Fix Quality
What's correct:
WS_EX_LAYOUTRTLis the right mechanism (confirmed by exhaustive exploration)- Setting
WindowRootView.FlowDirectionto sync WinUI content layout is correct - Clearing the flag when reverting to LTR is correct
- The test covers the Windows scenario and gate passes
Issues requiring changes:
1. 🔴 32-bit P/Invoke on 64-bit process (SetWindowLong vs SetWindowLongPtr)
// ❌ PR uses:
[DllImport("user32.dll", SetLastError = true)]
static extern int GetWindowLong(IntPtr hWnd, int nIndex);
static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
// ✅ Should use pointer-sized variants:
[DllImport("user32.dll", EntryPoint = "GetWindowLongPtrW", SetLastError = true)]
static extern nint GetWindowLongPtr(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtrW", SetLastError = true)]
static extern nint SetWindowLongPtr(IntPtr hWnd, int nIndex, nint dwNewLong);SetWindowLong is 32-bit only; SetWindowLongPtr is required for correct behavior in 64-bit processes on modern Windows.
2. 🔴 Fragile window resolution via DI (Services.GetService<Window>())
// ❌ PR uses:
var window = handler.MauiContext?.Services?.GetService<Microsoft.UI.Xaml.Window>();
// ✅ Should use MAUI's window abstraction:
var mauiWindow = view.Window ?? handler.MauiContext?.GetPlatformWindow()?.GetWindow();
if (mauiWindow?.Handler?.PlatformView is not Microsoft.UI.Xaml.Window platformWindow) return;Services.GetService<Window>() will return an arbitrary or null window in multi-window MAUI apps. view.Window is the correct, contextual reference.
3. 🟡 P/Invoke declarations at handler class level (minor)
P/Invoke declarations are scattered at the top of ShellHandler class. They should be encapsulated in a nested private static class NativeMethods for clarity (as demonstrated in attempt 4's diff).
4. 🟡 Test uses Task.Delay(500).Wait() (anti-pattern)
// ❌ PR test:
Task.Delay(500).Wait();
VerifyScreenshot(includeTitleBar: true);
// ✅ Should use:
VerifyScreenshot(includeTitleBar: true, retryTimeout: TimeSpan.FromSeconds(2));Per UI test guidelines, explicit delays before VerifyScreenshot should be replaced with retryTimeout.
5. 🟡 Double assignment in HostApp test code
// ❌ Bug in Issue32476.cs HostApp:
FlowDirection = FlowDirection = FlowDirection.RightToLeft; // double assignmentThe assignment FlowDirection = FlowDirection = X is redundant — should be FlowDirection = FlowDirection.RightToLeft;.
6. 🟡 Missing newlines at end of test files
Both src/Controls/tests/TestCases.HostApp/Issues/Issue32476.cs and src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32476.cs are missing trailing newlines.
Suggested Implementation (from Try-Fix attempt 4 — ✅ PASS)
The diff from attempt 4 provides a drop-in replacement that addresses all critical issues:
- Resolves window via
view.Window(MAUI API) - Uses
GetWindowLongPtrW/SetWindowLongPtrW(64-bit safe) - Encapsulates P/Invoke in nested
NativeMethodsclass - Single-file change to
ShellHandler.Windows.cs
See CustomAgentLogsTmp/PRState/33109/PRAgent/try-fix/attempt-2/fix.diff for the exact diff.
Selected Fix: Agent Fix (attempt 4) — cleaner implementation of same mechanism
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 33109Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 33109" |
Concern 1: PlatformMethods is internal to the Microsoft.Maui.Essentials assembly (Microsoft.Maui.ApplicationModel namespace) and is not accessible from the handler layer. Using raw P/Invokes is the only viable option here. Concern 2: This concern is not valid. handler.MauiContext?.Services?.GetService() resolves from the handler's own scoped MauiContext, which is already bound to the current window — it cannot return an arbitrary window. Concern 3: No other handler in the MAUI codebase uses a nested NativeMethods class, so this is not an established pattern to follow. The current approach is consistent with the rest of the codebase. Concern 4 : Using retryTimeout caused errors during testing, so it was intentionally avoided. concern 5 & 6 : I resolved the concerns. |
There was a problem hiding this comment.
Pull request overview
Fixes a Windows Shell RTL layout issue where the Shell title/hamburger can overlap native window caption buttons by mirroring the window’s non-client area when Shell.FlowDirection is RightToLeft, and adds UI test coverage/snapshots for the scenario.
Changes:
- Update Windows
ShellHandlerflow-direction mapping to applyWS_EX_LAYOUTRTLand adjust the root view flow direction. - Add new HostApp repro (
Issue32476) and a corresponding UI test with updated baseline screenshots for Windows and Mac. - Add new snapshot baselines for
ShellRTLFlowDirectionShouldNotCauseOverlapon WinUI and Mac.
Reviewed changes
Copilot reviewed 3 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Controls/src/Core/Handlers/Shell/ShellHandler.Windows.cs | Applies a Win32 extended window style during RTL to mirror the title bar/caption buttons and sets WindowRootView.FlowDirection. |
| src/Controls/tests/TestCases.HostApp/Issues/Issue32476.cs | Adds a Shell-based repro page with a button intended to switch FlowDirection to RTL. |
| src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32476.cs | Adds an Appium/NUnit UI test validating no overlap (via screenshot). |
| src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShellRTLFlowDirectionShouldNotCauseOverlap.png | Adds/updates the Windows screenshot baseline for the new test. |
| src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ShellRTLFlowDirectionShouldNotCauseOverlap.png | Adds/updates the Mac screenshot baseline for the new test. |
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Issue Details
When Shell.FlowDirection is set to RightToLeft on Windows, the Shell title and hamburger menu overlap with native window control buttons (minimize, maximize, close) because the content mirrors to RTL while title bar buttons remain on the right side.
Root Cause
Windows title bar buttons stay fixed on the right side by default, but Shell UI elements (title, hamburger menu) mirror to the right in RTL mode, causing visual overlap and making window controls unusable.
Description of Change
Applied WS_EX_LAYOUTRTL extended window style flag to mirror the entire window including title bar buttons to the left side, and set WindowRootView.FlowDirection to properly handle WinUI content layout, ensuring all elements move together consistently in RTL mode without overlap.
Validated the behavior in the following platforms
Issues Fixed
Fixes #32476
Output ScreenShot