diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index 9d4390de8270f..ac34fa26d4cda 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -405,6 +405,7 @@ ../../../flutter/shell/platform/windows/direct_manipulation_unittests.cc ../../../flutter/shell/platform/windows/dpi_utils_unittests.cc ../../../flutter/shell/platform/windows/fixtures +../../../flutter/shell/platform/windows/flutter_host_window_controller_unittests.cc ../../../flutter/shell/platform/windows/flutter_project_bundle_unittests.cc ../../../flutter/shell/platform/windows/flutter_window_unittests.cc ../../../flutter/shell/platform/windows/flutter_windows_engine_unittests.cc @@ -425,6 +426,7 @@ ../../../flutter/shell/platform/windows/text_input_plugin_unittest.cc ../../../flutter/shell/platform/windows/window_proc_delegate_manager_unittests.cc ../../../flutter/shell/platform/windows/window_unittests.cc +../../../flutter/shell/platform/windows/windowing_handler_unittests.cc ../../../flutter/shell/platform/windows/windows_lifecycle_manager_unittests.cc ../../../flutter/shell/profiling/sampling_profiler_unittest.cc ../../../flutter/shell/testing diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 36f280bd78ea5..8cdc5d3ed58a0 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -47462,6 +47462,7 @@ FILE: ../../../flutter/shell/platform/common/text_editing_delta.h FILE: ../../../flutter/shell/platform/common/text_input_model.cc FILE: ../../../flutter/shell/platform/common/text_input_model.h FILE: ../../../flutter/shell/platform/common/text_range.h +FILE: ../../../flutter/shell/platform/common/windowing.h FILE: ../../../flutter/shell/platform/darwin/common/availability_version_check.cc FILE: ../../../flutter/shell/platform/darwin/common/availability_version_check.h FILE: ../../../flutter/shell/platform/darwin/common/buffer_conversions.h @@ -48208,6 +48209,10 @@ FILE: ../../../flutter/shell/platform/windows/flutter_platform_node_delegate_win FILE: ../../../flutter/shell/platform/windows/flutter_platform_node_delegate_windows.h FILE: ../../../flutter/shell/platform/windows/flutter_project_bundle.cc FILE: ../../../flutter/shell/platform/windows/flutter_project_bundle.h +FILE: ../../../flutter/shell/platform/windows/flutter_host_window.cc +FILE: ../../../flutter/shell/platform/windows/flutter_host_window.h +FILE: ../../../flutter/shell/platform/windows/flutter_host_window_controller.cc +FILE: ../../../flutter/shell/platform/windows/flutter_host_window_controller.h FILE: ../../../flutter/shell/platform/windows/flutter_window.cc FILE: ../../../flutter/shell/platform/windows/flutter_window.h FILE: ../../../flutter/shell/platform/windows/flutter_windows.cc @@ -48257,6 +48262,8 @@ FILE: ../../../flutter/shell/platform/windows/window_binding_handler_delegate.h FILE: ../../../flutter/shell/platform/windows/window_proc_delegate_manager.cc FILE: ../../../flutter/shell/platform/windows/window_proc_delegate_manager.h FILE: ../../../flutter/shell/platform/windows/window_state.h +FILE: ../../../flutter/shell/platform/windows/windowing_handler.cc +FILE: ../../../flutter/shell/platform/windows/windowing_handler.h FILE: ../../../flutter/shell/platform/windows/windows_lifecycle_manager.cc FILE: ../../../flutter/shell/platform/windows/windows_lifecycle_manager.h FILE: ../../../flutter/shell/platform/windows/windows_proc_table.cc diff --git a/common/settings.h b/common/settings.h index 617ac202adb81..507a1ebe656cd 100644 --- a/common/settings.h +++ b/common/settings.h @@ -367,6 +367,10 @@ struct Settings { // If true, the UI thread is the platform thread on supported // platforms. bool merged_platform_ui_thread = true; + + // Enable support for multiple windows. Ignored if not supported on the + // platform. + bool enable_multi_window = false; }; } // namespace flutter diff --git a/shell/common/switches.cc b/shell/common/switches.cc index 9aa9b1528f0d6..cb56d13fa6c32 100644 --- a/shell/common/switches.cc +++ b/shell/common/switches.cc @@ -532,6 +532,17 @@ Settings SettingsFromCommandLine(const fml::CommandLine& command_line) { settings.merged_platform_ui_thread = !command_line.HasOption( FlagForSwitch(Switch::DisableMergedPlatformUIThread)); +#if FML_OS_WIN + // Process the EnableMultiWindow switch on Windows. + { + std::string enable_multi_window_value; + if (command_line.GetOptionValue(FlagForSwitch(Switch::EnableMultiWindow), + &enable_multi_window_value)) { + settings.enable_multi_window = "true" == enable_multi_window_value; + } + } +#endif // FML_OS_WIN + return settings; } diff --git a/shell/common/switches.h b/shell/common/switches.h index 1c1fb595b35d8..b80528ce06fb5 100644 --- a/shell/common/switches.h +++ b/shell/common/switches.h @@ -300,6 +300,10 @@ DEF_SWITCH(DisableMergedPlatformUIThread, DEF_SWITCH(DisableAndroidSurfaceControl, "disable-surface-control", "Disable the SurfaceControl backed swapchain even when supported.") +DEF_SWITCH(EnableMultiWindow, + "enable-multi-window", + "Enable support for multiple windows. Ignored if not supported on " + "the platform.") DEF_SWITCHES_END void PrintUsage(const std::string& executable_name); diff --git a/shell/platform/common/BUILD.gn b/shell/platform/common/BUILD.gn index 5ebfb2236d2c5..f3b352e42ac4e 100644 --- a/shell/platform/common/BUILD.gn +++ b/shell/platform/common/BUILD.gn @@ -142,6 +142,7 @@ source_set("common_cpp_core") { public = [ "geometry.h", "path_utils.h", + "windowing.h", ] sources = [ "path_utils.cc" ] diff --git a/shell/platform/common/windowing.h b/shell/platform/common/windowing.h new file mode 100644 index 0000000000000..3645ab6711965 --- /dev/null +++ b/shell/platform/common/windowing.h @@ -0,0 +1,87 @@ +// Copyright 2013 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. + +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_WINDOWING_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_WINDOWING_H_ + +#include + +namespace flutter { + +// A unique identifier for a view. +using FlutterViewId = int64_t; + +// A point in 2D space for window positioning using integer coordinates. +struct WindowPoint { + int x = 0; + int y = 0; + + friend WindowPoint operator+(WindowPoint const& lhs, WindowPoint const& rhs) { + return {lhs.x + rhs.x, lhs.y + rhs.y}; + } + + friend WindowPoint operator-(WindowPoint const& lhs, WindowPoint const& rhs) { + return {lhs.x - rhs.x, lhs.y - rhs.y}; + } + + friend bool operator==(WindowPoint const& lhs, WindowPoint const& rhs) { + return lhs.x == rhs.x && lhs.y == rhs.y; + } +}; + +// A 2D size using integer dimensions. +struct WindowSize { + int width = 0; + int height = 0; + + explicit operator WindowPoint() const { return {width, height}; } + + friend bool operator==(WindowSize const& lhs, WindowSize const& rhs) { + return lhs.width == rhs.width && lhs.height == rhs.height; + } +}; + +// A rectangular area defined by a top-left point and size. +struct WindowRectangle { + WindowPoint top_left; + WindowSize size; + + // Checks if this rectangle fully contains |rect|. + // Note: An empty rectangle can still contain other empty rectangles, + // which are treated as points or lines of thickness zero + bool contains(WindowRectangle const& rect) const { + return rect.top_left.x >= top_left.x && + rect.top_left.x + rect.size.width <= top_left.x + size.width && + rect.top_left.y >= top_left.y && + rect.top_left.y + rect.size.height <= top_left.y + size.height; + } + + friend bool operator==(WindowRectangle const& lhs, + WindowRectangle const& rhs) { + return lhs.top_left == rhs.top_left && lhs.size == rhs.size; + } +}; + +// Types of windows. +enum class WindowArchetype { + // Regular top-level window. + regular, +}; + +// Window metadata returned as the result of creating a Flutter window. +struct WindowMetadata { + // The ID of the view used for this window, which is unique to each window. + FlutterViewId view_id = 0; + // The type of the window. + WindowArchetype archetype = WindowArchetype::regular; + // Size of the created window, in logical coordinates. + WindowSize size; + // The ID of the view used by the parent window. If not set, the window is + // assumed a top-level window. + std::optional parent_id; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_COMMON_WINDOWING_H_ diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn index e80662e592a72..a412f4eae04c9 100644 --- a/shell/platform/windows/BUILD.gn +++ b/shell/platform/windows/BUILD.gn @@ -73,6 +73,10 @@ source_set("flutter_windows_source") { "external_texture_d3d.h", "external_texture_pixelbuffer.cc", "external_texture_pixelbuffer.h", + "flutter_host_window.cc", + "flutter_host_window.h", + "flutter_host_window_controller.cc", + "flutter_host_window_controller.h", "flutter_key_map.g.cc", "flutter_platform_node_delegate_windows.cc", "flutter_platform_node_delegate_windows.h", @@ -125,6 +129,8 @@ source_set("flutter_windows_source") { "window_proc_delegate_manager.cc", "window_proc_delegate_manager.h", "window_state.h", + "windowing_handler.cc", + "windowing_handler.h", "windows_lifecycle_manager.cc", "windows_lifecycle_manager.h", "windows_proc_table.cc", @@ -202,6 +208,7 @@ executable("flutter_windows_unittests") { "cursor_handler_unittests.cc", "direct_manipulation_unittests.cc", "dpi_utils_unittests.cc", + "flutter_host_window_controller_unittests.cc", "flutter_project_bundle_unittests.cc", "flutter_window_unittests.cc", "flutter_windows_engine_unittests.cc", @@ -249,6 +256,7 @@ executable("flutter_windows_unittests") { "text_input_plugin_unittest.cc", "window_proc_delegate_manager_unittests.cc", "window_unittests.cc", + "windowing_handler_unittests.cc", "windows_lifecycle_manager_unittests.cc", ] diff --git a/shell/platform/windows/flutter_host_window.cc b/shell/platform/windows/flutter_host_window.cc new file mode 100644 index 0000000000000..1759021268358 --- /dev/null +++ b/shell/platform/windows/flutter_host_window.cc @@ -0,0 +1,484 @@ +// Copyright 2013 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. + +#include "flutter/shell/platform/windows/flutter_host_window.h" + +#include + +#include "flutter/shell/platform/windows/dpi_utils.h" +#include "flutter/shell/platform/windows/flutter_host_window_controller.h" +#include "flutter/shell/platform/windows/flutter_window.h" +#include "flutter/shell/platform/windows/flutter_windows_view_controller.h" + +namespace { + +constexpr wchar_t kWindowClassName[] = L"FLUTTER_HOST_WINDOW"; + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module +// so that the non-client area automatically responds to changes in DPI. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + + using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + + FreeLibrary(user32_module); +} + +// Dynamically loads |SetWindowCompositionAttribute| from the User32 module to +// make the window's background transparent. +void EnableTransparentWindowBackground(HWND hwnd) { + HMODULE const user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + + enum WINDOWCOMPOSITIONATTRIB { WCA_ACCENT_POLICY = 19 }; + + struct WINDOWCOMPOSITIONATTRIBDATA { + WINDOWCOMPOSITIONATTRIB Attrib; + PVOID pvData; + SIZE_T cbData; + }; + + using SetWindowCompositionAttribute = + BOOL(__stdcall*)(HWND, WINDOWCOMPOSITIONATTRIBDATA*); + + auto set_window_composition_attribute = + reinterpret_cast( + GetProcAddress(user32_module, "SetWindowCompositionAttribute")); + if (set_window_composition_attribute != nullptr) { + enum ACCENT_STATE { ACCENT_DISABLED = 0 }; + + struct ACCENT_POLICY { + ACCENT_STATE AccentState; + DWORD AccentFlags; + DWORD GradientColor; + DWORD AnimationId; + }; + + // Set the accent policy to disable window composition. + ACCENT_POLICY accent = {ACCENT_DISABLED, 2, static_cast(0), 0}; + WINDOWCOMPOSITIONATTRIBDATA data = {.Attrib = WCA_ACCENT_POLICY, + .pvData = &accent, + .cbData = sizeof(accent)}; + set_window_composition_attribute(hwnd, &data); + + // Extend the frame into the client area and set the window's system + // backdrop type for visual effects. + MARGINS const margins = {-1}; + ::DwmExtendFrameIntoClientArea(hwnd, &margins); + INT effect_value = 1; + ::DwmSetWindowAttribute(hwnd, DWMWA_SYSTEMBACKDROP_TYPE, &effect_value, + sizeof(BOOL)); + } + + FreeLibrary(user32_module); +} + +// Retrieves the calling thread's last-error code message as a string, +// or a fallback message if the error message cannot be formatted. +std::string GetLastErrorAsString() { + LPWSTR message_buffer = nullptr; + + if (DWORD const size = FormatMessage( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&message_buffer), 0, nullptr)) { + std::wstring const wide_message(message_buffer, size); + LocalFree(message_buffer); + message_buffer = nullptr; + + if (int const buffer_size = + WideCharToMultiByte(CP_UTF8, 0, wide_message.c_str(), -1, nullptr, + 0, nullptr, nullptr)) { + std::string message(buffer_size, 0); + WideCharToMultiByte(CP_UTF8, 0, wide_message.c_str(), -1, &message[0], + buffer_size, nullptr, nullptr); + return message; + } + } + + if (message_buffer) { + LocalFree(message_buffer); + } + std::ostringstream oss; + oss << "Format message failed with 0x" << std::hex << std::setfill('0') + << std::setw(8) << GetLastError(); + return oss.str(); +} + +// Calculates the required window size, in physical coordinates, to +// accommodate the given |client_size|, in logical coordinates, for a window +// with the specified |window_style| and |extended_window_style|. The result +// accounts for window borders, non-client areas, and the drop-shadow area. +flutter::WindowSize GetWindowSizeForClientSize( + flutter::WindowSize const& client_size, + DWORD window_style, + DWORD extended_window_style, + HWND owner_hwnd) { + UINT const dpi = flutter::GetDpiForHWND(owner_hwnd); + double const scale_factor = + static_cast(dpi) / USER_DEFAULT_SCREEN_DPI; + RECT rect = {.left = 0, + .top = 0, + .right = static_cast(client_size.width * scale_factor), + .bottom = static_cast(client_size.height * scale_factor)}; + + HMODULE const user32_module = LoadLibraryA("User32.dll"); + if (user32_module) { + using AdjustWindowRectExForDpi = BOOL __stdcall( + LPRECT lpRect, DWORD dwStyle, BOOL bMenu, DWORD dwExStyle, UINT dpi); + + auto* const adjust_window_rect_ext_for_dpi = + reinterpret_cast( + GetProcAddress(user32_module, "AdjustWindowRectExForDpi")); + if (adjust_window_rect_ext_for_dpi) { + if (adjust_window_rect_ext_for_dpi(&rect, window_style, FALSE, + extended_window_style, dpi)) { + FreeLibrary(user32_module); + return {static_cast(rect.right - rect.left), + static_cast(rect.bottom - rect.top)}; + } else { + FML_LOG(WARNING) << "Failed to run AdjustWindowRectExForDpi: " + << GetLastErrorAsString(); + } + } else { + FML_LOG(WARNING) + << "Failed to retrieve AdjustWindowRectExForDpi address from " + "User32.dll."; + } + FreeLibrary(user32_module); + } else { + FML_LOG(WARNING) << "Failed to load User32.dll.\n"; + } + + if (!AdjustWindowRectEx(&rect, window_style, FALSE, extended_window_style)) { + FML_LOG(WARNING) << "Failed to run AdjustWindowRectEx: " + << GetLastErrorAsString(); + } + return {static_cast(rect.right - rect.left), + static_cast(rect.bottom - rect.top)}; +} + +// Checks whether the window class of name |class_name| is registered for the +// current application. +bool IsClassRegistered(LPCWSTR class_name) { + WNDCLASSEX window_class = {}; + return GetClassInfoEx(GetModuleHandle(nullptr), class_name, &window_class) != + 0; +} + +// 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 + +// Update the window frame's theme to match the system theme. +void UpdateTheme(HWND window) { + // Registry key for app theme preference. + const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; + const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + + // A value of 0 indicates apps should use dark mode. A non-zero or missing + // value indicates apps should use light mode. + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS const 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)); + } +} + +} // namespace + +namespace flutter { + +FlutterHostWindow::FlutterHostWindow(FlutterHostWindowController* controller, + std::wstring const& title, + WindowSize const& preferred_client_size, + WindowArchetype archetype) + : window_controller_(controller) { + archetype_ = archetype; + + // Check preconditions and set window styles based on window type. + DWORD window_style = 0; + DWORD extended_window_style = 0; + switch (archetype) { + case WindowArchetype::regular: + window_style |= WS_OVERLAPPEDWINDOW; + break; + default: + FML_UNREACHABLE(); + } + + // Calculate the screen space window rectangle for the new window. + // Default positioning values (CW_USEDEFAULT) are used + // if the window has no owner or positioner. + WindowRectangle const window_rect = [&]() -> WindowRectangle { + WindowSize const window_size = GetWindowSizeForClientSize( + preferred_client_size, window_style, extended_window_style, nullptr); + return {{CW_USEDEFAULT, CW_USEDEFAULT}, window_size}; + }(); + + // Register the window class. + if (!IsClassRegistered(kWindowClassName)) { + auto const idi_app_icon = 101; + WNDCLASSEX window_class = {}; + window_class.cbSize = sizeof(WNDCLASSEX); + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.lpfnWndProc = FlutterHostWindow::WndProc; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(idi_app_icon)); + if (!window_class.hIcon) { + window_class.hIcon = LoadIcon(nullptr, IDI_APPLICATION); + } + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + + if (!RegisterClassEx(&window_class)) { + FML_LOG(ERROR) << "Cannot register window class " << kWindowClassName + << ": " << GetLastErrorAsString(); + } + } + + // Create the native window. + HWND hwnd = CreateWindowEx(extended_window_style, kWindowClassName, + title.c_str(), window_style, + window_rect.top_left.x, window_rect.top_left.y, + window_rect.size.width, window_rect.size.height, + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!hwnd) { + FML_LOG(ERROR) << "Cannot create window: " << GetLastErrorAsString(); + return; + } + + // Adjust the window position so its origin aligns with the top-left corner + // of the window frame, not the window rectangle (which includes the + // drop-shadow). This adjustment must be done post-creation since the frame + // rectangle is only available after the window has been created. + RECT frame_rc; + DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rc, + sizeof(frame_rc)); + RECT window_rc; + GetWindowRect(hwnd, &window_rc); + LONG const left_dropshadow_width = frame_rc.left - window_rc.left; + LONG const top_dropshadow_height = window_rc.top - frame_rc.top; + SetWindowPos(hwnd, nullptr, window_rc.left - left_dropshadow_width, + window_rc.top - top_dropshadow_height, 0, 0, + SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + + // Set up the view. + RECT client_rect; + GetClientRect(hwnd, &client_rect); + int const width = client_rect.right - client_rect.left; + int const height = client_rect.bottom - client_rect.top; + + FlutterWindowsEngine* const engine = window_controller_->engine(); + auto view_window = std::make_unique( + width, height, engine->windows_proc_table()); + + std::unique_ptr view = + engine->CreateView(std::move(view_window)); + if (!view) { + FML_LOG(ERROR) << "Failed to create view"; + return; + } + + view_controller_ = + std::make_unique(nullptr, std::move(view)); + + // Launch the engine if it is not running already. + if (!engine->running() && !engine->Run()) { + FML_LOG(ERROR) << "Failed to launch engine"; + return; + } + // Must happen after engine is running. + view_controller_->view()->SendInitialBounds(); + // The Windows embedder listens to accessibility updates using the + // view's HWND. The embedder's accessibility features may be stale if + // the app was in headless mode. + view_controller_->engine()->UpdateAccessibilityFeatures(); + + // Ensure that basic setup of the view controller was successful. + if (!view_controller_->view()) { + FML_LOG(ERROR) << "Failed to set up the view controller"; + return; + } + + UpdateTheme(hwnd); + + SetChildContent(view_controller_->view()->GetWindowHandle()); + + // TODO(loicsharma): Hide the window until the first frame is rendered. + // Single window apps use the engine's next frame callback to show the window. + // This doesn't work for multi window apps as the engine cannot have multiple + // next frame callbacks. If multiple windows are created, only the last one + // will be shown. + ShowWindow(hwnd, SW_SHOW); + + window_handle_ = hwnd; +} + +FlutterHostWindow::~FlutterHostWindow() { + if (HWND const hwnd = window_handle_) { + window_handle_ = nullptr; + DestroyWindow(hwnd); + + // Unregisters the window class. It will fail silently if there are + // other windows using the class, as only the last window can + // successfully unregister the class. + if (!UnregisterClass(kWindowClassName, GetModuleHandle(nullptr))) { + // Clears the error information after the failed unregistering. + SetLastError(ERROR_SUCCESS); + } + } +} + +FlutterHostWindow* FlutterHostWindow::GetThisFromHandle(HWND hwnd) { + return reinterpret_cast( + GetWindowLongPtr(hwnd, GWLP_USERDATA)); +} + +WindowArchetype FlutterHostWindow::GetArchetype() const { + return archetype_; +} + +std::optional FlutterHostWindow::GetFlutterViewId() const { + if (!view_controller_ || !view_controller_->view()) { + return std::nullopt; + } + return view_controller_->view()->view_id(); +}; + +HWND FlutterHostWindow::GetWindowHandle() const { + return window_handle_; +} + +void FlutterHostWindow::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool FlutterHostWindow::GetQuitOnClose() const { + return quit_on_close_; +} + +void FlutterHostWindow::FocusViewOf(FlutterHostWindow* window) { + if (window != nullptr && window->child_content_ != nullptr) { + SetFocus(window->child_content_); + } +}; + +LRESULT FlutterHostWindow::WndProc(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) { + if (message == WM_NCCREATE) { + auto* const create_struct = reinterpret_cast(lparam); + SetWindowLongPtr(hwnd, GWLP_USERDATA, + reinterpret_cast(create_struct->lpCreateParams)); + auto* const window = + static_cast(create_struct->lpCreateParams); + window->window_handle_ = hwnd; + + EnableFullDpiSupportIfAvailable(hwnd); + EnableTransparentWindowBackground(hwnd); + } else if (FlutterHostWindow* const window = GetThisFromHandle(hwnd)) { + return window->window_controller_->HandleMessage(hwnd, message, wparam, + lparam); + } + + return DefWindowProc(hwnd, message, wparam, lparam); +} + +LRESULT FlutterHostWindow::HandleMessage(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) { + switch (message) { + case WM_DESTROY: + if (window_handle_ && quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto* const new_scaled_window_rect = reinterpret_cast(lparam); + LONG const width = + new_scaled_window_rect->right - new_scaled_window_rect->left; + LONG const height = + new_scaled_window_rect->bottom - new_scaled_window_rect->top; + SetWindowPos(hwnd, nullptr, new_scaled_window_rect->left, + new_scaled_window_rect->top, width, height, + SWP_NOZORDER | SWP_NOACTIVATE); + return 0; + } + + case WM_SIZE: { + if (child_content_ != nullptr) { + // Resize and reposition the child content window + RECT client_rect; + GetClientRect(hwnd, &client_rect); + MoveWindow(child_content_, client_rect.left, client_rect.top, + client_rect.right - client_rect.left, + client_rect.bottom - client_rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + FocusViewOf(this); + return 0; + + case WM_MOUSEACTIVATE: + FocusViewOf(this); + return MA_ACTIVATE; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + + default: + break; + } + + return DefWindowProc(hwnd, message, wparam, lparam); +} + +void FlutterHostWindow::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT client_rect; + GetClientRect(window_handle_, &client_rect); + MoveWindow(content, client_rect.left, client_rect.top, + client_rect.right - client_rect.left, + client_rect.bottom - client_rect.top, true); +} + +} // namespace flutter diff --git a/shell/platform/windows/flutter_host_window.h b/shell/platform/windows/flutter_host_window.h new file mode 100644 index 0000000000000..7b6fdeecc2e91 --- /dev/null +++ b/shell/platform/windows/flutter_host_window.h @@ -0,0 +1,99 @@ +// Copyright 2013 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. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_HOST_WINDOW_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_HOST_WINDOW_H_ + +#include + +#include +#include +#include + +#include "flutter/fml/macros.h" +#include "flutter/shell/platform/common/windowing.h" + +namespace flutter { + +class FlutterHostWindowController; +class FlutterWindowsViewController; + +// A Win32 window that hosts a |FlutterWindow| in its client area. +class FlutterHostWindow { + public: + // Creates a native Win32 window with a child view confined to its client + // area. |controller| manages the window. |title| is the window title. + // |preferred_client_size| is the preferred size of the client rectangle in + // logical coordinates. The window style is defined by |archetype|. + // On success, a valid window handle can be retrieved via + // |FlutterHostWindow::GetWindowHandle|. + FlutterHostWindow(FlutterHostWindowController* controller, + std::wstring const& title, + WindowSize const& preferred_client_size, + WindowArchetype archetype); + virtual ~FlutterHostWindow(); + + // Returns the instance pointer for |hwnd| or nulllptr if invalid. + static FlutterHostWindow* GetThisFromHandle(HWND hwnd); + + // Returns the window archetype. + WindowArchetype GetArchetype() const; + + // Returns the hosted Flutter view's ID or std::nullopt if not created. + std::optional GetFlutterViewId() const; + + // Returns the backing window handle, or nullptr if the native window is not + // created or has already been destroyed. + HWND GetWindowHandle() const; + + // Sets whether closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Returns whether closing this window will quit the application. + bool GetQuitOnClose() const; + + private: + friend FlutterHostWindowController; + + // Set the focus to the child view window of |window|. + static void FocusViewOf(FlutterHostWindow* window); + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. Delegates other messages to the controller. + static LRESULT WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); + + // Processes and routes salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + LRESULT HandleMessage(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Controller for this window. + FlutterHostWindowController* const window_controller_; + + // Controller for the view hosted by this window. + std::unique_ptr view_controller_; + + // The window archetype. + WindowArchetype archetype_ = WindowArchetype::regular; + + // Indicates if closing this window will quit the application. + bool quit_on_close_ = false; + + // Backing handle for this window. + HWND window_handle_ = nullptr; + + // Backing handle for the hosted view window. + HWND child_content_ = nullptr; + + FML_DISALLOW_COPY_AND_ASSIGN(FlutterHostWindow); +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_HOST_WINDOW_H_ diff --git a/shell/platform/windows/flutter_host_window_controller.cc b/shell/platform/windows/flutter_host_window_controller.cc new file mode 100644 index 0000000000000..1220d268069bd --- /dev/null +++ b/shell/platform/windows/flutter_host_window_controller.cc @@ -0,0 +1,215 @@ +// Copyright 2013 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. + +#include "flutter/shell/platform/windows/flutter_host_window_controller.h" + +#include + +#include "flutter/shell/platform/windows/flutter_windows_engine.h" + +namespace flutter { + +namespace { + +// Names of the messages sent by the controller in response to window events. +constexpr char kOnWindowChangedMethod[] = "onWindowChanged"; +constexpr char kOnWindowCreatedMethod[] = "onWindowCreated"; +constexpr char kOnWindowDestroyedMethod[] = "onWindowDestroyed"; + +// Keys used in the onWindow* messages sent through the channel. +constexpr char kIsMovingKey[] = "isMoving"; +constexpr char kParentViewIdKey[] = "parentViewId"; +constexpr char kRelativePositionKey[] = "relativePosition"; +constexpr char kSizeKey[] = "size"; +constexpr char kViewIdKey[] = "viewId"; + +} // namespace + +FlutterHostWindowController::FlutterHostWindowController( + FlutterWindowsEngine* engine) + : engine_(engine) {} + +FlutterHostWindowController::~FlutterHostWindowController() { + DestroyAllWindows(); +} + +std::optional FlutterHostWindowController::CreateHostWindow( + std::wstring const& title, + WindowSize const& preferred_size, + WindowArchetype archetype) { + auto window = std::make_unique(this, title, preferred_size, + archetype); + if (!window->GetWindowHandle()) { + return std::nullopt; + } + + // Assume first window is the main window. + if (windows_.empty()) { + window->SetQuitOnClose(true); + } + + FlutterViewId const view_id = window->GetFlutterViewId().value(); + windows_[view_id] = std::move(window); + + SendOnWindowCreated(view_id, std::nullopt); + + WindowMetadata result = {.view_id = view_id, + .archetype = archetype, + .size = GetWindowSize(view_id), + .parent_id = std::nullopt}; + + return result; +} + +bool FlutterHostWindowController::DestroyHostWindow(FlutterViewId view_id) { + if (auto it = windows_.find(view_id); it != windows_.end()) { + FlutterHostWindow* const window = it->second.get(); + HWND const window_handle = window->GetWindowHandle(); + + // |window| will be removed from |windows_| when WM_NCDESTROY is handled. + PostMessage(window->GetWindowHandle(), WM_CLOSE, 0, 0); + + return true; + } + return false; +} + +FlutterHostWindow* FlutterHostWindowController::GetHostWindow( + FlutterViewId view_id) const { + if (auto it = windows_.find(view_id); it != windows_.end()) { + return it->second.get(); + } + return nullptr; +} + +LRESULT FlutterHostWindowController::HandleMessage(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) { + switch (message) { + case WM_NCDESTROY: { + auto const it = std::find_if( + windows_.begin(), windows_.end(), [hwnd](auto const& window) { + return window.second->GetWindowHandle() == hwnd; + }); + if (it != windows_.end()) { + FlutterViewId const view_id = it->first; + bool const quit_on_close = it->second->GetQuitOnClose(); + + windows_.erase(it); + + SendOnWindowDestroyed(view_id); + + if (quit_on_close) { + DestroyAllWindows(); + } + } + } + return 0; + case WM_SIZE: { + auto const it = std::find_if( + windows_.begin(), windows_.end(), [hwnd](auto const& window) { + return window.second->GetWindowHandle() == hwnd; + }); + if (it != windows_.end()) { + FlutterViewId const view_id = it->first; + SendOnWindowChanged(view_id); + } + } break; + default: + break; + } + + if (FlutterHostWindow* const window = + FlutterHostWindow::GetThisFromHandle(hwnd)) { + return window->HandleMessage(hwnd, message, wparam, lparam); + } + return DefWindowProc(hwnd, message, wparam, lparam); +} + +void FlutterHostWindowController::SetMethodChannel( + std::shared_ptr> channel) { + channel_ = std::move(channel); +} + +FlutterWindowsEngine* FlutterHostWindowController::engine() const { + return engine_; +} + +void FlutterHostWindowController::DestroyAllWindows() { + if (!windows_.empty()) { + // Destroy windows in reverse order of creation. + for (auto it = std::prev(windows_.end()); + it != std::prev(windows_.begin());) { + auto current = it--; + auto const& [view_id, window] = *current; + if (window->GetWindowHandle()) { + DestroyHostWindow(view_id); + } + } + } +} + +WindowSize FlutterHostWindowController::GetWindowSize( + FlutterViewId view_id) const { + HWND const hwnd = windows_.at(view_id)->GetWindowHandle(); + RECT frame_rect; + DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rect, + sizeof(frame_rect)); + + // Convert to logical coordinates. + auto const dpr = FlutterDesktopGetDpiForHWND(hwnd) / + static_cast(USER_DEFAULT_SCREEN_DPI); + frame_rect.left = static_cast(frame_rect.left / dpr); + frame_rect.top = static_cast(frame_rect.top / dpr); + frame_rect.right = static_cast(frame_rect.right / dpr); + frame_rect.bottom = static_cast(frame_rect.bottom / dpr); + + auto const width = frame_rect.right - frame_rect.left; + auto const height = frame_rect.bottom - frame_rect.top; + return {static_cast(width), static_cast(height)}; +} + +void FlutterHostWindowController::SendOnWindowChanged( + FlutterViewId view_id) const { + if (channel_) { + WindowSize const size = GetWindowSize(view_id); + channel_->InvokeMethod( + kOnWindowChangedMethod, + std::make_unique(EncodableMap{ + {EncodableValue(kViewIdKey), EncodableValue(view_id)}, + {EncodableValue(kSizeKey), + EncodableValue(EncodableList{EncodableValue(size.width), + EncodableValue(size.height)})}, + {EncodableValue(kRelativePositionKey), EncodableValue()}, + {EncodableValue(kIsMovingKey), EncodableValue()}})); + } +} + +void FlutterHostWindowController::SendOnWindowCreated( + FlutterViewId view_id, + std::optional parent_view_id) const { + if (channel_) { + channel_->InvokeMethod( + kOnWindowCreatedMethod, + std::make_unique(EncodableMap{ + {EncodableValue(kViewIdKey), EncodableValue(view_id)}, + {EncodableValue(kParentViewIdKey), + parent_view_id ? EncodableValue(parent_view_id.value()) + : EncodableValue()}})); + } +} + +void FlutterHostWindowController::SendOnWindowDestroyed( + FlutterViewId view_id) const { + if (channel_) { + channel_->InvokeMethod( + kOnWindowDestroyedMethod, + std::make_unique(EncodableMap{ + {EncodableValue(kViewIdKey), EncodableValue(view_id)}, + })); + } +} + +} // namespace flutter diff --git a/shell/platform/windows/flutter_host_window_controller.h b/shell/platform/windows/flutter_host_window_controller.h new file mode 100644 index 0000000000000..3d73a838a4d8d --- /dev/null +++ b/shell/platform/windows/flutter_host_window_controller.h @@ -0,0 +1,101 @@ +// Copyright 2013 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. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_HOST_WINDOW_CONTROLLER_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_HOST_WINDOW_CONTROLLER_H_ + +#include + +#include "flutter/fml/macros.h" +#include "flutter/shell/platform/common/client_wrapper/include/flutter/method_channel.h" +#include "flutter/shell/platform/windows/flutter_host_window.h" + +namespace flutter { + +class FlutterWindowsEngine; + +// A controller class for managing |FlutterHostWindow| instances. +// A unique instance of this class is owned by |FlutterWindowsEngine| and used +// in |WindowingHandler| to handle methods and messages enabling multi-window +// support. +class FlutterHostWindowController { + public: + explicit FlutterHostWindowController(FlutterWindowsEngine* engine); + virtual ~FlutterHostWindowController(); + + // Creates a |FlutterHostWindow|, i.e., a native Win32 window with a + // |FlutterWindow| parented to it. The child |FlutterWindow| implements a + // Flutter view that is displayed in the client area of the + // |FlutterHostWindow|. + // + // |title| is the window title string. |preferred_size| is the preferred size + // of the client rectangle, i.e., the size expected for the child view, in + // logical coordinates. The actual size may differ. The window style is + // determined by |archetype|. + // + // Returns a |WindowMetadata| with the metadata of the window just created, or + // std::nullopt if the window could not be created. + virtual std::optional CreateHostWindow( + std::wstring const& title, + WindowSize const& preferred_size, + WindowArchetype archetype); + + // Destroys the window that hosts the view with ID |view_id|. + // + // Returns false if the controller does not have a window hosting a view with + // ID |view_id|. + virtual bool DestroyHostWindow(FlutterViewId view_id); + + // Gets the window hosting the view with ID |view_id|. + // + // Returns nullptr if the controller does not have a window hosting a view + // with ID |view_id|. + FlutterHostWindow* GetHostWindow(FlutterViewId view_id) const; + + // Message handler called by |FlutterHostWindow::WndProc| to process window + // messages before delegating them to the host window. This allows the + // controller to process messages that affect the state of other host windows. + LRESULT HandleMessage(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); + + // Sets the method channel through which the controller will send the window + // events "onWindowCreated", "onWindowDestroyed", and "onWindowChanged". + void SetMethodChannel(std::shared_ptr> channel); + + // Gets the engine that owns this controller. + FlutterWindowsEngine* engine() const; + + private: + // Destroys all windows managed by this controller. + void DestroyAllWindows(); + + // Gets the size of the window hosting the view with ID |view_id|. This is the + // size the host window frame, in logical coordinates, and does not include + // the dimensions of the drop-shadow area. + WindowSize GetWindowSize(FlutterViewId view_id) const; + + // Sends the "onWindowChanged" message to the Flutter engine. + void SendOnWindowChanged(FlutterViewId view_id) const; + + // Sends the "onWindowCreated" message to the Flutter engine. + void SendOnWindowCreated(FlutterViewId view_id, + std::optional parent_view_id) const; + + // Sends the "onWindowDestroyed" message to the Flutter engine. + void SendOnWindowDestroyed(FlutterViewId view_id) const; + + // The Flutter engine that owns this controller. + FlutterWindowsEngine* const engine_; + + // The windowing channel through which the controller sends messages. + std::shared_ptr> channel_; + + // The host windows managed by this controller. + std::map> windows_; + + FML_DISALLOW_COPY_AND_ASSIGN(FlutterHostWindowController); +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_HOST_WINDOW_CONTROLLER_H_ diff --git a/shell/platform/windows/flutter_host_window_controller_unittests.cc b/shell/platform/windows/flutter_host_window_controller_unittests.cc new file mode 100644 index 0000000000000..ee41fda6c17ac --- /dev/null +++ b/shell/platform/windows/flutter_host_window_controller_unittests.cc @@ -0,0 +1,198 @@ +// Copyright 2013 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. +#include "flutter/shell/platform/windows/windowing_handler.h" + +#include "flutter/fml/macros.h" +#include "flutter/shell/platform/common/client_wrapper/include/flutter/standard_method_codec.h" +#include "flutter/shell/platform/windows/flutter_host_window_controller.h" +#include "flutter/shell/platform/windows/testing/flutter_windows_engine_builder.h" +#include "flutter/shell/platform/windows/testing/test_binary_messenger.h" +#include "flutter/shell/platform/windows/testing/windows_test.h" +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { + +namespace { + +static constexpr char kChannelName[] = "flutter/windowing"; +static constexpr char kOnWindowCreatedMethod[] = "onWindowCreated"; +static constexpr char kOnWindowDestroyedMethod[] = "onWindowDestroyed"; +static constexpr char kViewIdKey[] = "viewId"; +static constexpr char kParentViewIdKey[] = "parentViewId"; + +// Process the next Win32 message if there is one. This can be used to +// pump the Windows platform thread task runner. +void PumpMessage() { + ::MSG msg; + if (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } +} + +class FlutterHostWindowControllerTest : public WindowsTest { + public: + FlutterHostWindowControllerTest() = default; + virtual ~FlutterHostWindowControllerTest() = default; + + protected: + void SetUp() override { + InitializeCOM(); + FlutterWindowsEngineBuilder builder(GetContext()); + engine_ = builder.Build(); + controller_ = std::make_unique(engine_.get()); + } + + FlutterWindowsEngine* engine() { return engine_.get(); } + FlutterHostWindowController* host_window_controller() { + return controller_.get(); + } + + private: + void InitializeCOM() const { + FML_CHECK(SUCCEEDED(::CoInitializeEx(nullptr, COINIT_MULTITHREADED))); + } + + std::unique_ptr engine_; + std::unique_ptr controller_; + + FML_DISALLOW_COPY_AND_ASSIGN(FlutterHostWindowControllerTest); +}; + +} // namespace + +TEST_F(FlutterHostWindowControllerTest, CreateRegularWindow) { + bool called_onWindowCreated = false; + + // Test messenger with a handler for onWindowCreated. + TestBinaryMessenger messenger([&](const std::string& channel, + const uint8_t* message, size_t size, + BinaryReply reply) { + // Ensure the message is sent on the windowing channel. + ASSERT_EQ(channel, kChannelName); + + // Ensure the decoded method call is valid. + auto const method = StandardMethodCodec::GetInstance().DecodeMethodCall( + std::vector(message, message + size)); + ASSERT_NE(method, nullptr); + + // Handle the onWindowCreated method. + if (method->method_name() == kOnWindowCreatedMethod) { + called_onWindowCreated = true; + + // Validate the method arguments. + auto const& args = *method->arguments(); + ASSERT_TRUE(std::holds_alternative(args)); + auto const& args_map = std::get(args); + + // Ensure the viewId is present and valid. + auto const& it_viewId = args_map.find(EncodableValue(kViewIdKey)); + ASSERT_NE(it_viewId, args_map.end()); + auto const* value_viewId = std::get_if(&it_viewId->second); + ASSERT_NE(value_viewId, nullptr); + EXPECT_GE(*value_viewId, 0); + EXPECT_NE(engine()->view(*value_viewId), nullptr); + + // Ensure the parentViewId is a std::monostate (indicating no parent). + auto const& it_parentViewId = + args_map.find(EncodableValue(kParentViewIdKey)); + ASSERT_NE(it_parentViewId, args_map.end()); + auto const* value_parentViewId = + std::get_if(&it_parentViewId->second); + EXPECT_NE(value_parentViewId, nullptr); + } + }); + + // Create the windowing handler with the test messenger. + WindowingHandler windowing_handler(&messenger, host_window_controller()); + + // Define parameters for the window to be created. + WindowSize const size = {800, 600}; + wchar_t const* const title = L"window"; + WindowArchetype const archetype = WindowArchetype::regular; + + // Create the window. + std::optional const result = + host_window_controller()->CreateHostWindow(title, size, archetype); + + // Verify the onWindowCreated callback was invoked. + EXPECT_TRUE(called_onWindowCreated); + + // Validate the returned metadata. + ASSERT_TRUE(result.has_value()); + EXPECT_NE(engine()->view(result->view_id), nullptr); + EXPECT_EQ(result->archetype, archetype); + EXPECT_GE(result->size.width, size.width); + EXPECT_GE(result->size.height, size.height); + EXPECT_FALSE(result->parent_id.has_value()); + + // Verify the window exists and the view has the expected dimensions. + FlutterHostWindow* const window = + host_window_controller()->GetHostWindow(result->view_id); + ASSERT_NE(window, nullptr); + RECT client_rect; + GetClientRect(window->GetWindowHandle(), &client_rect); + EXPECT_EQ(client_rect.right - client_rect.left, size.width); + EXPECT_EQ(client_rect.bottom - client_rect.top, size.height); +} + +TEST_F(FlutterHostWindowControllerTest, DestroyWindow) { + bool done = false; + + // Test messenger with a handler for onWindowDestroyed. + TestBinaryMessenger messenger([&](const std::string& channel, + const uint8_t* message, size_t size, + BinaryReply reply) { + // Ensure the message is sent on the windowing channel. + ASSERT_EQ(channel, kChannelName); + + // Ensure the decoded method call is valid. + auto const method = StandardMethodCodec::GetInstance().DecodeMethodCall( + std::vector(message, message + size)); + ASSERT_NE(method, nullptr); + + // Handle the onWindowDestroyed method. + if (method->method_name() == kOnWindowDestroyedMethod) { + // Validate the method arguments. + auto const& args = *method->arguments(); + ASSERT_TRUE(std::holds_alternative(args)); + auto const& args_map = std::get(args); + + // Ensure the viewId is present but not valid anymore. + auto const& it_viewId = args_map.find(EncodableValue(kViewIdKey)); + ASSERT_NE(it_viewId, args_map.end()); + auto const* value_viewId = std::get_if(&it_viewId->second); + ASSERT_NE(value_viewId, nullptr); + EXPECT_GE(*value_viewId, 0); + EXPECT_EQ(engine()->view(*value_viewId), nullptr); + + done = true; + } + }); + + // Create the windowing handler with the test messenger. + WindowingHandler windowing_handler(&messenger, host_window_controller()); + + // Define parameters for the window to be created. + WindowSize const size = {800, 600}; + wchar_t const* const title = L"window"; + WindowArchetype const archetype = WindowArchetype::regular; + + // Create the window. + std::optional const result = + host_window_controller()->CreateHostWindow(title, size, archetype); + ASSERT_TRUE(result.has_value()); + + // Destroy the window and ensure onWindowDestroyed was invoked. + EXPECT_TRUE(host_window_controller()->DestroyHostWindow(result->view_id)); + + // Pump messages for the Windows platform task runner. + while (!done) { + PumpMessage(); + } +} + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc index d08591a10b520..b8add6beee8f6 100644 --- a/shell/platform/windows/flutter_windows_engine.cc +++ b/shell/platform/windows/flutter_windows_engine.cc @@ -194,6 +194,10 @@ FlutterWindowsEngine::FlutterWindowsEngine( enable_impeller_ = std::find(switches.begin(), switches.end(), "--enable-impeller=true") != switches.end(); + enable_multi_window_ = + std::find(switches.begin(), switches.end(), + "--enable-multi-window=true") != switches.end(); + egl_manager_ = egl::Manager::Create(); window_proc_delegate_manager_ = std::make_unique(); window_proc_delegate_manager_->RegisterTopLevelWindowProcDelegate( @@ -222,6 +226,12 @@ FlutterWindowsEngine::FlutterWindowsEngine( std::make_unique(messenger_wrapper_.get(), this); platform_handler_ = std::make_unique(messenger_wrapper_.get(), this); + if (enable_multi_window_) { + host_window_controller_ = + std::make_unique(this); + windowing_handler_ = std::make_unique( + messenger_wrapper_.get(), host_window_controller_.get()); + } settings_plugin_ = std::make_unique(messenger_wrapper_.get(), task_runner_.get()); } diff --git a/shell/platform/windows/flutter_windows_engine.h b/shell/platform/windows/flutter_windows_engine.h index 2d7b730580099..011020b25dd3f 100644 --- a/shell/platform/windows/flutter_windows_engine.h +++ b/shell/platform/windows/flutter_windows_engine.h @@ -30,6 +30,7 @@ #include "flutter/shell/platform/windows/egl/manager.h" #include "flutter/shell/platform/windows/egl/proc_table.h" #include "flutter/shell/platform/windows/flutter_desktop_messenger.h" +#include "flutter/shell/platform/windows/flutter_host_window.h" #include "flutter/shell/platform/windows/flutter_project_bundle.h" #include "flutter/shell/platform/windows/flutter_windows_texture_registrar.h" #include "flutter/shell/platform/windows/keyboard_handler_base.h" @@ -42,6 +43,7 @@ #include "flutter/shell/platform/windows/text_input_plugin.h" #include "flutter/shell/platform/windows/window_proc_delegate_manager.h" #include "flutter/shell/platform/windows/window_state.h" +#include "flutter/shell/platform/windows/windowing_handler.h" #include "flutter/shell/platform/windows/windows_lifecycle_manager.h" #include "flutter/shell/platform/windows/windows_proc_table.h" #include "third_party/rapidjson/include/rapidjson/document.h" @@ -425,9 +427,16 @@ class FlutterWindowsEngine { // Handler for the flutter/platform channel. std::unique_ptr platform_handler_; + // Handler for the flutter/windowing channel. + std::unique_ptr windowing_handler_; + // Handlers for keyboard events from Windows. std::unique_ptr keyboard_key_handler_; + // The controller that manages the lifecycle of |FlutterHostWindow|s, native + // Win32 windows hosting a Flutter view in their client area. + std::unique_ptr host_window_controller_; + // Handlers for text events from Windows. std::unique_ptr text_input_plugin_; @@ -456,6 +465,8 @@ class FlutterWindowsEngine { bool enable_impeller_ = false; + bool enable_multi_window_ = false; + // The manager for WindowProc delegate registration and callbacks. std::unique_ptr window_proc_delegate_manager_; diff --git a/shell/platform/windows/windowing_handler.cc b/shell/platform/windows/windowing_handler.cc new file mode 100644 index 0000000000000..1682c54d29f21 --- /dev/null +++ b/shell/platform/windows/windowing_handler.cc @@ -0,0 +1,224 @@ +// Copyright 2013 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. + +#include "flutter/shell/platform/windows/windowing_handler.h" + +#include "flutter/fml/logging.h" +#include "flutter/shell/platform/common/client_wrapper/include/flutter/standard_method_codec.h" + +namespace { + +// Name of the windowing channel. +constexpr char kChannelName[] = "flutter/windowing"; + +// Methods for creating different types of windows. +constexpr char kCreateWindowMethod[] = "createWindow"; + +// The method to destroy a window. +constexpr char kDestroyWindowMethod[] = "destroyWindow"; + +// Keys used in method calls. +constexpr char kAnchorRectKey[] = "anchorRect"; +constexpr char kArchetypeKey[] = "archetype"; +constexpr char kParentKey[] = "parent"; +constexpr char kParentViewIdKey[] = "parentViewId"; +constexpr char kPositionerChildAnchorKey[] = "positionerChildAnchor"; +constexpr char kPositionerConstraintAdjustmentKey[] = + "positionerConstraintAdjustment"; +constexpr char kPositionerOffsetKey[] = "positionerOffset"; +constexpr char kPositionerParentAnchorKey[] = "positionerParentAnchor"; +constexpr char kSizeKey[] = "size"; +constexpr char kViewIdKey[] = "viewId"; + +// Error codes used for responses. +constexpr char kInvalidValueError[] = "Invalid Value"; +constexpr char kUnavailableError[] = "Unavailable"; + +// Retrieves the value associated with |key| from |map|, ensuring it matches +// the expected type |T|. Returns the value if found and correctly typed, +// otherwise logs an error in |result| and returns std::nullopt. +template +std::optional GetSingleValueForKeyOrSendError( + std::string const& key, + flutter::EncodableMap const* map, + flutter::MethodResult<>& result) { + if (auto const it = map->find(flutter::EncodableValue(key)); + it != map->end()) { + if (auto const* const value = std::get_if(&it->second)) { + return *value; + } else { + result.Error(kInvalidValueError, "Value for '" + key + + "' key must be of type '" + + typeid(T).name() + "'."); + } + } else { + result.Error(kInvalidValueError, + "Map does not contain required '" + key + "' key."); + } + return std::nullopt; +} + +// Retrieves a list of values associated with |key| from |map|, ensuring the +// list has |Size| elements, all of type |T|. Returns the list if found and +// valid, otherwise logs an error in |result| and returns std::nullopt. +template +std::optional> GetListValuesForKeyOrSendError( + std::string const& key, + flutter::EncodableMap const* map, + flutter::MethodResult<>& result) { + if (auto const it = map->find(flutter::EncodableValue(key)); + it != map->end()) { + if (auto const* const array = + std::get_if>(&it->second)) { + if (array->size() != Size) { + result.Error(kInvalidValueError, "Array for '" + key + + "' key must have " + + std::to_string(Size) + " values."); + return std::nullopt; + } + std::vector decoded_values; + for (flutter::EncodableValue const& value : *array) { + if (std::holds_alternative(value)) { + decoded_values.push_back(std::get(value)); + } else { + result.Error(kInvalidValueError, + "Array for '" + key + + "' key must only have values of type '" + + typeid(T).name() + "'."); + return std::nullopt; + } + } + return decoded_values; + } else { + result.Error(kInvalidValueError, + "Value for '" + key + "' key must be an array."); + } + } else { + result.Error(kInvalidValueError, + "Map does not contain required '" + key + "' key."); + } + return std::nullopt; +} + +// Converts a |flutter::WindowArchetype| to its corresponding wide string +// representation. +std::wstring ArchetypeToWideString(flutter::WindowArchetype archetype) { + switch (archetype) { + case flutter::WindowArchetype::regular: + return L"regular"; + } + FML_UNREACHABLE(); +} + +} // namespace + +namespace flutter { + +WindowingHandler::WindowingHandler(BinaryMessenger* messenger, + FlutterHostWindowController* controller) + : channel_(std::make_shared>( + messenger, + kChannelName, + &StandardMethodCodec::GetInstance())), + controller_(controller) { + channel_->SetMethodCallHandler( + [this](const MethodCall& call, + std::unique_ptr> result) { + HandleMethodCall(call, std::move(result)); + }); + controller_->SetMethodChannel(channel_); +} + +void WindowingHandler::HandleMethodCall( + const MethodCall& method_call, + std::unique_ptr> result) { + const std::string& method = method_call.method_name(); + + if (method == kCreateWindowMethod) { + HandleCreateWindow(WindowArchetype::regular, method_call, *result); + } else if (method == kDestroyWindowMethod) { + HandleDestroyWindow(method_call, *result); + } else { + result->NotImplemented(); + } +} + +void WindowingHandler::HandleCreateWindow(WindowArchetype archetype, + MethodCall<> const& call, + MethodResult<>& result) { + auto const* const arguments = call.arguments(); + auto const* const map = std::get_if(arguments); + if (!map) { + result.Error(kInvalidValueError, "Method call argument is not a map."); + return; + } + + std::wstring const title = ArchetypeToWideString(archetype); + + auto const size_list = + GetListValuesForKeyOrSendError(kSizeKey, map, result); + if (!size_list) { + return; + } + if (size_list->at(0) < 0 || size_list->at(1) < 0) { + result.Error(kInvalidValueError, + "Values for '" + std::string(kSizeKey) + "' key (" + + std::to_string(size_list->at(0)) + ", " + + std::to_string(size_list->at(1)) + + ") must be nonnegative."); + return; + } + + if (std::optional const data_opt = + controller_->CreateHostWindow( + title, {.width = size_list->at(0), .height = size_list->at(1)}, + archetype)) { + WindowMetadata const& data = data_opt.value(); + result.Success(EncodableValue(EncodableMap{ + {EncodableValue(kViewIdKey), EncodableValue(data.view_id)}, + {EncodableValue(kArchetypeKey), + EncodableValue(static_cast(data.archetype))}, + {EncodableValue(kSizeKey), + EncodableValue(EncodableList{EncodableValue(data.size.width), + EncodableValue(data.size.height)})}, + {EncodableValue(kParentViewIdKey), + data.parent_id ? EncodableValue(data.parent_id.value()) + : EncodableValue()}})); + } else { + result.Error(kUnavailableError, "Can't create window."); + } +} + +void WindowingHandler::HandleDestroyWindow(MethodCall<> const& call, + MethodResult<>& result) { + auto const* const arguments = call.arguments(); + auto const* const map = std::get_if(arguments); + if (!map) { + result.Error(kInvalidValueError, "Method call argument is not a map."); + return; + } + + auto const view_id = + GetSingleValueForKeyOrSendError(kViewIdKey, map, result); + if (!view_id) { + return; + } + if (view_id.value() < 0) { + result.Error(kInvalidValueError, + "Value for '" + std::string(kViewIdKey) + "' (" + + std::to_string(view_id.value()) + ") cannot be negative."); + return; + } + + if (!controller_->DestroyHostWindow(view_id.value())) { + result.Error(kInvalidValueError, + "Can't find window with '" + std::string(kViewIdKey) + "' (" + + std::to_string(view_id.value()) + ")."); + return; + } + + result.Success(); +} + +} // namespace flutter diff --git a/shell/platform/windows/windowing_handler.h b/shell/platform/windows/windowing_handler.h new file mode 100644 index 0000000000000..c527826eda50b --- /dev/null +++ b/shell/platform/windows/windowing_handler.h @@ -0,0 +1,46 @@ +// Copyright 2013 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. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOWING_HANDLER_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOWING_HANDLER_H_ + +#include "flutter/fml/macros.h" +#include "flutter/shell/platform/common/client_wrapper/include/flutter/method_channel.h" +#include "flutter/shell/platform/windows/flutter_host_window_controller.h" + +namespace flutter { + +// Handler for the windowing channel. +class WindowingHandler { + public: + explicit WindowingHandler(flutter::BinaryMessenger* messenger, + flutter::FlutterHostWindowController* controller); + + private: + // Handler for method calls received on |channel_|. Messages are + // redirected to either HandleCreateWindow or HandleDestroyWindow. + void HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result); + + // Handles the creation of windows. + void HandleCreateWindow(flutter::WindowArchetype archetype, + flutter::MethodCall<> const& call, + flutter::MethodResult<>& result); + // Handles the destruction of windows. + void HandleDestroyWindow(flutter::MethodCall<> const& call, + flutter::MethodResult<>& result); + + // The MethodChannel used for communication with the Flutter engine. + std::shared_ptr> channel_; + + // The controller of the host windows. + flutter::FlutterHostWindowController* controller_; + + FML_DISALLOW_COPY_AND_ASSIGN(WindowingHandler); +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOWING_HANDLER_H_ diff --git a/shell/platform/windows/windowing_handler_unittests.cc b/shell/platform/windows/windowing_handler_unittests.cc new file mode 100644 index 0000000000000..06b2d1ee015c4 --- /dev/null +++ b/shell/platform/windows/windowing_handler_unittests.cc @@ -0,0 +1,147 @@ +// Copyright 2013 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. +#include "flutter/shell/platform/windows/windowing_handler.h" + +#include "flutter/fml/macros.h" +#include "flutter/shell/platform/common/client_wrapper/include/flutter/method_result_functions.h" +#include "flutter/shell/platform/common/client_wrapper/include/flutter/standard_method_codec.h" +#include "flutter/shell/platform/windows/flutter_host_window_controller.h" +#include "flutter/shell/platform/windows/testing/flutter_windows_engine_builder.h" +#include "flutter/shell/platform/windows/testing/test_binary_messenger.h" +#include "flutter/shell/platform/windows/testing/windows_test.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { + +namespace { +using ::testing::_; +using ::testing::Eq; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::StrEq; + +static constexpr char kChannelName[] = "flutter/windowing"; + +static constexpr char kCreateWindowMethod[] = "createWindow"; +static constexpr char kDestroyWindowMethod[] = "destroyWindow"; + +void SimulateWindowingMessage(TestBinaryMessenger* messenger, + const std::string& method_name, + std::unique_ptr arguments, + MethodResult* result_handler) { + MethodCall<> call(method_name, std::move(arguments)); + + auto message = StandardMethodCodec::GetInstance().EncodeMethodCall(call); + + EXPECT_TRUE(messenger->SimulateEngineMessage( + kChannelName, message->data(), message->size(), + [&result_handler](const uint8_t* reply, size_t reply_size) { + StandardMethodCodec::GetInstance().DecodeAndProcessResponseEnvelope( + reply, reply_size, result_handler); + })); +} + +class MockFlutterHostWindowController : public FlutterHostWindowController { + public: + MockFlutterHostWindowController(FlutterWindowsEngine* engine) + : FlutterHostWindowController(engine) {} + ~MockFlutterHostWindowController() = default; + + MOCK_METHOD(std::optional, + CreateHostWindow, + (std::wstring const& title, + WindowSize const& size, + WindowArchetype archetype), + (override)); + MOCK_METHOD(bool, DestroyHostWindow, (FlutterViewId view_id), (override)); + + private: + FML_DISALLOW_COPY_AND_ASSIGN(MockFlutterHostWindowController); +}; + +} // namespace + +class WindowingHandlerTest : public WindowsTest { + public: + WindowingHandlerTest() = default; + virtual ~WindowingHandlerTest() = default; + + protected: + void SetUp() override { + FlutterWindowsEngineBuilder builder(GetContext()); + engine_ = builder.Build(); + + mock_controller_ = + std::make_unique>( + engine_.get()); + + ON_CALL(*mock_controller_, CreateHostWindow) + .WillByDefault(Return(WindowMetadata{})); + ON_CALL(*mock_controller_, DestroyHostWindow).WillByDefault(Return(true)); + } + + MockFlutterHostWindowController* controller() { + return mock_controller_.get(); + } + + private: + std::unique_ptr engine_; + std::unique_ptr> mock_controller_; + + FML_DISALLOW_COPY_AND_ASSIGN(WindowingHandlerTest); +}; + +TEST_F(WindowingHandlerTest, HandleCreateRegularWindow) { + TestBinaryMessenger messenger; + WindowingHandler windowing_handler(&messenger, controller()); + + WindowSize const size = {800, 600}; + EncodableMap const arguments = { + {EncodableValue("size"), + EncodableValue(EncodableList{EncodableValue(size.width), + EncodableValue(size.height)})}, + }; + + bool success = false; + MethodResultFunctions<> result_handler( + [&success](const EncodableValue* result) { success = true; }, nullptr, + nullptr); + + EXPECT_CALL(*controller(), CreateHostWindow(StrEq(L"regular"), size, + WindowArchetype::regular)) + .Times(1); + + SimulateWindowingMessage(&messenger, kCreateWindowMethod, + std::make_unique(arguments), + &result_handler); + + EXPECT_TRUE(success); +} + +TEST_F(WindowingHandlerTest, HandleDestroyWindow) { + TestBinaryMessenger messenger; + WindowingHandler windowing_handler(&messenger, controller()); + + EncodableMap const arguments = { + {EncodableValue("viewId"), EncodableValue(1)}, + }; + + bool success = false; + MethodResultFunctions<> result_handler( + [&success](const EncodableValue* result) { success = true; }, nullptr, + nullptr); + + EXPECT_CALL(*controller(), DestroyHostWindow(1)).Times(1); + + SimulateWindowingMessage(&messenger, kDestroyWindowMethod, + std::make_unique(arguments), + &result_handler); + + EXPECT_TRUE(success); +} + +} // namespace testing +} // namespace flutter