Skip to content

Commit

Permalink
[Windows] Use dark title bar on dark system theme (#110615)
Browse files Browse the repository at this point in the history
  • Loading branch information
loic-sharma committed Sep 2, 2022
1 parent b1990dd commit 2fb5b27
Show file tree
Hide file tree
Showing 22 changed files with 361 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
# Add dependency libraries and include directories. Add any application-specific
# dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")

# Run the Flutter tool portions of the build. This must not be removed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,28 @@

#include "win32_window.h"

#include <dwmapi.h>
#include <flutter_windows.h>

#include "resource.h"

namespace {

/// Window attribute that enables dark mode window decorations.
///
/// Redefined in case the developer's machine has a Windows SDK older than
/// version 10.0.22000.0.
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif

constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";

constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";

// The number of Win32Window objects that currently exist.
static int g_active_window_count = 0;

Expand Down Expand Up @@ -130,6 +144,8 @@ bool Win32Window::Create(const std::wstring& title,
return false;
}

UpdateTheme(window);

return OnCreate();
}

Expand Down Expand Up @@ -196,6 +212,10 @@ Win32Window::MessageHandler(HWND hwnd,
SetFocus(child_content_);
}
return 0;

case WM_DWMCOLORIZATIONCOLORCHANGED:
UpdateTheme(hwnd);
return 0;
}

return DefWindowProc(window_handle_, message, wparam, lparam);
Expand Down Expand Up @@ -251,3 +271,17 @@ bool Win32Window::OnCreate() {
void Win32Window::OnDestroy() {
// No-op; provided for subclasses.
}

void Win32Window::UpdateTheme(HWND const window) {
DWORD light_mode;
DWORD light_mode_size = sizeof(light_mode);
LONG result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD,
nullptr, &light_mode, &light_mode_size);

if (result == ERROR_SUCCESS) {
BOOL enable_dark_mode = light_mode == 0;
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
&enable_dark_mode, sizeof(enable_dark_mode));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ class Win32Window {
// Retrieves a class instance pointer for |window|
static Win32Window* GetThisFromHandle(HWND const window) noexcept;

// Update the window frame's theme to match the system theme.
static void UpdateTheme(HWND const window);

bool quit_on_close_ = false;

// window handle for top level window.
Expand Down
42 changes: 24 additions & 18 deletions dev/integration_tests/windows_startup_test/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
import 'dart:async';
import 'dart:ui' as ui;

import 'package:flutter/services.dart';
import 'package:flutter_driver/driver_extension.dart';

import 'windows.dart';

void drawHelloWorld() {
final ui.ParagraphStyle style = ui.ParagraphStyle();
final ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(style)
Expand All @@ -30,33 +31,38 @@ void drawHelloWorld() {
}

void main() async {
// Create a completer to send the result back to the integration test.
final Completer<String> completer = Completer<String>();
enableFlutterDriverExtension(handler: (String? message) => completer.future);
// Create a completer to send the window visibility result back to the
// integration test.
final Completer<String> visibilityCompleter = Completer<String>();
enableFlutterDriverExtension(handler: (String? message) async {
if (message == 'verifyWindowVisibility') {
return visibilityCompleter.future;
} else if (message == 'verifyTheme') {
final bool app = await isAppDarkModeEnabled();
final bool system = await isSystemDarkModeEnabled();

return (app == system)
? 'success'
: 'error: app dark mode ($app) does not match system dark mode ($system)';
}

try {
const MethodChannel methodChannel =
MethodChannel('tests.flutter.dev/windows_startup_test');
throw 'Unrecognized message: $message';
});

final bool? visible = await methodChannel.invokeMethod('isWindowVisible');
if (visible == null || visible == true) {
try {
if (await isWindowVisible()) {
throw 'Window should be hidden at startup';
}

bool firstFrame = true;
ui.PlatformDispatcher.instance.onBeginFrame = (Duration duration) async {
final bool? visible = await methodChannel.invokeMethod('isWindowVisible');
if (visible == null) {
throw 'Method channel unavailable';
}

if (visible == true) {
if (await isWindowVisible()) {
if (firstFrame) {
throw 'Window should be hidden on first frame';
}

if (!completer.isCompleted) {
completer.complete('success');
if (!visibilityCompleter.isCompleted) {
visibilityCompleter.complete('success');
}
}

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

ui.PlatformDispatcher.instance.scheduleFrame();
} catch (e) {
completer.completeError(e);
visibilityCompleter.completeError(e);
rethrow;
}
}
38 changes: 38 additions & 0 deletions dev/integration_tests/windows_startup_test/lib/windows.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/services.dart';

const MethodChannel _kMethodChannel =
MethodChannel('tests.flutter.dev/windows_startup_test');

/// Returns true if the application's window is visible.
Future<bool> isWindowVisible() async {
final bool? visible = await _kMethodChannel.invokeMethod<bool?>('isWindowVisible');
if (visible == null) {
throw 'Method channel unavailable';
}

return visible;
}

/// Returns true if the app's dark mode is enabled.
Future<bool> isAppDarkModeEnabled() async {
final bool? enabled = await _kMethodChannel.invokeMethod<bool?>('isAppDarkModeEnabled');
if (enabled == null) {
throw 'Method channel unavailable';
}

return enabled;
}

/// Returns true if the operating system dark mode setting is enabled.
Future<bool> isSystemDarkModeEnabled() async {
final bool? enabled = await _kMethodChannel.invokeMethod<bool?>('isSystemDarkModeEnabled');
if (enabled == null) {
throw 'Method channel unavailable';
}

return enabled;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@ import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
void main() {
test('Windows app starts and draws frame', () async {
final FlutterDriver driver = await FlutterDriver.connect(printCommunication: true);
final String result = await driver.requestData(null);
final String result = await driver.requestData('verifyWindowVisibility');

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

await driver.close();
}, timeout: Timeout.none);

test('Windows app theme matches system theme', () async {
final FlutterDriver driver = await FlutterDriver.connect(printCommunication: true);
final String result = await driver.requestData('verifyTheme');

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
# Add dependency libraries and include directories. Add any application-specific
# dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")

# Run the Flutter tool portions of the build. This must not be removed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,25 @@
#include <optional>
#include <mutex>

#include <dwmapi.h>
#include <flutter/method_channel.h>
#include <flutter/standard_method_codec.h>

#include "flutter/generated_plugin_registrant.h"

/// Window attribute that enables dark mode window decorations.
///
/// Redefined in case the developer's machine has a Windows SDK older than
/// version 10.0.22000.0.
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif

constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";

FlutterWindow::FlutterWindow(const flutter::DartProject& project)
: project_(project) {}

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

channel.SetMethodCallHandler(
[](const flutter::MethodCall<>& call,
[&](const flutter::MethodCall<>& call,
std::unique_ptr<flutter::MethodResult<>> result) {
std::scoped_lock lock(visible_mutex);
if (call.method_name() == "isWindowVisible") {
std::string method = call.method_name();

if (method == "isWindowVisible") {
std::scoped_lock lock(visible_mutex);
result->Success(visible);
} else if (method == "isAppDarkModeEnabled") {
BOOL enabled;
HRESULT hr = DwmGetWindowAttribute(GetHandle(),
DWMWA_USE_IMMERSIVE_DARK_MODE,
&enabled, sizeof(enabled));
if (SUCCEEDED(hr)) {
result->Success((bool)enabled);
} else {
result->Error("error", "Received result handle " + hr);
}
} else if (method == "isSystemDarkModeEnabled") {
DWORD data;
DWORD data_size = sizeof(data);
LONG status = RegGetValue(HKEY_CURRENT_USER,
kGetPreferredBrightnessRegKey,
kGetPreferredBrightnessRegValue,
RRF_RT_REG_DWORD, nullptr, &data, &data_size);

if (status == ERROR_SUCCESS) {
// Preferred brightness is 0 if dark mode is enabled,
// otherwise non-zero.
result->Success(data == 0);
} else {
result->Error("error", "Received status " + status);
}
} else {
result->NotImplemented();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,28 @@

#include "win32_window.h"

#include <dwmapi.h>
#include <flutter_windows.h>

#include "resource.h"

namespace {

/// Window attribute that enables dark mode window decorations.
///
/// Redefined in case the developer's machine has a Windows SDK older than
/// version 10.0.22000.0.
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif

constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";

constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";

// The number of Win32Window objects that currently exist.
static int g_active_window_count = 0;

Expand Down Expand Up @@ -130,6 +144,8 @@ bool Win32Window::Create(const std::wstring& title,
return false;
}

UpdateTheme(window);

return OnCreate();
}

Expand Down Expand Up @@ -196,6 +212,10 @@ Win32Window::MessageHandler(HWND hwnd,
SetFocus(child_content_);
}
return 0;

case WM_DWMCOLORIZATIONCOLORCHANGED:
UpdateTheme(hwnd);
return 0;
}

return DefWindowProc(window_handle_, message, wparam, lparam);
Expand Down Expand Up @@ -251,3 +271,17 @@ bool Win32Window::OnCreate() {
void Win32Window::OnDestroy() {
// No-op; provided for subclasses.
}

void Win32Window::UpdateTheme(HWND const window) {
DWORD light_mode;
DWORD light_mode_size = sizeof(light_mode);
LONG result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD,
nullptr, &light_mode, &light_mode_size);

if (result == ERROR_SUCCESS) {
BOOL enable_dark_mode = light_mode == 0;
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
&enable_dark_mode, sizeof(enable_dark_mode));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ class Win32Window {
// Retrieves a class instance pointer for |window|
static Win32Window* GetThisFromHandle(HWND const window) noexcept;

// Update the window frame's theme to match the system theme.
static void UpdateTheme(HWND const window);

bool quit_on_close_ = false;

// window handle for top level window.
Expand Down
1 change: 1 addition & 0 deletions dev/manual_tests/windows/runner/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
# Add dependency libraries and include directories. Add any application-specific
# dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")

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

0 comments on commit 2fb5b27

Please sign in to comment.