Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 148 additions & 1 deletion src/Controls/src/Core/Application/Application.Windows.cs
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HandlerChanged event handler continues calling ApplyThemeToAllWindows every time handlers change, even if the theme hasn't changed. This could impact the performance. Could use a Dictionary<Window, bool>, to track if theme has already been applied for a specific window.

Copy link
Member Author

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 HandlerChanged event for the Window. 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 the Window.HandlerChanged event is expected to fire that I might have missed?

}
}

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;

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(() =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also check if (platformWindow.DispatcherQueue is null)? Not a common case, but could happen if the Window is disposed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jsuarezruiz, okay, as suggested, I have added a null check for platformWindow.DispatcherQueue and return early if it’s null.

{
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;

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;
}
}
23 changes: 23 additions & 0 deletions src/Controls/src/Core/Application/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,11 @@ void TriggerThemeChangedActual()
_lastAppTheme = newTheme;

OnPropertyChanged(nameof(UserAppTheme));
#if WINDOWS
// Notify platform so it can apply the correct UI theme
OnRequestedThemeChangedPlatform(newTheme);
#endif

OnParentResourcesChanged([new KeyValuePair<string, object>(AppThemeBinding.AppThemeResource, newTheme)]);
_weakEventManager.HandleEvent(this, new AppThemeChangedEventArgs(newTheme), nameof(RequestedThemeChanged));
}
Expand All @@ -271,6 +276,10 @@ void TriggerThemeChangedActual()
}
}

#if WINDOWS
partial void OnRequestedThemeChangedPlatform(AppTheme newTheme);
#endif

public event EventHandler<ModalPoppedEventArgs>? ModalPopped;

public event EventHandler<ModalPoppingEventArgs>? ModalPopping;
Expand Down Expand Up @@ -498,6 +507,10 @@ internal void RemoveWindow(Window window)
}

_windows.Remove(window);

#if WINDOWS
OnPlatformWindowRemoved(window);
#endif
}

public virtual void OpenWindow(Window window)
Expand Down Expand Up @@ -571,6 +584,16 @@ internal void AddWindow(Window window)
// up to the window before triggering any down stream life cycle
// events.
window.FinishedAddingWindowToApplication(this);

#if WINDOWS
OnPlatformWindowAdded(window);
#endif
}

#if WINDOWS
// Windows-specific hook implemented in Application.Windows.cs
partial void OnPlatformWindowAdded(Window window);
partial void OnPlatformWindowRemoved(Window window);
#endif
}
}
82 changes: 82 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue22058.cs
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;
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading