-
Notifications
You must be signed in to change notification settings - Fork 1.9k
[Windows] Fix runtime theme update for controls and TitleBar #31714
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
296c386
5473abe
2aabacd
212f538
7d33f8b
7e79088
de99a17
fb9e4a0
983dd19
ce62a1a
d73f708
0eda198
e077441
4e5c1b2
8984f90
09ff3e9
f89141e
08db4af
978bd1b
63795bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,153 @@ | ||
| namespace Microsoft.Maui.Controls | ||
| using System; | ||
| using Microsoft.Maui.ApplicationModel; | ||
| using Microsoft.UI.Windowing; | ||
| using Microsoft.UI.Xaml; | ||
| using Windows.UI; | ||
|
|
||
| namespace Microsoft.Maui.Controls | ||
| { | ||
| public partial class Application | ||
| { | ||
| AppTheme? _currentThemeForWindows; | ||
|
|
||
| partial void OnRequestedThemeChangedPlatform(AppTheme newTheme) | ||
| { | ||
| _currentThemeForWindows = newTheme; | ||
|
|
||
| ApplyThemeToAllWindows(newTheme, UserAppTheme == AppTheme.Unspecified); | ||
| } | ||
|
|
||
| partial void OnPlatformWindowAdded(Window window) | ||
| { | ||
| window.HandlerChanged += OnWindowHandlerChanged; | ||
|
|
||
| if (_currentThemeForWindows is not null && window.Handler is not null) | ||
| { | ||
| TryApplyThemeToWindow(window); | ||
| } | ||
| } | ||
|
|
||
| partial void OnPlatformWindowRemoved(Window window) | ||
| { | ||
| window.HandlerChanged -= OnWindowHandlerChanged; | ||
| } | ||
|
|
||
| void OnWindowHandlerChanged(object? sender, EventArgs e) | ||
| { | ||
| if (sender is Window window) | ||
| { | ||
| TryApplyThemeToWindow(window); | ||
| } | ||
| } | ||
|
|
||
| void TryApplyThemeToWindow(Window window) | ||
| { | ||
| if (_currentThemeForWindows is AppTheme theme) | ||
| { | ||
| var forcedElementTheme = GetElementTheme(); | ||
|
|
||
| if (IsWindowReady(window)) | ||
| { | ||
| ApplyThemeToWindow(window, UserAppTheme == AppTheme.Unspecified, forcedElementTheme); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| bool IsWindowReady(Window window) | ||
| { | ||
| var platformWindow = window?.Handler?.PlatformView as UI.Xaml.Window; | ||
| return platformWindow?.Content is FrameworkElement; | ||
| } | ||
|
|
||
| void ApplyThemeToAllWindows(AppTheme newTheme, bool followSystem) | ||
| { | ||
| var forcedElementTheme = GetElementTheme(); | ||
|
|
||
| foreach (var window in Windows) | ||
| { | ||
| if (IsWindowReady(window)) | ||
| { | ||
| ApplyThemeToWindow(window, followSystem, forcedElementTheme); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| ElementTheme GetElementTheme() | ||
| { | ||
| if (_currentThemeForWindows is AppTheme theme) | ||
| { | ||
| return theme switch | ||
| { | ||
| AppTheme.Dark => ElementTheme.Dark, | ||
| AppTheme.Light => ElementTheme.Light, | ||
| _ => ElementTheme.Default | ||
| }; | ||
| } | ||
| return ElementTheme.Default; | ||
| } | ||
|
|
||
| void ApplyThemeToWindow(Window? window, bool followSystem, ElementTheme forcedElementTheme) | ||
| { | ||
| var platformWindow = window?.Handler?.PlatformView as UI.Xaml.Window; | ||
Tamilarasan-Paranthaman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| if (platformWindow is null) | ||
| { | ||
| System.Diagnostics.Debug.WriteLine("ApplyThemeToWindow: platformWindow is null. Unable to apply theme to the root element."); | ||
| return; | ||
| } | ||
|
|
||
| if (platformWindow.DispatcherQueue is null) | ||
| { | ||
| System.Diagnostics.Debug.WriteLine("ApplyThemeToWindow: platformWindow.DispatcherQueue is null. Unable to apply theme to the root element."); | ||
| return; | ||
| } | ||
|
|
||
| platformWindow.DispatcherQueue.TryEnqueue(() => | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could also check if
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jsuarezruiz, okay, as suggested, I have added a null check for |
||
| { | ||
| if (platformWindow.Content is not FrameworkElement root) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Setting RequestedTheme on the root element automatically applies the theme to all child controls. | ||
| root.RequestedTheme = followSystem ? ElementTheme.Default : forcedElementTheme; | ||
Tamilarasan-Paranthaman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| var isDark = followSystem | ||
| ? (UI.Xaml.Application.Current.RequestedTheme == ApplicationTheme.Dark) | ||
| : (forcedElementTheme == ElementTheme.Dark); | ||
|
|
||
| SetTitleBarButtonColors(platformWindow, isDark); | ||
| }); | ||
| } | ||
|
|
||
| void SetTitleBarButtonColors(UI.Xaml.Window platformWindow, bool isDark) | ||
| { | ||
| // Color references: | ||
| // https://github.com/microsoft/WinUI-Gallery/blob/main/WinUIGallery/Helpers/TitleBarHelper.cs | ||
| // https://github.com/dotnet/maui/blob/main/src/Core/src/Platform/Windows/MauiWinUIWindow.cs#L218 | ||
| if (AppWindowTitleBar.IsCustomizationSupported()) | ||
| { | ||
| var titleBar = platformWindow.GetAppWindow()?.TitleBar; | ||
| if (titleBar is not null) | ||
| { | ||
| titleBar.ButtonBackgroundColor = UI.Colors.Transparent; | ||
| titleBar.ButtonInactiveBackgroundColor = UI.Colors.Transparent; | ||
| titleBar.ButtonHoverBackgroundColor = isDark ? TitleBarColors.DarkHoverBackground : TitleBarColors.LightHoverBackground; | ||
| titleBar.ButtonPressedBackgroundColor = isDark ? TitleBarColors.DarkPressedBackground : TitleBarColors.LightPressedBackground; | ||
| titleBar.ButtonHoverForegroundColor = isDark ? TitleBarColors.DarkForeground : TitleBarColors.LightForeground; | ||
| titleBar.ButtonPressedForegroundColor = isDark ? TitleBarColors.DarkForeground : TitleBarColors.LightForeground; | ||
| titleBar.ButtonForegroundColor = isDark ? TitleBarColors.DarkForeground : TitleBarColors.LightForeground; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| static class TitleBarColors | ||
| { | ||
| public static readonly Color LightHoverBackground = UI.ColorHelper.FromArgb(24, 0, 0, 0); | ||
| public static readonly Color DarkHoverBackground = UI.ColorHelper.FromArgb(24, 255, 255, 255); | ||
| public static readonly Color LightPressedBackground = UI.ColorHelper.FromArgb(31, 0, 0, 0); | ||
| public static readonly Color DarkPressedBackground = UI.ColorHelper.FromArgb(31, 255, 255, 255); | ||
| public static readonly Color LightForeground = UI.Colors.Black; | ||
| public static readonly Color DarkForeground = UI.Colors.White; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| namespace Maui.Controls.Sample.Issues; | ||
|
|
||
| [Issue(IssueTracker.Github, 22058, "[Windows] OS system components ignore app theme", PlatformAffected.UWP)] | ||
| public class Issue22058 : ContentPage | ||
| { | ||
| TitleBar customTitleBar; | ||
|
|
||
| public Issue22058() | ||
| { | ||
| this.SetAppThemeColor(BackgroundProperty, Colors.White, Colors.Black); | ||
| customTitleBar = new TitleBar | ||
| { | ||
| Title = "MauiApp1", | ||
| Subtitle = "Welcome to .NET MAUI", | ||
| HeightRequest = 32 | ||
| }; | ||
|
|
||
| var button = new Button | ||
| { | ||
| Text = "Change To Dark User App Theme", | ||
| AutomationId = "ThemeChangeButton", | ||
| VerticalOptions = LayoutOptions.Center, | ||
| HorizontalOptions = LayoutOptions.Start, | ||
| }; | ||
|
|
||
| button.Clicked += (sender, e) => | ||
| { | ||
| if (Application.Current is not null) | ||
| { | ||
| Application.Current.UserAppTheme = AppTheme.Dark; | ||
| } | ||
| }; | ||
|
|
||
| var resetThemeButton = new Button | ||
| { | ||
| Text = "Reset User App Theme", | ||
| AutomationId = "ResetThemeButton", | ||
| VerticalOptions = LayoutOptions.Center, | ||
| HorizontalOptions = LayoutOptions.Start, | ||
| }; | ||
|
|
||
| resetThemeButton.Clicked += (sender, e) => | ||
| { | ||
| if (Application.Current is not null) | ||
| { | ||
| Application.Current.UserAppTheme = AppTheme.Unspecified; | ||
| } | ||
| }; | ||
|
|
||
| var timePicker = new TimePicker | ||
| { | ||
| VerticalOptions = LayoutOptions.Center, | ||
| AutomationId = "TimePickerControl", | ||
| HorizontalOptions = LayoutOptions.Center, | ||
| }; | ||
|
|
||
| var verticalStackLayout = new VerticalStackLayout() | ||
| { | ||
| Spacing = 20, | ||
| Padding = new Thickness(20), | ||
| }; | ||
|
|
||
| verticalStackLayout.Add(button); | ||
| verticalStackLayout.Add(resetThemeButton); | ||
| verticalStackLayout.Add(timePicker); | ||
|
|
||
| Content = verticalStackLayout; | ||
| } | ||
|
|
||
| protected override void OnAppearing() | ||
| { | ||
| base.OnAppearing(); | ||
| if (Window is not null) | ||
| { | ||
| Window.TitleBar = customTitleBar; | ||
| } | ||
| else if (Shell.Current?.Window is not null) | ||
| { | ||
| Shell.Current.Window.TitleBar = customTitleBar; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| #if WINDOWS || MACCATALYST // TitleBar is only supported on Windows and MacCatalyst | ||
| using NUnit.Framework; | ||
| using UITest.Appium; | ||
| using UITest.Core; | ||
|
|
||
| namespace Microsoft.Maui.TestCases.Tests.Issues; | ||
|
|
||
| public class Issue22058 : _IssuesUITest | ||
| { | ||
| public override string Issue => "[Windows] OS system components ignore app theme"; | ||
|
|
||
| public Issue22058(TestDevice device) | ||
| : base(device) | ||
| { } | ||
|
|
||
| [Test, Order(1)] | ||
| [Category(UITestCategories.TitleView)] | ||
| public async Task VerifyTitleBarBackgroundColorChange() | ||
| { | ||
| try | ||
| { | ||
| App.WaitForElement("ThemeChangeButton"); | ||
| App.Tap("ThemeChangeButton"); | ||
| await Task.Delay(1000); // Slight delay to apply the theme change | ||
| VerifyScreenshot(includeTitleBar: true); | ||
| } | ||
| finally | ||
| { | ||
| App.Tap("ResetThemeButton"); | ||
| } | ||
| } | ||
|
|
||
| [Test, Order(2)] | ||
| [Category(UITestCategories.TitleView)] | ||
| public async Task VerifyTimePickerTheme() | ||
| { | ||
| try | ||
| { | ||
| App.WaitForElement("ThemeChangeButton"); | ||
| App.Tap("ThemeChangeButton"); | ||
| await Task.Delay(1000); // Slight delay to apply the theme change | ||
|
|
||
| #if WINDOWS // TimePicker pop up is only supported on Windows | ||
| App.Tap("TimePickerControl"); | ||
| #endif | ||
| VerifyScreenshot(includeTitleBar: true); | ||
| } | ||
| finally | ||
| { | ||
| App.TapCoordinates(50, 50); | ||
| App.Tap("ResetThemeButton"); | ||
| } | ||
| } | ||
| } | ||
| #endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
HandlerChangedevent handler continues callingApplyThemeToAllWindowsevery time handlers change, even if the theme hasn't changed. This could impact the performance. Could use aDictionary<Window, bool>, to track if theme has already been applied for a specific window.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jsuarezruiz, I have wired the
HandlerChangedevent for theWindow. I tested a few scenarios on my end, and the event was triggered only when a new window was opened. Also, since we unsubscribe from the HandlerChanged event when removing a window, it wasn’t called during window removal. Unfortunately, I couldn’t find any other cases where this event is triggered. Could you please share if there are any specific scenarios where theWindow.HandlerChangedevent is expected to fire that I might have missed?