From 1fc03b6616cebdfa4156ccfb23bfb717243bc28c Mon Sep 17 00:00:00 2001 From: mrvokintos <184386663+mrvokintos@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:27:56 +0300 Subject: [PATCH 1/5] Add title bar color sync with app theme on Windows Implements platform channel to sync the native window title bar color with the app's theme mode. When the user switches between light and dark themes, the title bar automatically updates to match. Changes: - Add MethodChannel 'com.extera.app/window' in Dart for theme-related native calls - Implement SetTitleBarColor() in Windows native code using DwmSetWindowAttribute - Sync title bar brightness in real-time when app theme changes - Only applies on Windows platform --- lib/widgets/fluffy_chat_app.dart | 103 +++++++++++++++++++----------- windows/runner/flutter_window.cpp | 35 +++++++++- windows/runner/flutter_window.h | 12 ++-- windows/runner/win32_window.cpp | 12 ++++ windows/runner/win32_window.h | 4 ++ 5 files changed, 120 insertions(+), 46 deletions(-) diff --git a/lib/widgets/fluffy_chat_app.dart b/lib/widgets/fluffy_chat_app.dart index 060ce98ec..4d90cd304 100644 --- a/lib/widgets/fluffy_chat_app.dart +++ b/lib/widgets/fluffy_chat_app.dart @@ -51,6 +51,12 @@ class FluffyChatApp extends StatefulWidget { class _FluffyChatAppState extends State { final _androidSystemFontPlugin = AndroidSystemFont(); + static const _platform = MethodChannel('com.extera.app/window'); + + // Keeps track of the last Brightness applied to the native window frame + // (title bar) so we don't spam the platform channel with redundant calls + // on every rebuild. + Brightness? _lastWindowBrightness; @override void initState() { @@ -88,6 +94,25 @@ class _FluffyChatAppState extends State { }); } + void _syncWindowBrightness(BuildContext context, ThemeMode themeMode) { + if (!Platform.isWindows) return; + + final resolvedBrightness = switch (themeMode) { + ThemeMode.dark => Brightness.dark, + ThemeMode.light => Brightness.light, + ThemeMode.system => MediaQuery.platformBrightnessOf(context), + }; + + if (resolvedBrightness == _lastWindowBrightness) return; + _lastWindowBrightness = resolvedBrightness; + + _platform.invokeMethod('setTitleBarDarkMode', { + 'isDark': resolvedBrightness == Brightness.dark, + }).catchError((e) { + debugPrint('Failed to set title bar color: $e'); + }); + } + @override Widget build(BuildContext context) { return ThemeBuilder( @@ -99,45 +124,49 @@ class _FluffyChatAppState extends State { schemeVariant, pureBlack, twemoji, - ) => MaterialApp.router( - title: AppConfig.applicationName, - themeMode: themeMode, - theme: FluffyThemes.buildTheme( - context, - Brightness.light, - primaryColor, - schemeVariant, - pureBlack, - twemoji, - ), - darkTheme: FluffyThemes.buildTheme( - context, - Brightness.dark, - primaryColor, - schemeVariant, - pureBlack, - twemoji, - ), - scrollBehavior: CustomScrollBehavior(), - localizationsDelegates: L10n.localizationsDelegates, - supportedLocales: L10n.supportedLocales, - routerConfig: FluffyChatApp.router, - builder: (context, child) => AppLockWidget( - pincode: widget.pincode, - clients: widget.clients, - // Need a navigator above the Matrix widget for - // displaying dialogs - child: DownloadManager( - child: BackgroundAudioPlayer( - child: Matrix( - clients: widget.clients, - store: widget.store, - child: widget.testWidget ?? child, + ) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _syncWindowBrightness(context, themeMode); + }); + + return MaterialApp.router( + title: AppConfig.applicationName, + themeMode: themeMode, + theme: FluffyThemes.buildTheme( + context, + Brightness.light, + primaryColor, + schemeVariant, + pureBlack, + twemoji, + ), + darkTheme: FluffyThemes.buildTheme( + context, + Brightness.dark, + primaryColor, + schemeVariant, + pureBlack, + twemoji, + ), + scrollBehavior: CustomScrollBehavior(), + localizationsDelegates: L10n.localizationsDelegates, + supportedLocales: L10n.supportedLocales, + routerConfig: FluffyChatApp.router, + builder: (context, child) => AppLockWidget( + pincode: widget.pincode, + clients: widget.clients, + child: DownloadManager( + child: BackgroundAudioPlayer( + child: Matrix( + clients: widget.clients, + store: widget.store, + child: widget.testWidget ?? child, + ), ), ), ), - ), - ), + ); + }, ); } -} +} \ No newline at end of file diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index b43b9095e..8a8ebfbd2 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -16,19 +16,48 @@ bool FlutterWindow::OnCreate() { RECT frame = GetClientArea(); - // The size here must match the window dimensions to avoid unnecessary surface - // creation / destruction in the startup path. flutter_controller_ = std::make_unique( frame.right - frame.left, frame.bottom - frame.top, project_); - // Ensure that basic setup of the controller was successful. if (!flutter_controller_->engine() || !flutter_controller_->view()) { return false; } RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + SetupChannelHandlers(); return true; } +void FlutterWindow::SetupChannelHandlers() { + auto channel = + std::make_unique>( + flutter_controller_->engine()->messenger(), "com.extera.app/window", + &flutter::StandardMethodCodec::GetInstance()); + + channel->SetMethodCallHandler( + [this](const flutter::MethodCall& call, + std::unique_ptr> + result) { + if (call.method_name() == "setTitleBarDarkMode") { + const auto* arguments = std::get_if(call.arguments()); + if (arguments) { + auto isDark_variant = arguments->find(flutter::EncodableValue("isDark")); + if (isDark_variant != arguments->end()) { + bool isDark = std::get(isDark_variant->second); + SetTitleBarColor(isDark); + result->Success(flutter::EncodableValue(true)); + return; + } + } + result->Error("INVALID_ARGUMENT", "isDark parameter is required"); + } else { + result->NotImplemented(); + } + }); + + method_channel_ = std::move(channel); +} + void FlutterWindow::OnDestroy() { if (flutter_controller_) { flutter_controller_ = nullptr; diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h index 6da0652f0..4c8c92708 100644 --- a/windows/runner/flutter_window.h +++ b/windows/runner/flutter_window.h @@ -3,31 +3,31 @@ #include #include +#include +#include #include #include "win32_window.h" -// A window that does nothing but host a Flutter view. class FlutterWindow : public Win32Window { public: - // Creates a new FlutterWindow hosting a Flutter view running |project|. explicit FlutterWindow(const flutter::DartProject& project); virtual ~FlutterWindow(); protected: - // Win32Window: bool OnCreate() override; void OnDestroy() override; LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; private: - // The project to run. flutter::DartProject project_; - - // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; + std::unique_ptr> + method_channel_; + + void SetupChannelHandlers(); }; #endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp index c10f08dc7..9d4e1afd9 100644 --- a/windows/runner/win32_window.cpp +++ b/windows/runner/win32_window.cpp @@ -1,9 +1,12 @@ #include "win32_window.h" #include +#include #include "resource.h" +#pragma comment(lib, "dwmapi.lib") + namespace { constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; @@ -235,6 +238,15 @@ void Win32Window::SetQuitOnClose(bool quit_on_close) { quit_on_close_ = quit_on_close; } +void Win32Window::SetTitleBarColor(bool isDark) { + if (!window_handle_) return; + + // Use DwmSetWindowAttribute to set the title bar color + // DWMWA_USE_IMMERSIVE_DARK_MODE = 20 for Windows 11 + BOOL value = isDark ? TRUE : FALSE; + DwmSetWindowAttribute(window_handle_, 20, &value, sizeof(value)); +} + bool Win32Window::OnCreate() { // No-op; provided for subclasses. return true; diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h index 17ba43112..1025e9aaa 100644 --- a/windows/runner/win32_window.h +++ b/windows/runner/win32_window.h @@ -54,6 +54,10 @@ class Win32Window { // Return a RECT representing the bounds of the current client area. RECT GetClientArea(); + // Sets the title bar color based on brightness (dark/light mode). + // isDark: true for dark mode (dark title bar), false for light mode (light title bar) + void SetTitleBarColor(bool isDark); + protected: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that From f0bff2239c529faf4f31e390c75e4bb868709bbb Mon Sep 17 00:00:00 2001 From: mrvokintos <184386663+mrvokintos@users.noreply.github.com> Date: Sat, 18 Jul 2026 18:56:15 +0300 Subject: [PATCH 2/5] Update lib/widgets/fluffy_chat_app.dart Co-authored-by: meoovv <296894618+meoovv@users.noreply.github.com> --- lib/widgets/fluffy_chat_app.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/fluffy_chat_app.dart b/lib/widgets/fluffy_chat_app.dart index 4d90cd304..c75ba1e03 100644 --- a/lib/widgets/fluffy_chat_app.dart +++ b/lib/widgets/fluffy_chat_app.dart @@ -51,7 +51,7 @@ class FluffyChatApp extends StatefulWidget { class _FluffyChatAppState extends State { final _androidSystemFontPlugin = AndroidSystemFont(); - static const _platform = MethodChannel('com.extera.app/window'); + static const _platform = MethodChannel('xyz.extera.extera/window'); // Keeps track of the last Brightness applied to the native window frame // (title bar) so we don't spam the platform channel with redundant calls From 04549e42379303392c402535776be7689be78461 Mon Sep 17 00:00:00 2001 From: mrvokintos <184386663+mrvokintos@users.noreply.github.com> Date: Sat, 18 Jul 2026 19:03:26 +0300 Subject: [PATCH 3/5] improve comments --- windows/runner/flutter_window.cpp | 6 ++++-- windows/runner/flutter_window.h | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index 8a8ebfbd2..da451ba73 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -15,9 +15,11 @@ bool FlutterWindow::OnCreate() { } RECT frame = GetClientArea(); - + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. flutter_controller_ = std::make_unique( frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. if (!flutter_controller_->engine() || !flutter_controller_->view()) { return false; } @@ -31,7 +33,7 @@ bool FlutterWindow::OnCreate() { void FlutterWindow::SetupChannelHandlers() { auto channel = std::make_unique>( - flutter_controller_->engine()->messenger(), "com.extera.app/window", + flutter_controller_->engine()->messenger(), "xyz.extera.extera/window", &flutter::StandardMethodCodec::GetInstance()); channel->SetMethodCallHandler( diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h index 4c8c92708..945958f86 100644 --- a/windows/runner/flutter_window.h +++ b/windows/runner/flutter_window.h @@ -10,19 +10,25 @@ #include "win32_window.h" +// A window that does nothing but host a Flutter view. class FlutterWindow : public Win32Window { public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. explicit FlutterWindow(const flutter::DartProject& project); virtual ~FlutterWindow(); protected: + // Win32Window: bool OnCreate() override; void OnDestroy() override; LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; private: + // The project to run. flutter::DartProject project_; + + // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; std::unique_ptr> method_channel_; From f46718f293941a2d61ab660aed7a5dc8734b62ef Mon Sep 17 00:00:00 2001 From: mrvokintos <184386663+mrvokintos@users.noreply.github.com> Date: Sat, 18 Jul 2026 19:23:14 +0300 Subject: [PATCH 4/5] specifying the name of the window method channel and other comments --- lib/widgets/fluffy_chat_app.dart | 4 +++- windows/runner/flutter_window.cpp | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/widgets/fluffy_chat_app.dart b/lib/widgets/fluffy_chat_app.dart index c75ba1e03..599b2fbe0 100644 --- a/lib/widgets/fluffy_chat_app.dart +++ b/lib/widgets/fluffy_chat_app.dart @@ -154,7 +154,9 @@ class _FluffyChatAppState extends State { routerConfig: FluffyChatApp.router, builder: (context, child) => AppLockWidget( pincode: widget.pincode, - clients: widget.clients, + clients: widget.clients, + // Need a navigator above the Matrix widget for + // displaying dialogs child: DownloadManager( child: BackgroundAudioPlayer( child: Matrix( diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index da451ba73..367fb477b 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -33,7 +33,7 @@ bool FlutterWindow::OnCreate() { void FlutterWindow::SetupChannelHandlers() { auto channel = std::make_unique>( - flutter_controller_->engine()->messenger(), "xyz.extera.extera/window", + flutter_controller_->engine()->messenger(), "xyz.extera.next/window", &flutter::StandardMethodCodec::GetInstance()); channel->SetMethodCallHandler( From bf6097a9d49be97ecc6cde56b697aabd5abdcbdc Mon Sep 17 00:00:00 2001 From: mrvokintos <184386663+mrvokintos@users.noreply.github.com> Date: Sat, 18 Jul 2026 20:32:27 +0300 Subject: [PATCH 5/5] last one? --- lib/widgets/fluffy_chat_app.dart | 2 +- windows/runner/flutter_window.cpp | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/widgets/fluffy_chat_app.dart b/lib/widgets/fluffy_chat_app.dart index 599b2fbe0..4f1c62e7a 100644 --- a/lib/widgets/fluffy_chat_app.dart +++ b/lib/widgets/fluffy_chat_app.dart @@ -51,7 +51,7 @@ class FluffyChatApp extends StatefulWidget { class _FluffyChatAppState extends State { final _androidSystemFontPlugin = AndroidSystemFont(); - static const _platform = MethodChannel('xyz.extera.extera/window'); + static const _platform = MethodChannel('xyz.extera.next/window'); // Keeps track of the last Brightness applied to the native window frame // (title bar) so we don't spam the platform channel with redundant calls diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index 367fb477b..832aec2a7 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -15,6 +15,7 @@ bool FlutterWindow::OnCreate() { } RECT frame = GetClientArea(); + // The size here must match the window dimensions to avoid unnecessary surface // creation / destruction in the startup path. flutter_controller_ = std::make_unique( @@ -45,10 +46,11 @@ void FlutterWindow::SetupChannelHandlers() { if (arguments) { auto isDark_variant = arguments->find(flutter::EncodableValue("isDark")); if (isDark_variant != arguments->end()) { - bool isDark = std::get(isDark_variant->second); - SetTitleBarColor(isDark); - result->Success(flutter::EncodableValue(true)); - return; + if (const auto* isDark = std::get_if(&isDark_variant->second)) { + SetTitleBarColor(*isDark); + result->Success(); + return; + } } } result->Error("INVALID_ARGUMENT", "isDark parameter is required");