Skip to content

Commit 2fb5b27

Browse files
authored
[Windows] Use dark title bar on dark system theme (#110615)
1 parent b1990dd commit 2fb5b27

File tree

22 files changed

+361
-28
lines changed

22 files changed

+361
-28
lines changed

dev/integration_tests/flutter_gallery/windows/runner/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
3333
# Add dependency libraries and include directories. Add any application-specific
3434
# dependencies here.
3535
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
36+
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
3637
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
3738

3839
# Run the Flutter tool portions of the build. This must not be removed.

dev/integration_tests/flutter_gallery/windows/runner/win32_window.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,28 @@
44

55
#include "win32_window.h"
66

7+
#include <dwmapi.h>
78
#include <flutter_windows.h>
89

910
#include "resource.h"
1011

1112
namespace {
1213

14+
/// Window attribute that enables dark mode window decorations.
15+
///
16+
/// Redefined in case the developer's machine has a Windows SDK older than
17+
/// version 10.0.22000.0.
18+
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
19+
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
20+
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
21+
#endif
22+
1323
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
1424

25+
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
26+
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
27+
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
28+
1529
// The number of Win32Window objects that currently exist.
1630
static int g_active_window_count = 0;
1731

@@ -130,6 +144,8 @@ bool Win32Window::Create(const std::wstring& title,
130144
return false;
131145
}
132146

147+
UpdateTheme(window);
148+
133149
return OnCreate();
134150
}
135151

@@ -196,6 +212,10 @@ Win32Window::MessageHandler(HWND hwnd,
196212
SetFocus(child_content_);
197213
}
198214
return 0;
215+
216+
case WM_DWMCOLORIZATIONCOLORCHANGED:
217+
UpdateTheme(hwnd);
218+
return 0;
199219
}
200220

201221
return DefWindowProc(window_handle_, message, wparam, lparam);
@@ -251,3 +271,17 @@ bool Win32Window::OnCreate() {
251271
void Win32Window::OnDestroy() {
252272
// No-op; provided for subclasses.
253273
}
274+
275+
void Win32Window::UpdateTheme(HWND const window) {
276+
DWORD light_mode;
277+
DWORD light_mode_size = sizeof(light_mode);
278+
LONG result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
279+
kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD,
280+
nullptr, &light_mode, &light_mode_size);
281+
282+
if (result == ERROR_SUCCESS) {
283+
BOOL enable_dark_mode = light_mode == 0;
284+
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
285+
&enable_dark_mode, sizeof(enable_dark_mode));
286+
}
287+
}

dev/integration_tests/flutter_gallery/windows/runner/win32_window.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ class Win32Window {
9191
// Retrieves a class instance pointer for |window|
9292
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
9393

94+
// Update the window frame's theme to match the system theme.
95+
static void UpdateTheme(HWND const window);
96+
9497
bool quit_on_close_ = false;
9598

9699
// window handle for top level window.

dev/integration_tests/windows_startup_test/lib/main.dart

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
import 'dart:async';
66
import 'dart:ui' as ui;
77

8-
import 'package:flutter/services.dart';
98
import 'package:flutter_driver/driver_extension.dart';
109

10+
import 'windows.dart';
11+
1112
void drawHelloWorld() {
1213
final ui.ParagraphStyle style = ui.ParagraphStyle();
1314
final ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(style)
@@ -30,33 +31,38 @@ void drawHelloWorld() {
3031
}
3132

3233
void main() async {
33-
// Create a completer to send the result back to the integration test.
34-
final Completer<String> completer = Completer<String>();
35-
enableFlutterDriverExtension(handler: (String? message) => completer.future);
34+
// Create a completer to send the window visibility result back to the
35+
// integration test.
36+
final Completer<String> visibilityCompleter = Completer<String>();
37+
enableFlutterDriverExtension(handler: (String? message) async {
38+
if (message == 'verifyWindowVisibility') {
39+
return visibilityCompleter.future;
40+
} else if (message == 'verifyTheme') {
41+
final bool app = await isAppDarkModeEnabled();
42+
final bool system = await isSystemDarkModeEnabled();
43+
44+
return (app == system)
45+
? 'success'
46+
: 'error: app dark mode ($app) does not match system dark mode ($system)';
47+
}
3648

37-
try {
38-
const MethodChannel methodChannel =
39-
MethodChannel('tests.flutter.dev/windows_startup_test');
49+
throw 'Unrecognized message: $message';
50+
});
4051

41-
final bool? visible = await methodChannel.invokeMethod('isWindowVisible');
42-
if (visible == null || visible == true) {
52+
try {
53+
if (await isWindowVisible()) {
4354
throw 'Window should be hidden at startup';
4455
}
4556

4657
bool firstFrame = true;
4758
ui.PlatformDispatcher.instance.onBeginFrame = (Duration duration) async {
48-
final bool? visible = await methodChannel.invokeMethod('isWindowVisible');
49-
if (visible == null) {
50-
throw 'Method channel unavailable';
51-
}
52-
53-
if (visible == true) {
59+
if (await isWindowVisible()) {
5460
if (firstFrame) {
5561
throw 'Window should be hidden on first frame';
5662
}
5763

58-
if (!completer.isCompleted) {
59-
completer.complete('success');
64+
if (!visibilityCompleter.isCompleted) {
65+
visibilityCompleter.complete('success');
6066
}
6167
}
6268

@@ -68,7 +74,7 @@ void main() async {
6874

6975
ui.PlatformDispatcher.instance.scheduleFrame();
7076
} catch (e) {
71-
completer.completeError(e);
77+
visibilityCompleter.completeError(e);
7278
rethrow;
7379
}
7480
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/services.dart';
6+
7+
const MethodChannel _kMethodChannel =
8+
MethodChannel('tests.flutter.dev/windows_startup_test');
9+
10+
/// Returns true if the application's window is visible.
11+
Future<bool> isWindowVisible() async {
12+
final bool? visible = await _kMethodChannel.invokeMethod<bool?>('isWindowVisible');
13+
if (visible == null) {
14+
throw 'Method channel unavailable';
15+
}
16+
17+
return visible;
18+
}
19+
20+
/// Returns true if the app's dark mode is enabled.
21+
Future<bool> isAppDarkModeEnabled() async {
22+
final bool? enabled = await _kMethodChannel.invokeMethod<bool?>('isAppDarkModeEnabled');
23+
if (enabled == null) {
24+
throw 'Method channel unavailable';
25+
}
26+
27+
return enabled;
28+
}
29+
30+
/// Returns true if the operating system dark mode setting is enabled.
31+
Future<bool> isSystemDarkModeEnabled() async {
32+
final bool? enabled = await _kMethodChannel.invokeMethod<bool?>('isSystemDarkModeEnabled');
33+
if (enabled == null) {
34+
throw 'Method channel unavailable';
35+
}
36+
37+
return enabled;
38+
}

dev/integration_tests/windows_startup_test/test_driver/main_test.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,16 @@ import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
88
void main() {
99
test('Windows app starts and draws frame', () async {
1010
final FlutterDriver driver = await FlutterDriver.connect(printCommunication: true);
11-
final String result = await driver.requestData(null);
11+
final String result = await driver.requestData('verifyWindowVisibility');
12+
13+
expect(result, equals('success'));
14+
15+
await driver.close();
16+
}, timeout: Timeout.none);
17+
18+
test('Windows app theme matches system theme', () async {
19+
final FlutterDriver driver = await FlutterDriver.connect(printCommunication: true);
20+
final String result = await driver.requestData('verifyTheme');
1221

1322
expect(result, equals('success'));
1423

dev/integration_tests/windows_startup_test/windows/runner/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
3333
# Add dependency libraries and include directories. Add any application-specific
3434
# dependencies here.
3535
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
36+
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
3637
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
3738

3839
# Run the Flutter tool portions of the build. This must not be removed.

dev/integration_tests/windows_startup_test/windows/runner/flutter_window.cpp

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,25 @@
77
#include <optional>
88
#include <mutex>
99

10+
#include <dwmapi.h>
1011
#include <flutter/method_channel.h>
1112
#include <flutter/standard_method_codec.h>
1213

1314
#include "flutter/generated_plugin_registrant.h"
1415

16+
/// Window attribute that enables dark mode window decorations.
17+
///
18+
/// Redefined in case the developer's machine has a Windows SDK older than
19+
/// version 10.0.22000.0.
20+
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
21+
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
22+
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
23+
#endif
24+
25+
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
26+
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
27+
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
28+
1529
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
1630
: project_(project) {}
1731

@@ -50,11 +64,38 @@ bool FlutterWindow::OnCreate() {
5064
&flutter::StandardMethodCodec::GetInstance());
5165

5266
channel.SetMethodCallHandler(
53-
[](const flutter::MethodCall<>& call,
67+
[&](const flutter::MethodCall<>& call,
5468
std::unique_ptr<flutter::MethodResult<>> result) {
55-
std::scoped_lock lock(visible_mutex);
56-
if (call.method_name() == "isWindowVisible") {
69+
std::string method = call.method_name();
70+
71+
if (method == "isWindowVisible") {
72+
std::scoped_lock lock(visible_mutex);
5773
result->Success(visible);
74+
} else if (method == "isAppDarkModeEnabled") {
75+
BOOL enabled;
76+
HRESULT hr = DwmGetWindowAttribute(GetHandle(),
77+
DWMWA_USE_IMMERSIVE_DARK_MODE,
78+
&enabled, sizeof(enabled));
79+
if (SUCCEEDED(hr)) {
80+
result->Success((bool)enabled);
81+
} else {
82+
result->Error("error", "Received result handle " + hr);
83+
}
84+
} else if (method == "isSystemDarkModeEnabled") {
85+
DWORD data;
86+
DWORD data_size = sizeof(data);
87+
LONG status = RegGetValue(HKEY_CURRENT_USER,
88+
kGetPreferredBrightnessRegKey,
89+
kGetPreferredBrightnessRegValue,
90+
RRF_RT_REG_DWORD, nullptr, &data, &data_size);
91+
92+
if (status == ERROR_SUCCESS) {
93+
// Preferred brightness is 0 if dark mode is enabled,
94+
// otherwise non-zero.
95+
result->Success(data == 0);
96+
} else {
97+
result->Error("error", "Received status " + status);
98+
}
5899
} else {
59100
result->NotImplemented();
60101
}

dev/integration_tests/windows_startup_test/windows/runner/win32_window.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,28 @@
44

55
#include "win32_window.h"
66

7+
#include <dwmapi.h>
78
#include <flutter_windows.h>
89

910
#include "resource.h"
1011

1112
namespace {
1213

14+
/// Window attribute that enables dark mode window decorations.
15+
///
16+
/// Redefined in case the developer's machine has a Windows SDK older than
17+
/// version 10.0.22000.0.
18+
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
19+
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
20+
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
21+
#endif
22+
1323
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
1424

25+
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
26+
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
27+
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
28+
1529
// The number of Win32Window objects that currently exist.
1630
static int g_active_window_count = 0;
1731

@@ -130,6 +144,8 @@ bool Win32Window::Create(const std::wstring& title,
130144
return false;
131145
}
132146

147+
UpdateTheme(window);
148+
133149
return OnCreate();
134150
}
135151

@@ -196,6 +212,10 @@ Win32Window::MessageHandler(HWND hwnd,
196212
SetFocus(child_content_);
197213
}
198214
return 0;
215+
216+
case WM_DWMCOLORIZATIONCOLORCHANGED:
217+
UpdateTheme(hwnd);
218+
return 0;
199219
}
200220

201221
return DefWindowProc(window_handle_, message, wparam, lparam);
@@ -251,3 +271,17 @@ bool Win32Window::OnCreate() {
251271
void Win32Window::OnDestroy() {
252272
// No-op; provided for subclasses.
253273
}
274+
275+
void Win32Window::UpdateTheme(HWND const window) {
276+
DWORD light_mode;
277+
DWORD light_mode_size = sizeof(light_mode);
278+
LONG result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
279+
kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD,
280+
nullptr, &light_mode, &light_mode_size);
281+
282+
if (result == ERROR_SUCCESS) {
283+
BOOL enable_dark_mode = light_mode == 0;
284+
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
285+
&enable_dark_mode, sizeof(enable_dark_mode));
286+
}
287+
}

dev/integration_tests/windows_startup_test/windows/runner/win32_window.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ class Win32Window {
9191
// Retrieves a class instance pointer for |window|
9292
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
9393

94+
// Update the window frame's theme to match the system theme.
95+
static void UpdateTheme(HWND const window);
96+
9497
bool quit_on_close_ = false;
9598

9699
// window handle for top level window.

0 commit comments

Comments
 (0)