#include "flutter_window.h" #include #include #include #include #include #include #include #include "flutter/generated_plugin_registrant.h" namespace { // Smallest client area, in logical pixels, that the desktop layouts support. constexpr int kMinimumClientWidth = 800; constexpr int kMinimumClientHeight = 600; // Gap, in logical pixels, between a new mini player and the screen corner. constexpr int kMiniPlayerScreenInset = 16; // Reads a number from the call arguments, or |fallback| when it is missing. double NumberArgument(const flutter::EncodableValue* arguments, const char* key, double fallback) { const auto* map = std::get_if(arguments); if (map == nullptr) { return fallback; } auto entry = map->find(flutter::EncodableValue(key)); if (entry == map->end()) { return fallback; } if (const auto* value = std::get_if(&entry->second)) { return *value; } if (const auto* value = std::get_if(&entry->second)) { return *value; } return fallback; } std::string StringArgument(const flutter::EncodableValue* arguments, const char* key) { if (arguments == nullptr) return {}; const auto* map = std::get_if(arguments); if (map == nullptr) return {}; const auto entry = map->find(flutter::EncodableValue(key)); if (entry == map->end()) return {}; const auto* value = std::get_if(&entry->second); return value == nullptr ? std::string() : *value; } } // namespace FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} FlutterWindow::~FlutterWindow() {} bool FlutterWindow::OnCreate() { if (!Win32Window::OnCreate()) { return false; } 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_); if (!flutter_controller_->engine() || !flutter_controller_->view()) { return false; } RegisterPlugins(flutter_controller_->engine()); window_channel_ = std::make_unique>( flutter_controller_->engine()->messenger(), "com.zarz.mornye/window", &flutter::StandardMethodCodec::GetInstance()); window_channel_->SetMethodCallHandler( [this](const flutter::MethodCall& call, std::unique_ptr> result) { HandleWindowCall(call, std::move(result)); }); SetChildContent(flutter_controller_->view()->GetNativeWindow()); flutter_controller_->engine()->SetNextFrameCallback([&]() { this->Show(); }); // Flutter can complete the first frame before the "show window" callback is // registered. The following call ensures a frame is pending to ensure the // window is shown. It is a no-op if the first frame hasn't completed yet. flutter_controller_->ForceRedraw(); return true; } void FlutterWindow::OnDestroy() { window_channel_ = nullptr; if (flutter_controller_) { flutter_controller_ = nullptr; } Win32Window::OnDestroy(); } LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { // Give Flutter, including plugins, an opportunity to handle window messages. if (flutter_controller_) { std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, lparam); if (result) { return *result; } } switch (message) { case WM_NCCALCSIZE: if (window_mode_ == "borderless" && !mini_player_ && wparam) { if (IsZoomed(hwnd)) { auto* size = reinterpret_cast(lparam); MONITORINFO monitor = {}; monitor.cbSize = sizeof(monitor); if (GetMonitorInfo( MonitorFromRect(&size->rgrc[0], MONITOR_DEFAULTTONEAREST), &monitor)) { size->rgrc[0] = monitor.rcWork; } } return 0; } break; case WM_NCHITTEST: if (window_mode_ == "borderless" && !mini_player_ && !IsZoomed(hwnd)) { RECT bounds; GetWindowRect(hwnd, &bounds); const UINT dpi = GetDpiForWindow(hwnd); const int edge = MulDiv(8, dpi == 0 ? USER_DEFAULT_SCREEN_DPI : dpi, USER_DEFAULT_SCREEN_DPI); const int x = GET_X_LPARAM(lparam); const int y = GET_Y_LPARAM(lparam); const bool left = x < bounds.left + edge; const bool right = x >= bounds.right - edge; const bool top = y < bounds.top + edge; const bool bottom = y >= bounds.bottom - edge; if (top && left) return HTTOPLEFT; if (top && right) return HTTOPRIGHT; if (bottom && left) return HTBOTTOMLEFT; if (bottom && right) return HTBOTTOMRIGHT; if (left) return HTLEFT; if (right) return HTRIGHT; if (top) return HTTOP; if (bottom) return HTBOTTOM; } break; case WM_DISPLAYCHANGE: case WM_DPICHANGED: if (window_mode_ == "fullscreen") { FitFullScreen(); return 0; } break; case WM_FONTCHANGE: flutter_controller_->engine()->ReloadSystemFonts(); break; case WM_GETMINMAXINFO: { // The mini player is far smaller than the full layouts' minimum. if (mini_player_ || window_mode_ == "fullscreen") { break; } UINT dpi = GetDpiForWindow(hwnd); if (dpi == 0) { dpi = USER_DEFAULT_SCREEN_DPI; } RECT frame = {0, 0, MulDiv(kMinimumClientWidth, dpi, USER_DEFAULT_SCREEN_DPI), MulDiv(kMinimumClientHeight, dpi, USER_DEFAULT_SCREEN_DPI)}; AdjustWindowRectExForDpi( &frame, static_cast(GetWindowLongPtr(hwnd, GWL_STYLE)), FALSE, static_cast(GetWindowLongPtr(hwnd, GWL_EXSTYLE)), dpi); auto* info = reinterpret_cast(lparam); info->ptMinTrackSize.x = frame.right - frame.left; info->ptMinTrackSize.y = frame.bottom - frame.top; return 0; } } return Win32Window::MessageHandler(hwnd, message, wparam, lparam); } void FlutterWindow::HandleWindowCall( const flutter::MethodCall& call, std::unique_ptr> result) { const std::string& method = call.method_name(); if (method == "openMiniPlayer") { const double width = NumberArgument(call.arguments(), "width", 360); const double height = NumberArgument(call.arguments(), "height", 140); result->Success(flutter::EncodableValue(OpenMiniPlayer(width, height))); } else if (method == "closeMiniPlayer") { CloseMiniPlayer(); result->Success(); } else if (method == "startDrag") { // Answer first: the move loop runs until the mouse button is released. result->Success(); StartDrag(); } else if (method == "getWindowMode") { result->Success(flutter::EncodableValue(window_mode_)); } else if (method == "isFullScreen") { result->Success(flutter::EncodableValue(window_mode_ == "fullscreen")); } else if (method == "setWindowMode") { result->Success(flutter::EncodableValue( SetWindowMode(StringArgument(call.arguments(), "mode")))); } else if (method == "minimizeWindow") { ShowWindow(GetHandle(), SW_MINIMIZE); result->Success(); } else if (method == "zoomWindow" || method == "titleBarDoubleClick") { ZoomWindow(); result->Success(); } else if (method == "closeWindow") { result->Success(); PostMessage(GetHandle(), WM_CLOSE, 0, 0); } else if (method == "startResize") { const bool from_left = StringArgument(call.arguments(), "edge") == "bottomLeft"; result->Success(); if (window_mode_ == "borderless" && !mini_player_ && !IsZoomed(GetHandle())) { ReleaseCapture(); SendMessage(GetHandle(), WM_SYSCOMMAND, SC_SIZE | (from_left ? WMSZ_BOTTOMLEFT : WMSZ_BOTTOMRIGHT), 0); } } else { result->NotImplemented(); } } bool FlutterWindow::OpenMiniPlayer(double width, double height) { HWND hwnd = GetHandle(); if (hwnd == nullptr || IsIconic(hwnd) || window_mode_ == "fullscreen") { return false; } UINT dpi = GetDpiForWindow(hwnd); if (dpi == 0) { dpi = USER_DEFAULT_SCREEN_DPI; } const int panel_width = MulDiv(static_cast(std::lround(width)), dpi, USER_DEFAULT_SCREEN_DPI); const int panel_height = MulDiv(static_cast(std::lround(height)), dpi, USER_DEFAULT_SCREEN_DPI); const bool opening = !mini_player_; if (opening) { mini_player_restore_placement_.length = sizeof(WINDOWPLACEMENT); GetWindowPlacement(hwnd, &mini_player_restore_placement_); mini_player_restore_style_ = GetWindowLongPtr(hwnd, GWL_STYLE); mini_player_ = true; if (IsZoomed(hwnd)) { ShowWindow(hwnd, SW_RESTORE); } // No caption or sizing border: the Flutter content moves the panel. SetWindowLongPtr(hwnd, GWL_STYLE, mini_player_restore_style_ & ~(WS_CAPTION | WS_THICKFRAME | WS_MAXIMIZEBOX | WS_MAXIMIZE)); } std::optional origin; if (!opening) { RECT window_rect; GetWindowRect(hwnd, &window_rect); origin = POINT{window_rect.left, window_rect.top}; } else { origin = mini_player_origin_; } // Keep the whole panel on the screen it opens on. HMONITOR monitor = origin ? MonitorFromPoint(*origin, MONITOR_DEFAULTTONEAREST) : MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); MONITORINFO monitor_info = {}; monitor_info.cbSize = sizeof(MONITORINFO); GetMonitorInfo(monitor, &monitor_info); const RECT& work = monitor_info.rcWork; if (!origin) { const int inset = MulDiv(kMiniPlayerScreenInset, dpi, USER_DEFAULT_SCREEN_DPI); origin = POINT{work.right - panel_width - inset, work.top + inset}; } const LONG x = std::clamp( origin->x, work.left, std::max(work.left, work.right - panel_width)); const LONG y = std::clamp( origin->y, work.top, std::max(work.top, work.bottom - panel_height)); SetWindowPos(hwnd, HWND_TOPMOST, x, y, panel_width, panel_height, SWP_FRAMECHANGED | SWP_SHOWWINDOW); return true; } void FlutterWindow::CloseMiniPlayer() { if (!mini_player_) { return; } HWND hwnd = GetHandle(); RECT window_rect; GetWindowRect(hwnd, &window_rect); mini_player_origin_ = POINT{window_rect.left, window_rect.top}; mini_player_ = false; SetWindowLongPtr(hwnd, GWL_STYLE, mini_player_restore_style_ & ~(WS_MAXIMIZE | WS_MINIMIZE)); SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED); // Brings back the size and place, maximized when it was. SetWindowPlacement(hwnd, &mini_player_restore_placement_); } void FlutterWindow::StartDrag() { HWND hwnd = GetHandle(); if (hwnd == nullptr || window_mode_ == "fullscreen") { return; } // The Flutter view holds the mouse capture from the button press; hand // the drag to the system move loop, as a title bar would. ReleaseCapture(); SendMessage(hwnd, WM_SYSCOMMAND, SC_MOVE | HTCAPTION, 0); } void FlutterWindow::NotifyWindowMode() { if (window_channel_) { window_channel_->InvokeMethod( "windowModeChanged", std::make_unique(window_mode_)); } } void FlutterWindow::FitFullScreen() { const HWND hwnd = GetHandle(); MONITORINFO monitor = {}; monitor.cbSize = sizeof(monitor); if (!GetMonitorInfo(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), &monitor)) return; const RECT& bounds = monitor.rcMonitor; SetWindowPos(hwnd, HWND_TOP, bounds.left, bounds.top, bounds.right - bounds.left, bounds.bottom - bounds.top, SWP_FRAMECHANGED | SWP_NOOWNERZORDER); } bool FlutterWindow::SetWindowMode(const std::string& mode) { const HWND hwnd = GetHandle(); if (hwnd == nullptr || mini_player_ || (mode != "windowed" && mode != "fullscreen" && mode != "borderless")) { return false; } if (mode == window_mode_) { NotifyWindowMode(); return true; } const bool was_fullscreen = window_mode_ == "fullscreen"; if (window_mode_ == "windowed") { windowed_style_ = GetWindowLongPtr(hwnd, GWL_STYLE) & ~(WS_MAXIMIZE | WS_MINIMIZE); windowed_ex_style_ = GetWindowLongPtr(hwnd, GWL_EXSTYLE); } if (mode == "fullscreen") { fullscreen_restore_placement_.length = sizeof(WINDOWPLACEMENT); GetWindowPlacement(hwnd, &fullscreen_restore_placement_); if (IsZoomed(hwnd) || IsIconic(hwnd)) ShowWindow(hwnd, SW_RESTORE); } const bool maximized = IsZoomed(hwnd); window_mode_ = mode; LONG_PTR style = windowed_style_; LONG_PTR ex_style = windowed_ex_style_; if (mode == "fullscreen") { style &= ~WS_OVERLAPPEDWINDOW; } else if (mode == "borderless") { style &= ~WS_CAPTION; } if (maximized && mode != "fullscreen") style |= WS_MAXIMIZE; if (mode != "windowed") { ex_style &= ~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE | WS_EX_STATICEDGE); } SetWindowLongPtr(hwnd, GWL_STYLE, style); SetWindowLongPtr(hwnd, GWL_EXSTYLE, ex_style); if (mode == "fullscreen") { FitFullScreen(); } else { SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); if (was_fullscreen) SetWindowPlacement(hwnd, &fullscreen_restore_placement_); } NotifyWindowMode(); return true; } void FlutterWindow::ZoomWindow() { if (window_mode_ != "fullscreen" && !mini_player_) { ShowWindow(GetHandle(), IsZoomed(GetHandle()) ? SW_RESTORE : SW_MAXIMIZE); } }