From bc482de44e7b37613b15f1f5cb66b3d10239a78a Mon Sep 17 00:00:00 2001 From: Harlen Batagelo Date: Tue, 22 Oct 2024 13:32:14 -0300 Subject: [PATCH 1/7] Add support for multiple top-level windows on Windows --- ci/licenses_golden/licenses_flutter | 1 + .../client_wrapper/core_wrapper_files.gni | 1 + .../include/flutter/windowing.h | 148 ++++++ .../platform/windows/client_wrapper/BUILD.gn | 14 + .../windows/client_wrapper/flutter_engine.cc | 5 +- .../client_wrapper/flutter_view_controller.cc | 20 +- .../flutter_view_controller_unittests.cc | 14 +- .../client_wrapper/flutter_win32_window.cc | 77 +++ .../flutter_window_controller.cc | 407 +++++++++++++++ .../flutter_window_controller_unittests.cc | 294 +++++++++++ .../include/flutter/flutter_engine.h | 10 +- .../include/flutter/flutter_view_controller.h | 13 + .../include/flutter/flutter_win32_window.h | 40 ++ .../flutter/flutter_window_controller.h | 69 +++ .../include/flutter/win32_window.h | 106 ++++ .../include/flutter/win32_wrapper.h | 34 ++ .../testing/stub_flutter_windows_api.cc | 23 + .../testing/stub_flutter_windows_api.h | 12 + .../windows/client_wrapper/win32_window.cc | 462 ++++++++++++++++++ .../windows/flutter_windows_internal.h | 25 - .../platform/windows/public/flutter_windows.h | 25 + 21 files changed, 1753 insertions(+), 47 deletions(-) create mode 100644 shell/platform/common/client_wrapper/include/flutter/windowing.h create mode 100644 shell/platform/windows/client_wrapper/flutter_win32_window.cc create mode 100644 shell/platform/windows/client_wrapper/flutter_window_controller.cc create mode 100644 shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc create mode 100644 shell/platform/windows/client_wrapper/include/flutter/flutter_win32_window.h create mode 100644 shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h create mode 100644 shell/platform/windows/client_wrapper/include/flutter/win32_window.h create mode 100644 shell/platform/windows/client_wrapper/include/flutter/win32_wrapper.h create mode 100644 shell/platform/windows/client_wrapper/win32_window.cc diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 362641dc9f466..35835f6bc35ff 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -47306,6 +47306,7 @@ FILE: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/stan FILE: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/standard_message_codec.h FILE: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/standard_method_codec.h FILE: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/texture_registrar.h +FILE: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/windowing.h FILE: ../../../flutter/shell/platform/common/client_wrapper/plugin_registrar.cc FILE: ../../../flutter/shell/platform/common/client_wrapper/standard_codec.cc FILE: ../../../flutter/shell/platform/common/client_wrapper/texture_registrar_impl.h diff --git a/shell/platform/common/client_wrapper/core_wrapper_files.gni b/shell/platform/common/client_wrapper/core_wrapper_files.gni index c2ee524e0f117..013c758c7f83c 100644 --- a/shell/platform/common/client_wrapper/core_wrapper_files.gni +++ b/shell/platform/common/client_wrapper/core_wrapper_files.gni @@ -25,6 +25,7 @@ core_cpp_client_wrapper_includes = "include/flutter/standard_message_codec.h", "include/flutter/standard_method_codec.h", "include/flutter/texture_registrar.h", + "include/flutter/windowing.h", ], "abspath") diff --git a/shell/platform/common/client_wrapper/include/flutter/windowing.h b/shell/platform/common/client_wrapper/include/flutter/windowing.h new file mode 100644 index 0000000000000..72e3dd5cfaca7 --- /dev/null +++ b/shell/platform/common/client_wrapper/include/flutter/windowing.h @@ -0,0 +1,148 @@ +// 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_CLIENT_WRAPPER_INCLUDE_FLUTTER_WINDOWING_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_CLIENT_WRAPPER_INCLUDE_FLUTTER_WINDOWING_H_ + +#include + +namespace flutter { + +// The unique identifier for a view. +using FlutterViewId = int64_t; + +// A point (x, y) in 2D space for window positioning. +struct WindowPoint { + int x{0}; + int y{0}; + + friend auto operator+(WindowPoint const& lhs, + WindowPoint const& rhs) -> WindowPoint { + return {lhs.x + rhs.x, lhs.y + rhs.y}; + } + + friend auto operator-(WindowPoint const& lhs, + WindowPoint const& rhs) -> WindowPoint { + 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 size (width, height) in 2D space. +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 + auto contains(WindowRectangle const& rect) const -> bool { + 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; + } +}; + +// Defines how a child window should be positioned relative to its parent. +struct WindowPositioner { + // Allowed anchor positions. + enum class Anchor { + center, // Center. + top, // Top, centered horizontally. + bottom, // Bottom, centered horizontally. + left, // Left, centered vertically. + right, // Right, centered vertically. + top_left, // Top-left corner. + bottom_left, // Bottom-left corner. + top_right, // Top-right corner. + bottom_right, // Bottom-right corner. + }; + + // Specifies how a window should be adjusted if it doesn't fit the placement + // bounds. In order of precedence: + // 1. 'flip_{x|y|any}': reverse the anchor points and offset along an axis. + // 2. 'slide_{x|y|any}': adjust the offset along an axis. + // 3. 'resize_{x|y|any}': adjust the window size along an axis. + enum class ConstraintAdjustment { + none = 0, // No adjustment. + slide_x = 1 << 0, // Slide horizontally to fit. + slide_y = 1 << 1, // Slide vertically to fit. + flip_x = 1 << 2, // Flip horizontally to fit. + flip_y = 1 << 3, // Flip vertically to fit. + resize_x = 1 << 4, // Resize horizontally to fit. + resize_y = 1 << 5, // Resize vertically to fit. + flip_any = flip_x | flip_y, // Flip in any direction to fit. + slide_any = slide_x | slide_y, // Slide in any direction to fit. + resize_any = resize_x | resize_y, // Resize in any direction to fit. + }; + + // The reference anchor rectangle relative to the client rectangle of the + // parent window. If nullopt, the anchor rectangle is assumed to be the window + // rectangle. + std::optional anchor_rect; + // Specifies which anchor of the parent window to align to. + Anchor parent_anchor{Anchor::center}; + // Specifies which anchor of the child window to align with the parent. + Anchor child_anchor{Anchor::center}; + // Offset relative to the position of the anchor on the anchor rectangle and + // the anchor on the child. + WindowPoint offset; + // The adjustments to apply if the window doesn't fit the available space. + // The order of precedence is: 1) Flip, 2) Slide, 3) Resize. + ConstraintAdjustment constraint_adjustment{ConstraintAdjustment::none}; +}; + +// Types of windows. +enum class WindowArchetype { + // Regular top-level window. + regular, + // A window that is on a layer above regular windows and is not dockable. + floating_regular, + // Dialog window. + dialog, + // Satellite window attached to a regular, floating_regular or dialog window. + satellite, + // Popup. + popup, + // Tooltip. + tip, +}; + +// 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 (e.g., regular, dialog, popup, etc). + 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_CLIENT_WRAPPER_INCLUDE_FLUTTER_WINDOWING_H_ diff --git a/shell/platform/windows/client_wrapper/BUILD.gn b/shell/platform/windows/client_wrapper/BUILD.gn index 1d40c3c1314fb..aaf2abf2ef97d 100644 --- a/shell/platform/windows/client_wrapper/BUILD.gn +++ b/shell/platform/windows/client_wrapper/BUILD.gn @@ -11,12 +11,19 @@ _wrapper_includes = [ "include/flutter/flutter_engine.h", "include/flutter/flutter_view_controller.h", "include/flutter/flutter_view.h", + "include/flutter/flutter_win32_window.h", + "include/flutter/flutter_window_controller.h", "include/flutter/plugin_registrar_windows.h", + "include/flutter/win32_window.h", + "include/flutter/win32_wrapper.h", ] _wrapper_sources = [ "flutter_engine.cc", "flutter_view_controller.cc", + "flutter_win32_window.cc", + "flutter_window_controller.cc", + "win32_window.cc", ] # This code will be merged into .../common/client_wrapper for client use, @@ -40,6 +47,8 @@ source_set("client_wrapper_windows") { "//flutter/shell/platform/windows:flutter_windows_headers", ] + libs = [ "dwmapi.lib" ] + configs += [ "//flutter/shell/platform/common:desktop_library_implementation" ] @@ -79,6 +88,7 @@ executable("client_wrapper_windows_unittests") { "flutter_engine_unittests.cc", "flutter_view_controller_unittests.cc", "flutter_view_unittests.cc", + "flutter_window_controller_unittests.cc", "plugin_registrar_windows_unittests.cc", ] @@ -89,6 +99,7 @@ executable("client_wrapper_windows_unittests") { ":client_wrapper_library_stubs_windows", ":client_wrapper_windows", ":client_wrapper_windows_fixtures", + "//flutter/shell/platform/common/client_wrapper", "//flutter/shell/platform/common/client_wrapper:client_wrapper_library_stubs", "//flutter/testing", @@ -110,6 +121,9 @@ client_wrapper_file_archive_list = [ win_client_wrapper_file_archive_list = [ "flutter_engine.cc", "flutter_view_controller.cc", + "flutter_win32_window.cc", + "flutter_window_controller.cc", + "win32_window.cc", ] zip_bundle("client_wrapper_archive") { diff --git a/shell/platform/windows/client_wrapper/flutter_engine.cc b/shell/platform/windows/client_wrapper/flutter_engine.cc index 7860947aa068b..3cbe59622fc4f 100644 --- a/shell/platform/windows/client_wrapper/flutter_engine.cc +++ b/shell/platform/windows/client_wrapper/flutter_engine.cc @@ -63,7 +63,7 @@ bool FlutterEngine::Run(const char* entry_point) { } void FlutterEngine::ShutDown() { - if (engine_ && owns_engine_) { + if (engine_) { FlutterDesktopEngineDestroy(engine_); } engine_ = nullptr; @@ -113,8 +113,7 @@ std::optional FlutterEngine::ProcessExternalWindowMessage( return std::nullopt; } -FlutterDesktopEngineRef FlutterEngine::RelinquishEngine() { - owns_engine_ = false; +FlutterDesktopEngineRef FlutterEngine::engine() const { return engine_; } diff --git a/shell/platform/windows/client_wrapper/flutter_view_controller.cc b/shell/platform/windows/client_wrapper/flutter_view_controller.cc index 98c65e10c27bc..1dbc5a0dfa96f 100644 --- a/shell/platform/windows/client_wrapper/flutter_view_controller.cc +++ b/shell/platform/windows/client_wrapper/flutter_view_controller.cc @@ -11,10 +11,22 @@ namespace flutter { FlutterViewController::FlutterViewController(int width, int height, - const DartProject& project) { - engine_ = std::make_shared(project); - controller_ = FlutterDesktopViewControllerCreate(width, height, - engine_->RelinquishEngine()); + const DartProject& project) + : FlutterViewController(width, + height, + std::make_shared(project)) {} + +FlutterViewController::FlutterViewController( + int width, + int height, + std::shared_ptr engine) { + FlutterDesktopViewControllerProperties properties = {}; + properties.width = width; + properties.height = height; + + engine_ = std::move(engine); + controller_ = + FlutterDesktopEngineCreateViewController(engine_->engine(), &properties); if (!controller_) { std::cerr << "Failed to create view controller." << std::endl; return; diff --git a/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc b/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc index 837c2e13e583d..f79842f280978 100644 --- a/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc +++ b/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc @@ -17,10 +17,8 @@ namespace { class TestWindowsApi : public testing::StubFlutterWindowsApi { public: // |flutter::testing::StubFlutterWindowsApi| - FlutterDesktopViewControllerRef ViewControllerCreate( - int width, - int height, - FlutterDesktopEngineRef engine) override { + FlutterDesktopViewControllerRef EngineCreateViewController( + const FlutterDesktopViewControllerProperties* properties) override { return reinterpret_cast(2); } @@ -63,11 +61,13 @@ TEST(FlutterViewControllerTest, CreateDestroy) { testing::ScopedStubFlutterWindowsApi scoped_api_stub( std::make_unique()); auto test_api = static_cast(scoped_api_stub.stub()); + + // Create and destroy a view controller. + // This should also create and destroy an engine. { FlutterViewController controller(100, 100, project); } + EXPECT_TRUE(test_api->view_controller_destroyed()); - // Per the C API, once a view controller has taken ownership of an engine - // the engine destruction method should not be called. - EXPECT_FALSE(test_api->engine_destroyed()); + EXPECT_TRUE(test_api->engine_destroyed()); } TEST(FlutterViewControllerTest, GetViewId) { diff --git a/shell/platform/windows/client_wrapper/flutter_win32_window.cc b/shell/platform/windows/client_wrapper/flutter_win32_window.cc new file mode 100644 index 0000000000000..227508b8c571a --- /dev/null +++ b/shell/platform/windows/client_wrapper/flutter_win32_window.cc @@ -0,0 +1,77 @@ +#include "include/flutter/flutter_win32_window.h" + +#include + +namespace flutter { + +FlutterWin32Window::FlutterWin32Window(std::shared_ptr engine) + : engine_{std::move(engine)} {} + +FlutterWin32Window::FlutterWin32Window(std::shared_ptr engine, + std::shared_ptr wrapper) + : engine_{std::move(engine)}, Win32Window{std::move(wrapper)} {} + +auto FlutterWin32Window::GetFlutterViewId() const -> FlutterViewId { + return view_controller_->view_id(); +}; + +auto FlutterWin32Window::OnCreate() -> bool { + if (!Win32Window::OnCreate()) { + return false; + } + + auto const client_rect{GetClientArea()}; + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + view_controller_ = std::make_unique( + client_rect.right - client_rect.left, + client_rect.bottom - client_rect.top, engine_); + // Ensure that basic setup of the controller was successful. + if (!view_controller_->view()) { + return false; + } + + SetChildContent(view_controller_->view()->GetNativeWindow()); + + // 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. + return true; +} + +void FlutterWin32Window::OnDestroy() { + if (view_controller_) { + view_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +auto FlutterWin32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) -> LRESULT { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (view_controller_) { + auto const result{view_controller_->HandleTopLevelWindowProc( + hwnd, message, wparam, lparam)}; + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + engine_->ReloadSystemFonts(); + break; + default: + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} + +} // namespace flutter diff --git a/shell/platform/windows/client_wrapper/flutter_window_controller.cc b/shell/platform/windows/client_wrapper/flutter_window_controller.cc new file mode 100644 index 0000000000000..4585f38c27fcf --- /dev/null +++ b/shell/platform/windows/client_wrapper/flutter_window_controller.cc @@ -0,0 +1,407 @@ +#include "include/flutter/flutter_window_controller.h" + +#include "include/flutter/encodable_value.h" +#include "include/flutter/flutter_win32_window.h" +#include "include/flutter/standard_method_codec.h" + +#include +#include + +#include + +namespace { + +auto const* const kChannel{"flutter/windowing"}; +auto const* const kErrorCodeInvalidValue{"INVALID_VALUE"}; +auto const* const kErrorCodeUnavailable{"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 +auto GetSingleValueForKeyOrSendError(std::string const& key, + flutter::EncodableMap const* map, + flutter::MethodResult<>& result) + -> std::optional { + 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(kErrorCodeInvalidValue, "Value for '" + key + + "' key must be of type '" + + typeid(T).name() + "'."); + } + } else { + result.Error(kErrorCodeInvalidValue, + "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 +auto GetListValuesForKeyOrSendError(std::string const& key, + flutter::EncodableMap const* map, + flutter::MethodResult<>& result) + -> std::optional> { + 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(kErrorCodeInvalidValue, + "Array for '" + key + "' key must have " + + std::to_string(Size) + " values."); + return std::nullopt; + } + std::vector decoded_values; + for (auto const& value : *array) { + if (std::holds_alternative(value)) { + decoded_values.push_back(std::get(value)); + } else { + result.Error(kErrorCodeInvalidValue, + "Array for '" + key + + "' key must only have values of type '" + + typeid(T).name() + "'."); + return std::nullopt; + } + } + return decoded_values; + } else { + result.Error(kErrorCodeInvalidValue, + "Value for '" + key + "' key must be an array."); + } + } else { + result.Error(kErrorCodeInvalidValue, + "Map does not contain required '" + key + "' key."); + } + return std::nullopt; +} + +// Converts a |flutter::WindowArchetype| to its corresponding wide string +// representation. +auto ArchetypeToWideString(flutter::WindowArchetype archetype) -> std::wstring { + switch (archetype) { + case flutter::WindowArchetype::regular: + return L"regular"; + case flutter::WindowArchetype::floating_regular: + return L"floating_regular"; + case flutter::WindowArchetype::dialog: + return L"dialog"; + case flutter::WindowArchetype::satellite: + return L"satellite"; + case flutter::WindowArchetype::popup: + return L"popup"; + case flutter::WindowArchetype::tip: + return L"tip"; + } + std::cerr + << "Unhandled window archetype encountered in archetypeToWideString: " + << static_cast(archetype) << "\n"; + std::abort(); +} + +} // namespace + +namespace flutter { + +FlutterWindowController::~FlutterWindowController() { + { + std::lock_guard lock(mutex_); + if (channel_) { + channel_->SetMethodCallHandler(nullptr); + } + } + DestroyWindows(); +} + +void FlutterWindowController::DestroyWindows() { + std::unique_lock lock(mutex_); + std::vector view_ids; + view_ids.reserve(windows_.size()); + for (auto const& [view_id, _] : windows_) { + view_ids.push_back(view_id); + } + lock.unlock(); + for (auto const& view_id : view_ids) { + DestroyFlutterWindow(view_id); + } +} + +void FlutterWindowController::SetEngine(std::shared_ptr engine) { + DestroyWindows(); + std::lock_guard const lock(mutex_); + engine_ = std::move(engine); + channel_ = std::make_unique>( + engine_->messenger(), kChannel, &StandardMethodCodec::GetInstance()); + channel_->SetMethodCallHandler( + [this](MethodCall<> const& call, std::unique_ptr> result) { + MethodCallHandler(call, *result); + }); +} + +auto FlutterWindowController::CreateFlutterWindow(std::wstring const& title, + WindowSize const& size, + WindowArchetype archetype) + -> std::optional { + std::unique_lock lock(mutex_); + if (!engine_) { + std::cerr << "Cannot create window without an engine.\n"; + return std::nullopt; + } + + auto window{std::make_unique(engine_, win32_)}; + + lock.unlock(); + + if (!window->Create(title, size, archetype)) { + return std::nullopt; + } + + lock.lock(); + + // Assume first window is the main window + if (windows_.empty()) { + window->SetQuitOnClose(true); + } + + auto const view_id{window->GetFlutterViewId()}; + 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; +} + +auto FlutterWindowController::DestroyFlutterWindow(FlutterViewId view_id) + -> bool { + std::unique_lock lock(mutex_); + auto it{windows_.find(view_id)}; + if (it != windows_.end()) { + auto* const window{it->second.get()}; + + lock.unlock(); + + // |window| will be removed from |windows_| when WM_NCDESTROY is handled + win32_->DestroyWindow(window->GetHandle()); + + return true; + } + return false; +} + +FlutterWindowController::FlutterWindowController() + : win32_{std::make_shared()} {} + +FlutterWindowController::FlutterWindowController( + std::shared_ptr wrapper) + : win32_{std::move(wrapper)} {} + +void FlutterWindowController::MethodCallHandler(MethodCall<> const& call, + MethodResult<>& result) { + if (call.method_name() == "createWindow") { + HandleCreateWindow(WindowArchetype::regular, call, result); + } else if (call.method_name() == "destroyWindow") { + HandleDestroyWindow(call, result); + } else { + result.NotImplemented(); + } +} + +auto FlutterWindowController::MessageHandler(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) -> LRESULT { + switch (message) { + case WM_NCDESTROY: { + std::unique_lock lock{mutex_}; + auto const it{std::find_if(windows_.begin(), windows_.end(), + [hwnd](auto const& window) { + return window.second->GetHandle() == hwnd; + })}; + if (it != windows_.end()) { + auto const view_id{it->first}; + auto const quit_on_close{it->second.get()->GetQuitOnClose()}; + + windows_.erase(it); + + if (quit_on_close) { + auto it2{windows_.begin()}; + while (it2 != windows_.end()) { + auto const& that{it2->second}; + lock.unlock(); + DestroyWindow(that->GetHandle()); + lock.lock(); + it2 = windows_.begin(); + } + } + + SendOnWindowDestroyed(view_id); + } + } + return 0; + case WM_SIZE: { + std::lock_guard lock{mutex_}; + auto const it{std::find_if(windows_.begin(), windows_.end(), + [hwnd](auto const& window) { + return window.second->GetHandle() == hwnd; + })}; + if (it != windows_.end()) { + auto const view_id{it->first}; + SendOnWindowChanged(view_id); + } + } break; + default: + break; + } + + if (auto* const window{Win32Window::GetThisFromHandle(hwnd)}) { + return window->MessageHandler(hwnd, message, wparam, lparam); + } + return DefWindowProc(hwnd, message, wparam, lparam); +} + +void FlutterWindowController::SendOnWindowCreated( + FlutterViewId view_id, + std::optional parent_view_id) const { + if (channel_) { + channel_->InvokeMethod( + "onWindowCreated", + std::make_unique(EncodableMap{ + {EncodableValue("viewId"), EncodableValue(view_id)}, + {EncodableValue("parentViewId"), + parent_view_id ? EncodableValue(parent_view_id.value()) + : EncodableValue()}})); + } +} + +void FlutterWindowController::SendOnWindowDestroyed( + FlutterViewId view_id) const { + if (channel_) { + channel_->InvokeMethod( + "onWindowDestroyed", + std::make_unique(EncodableMap{ + {EncodableValue("viewId"), EncodableValue(view_id)}, + })); + } +} + +void FlutterWindowController::SendOnWindowChanged(FlutterViewId view_id) const { + if (channel_) { + auto const size{GetWindowSize(view_id)}; + channel_->InvokeMethod( + "onWindowChanged", + std::make_unique(EncodableMap{ + {EncodableValue("viewId"), EncodableValue(view_id)}, + {EncodableValue("size"), + EncodableValue(EncodableList{EncodableValue(size.width), + EncodableValue(size.height)})}, + {EncodableValue("relativePosition"), EncodableValue()}, // TODO + {EncodableValue("isMoving"), EncodableValue()}})); // TODO + } +} + +void FlutterWindowController::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(kErrorCodeInvalidValue, "Method call argument is not a map."); + return; + } + + std::wstring const title{ArchetypeToWideString(archetype)}; + + auto const size_list{ + GetListValuesForKeyOrSendError("size", map, result)}; + if (!size_list) { + return; + } + if (size_list->at(0) < 0 || size_list->at(1) < 0) { + result.Error(kErrorCodeInvalidValue, + "Values for 'size' key (" + std::to_string(size_list->at(0)) + + ", " + std::to_string(size_list->at(1)) + + ") must be nonnegative."); + return; + } + + if (auto const data_opt{CreateFlutterWindow( + title, {.width = size_list->at(0), .height = size_list->at(1)}, + archetype)}) { + auto const& data{data_opt.value()}; + result.Success(EncodableValue(EncodableMap{ + {EncodableValue("viewId"), EncodableValue(data.view_id)}, + {EncodableValue("archetype"), + EncodableValue(static_cast(data.archetype))}, + {EncodableValue("size"), + EncodableValue(EncodableList{EncodableValue(data.size.width), + EncodableValue(data.size.height)})}, + {EncodableValue("parentViewId"), + data.parent_id ? EncodableValue(data.parent_id.value()) + : EncodableValue()}})); + } else { + result.Error(kErrorCodeUnavailable, "Can't create window."); + } +} + +void FlutterWindowController::HandleDestroyWindow(MethodCall<> const& call, + MethodResult<>& result) { + auto const* const arguments{call.arguments()}; + auto const* const map{std::get_if(arguments)}; + if (!map) { + result.Error(kErrorCodeInvalidValue, "Method call argument is not a map."); + return; + } + + auto const view_id{ + GetSingleValueForKeyOrSendError("viewId", map, result)}; + if (!view_id) { + return; + } + if (view_id.value() < 0) { + result.Error(kErrorCodeInvalidValue, "Value for 'viewId' (" + + std::to_string(view_id.value()) + + ") cannot be negative."); + return; + } + + if (!DestroyFlutterWindow(view_id.value())) { + result.Error(kErrorCodeInvalidValue, "Can't find window with 'viewId' (" + + std::to_string(view_id.value()) + + ")."); + return; + } + + result.Success(); +} + +WindowSize FlutterWindowController::GetWindowSize( + flutter::FlutterViewId view_id) const { + auto* const hwnd{windows_.at(view_id)->GetHandle()}; + 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)}; +} + +} // namespace flutter diff --git a/shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc b/shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc new file mode 100644 index 0000000000000..fbea55efb506c --- /dev/null +++ b/shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc @@ -0,0 +1,294 @@ +// 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/common/client_wrapper/include/flutter/encodable_value.h" +#include "flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_win32_window.h" +#include "flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h" +#include "flutter/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using ::testing::_; +using ::testing::AnyNumber; +using ::testing::Eq; +using ::testing::Gt; +using ::testing::IsNull; +using ::testing::Mock; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::StrEq; + +namespace flutter { + +namespace { + +HWND const k_hwnd{reinterpret_cast(-1)}; + +// Stub implementation to validate calls to the API. +class TestWindowsApi : public testing::StubFlutterWindowsApi { + public: + // |flutter::testing::StubFlutterWindowsApi| + FlutterDesktopViewControllerRef EngineCreateViewController( + const FlutterDesktopViewControllerProperties* properties) override { + return reinterpret_cast(2); + } +}; + +// Mocked classes +class MockWin32Wrapper : public Win32Wrapper { + public: + MOCK_METHOD(HWND, + CreateWindowEx, + (DWORD dwExStyle, + LPCWSTR lpClassName, + LPCWSTR lpWindowName, + DWORD dwStyle, + int X, + int Y, + int nWidth, + int nHeight, + HWND hWndParent, + HMENU hMenu, + HINSTANCE hInstance, + LPVOID lpParam), + (override)); + MOCK_METHOD(BOOL, DestroyWindow, (HWND hWnd), (override)); +}; + +class MockMethodResult : public MethodResult<> { + public: + MOCK_METHOD(void, + SuccessInternal, + (EncodableValue const* result), + (override)); + MOCK_METHOD(void, + ErrorInternal, + (std::string const& error_code, + std::string const& error_message, + EncodableValue const* error_details), + (override)); + MOCK_METHOD(void, NotImplementedInternal, (), (override)); +}; + +class MockFlutterWindowController : public FlutterWindowController { + public: + using FlutterWindowController::MessageHandler; + using FlutterWindowController::MethodCallHandler; + + MockFlutterWindowController(std::shared_ptr wrapper) + : FlutterWindowController(std::move(wrapper)) {} + + MOCK_METHOD(void, + SendOnWindowCreated, + (FlutterViewId view_id, + std::optional parent_view_id), + (override, const)); + MOCK_METHOD(void, + SendOnWindowDestroyed, + (FlutterViewId view_id), + (override, const)); + MOCK_METHOD(void, + SendOnWindowChanged, + (FlutterViewId view_id), + (override, const)); +}; + +// Test fixture +class FlutterWindowControllerTest : public ::testing::Test { + protected: + void SetUp() override { + DartProject project(L"test"); + engine_ = std::make_shared(project); + mock_win32_ = std::make_shared>(); + mock_controller_ = + std::make_unique>(mock_win32_); + mock_controller_->SetEngine(engine_); + + ON_CALL(*mock_win32_, CreateWindowEx).WillByDefault(Return(k_hwnd)); + ON_CALL(*mock_win32_, DestroyWindow).WillByDefault([&](HWND hwnd) { + mock_controller_->MessageHandler(hwnd, WM_NCDESTROY, 0, 0); + return TRUE; + }); + } + + std::shared_ptr engine_; + std::shared_ptr> mock_win32_; + std::unique_ptr> mock_controller_; +}; + +} // namespace + +TEST_F(FlutterWindowControllerTest, CreateRegularWindow) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api{static_cast(scoped_api_stub.stub())}; + + auto const title{L"window"}; + WindowSize const size{800, 600}; + auto const archetype{WindowArchetype::regular}; + + EXPECT_CALL(*mock_win32_, + CreateWindowEx(0, _, StrEq(title), WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, CW_USEDEFAULT, Gt(size.width), + Gt(size.height), IsNull(), _, _, _)) + .Times(1); + + auto const result{ + mock_controller_->CreateFlutterWindow(title, size, archetype)}; + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->view_id, 1); + EXPECT_FALSE(result->parent_id.has_value()); + EXPECT_EQ(result->archetype, archetype); +} + +TEST_F(FlutterWindowControllerTest, DestroyWindow) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api{static_cast(scoped_api_stub.stub())}; + + auto const title{L"window"}; + WindowSize const size{800, 600}; + auto const archetype{WindowArchetype::regular}; + + auto const create_result{ + mock_controller_->CreateFlutterWindow(title, size, archetype)}; + + ASSERT_TRUE(create_result.has_value()); + + EXPECT_CALL(*mock_win32_, DestroyWindow(k_hwnd)).Times(1); + + EXPECT_TRUE(mock_controller_->DestroyFlutterWindow(1)); +} + +TEST_F(FlutterWindowControllerTest, DestroyWindowWithInvalidView) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api{static_cast(scoped_api_stub.stub())}; + + auto const title{L"window"}; + WindowSize const size{800, 600}; + auto const archetype{WindowArchetype::regular}; + + auto const create_result{ + mock_controller_->CreateFlutterWindow(title, size, archetype)}; + + ASSERT_TRUE(create_result.has_value()); + + EXPECT_CALL(*mock_win32_, DestroyWindow(k_hwnd)).Times(0); + + EXPECT_FALSE(mock_controller_->DestroyFlutterWindow(9999)); + + Mock::VerifyAndClearExpectations(mock_win32_.get()); +} + +TEST_F(FlutterWindowControllerTest, SendOnWindowCreated) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api{static_cast(scoped_api_stub.stub())}; + + auto const title{L"window"}; + WindowSize const size{800, 600}; + auto const archetype{WindowArchetype::regular}; + + EXPECT_CALL(*mock_controller_, SendOnWindowCreated(1, Eq(std::nullopt))) + .Times(1); + + auto const create_result{ + mock_controller_->CreateFlutterWindow(title, size, archetype)}; +} + +TEST_F(FlutterWindowControllerTest, SendOnWindowDestroyed) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api{static_cast(scoped_api_stub.stub())}; + + auto const title{L"window"}; + WindowSize const size{800, 600}; + auto const archetype{WindowArchetype::regular}; + + auto const create_result{ + mock_controller_->CreateFlutterWindow(title, size, archetype)}; + + ASSERT_TRUE(create_result.has_value()); + + EXPECT_CALL(*mock_controller_, SendOnWindowDestroyed).Times(1); + + mock_controller_->DestroyFlutterWindow(1); +} + +TEST_F(FlutterWindowControllerTest, SendOnWindowChangedWhenWindowIsResized) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api{static_cast(scoped_api_stub.stub())}; + + auto const title{L"window"}; + WindowSize const size{800, 600}; + auto const archetype{WindowArchetype::regular}; + + auto const create_result{ + mock_controller_->CreateFlutterWindow(title, size, archetype)}; + + EXPECT_CALL(*mock_controller_, SendOnWindowChanged(1)).Times(1); + + mock_controller_->MessageHandler(k_hwnd, WM_SIZE, 0, 0); +} + +TEST_F(FlutterWindowControllerTest, CreateRegularWindowUsingMethodCall) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api{static_cast(scoped_api_stub.stub())}; + + WindowSize const size{800, 600}; + + EncodableMap const arguments{ + {EncodableValue("size"), + EncodableValue(EncodableList{EncodableValue(size.width), + EncodableValue(size.height)})}, + }; + MethodCall<> call("createWindow", + std::make_unique(arguments)); + + NiceMock mock_result; + + EXPECT_CALL(mock_result, SuccessInternal(_)).Times(1); + EXPECT_CALL(*mock_win32_, + CreateWindowEx(0, _, StrEq(L"regular"), WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, CW_USEDEFAULT, Gt(size.width), + Gt(size.height), IsNull(), _, _, _)) + .Times(1); + + mock_controller_->MethodCallHandler(call, mock_result); +} + +TEST_F(FlutterWindowControllerTest, DestroyWindowUsingMethodCall) { + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api{static_cast(scoped_api_stub.stub())}; + + auto const title{L"window"}; + WindowSize size{800, 600}; + auto const archetype{WindowArchetype::regular}; + auto create_result{ + mock_controller_->CreateFlutterWindow(title, size, archetype)}; + + ASSERT_TRUE(create_result.has_value()); + + EncodableMap const arguments{ + {EncodableValue("viewId"), + EncodableValue(static_cast(create_result->view_id))}, + }; + MethodCall<> call("destroyWindow", + std::make_unique(arguments)); + + NiceMock mock_result; + + EXPECT_CALL(mock_result, SuccessInternal(_)).Times(1); + EXPECT_CALL(*mock_win32_, DestroyWindow(k_hwnd)).Times(1); + + mock_controller_->MethodCallHandler(call, mock_result); +} + +} // namespace flutter diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h index 0369db35a14fc..89ca2d188b46f 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h +++ b/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h @@ -98,11 +98,8 @@ class FlutterEngine : public PluginRegistry { // For access to the engine handle. friend class FlutterViewController; - // Gives up ownership of |engine_|, but keeps a weak reference to it. - // - // This is intended to be used by FlutterViewController, since the underlying - // C API for view controllers takes over engine ownership. - FlutterDesktopEngineRef RelinquishEngine(); + // Get the handle for interacting with the C API's engine reference. + FlutterDesktopEngineRef engine() const; // Handle for interacting with the C API's engine reference. FlutterDesktopEngineRef engine_ = nullptr; @@ -110,9 +107,6 @@ class FlutterEngine : public PluginRegistry { // Messenger for communicating with the engine. std::unique_ptr messenger_; - // Whether or not this wrapper owns |engine_|. - bool owns_engine_ = true; - // Whether |Run| has been called successfully. // // This is used to improve error messages. This can be false while the engine diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h index 4007534a5d73e..b26e017a6760a 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h +++ b/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h @@ -32,6 +32,16 @@ class FlutterViewController { // |dart_project| will be used to configure the engine backing this view. FlutterViewController(int width, int height, const DartProject& project); + // Creates a FlutterView that can be parented into a Windows View hierarchy + // either using HWNDs. + // + // This creates the view on an existing FlutterEngine. + // + // |dart_project| will be used to configure the engine backing this view. + FlutterViewController(int width, + int height, + std::shared_ptr engine); + virtual ~FlutterViewController(); // Prevent copying. @@ -44,6 +54,9 @@ class FlutterViewController { // Returns the engine running Flutter content in this view. FlutterEngine* engine() const { return engine_.get(); } + // Returns the engine running Flutter content in this view. + std::shared_ptr shared_engine() const { return engine_; } + // Returns the view managed by this controller. FlutterView* view() const { return view_.get(); } diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_win32_window.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_win32_window.h new file mode 100644 index 0000000000000..b91d50fef7c75 --- /dev/null +++ b/shell/platform/windows/client_wrapper/include/flutter/flutter_win32_window.h @@ -0,0 +1,40 @@ +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WIN32_WINDOW_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WIN32_WINDOW_H_ + +#include "flutter_view_controller.h" + +#include "win32_window.h" + +namespace flutter { + +// A window that does nothing but host a Flutter view. +class FlutterWin32Window : public Win32Window { + public: + // Creates a new FlutterWin32Window hosting a Flutter view running |engine|. + explicit FlutterWin32Window(std::shared_ptr engine); + FlutterWin32Window(std::shared_ptr engine, + std::shared_ptr wrapper); + ~FlutterWin32Window() override = default; + + auto GetFlutterViewId() const -> FlutterViewId; + + protected: + // Win32Window: + auto OnCreate() -> bool override; + void OnDestroy() override; + auto MessageHandler(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) -> LRESULT override; + + private: + // The engine this window is attached to. + std::shared_ptr engine_; + + // The Flutter instance hosted by this window. + std::unique_ptr view_controller_; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WIN32_WINDOW_H_ diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h new file mode 100644 index 0000000000000..81355e8048985 --- /dev/null +++ b/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h @@ -0,0 +1,69 @@ +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WINDOW_CONTROLLER_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WINDOW_CONTROLLER_H_ + +#include + +#include "flutter_engine.h" +#include "method_channel.h" +#include "win32_wrapper.h" +#include "windowing.h" + +namespace flutter { + +// A singleton controller for Flutter windows. +class FlutterWindowController { + public: + virtual ~FlutterWindowController(); + + // Prevent copying. + FlutterWindowController(FlutterWindowController const&) = delete; + FlutterWindowController& operator=(FlutterWindowController const&) = delete; + + void SetEngine(std::shared_ptr engine); + auto CreateFlutterWindow(std::wstring const& title, + WindowSize const& size, + WindowArchetype archetype) + -> std::optional; + auto DestroyFlutterWindow(FlutterViewId view_id) -> bool; + + static FlutterWindowController& GetInstance() { + static FlutterWindowController instance; + return instance; + } + + protected: + FlutterWindowController(); + FlutterWindowController(std::shared_ptr wrapper); + + void MethodCallHandler(MethodCall<> const& call, MethodResult<>& result); + auto MessageHandler(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) -> LRESULT; + + virtual void SendOnWindowCreated( + FlutterViewId view_id, + std::optional parent_view_id) const; + virtual void SendOnWindowDestroyed(FlutterViewId view_id) const; + virtual void SendOnWindowChanged(FlutterViewId view_id) const; + + private: + friend class Win32Window; + + void DestroyWindows(); + auto GetWindowSize(FlutterViewId view_id) const -> WindowSize; + void HandleCreateWindow(WindowArchetype archetype, + MethodCall<> const& call, + MethodResult<>& result); + void HandleDestroyWindow(MethodCall<> const& call, MethodResult<>& result); + + mutable std::mutex mutex_; + std::shared_ptr win32_; + std::unique_ptr> channel_; + std::shared_ptr engine_; + std::unordered_map> windows_; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WINDOW_CONTROLLER_H_ diff --git a/shell/platform/windows/client_wrapper/include/flutter/win32_window.h b/shell/platform/windows/client_wrapper/include/flutter/win32_window.h new file mode 100644 index 0000000000000..5df1fee8758e4 --- /dev/null +++ b/shell/platform/windows/client_wrapper/include/flutter/win32_window.h @@ -0,0 +1,106 @@ +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WINDOW_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WINDOW_H_ + +#include "win32_wrapper.h" +#include "windowing.h" + +#include + +#include +#include +#include +#include + +namespace flutter { + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling. +class Win32Window { + public: + Win32Window(); + explicit Win32Window(std::shared_ptr wrapper); + virtual ~Win32Window(); + + // Retrieves a class instance pointer for |hwnd|. + static auto GetThisFromHandle(HWND hwnd) -> Win32Window*; + + // Returns the backing window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + auto GetHandle() const -> HWND; + + // If |quit_on_close| is true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Returns true if closing this window will cause the application to quit. + auto GetQuitOnClose() const -> bool; + + // Returns the bounds of the current client area. + auto GetClientArea() const -> RECT; + + // Returns the current window archetype. + auto GetArchetype() const -> WindowArchetype; + + protected: + // Creates a native Win32 window. |title| is the window title string. + // |client_size| specifies the requested size of the client rectangle (i.e., + // the size of the view). The window style is determined by |archetype|. + // After successful creation, |OnCreate| is called, and its result is + // returned. Otherwise, the return value is false. + auto Create(std::wstring const& title, + WindowSize const& client_size, + WindowArchetype archetype) -> bool; + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual auto MessageHandler(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) -> LRESULT; + + // Called when Create is called, allowing subclass window-related setup. + // Subclasses should return false if setup fails. + virtual auto OnCreate() -> bool; + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class FlutterWindowController; + + // 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. All other messages are handled by the + // controller's MessageHandler. + static auto CALLBACK WndProc(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) -> LRESULT; + + // Wrapper for Win32 API calls. + std::shared_ptr win32_; + + // The window's archetype (e.g., regular, dialog, popup). + WindowArchetype archetype_{WindowArchetype::regular}; + + // Indicates whether closing this window will quit the application. + bool quit_on_close_{false}; + + // Handle for the top-level window. + HWND window_handle_{nullptr}; + + // Handle for hosted child content window. + HWND child_content_{nullptr}; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WINDOW_H_ diff --git a/shell/platform/windows/client_wrapper/include/flutter/win32_wrapper.h b/shell/platform/windows/client_wrapper/include/flutter/win32_wrapper.h new file mode 100644 index 0000000000000..5e81e2130f64f --- /dev/null +++ b/shell/platform/windows/client_wrapper/include/flutter/win32_wrapper.h @@ -0,0 +1,34 @@ +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WRAPPER_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WRAPPER_H_ + +#include + +namespace flutter { + +// Wraps Win32 API calls to enable mock-based testing. +class Win32Wrapper { + public: + virtual ~Win32Wrapper() = default; + + virtual HWND CreateWindowEx(DWORD dwExStyle, + LPCWSTR lpClassName, + LPCWSTR lpWindowName, + DWORD dwStyle, + int X, + int Y, + int nWidth, + int nHeight, + HWND hWndParent, + HMENU hMenu, + HINSTANCE hInstance, + LPVOID lpParam) { + return ::CreateWindowEx(dwExStyle, lpClassName, lpWindowName, dwStyle, X, Y, + nWidth, nHeight, hWndParent, hMenu, hInstance, + lpParam); + } + virtual BOOL DestroyWindow(HWND hWnd) { return ::DestroyWindow(hWnd); } +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WRAPPER_H_ diff --git a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc index f51d7f14ad879..b5428bc1d26b1 100644 --- a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc +++ b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc @@ -114,6 +114,15 @@ bool FlutterDesktopEngineRun(FlutterDesktopEngineRef engine, return true; } +FlutterDesktopViewControllerRef FlutterDesktopEngineCreateViewController( + FlutterDesktopEngineRef engine, + const FlutterDesktopViewControllerProperties* properties) { + if (s_stub_implementation) { + return s_stub_implementation->EngineCreateViewController(properties); + } + return nullptr; +} + uint64_t FlutterDesktopEngineProcessMessages(FlutterDesktopEngineRef engine) { if (s_stub_implementation) { return s_stub_implementation->EngineProcessMessages(); @@ -228,3 +237,17 @@ void FlutterDesktopPluginRegistrarUnregisterTopLevelWindowProcDelegate( ->PluginRegistrarUnregisterTopLevelWindowProcDelegate(delegate); } } + +UINT FlutterDesktopGetDpiForMonitor(HMONITOR monitor) { + if (s_stub_implementation) { + return s_stub_implementation->GetDpiForMonitor(monitor); + } + return 96; +} + +UINT FlutterDesktopGetDpiForHWND(HWND hwnd) { + if (s_stub_implementation) { + return s_stub_implementation->GetDpiForHWND(hwnd); + } + return 96; +} diff --git a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h index 8f3eb0905ac7a..a9780a8c3a53c 100644 --- a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h +++ b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h @@ -63,6 +63,12 @@ class StubFlutterWindowsApi { // Called for FlutterDesktopEngineRun. virtual bool EngineRun(const char* entry_point) { return true; } + // Called for FlutterDesktopEngineCreateViewController. + virtual FlutterDesktopViewControllerRef EngineCreateViewController( + const FlutterDesktopViewControllerProperties* properties) { + return nullptr; + } + // Called for FlutterDesktopEngineProcessMessages. virtual uint64_t EngineProcessMessages() { return 0; } @@ -115,6 +121,12 @@ class StubFlutterWindowsApi { LRESULT* result) { return false; } + + // Called for FlutterDesktopGetDpiForMonitor. + virtual UINT GetDpiForMonitor(HMONITOR monitor) { return 96; } + + // Called for FlutterDesktopGetDpiForHWND. + virtual UINT GetDpiForHWND(HWND hwnd) { return 96; } }; // A test helper that owns a stub implementation, making it the test stub for diff --git a/shell/platform/windows/client_wrapper/win32_window.cc b/shell/platform/windows/client_wrapper/win32_window.cc new file mode 100644 index 0000000000000..bfdb0f6a0a311 --- /dev/null +++ b/shell/platform/windows/client_wrapper/win32_window.cc @@ -0,0 +1,462 @@ +#include "include/flutter/win32_window.h" +#include "include/flutter/flutter_window_controller.h" + +#include "flutter_windows.h" + +#include +#include +#include +#include +#include + +#include + +namespace { + +auto const* const kWindowClassName{L"FLUTTER_WIN32_WINDOW"}; + +// The number of Win32Window objects that currently exist. +static int gActiveWindowCount{0}; +// A mutex for thread-safe use of the window count. +static std::mutex gActiveWindowMutex; + +// Retrieves the calling thread's last-error code message as a string, +// or a fallback message if the error message cannot be formatted. +auto GetLastErrorAsString() -> std::string { + LPWSTR message_buffer{nullptr}; + + if (auto 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 (auto 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() << '\n'; + 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 drop-shadow effects. +auto GetWindowSizeForClientSize(flutter::WindowSize const& client_size, + DWORD window_style, + DWORD extended_window_style, + HWND parent_hwnd) -> flutter::WindowSize { + auto const dpi{FlutterDesktopGetDpiForHWND(parent_hwnd)}; + auto 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 { + std::cerr << "Failed to run AdjustWindowRectExForDpi: " + << GetLastErrorAsString() << '\n'; + } + } else { + std::cerr << "Failed to retrieve AdjustWindowRectExForDpi address from " + "User32.dll.\n"; + } + FreeLibrary(user32_module); + } else { + std::cerr << "Failed to load User32.dll.\n"; + } + + if (!AdjustWindowRectEx(&rect, window_style, FALSE, extended_window_style)) { + std::cerr << "Failed to run AdjustWindowRectEx: " << GetLastErrorAsString() + << '\n'; + } + return {static_cast(rect.right - rect.left), + static_cast(rect.bottom - rect.top)}; +} + +// 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); +} + +/// 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)); + } +} + +auto IsClassRegistered(LPCWSTR class_name) -> bool { + WNDCLASSEX window_class{}; + return GetClassInfoEx(GetModuleHandle(nullptr), class_name, &window_class) != + 0; +} + +} // namespace + +namespace flutter { + +Win32Window::Win32Window() : win32_{std::make_shared()} {} + +Win32Window::Win32Window(std::shared_ptr wrapper) + : win32_{std::move(wrapper)} {} + +Win32Window::~Win32Window() { + std::lock_guard lock(gActiveWindowMutex); + if (--gActiveWindowCount == 0) { + UnregisterClass(kWindowClassName, GetModuleHandle(nullptr)); + } +} + +auto Win32Window::GetThisFromHandle(HWND hwnd) -> Win32Window* { + return reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); +} + +auto Win32Window::GetHandle() const -> HWND { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +auto Win32Window::GetQuitOnClose() const -> bool { + return quit_on_close_; +} + +auto Win32Window::GetClientArea() const -> RECT { + RECT client_rect; + GetClientRect(window_handle_, &client_rect); + return client_rect; +} + +auto Win32Window::GetArchetype() const -> WindowArchetype { + return archetype_; +} + +auto Win32Window::Create(std::wstring const& title, + WindowSize const& client_size, + WindowArchetype archetype) -> bool { + std::lock_guard lock(gActiveWindowMutex); + + archetype_ = archetype; + + DWORD window_style{}; + DWORD extended_window_style{}; + + switch (archetype) { + case WindowArchetype::regular: + window_style |= WS_OVERLAPPEDWINDOW; + break; + case WindowArchetype::floating_regular: + // TODO + break; + case WindowArchetype::dialog: + // TODO + break; + case WindowArchetype::satellite: + // TODO + break; + case WindowArchetype::popup: + // TODO + break; + case WindowArchetype::tip: + // TODO + break; + default: + std::cerr << "Unhandled window archetype: " << static_cast(archetype) + << "\n"; + std::abort(); + } + + // Window rectangle in physical coordinates. + // Default positioning values (CW_USEDEFAULT) are used. + auto const window_rect{[&]() -> WindowRectangle { + auto const window_size{GetWindowSizeForClientSize( + client_size, window_style, extended_window_style, nullptr)}; + return {{CW_USEDEFAULT, CW_USEDEFAULT}, window_size}; + }()}; + + 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 = Win32Window::WndProc; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(idi_app_icon)); + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpszClassName = kWindowClassName; + window_class.hIconSm = nullptr; + + RegisterClassEx(&window_class); + } + + window_handle_ = win32_->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 (!window_handle_) { + auto const error_message{GetLastErrorAsString()}; + std::cerr << "Cannot create window due to a CreateWindowEx error: " + << error_message.c_str() << '\n'; + return false; + } + + // 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(window_handle_, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rc, + sizeof(frame_rc)); + RECT window_rc; + GetWindowRect(window_handle_, &window_rc); + auto const left_dropshadow_width{frame_rc.left - window_rc.left}; + auto const top_dropshadow_height{window_rc.top - frame_rc.top}; + SetWindowPos(window_handle_, nullptr, window_rc.left - left_dropshadow_width, + window_rc.top - top_dropshadow_height, 0, 0, + SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + + UpdateTheme(window_handle_); + + gActiveWindowCount++; + + ShowWindow(window_handle_, SW_SHOW); + + return OnCreate(); +} + +void Win32Window::Destroy() { + OnDestroy(); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + auto const client_rect{GetClientArea()}; + + MoveWindow(content, client_rect.left, client_rect.top, + client_rect.right - client_rect.left, + client_rect.bottom - client_rect.top, true); + + SetFocus(child_content_); +} + +auto Win32Window::MessageHandler(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) -> LRESULT { + switch (message) { + case WM_DESTROY: + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto* const new_scaled_window_rect{reinterpret_cast(lparam)}; + auto const width{new_scaled_window_rect->right - + new_scaled_window_rect->left}; + auto 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 + auto const client_rect{GetClientArea()}; + 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: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_MOUSEACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return MA_ACTIVATE; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + + default: + break; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +auto Win32Window::OnCreate() -> bool { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() {} + +// static +auto CALLBACK Win32Window::WndProc(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) -> LRESULT { + 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 (auto* const window{GetThisFromHandle(hwnd)}) { + return FlutterWindowController::GetInstance().MessageHandler( + hwnd, message, wparam, lparam); + } + + return DefWindowProc(hwnd, message, wparam, lparam); +} + +} // namespace flutter diff --git a/shell/platform/windows/flutter_windows_internal.h b/shell/platform/windows/flutter_windows_internal.h index bb1e6f767905f..47b98e983a48a 100644 --- a/shell/platform/windows/flutter_windows_internal.h +++ b/shell/platform/windows/flutter_windows_internal.h @@ -14,31 +14,6 @@ extern "C" { // Declare functions that are currently in-progress and shall be exposed to the // public facing API upon completion. -// Properties for configuring a Flutter view controller. -typedef struct { - // The view's initial width. - int width; - - // The view's initial height. - int height; -} FlutterDesktopViewControllerProperties; - -// Creates a view for the given engine. -// -// The |engine| will be started if it is not already running. -// -// The caller owns the returned reference, and is responsible for calling -// |FlutterDesktopViewControllerDestroy|. Returns a null pointer in the event of -// an error. -// -// Unlike |FlutterDesktopViewControllerCreate|, this does *not* take ownership -// of |engine| and |FlutterDesktopEngineDestroy| must be called to destroy -// the engine. -FLUTTER_EXPORT FlutterDesktopViewControllerRef -FlutterDesktopEngineCreateViewController( - FlutterDesktopEngineRef engine, - const FlutterDesktopViewControllerProperties* properties); - typedef int64_t PlatformViewId; typedef struct { diff --git a/shell/platform/windows/public/flutter_windows.h b/shell/platform/windows/public/flutter_windows.h index 80d78766f9383..d7b2a30520b04 100644 --- a/shell/platform/windows/public/flutter_windows.h +++ b/shell/platform/windows/public/flutter_windows.h @@ -70,6 +70,15 @@ typedef struct { } FlutterDesktopEngineProperties; +// Properties for configuring a Flutter view controller. +typedef struct { + // The view's initial width. + int width; + + // The view's initial height. + int height; +} FlutterDesktopViewControllerProperties; + // ========== View Controller ========== // Creates a view that hosts and displays the given engine instance. @@ -165,6 +174,22 @@ FLUTTER_EXPORT bool FlutterDesktopEngineDestroy(FlutterDesktopEngineRef engine); FLUTTER_EXPORT bool FlutterDesktopEngineRun(FlutterDesktopEngineRef engine, const char* entry_point); +// Creates a view for the given engine. +// +// The |engine| will be started if it is not already running. +// +// The caller owns the returned reference, and is responsible for calling +// |FlutterDesktopViewControllerDestroy|. Returns a null pointer in the event of +// an error. +// +// Unlike |FlutterDesktopViewControllerCreate|, this does *not* take ownership +// of |engine| and |FlutterDesktopEngineDestroy| must be called to destroy +// the engine. +FLUTTER_EXPORT FlutterDesktopViewControllerRef +FlutterDesktopEngineCreateViewController( + FlutterDesktopEngineRef engine, + const FlutterDesktopViewControllerProperties* properties); + // DEPRECATED: This is no longer necessary to call, Flutter will take care of // processing engine messages transparently through DispatchMessage. // From 85c34ed38174879a94f06b6493412f712c753f77 Mon Sep 17 00:00:00 2001 From: Harlen Batagelo Date: Tue, 22 Oct 2024 13:57:30 -0300 Subject: [PATCH 2/7] Add support for popups --- ci/licenses_golden/excluded_files | 1 + shell/platform/common/client_wrapper/BUILD.gn | 1 + .../client_wrapper/core_wrapper_files.gni | 1 + .../include/flutter/windowing.h | 18 + .../common/client_wrapper/windowing.cc | 285 ++++++++ .../client_wrapper/windowing_unittests.cc | 645 ++++++++++++++++++ .../flutter_window_controller.cc | 149 +++- .../flutter_window_controller_unittests.cc | 30 +- .../flutter/flutter_window_controller.h | 4 +- .../include/flutter/win32_window.h | 27 +- .../windows/client_wrapper/win32_window.cc | 214 +++++- 11 files changed, 1342 insertions(+), 33 deletions(-) create mode 100644 shell/platform/common/client_wrapper/windowing.cc create mode 100644 shell/platform/common/client_wrapper/windowing_unittests.cc diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index 1009dc88cc425..d8ad1bf5356d0 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -323,6 +323,7 @@ ../../../flutter/shell/platform/common/client_wrapper/standard_method_codec_unittests.cc ../../../flutter/shell/platform/common/client_wrapper/testing ../../../flutter/shell/platform/common/client_wrapper/texture_registrar_unittests.cc +../../../flutter/shell/platform/common/client_wrapper/windowing_unittests.cc ../../../flutter/shell/platform/common/engine_switches_unittests.cc ../../../flutter/shell/platform/common/flutter_platform_node_delegate_unittests.cc ../../../flutter/shell/platform/common/geometry_unittests.cc diff --git a/shell/platform/common/client_wrapper/BUILD.gn b/shell/platform/common/client_wrapper/BUILD.gn index 91e7120b65734..5fb070ccae6b5 100644 --- a/shell/platform/common/client_wrapper/BUILD.gn +++ b/shell/platform/common/client_wrapper/BUILD.gn @@ -51,6 +51,7 @@ executable("client_wrapper_unittests") { "testing/test_codec_extensions.cc", "testing/test_codec_extensions.h", "texture_registrar_unittests.cc", + "windowing_unittests.cc", ] deps = [ diff --git a/shell/platform/common/client_wrapper/core_wrapper_files.gni b/shell/platform/common/client_wrapper/core_wrapper_files.gni index 013c758c7f83c..f123787a024d0 100644 --- a/shell/platform/common/client_wrapper/core_wrapper_files.gni +++ b/shell/platform/common/client_wrapper/core_wrapper_files.gni @@ -47,6 +47,7 @@ core_cpp_client_wrapper_sources = get_path_info([ "core_implementations.cc", "plugin_registrar.cc", "standard_codec.cc", + "windowing.cc", ], "abspath") diff --git a/shell/platform/common/client_wrapper/include/flutter/windowing.h b/shell/platform/common/client_wrapper/include/flutter/windowing.h index 72e3dd5cfaca7..c25d05c1cda49 100644 --- a/shell/platform/common/client_wrapper/include/flutter/windowing.h +++ b/shell/platform/common/client_wrapper/include/flutter/windowing.h @@ -143,6 +143,24 @@ struct WindowMetadata { std::optional parent_id; }; +namespace internal { + +// Computes the screen-space rectangle for a child window placed according to +// the given |positioner|. |child_size| is the frame size of the child window. +// |anchor_rect| is the rectangle relative to which the child window is placed. +// |parent_rect| is the parent window's rectangle. |output_rect| is the output +// display area where the child window will be placed. All sizes and rectangles +// are in physical coordinates. Note: WindowPositioner::anchor_rect is not used +// in this function; use |anchor_rect| to set the anchor rectangle for the +// child. +auto PlaceWindow(WindowPositioner const& positioner, + WindowSize child_size, + WindowRectangle const& anchor_rect, + WindowRectangle const& parent_rect, + WindowRectangle const& output_rect) -> WindowRectangle; + +} // namespace internal + } // namespace flutter #endif // FLUTTER_SHELL_PLATFORM_COMMON_CLIENT_WRAPPER_INCLUDE_FLUTTER_WINDOWING_H_ diff --git a/shell/platform/common/client_wrapper/windowing.cc b/shell/platform/common/client_wrapper/windowing.cc new file mode 100644 index 0000000000000..df30c38e13387 --- /dev/null +++ b/shell/platform/common/client_wrapper/windowing.cc @@ -0,0 +1,285 @@ +// 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 +#include + +#include "include/flutter/windowing.h" + +namespace flutter { + +namespace { + +auto offset_for(WindowSize const& size, + WindowPositioner::Anchor anchor) -> WindowPoint { + switch (anchor) { + case WindowPositioner::Anchor::top_left: + return {0, 0}; + case WindowPositioner::Anchor::top: + return {-size.width / 2, 0}; + case WindowPositioner::Anchor::top_right: + return {-1 * size.width, 0}; + case WindowPositioner::Anchor::left: + return {0, -size.height / 2}; + case WindowPositioner::Anchor::center: + return {-size.width / 2, -size.height / 2}; + case WindowPositioner::Anchor::right: + return {-1 * size.width, -size.height / 2}; + case WindowPositioner::Anchor::bottom_left: + return {0, -1 * size.height}; + case WindowPositioner::Anchor::bottom: + return {-size.width / 2, -1 * size.height}; + case WindowPositioner::Anchor::bottom_right: + return {-1 * size.width, -1 * size.height}; + default: + std::cerr << "Unknown anchor value: " << static_cast(anchor) << '\n'; + std::abort(); + } +} + +auto anchor_position_for(WindowRectangle const& rect, + WindowPositioner::Anchor anchor) -> WindowPoint { + switch (anchor) { + case WindowPositioner::Anchor::top_left: + return rect.top_left; + case WindowPositioner::Anchor::top: + return rect.top_left + WindowPoint{rect.size.width / 2, 0}; + case WindowPositioner::Anchor::top_right: + return rect.top_left + WindowPoint{rect.size.width, 0}; + case WindowPositioner::Anchor::left: + return rect.top_left + WindowPoint{0, rect.size.height / 2}; + case WindowPositioner::Anchor::center: + return rect.top_left + + WindowPoint{rect.size.width / 2, rect.size.height / 2}; + case WindowPositioner::Anchor::right: + return rect.top_left + WindowPoint{rect.size.width, rect.size.height / 2}; + case WindowPositioner::Anchor::bottom_left: + return rect.top_left + WindowPoint{0, rect.size.height}; + case WindowPositioner::Anchor::bottom: + return rect.top_left + WindowPoint{rect.size.width / 2, rect.size.height}; + case WindowPositioner::Anchor::bottom_right: + return rect.top_left + WindowPoint{rect.size.width, rect.size.height}; + default: + std::cerr << "Unknown anchor value: " << static_cast(anchor) << '\n'; + std::abort(); + } +} + +auto constrain_to(WindowRectangle const& r, + WindowPoint const& p) -> WindowPoint { + return {std::clamp(p.x, r.top_left.x, r.top_left.x + r.size.width), + std::clamp(p.y, r.top_left.y, r.top_left.y + r.size.height)}; +} + +auto flip_anchor_x(WindowPositioner::Anchor anchor) + -> WindowPositioner::Anchor { + switch (anchor) { + case WindowPositioner::Anchor::top_left: + return WindowPositioner::Anchor::top_right; + case WindowPositioner::Anchor::top_right: + return WindowPositioner::Anchor::top_left; + case WindowPositioner::Anchor::left: + return WindowPositioner::Anchor::right; + case WindowPositioner::Anchor::right: + return WindowPositioner::Anchor::left; + case WindowPositioner::Anchor::bottom_left: + return WindowPositioner::Anchor::bottom_right; + case WindowPositioner::Anchor::bottom_right: + return WindowPositioner::Anchor::bottom_left; + default: + return anchor; + } +} + +auto flip_anchor_y(WindowPositioner::Anchor anchor) + -> WindowPositioner::Anchor { + switch (anchor) { + case WindowPositioner::Anchor::top_left: + return WindowPositioner::Anchor::bottom_left; + case WindowPositioner::Anchor::top: + return WindowPositioner::Anchor::bottom; + case WindowPositioner::Anchor::top_right: + return WindowPositioner::Anchor::bottom_right; + case WindowPositioner::Anchor::bottom_left: + return WindowPositioner::Anchor::top_left; + case WindowPositioner::Anchor::bottom: + return WindowPositioner::Anchor::top; + case WindowPositioner::Anchor::bottom_right: + return WindowPositioner::Anchor::top_right; + default: + return anchor; + } +} + +auto flip_offset_x(WindowPoint const& p) -> WindowPoint { + return {-1 * p.x, p.y}; +} + +auto flip_offset_y(WindowPoint const& p) -> WindowPoint { + return {p.x, -1 * p.y}; +} + +} // namespace + +namespace internal { + +auto PlaceWindow(WindowPositioner const& positioner, + WindowSize child_size, + WindowRectangle const& anchor_rect, + WindowRectangle const& parent_rect, + WindowRectangle const& output_rect) -> WindowRectangle { + WindowRectangle default_result; + + { + auto const result{ + constrain_to(parent_rect, anchor_position_for( + anchor_rect, positioner.parent_anchor) + + positioner.offset) + + offset_for(child_size, positioner.child_anchor)}; + + if (output_rect.contains({result, child_size})) { + return WindowRectangle{result, child_size}; + } + + default_result = WindowRectangle{result, child_size}; + } + + if (static_cast(positioner.constraint_adjustment) & + static_cast(WindowPositioner::ConstraintAdjustment::flip_x)) { + auto const result{ + constrain_to(parent_rect, + anchor_position_for( + anchor_rect, flip_anchor_x(positioner.parent_anchor)) + + flip_offset_x(positioner.offset)) + + offset_for(child_size, flip_anchor_x(positioner.child_anchor))}; + + if (output_rect.contains({result, child_size})) { + return WindowRectangle{result, child_size}; + } + } + + if (static_cast(positioner.constraint_adjustment) & + static_cast(WindowPositioner::ConstraintAdjustment::flip_y)) { + auto const result{ + constrain_to(parent_rect, + anchor_position_for( + anchor_rect, flip_anchor_y(positioner.parent_anchor)) + + flip_offset_y(positioner.offset)) + + offset_for(child_size, flip_anchor_y(positioner.child_anchor))}; + + if (output_rect.contains({result, child_size})) { + return WindowRectangle{result, child_size}; + } + } + + if (static_cast(positioner.constraint_adjustment) & + static_cast(WindowPositioner::ConstraintAdjustment::flip_x) && + static_cast(positioner.constraint_adjustment) & + static_cast(WindowPositioner::ConstraintAdjustment::flip_y)) { + auto const result{ + constrain_to( + parent_rect, + anchor_position_for(anchor_rect, flip_anchor_x(flip_anchor_y( + positioner.parent_anchor))) + + flip_offset_x(flip_offset_y(positioner.offset))) + + offset_for(child_size, + flip_anchor_x(flip_anchor_y(positioner.child_anchor)))}; + + if (output_rect.contains({result, child_size})) { + return WindowRectangle{result, child_size}; + } + } + + { + auto result{constrain_to( + parent_rect, + anchor_position_for(anchor_rect, positioner.parent_anchor) + + positioner.offset) + + offset_for(child_size, positioner.child_anchor)}; + + if (static_cast(positioner.constraint_adjustment) & + static_cast(WindowPositioner::ConstraintAdjustment::slide_x)) { + auto const left_overhang{result.x - output_rect.top_left.x}; + auto const right_overhang{ + (result.x + child_size.width) - + (output_rect.top_left.x + output_rect.size.width)}; + + if (left_overhang < 0) { + result.x -= left_overhang; + } else if (right_overhang > 0) { + result.x -= right_overhang; + } + } + + if (static_cast(positioner.constraint_adjustment) & + static_cast(WindowPositioner::ConstraintAdjustment::slide_y)) { + auto const top_overhang{result.y - output_rect.top_left.y}; + auto const bot_overhang{ + (result.y + child_size.height) - + (output_rect.top_left.y + output_rect.size.height)}; + + if (top_overhang < 0) { + result.y -= top_overhang; + } else if (bot_overhang > 0) { + result.y -= bot_overhang; + } + } + + if (output_rect.contains({result, child_size})) { + return WindowRectangle{result, child_size}; + } + } + + { + auto result{constrain_to( + parent_rect, + anchor_position_for(anchor_rect, positioner.parent_anchor) + + positioner.offset) + + offset_for(child_size, positioner.child_anchor)}; + + if (static_cast(positioner.constraint_adjustment) & + static_cast(WindowPositioner::ConstraintAdjustment::resize_x)) { + auto const left_overhang{result.x - output_rect.top_left.x}; + auto const right_overhang{ + (result.x + child_size.width) - + (output_rect.top_left.x + output_rect.size.width)}; + + if (left_overhang < 0) { + result.x -= left_overhang; + child_size.width += left_overhang; + } + + if (right_overhang > 0) { + child_size.width -= right_overhang; + } + } + + if (static_cast(positioner.constraint_adjustment) & + static_cast(WindowPositioner::ConstraintAdjustment::resize_y)) { + auto const top_overhang{result.y - output_rect.top_left.y}; + auto const bot_overhang{ + (result.y + child_size.height) - + (output_rect.top_left.y + output_rect.size.height)}; + + if (top_overhang < 0) { + result.y -= top_overhang; + child_size.height += top_overhang; + } + + if (bot_overhang > 0) { + child_size.height -= bot_overhang; + } + } + + if (output_rect.contains({result, child_size})) { + return WindowRectangle{result, child_size}; + } + } + + return default_result; +} + +} // namespace internal + +} // namespace flutter diff --git a/shell/platform/common/client_wrapper/windowing_unittests.cc b/shell/platform/common/client_wrapper/windowing_unittests.cc new file mode 100644 index 0000000000000..b9f3448dbf8b7 --- /dev/null +++ b/shell/platform/common/client_wrapper/windowing_unittests.cc @@ -0,0 +1,645 @@ +// 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/common/client_wrapper/include/flutter/windowing.h" + +#include "gtest/gtest.h" + +namespace flutter { + +using Positioner = WindowPositioner; +using Anchor = Positioner::Anchor; +using Constraint = Positioner::ConstraintAdjustment; +using Rectangle = WindowRectangle; +using Point = WindowPoint; +using Size = WindowSize; + +std::ostream& operator<<(std::ostream& os, Point const& point) { + return os << "(x: " << point.x << ", y: " << point.y << ")"; +} + +std::ostream& operator<<(std::ostream& os, Size const& size) { + return os << "(width: " << size.width << ", height: " << size.height << ")"; +} + +std::ostream& operator<<(std::ostream& os, Rectangle const& rect) { + return os << "(x: " << rect.top_left.x << ", y: " << rect.top_left.y + << ", width: " << rect.size.width + << ", height: " << rect.size.height << ")"; +} + +namespace { + +struct WindowPlacementTest : testing::Test { + struct ClientAnchorsToParentConfig { + Rectangle const display_area{{0, 0}, {800, 600}}; + Size const parent_size{400, 300}; + Size const child_size{100, 50}; + Point const parent_position{ + (display_area.size.width - parent_size.width) / 2, + (display_area.size.height - parent_size.height) / 2}; + } client_anchors_to_parent_config; + + Rectangle const display_area{{0, 0}, {640, 480}}; + Size const parent_size{600, 400}; + Size const child_size{300, 300}; + Rectangle const rectangle_away_from_rhs{{20, 20}, {20, 20}}; + Rectangle const rectangle_near_rhs{{590, 20}, {10, 20}}; + Rectangle const rectangle_away_from_bottom{{20, 20}, {20, 20}}; + Rectangle const rectangle_near_bottom{{20, 380}, {20, 20}}; + Rectangle const rectangle_near_both_sides{{0, 20}, {600, 20}}; + Rectangle const rectangle_near_both_sides_and_bottom{{0, 380}, {600, 20}}; + Rectangle const rectangle_near_all_sides{{0, 20}, {600, 380}}; + Rectangle const rectangle_near_both_bottom_right{{400, 380}, {200, 20}}; + Point const parent_position{ + (display_area.size.width - parent_size.width) / 2, + (display_area.size.height - parent_size.height) / 2}; + + Positioner positioner; + + auto anchor_rect() -> Rectangle { + auto rectangle{positioner.anchor_rect.value()}; + return {rectangle.top_left + parent_position, rectangle.size}; + } + + auto parent_rect() -> Rectangle { return {parent_position, parent_size}; } + + auto on_top_edge() -> Point { + return anchor_rect().top_left - Point{0, child_size.height}; + } + + auto on_right_edge() -> Point { + auto const rect{anchor_rect()}; + return rect.top_left + Point{rect.size.width, 0}; + } + + auto on_left_edge() -> Point { + return anchor_rect().top_left - Point{child_size.width, 0}; + } + + auto on_bottom_edge() -> Point { + auto const rect{anchor_rect()}; + return rect.top_left + Point{0, rect.size.height}; + } +}; + +} // namespace + +TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenRectAnchorRightOfParent) { + auto const& display_area{client_anchors_to_parent_config.display_area}; + auto const& parent_size{client_anchors_to_parent_config.parent_size}; + auto const& child_size{client_anchors_to_parent_config.child_size}; + auto const& parent_position{client_anchors_to_parent_config.parent_position}; + + auto const rect_size{10}; + Rectangle const overlapping_right{ + parent_position + + Point{parent_size.width - rect_size / 2, parent_size.height / 2}, + {rect_size, rect_size}}; + + Positioner const positioner{.anchor_rect = overlapping_right, + .parent_anchor = Anchor::top_right, + .child_anchor = Anchor::top_left, + .constraint_adjustment = static_cast( + static_cast(Constraint::slide_y) | + static_cast(Constraint::resize_x))}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, positioner.anchor_rect.value(), + {parent_position, parent_size}, display_area)}; + + auto const expected_position{ + parent_position + Point{parent_size.width, parent_size.height / 2}}; + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, child_size); +} + +TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenRectAnchorAboveParent) { + auto const& display_area{client_anchors_to_parent_config.display_area}; + auto const& parent_size{client_anchors_to_parent_config.parent_size}; + auto const& child_size{client_anchors_to_parent_config.child_size}; + auto const& parent_position{client_anchors_to_parent_config.parent_position}; + + auto const rect_size{10}; + Rectangle const overlapping_above{ + parent_position + Point{parent_size.width / 2, -rect_size / 2}, + {rect_size, rect_size}}; + + Positioner const positioner{.anchor_rect = overlapping_above, + .parent_anchor = Anchor::top_right, + .child_anchor = Anchor::bottom_right, + .constraint_adjustment = Constraint::slide_x}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, positioner.anchor_rect.value(), + {parent_position, parent_size}, display_area)}; + + auto const expected_position{parent_position + + Point{parent_size.width / 2 + rect_size, 0} - + static_cast(child_size)}; + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, child_size); +} + +TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenOffsetRightOfParent) { + auto const& display_area{client_anchors_to_parent_config.display_area}; + auto const& parent_size{client_anchors_to_parent_config.parent_size}; + auto const& child_size{client_anchors_to_parent_config.child_size}; + auto const& parent_position{client_anchors_to_parent_config.parent_position}; + + auto const rect_size{10}; + Rectangle const mid_right{ + parent_position + + Point{parent_size.width - rect_size, parent_size.height / 2}, + {rect_size, rect_size}}; + + Positioner const positioner{.anchor_rect = mid_right, + .parent_anchor = Anchor::top_right, + .child_anchor = Anchor::top_left, + .offset = Point{rect_size, 0}, + .constraint_adjustment = static_cast( + static_cast(Constraint::slide_y) | + static_cast(Constraint::resize_x))}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, positioner.anchor_rect.value(), + {parent_position, parent_size}, display_area)}; + + auto const expected_position{ + parent_position + Point{parent_size.width, parent_size.height / 2}}; + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, child_size); +} + +TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenOffsetAboveParent) { + auto const& display_area{client_anchors_to_parent_config.display_area}; + auto const& parent_size{client_anchors_to_parent_config.parent_size}; + auto const& child_size{client_anchors_to_parent_config.child_size}; + auto const& parent_position{client_anchors_to_parent_config.parent_position}; + + auto const rect_size{10}; + Rectangle const mid_top{parent_position + Point{parent_size.width / 2, 0}, + {rect_size, rect_size}}; + + Positioner const positioner{.anchor_rect = mid_top, + .parent_anchor = Anchor::top_right, + .child_anchor = Anchor::bottom_right, + .offset = Point{0, -rect_size}, + .constraint_adjustment = Constraint::slide_x}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, positioner.anchor_rect.value(), + {parent_position, parent_size}, display_area)}; + + auto const expected_position{parent_position + + Point{parent_size.width / 2 + rect_size, 0} - + static_cast(child_size)}; + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, child_size); +} + +TEST_F(WindowPlacementTest, + ClientAnchorsToParentGivenRectAndOffsetBelowLeftParent) { + auto const& display_area{client_anchors_to_parent_config.display_area}; + auto const& parent_size{client_anchors_to_parent_config.parent_size}; + auto const& child_size{client_anchors_to_parent_config.child_size}; + auto const& parent_position{client_anchors_to_parent_config.parent_position}; + + auto const rect_size{10}; + Rectangle const below_left{ + parent_position + Point{-rect_size, parent_size.height}, + {rect_size, rect_size}}; + + Positioner const positioner{.anchor_rect = below_left, + .parent_anchor = Anchor::bottom_left, + .child_anchor = Anchor::top_right, + .offset = Point{-rect_size, rect_size}, + .constraint_adjustment = Constraint::resize_any}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, positioner.anchor_rect.value(), + {parent_position, parent_size}, display_area)}; + + auto const expected_position{parent_position + Point{0, parent_size.height} - + Point{child_size.width, 0}}; + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, child_size); +} + +TEST_F(WindowPlacementTest, + AttachesToRightEdgeGivenAnchorRectAwayFromRightSide) { + positioner = {.anchor_rect = rectangle_away_from_rhs, + .parent_anchor = Anchor::top_right, + .child_anchor = Anchor::top_left}; + + auto const expected_position{on_right_edge()}; + + auto const child_rect{ + internal::PlaceWindow(positioner, child_size, anchor_rect(), + {parent_position, parent_size}, display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, AttachesToLeftEdgeGivenAnchorRectNearRightSide) { + positioner = {.anchor_rect = rectangle_near_rhs, + .parent_anchor = Anchor::top_left, + .child_anchor = Anchor::top_right}; + + auto const expected_position{on_left_edge()}; + + auto const child_rect{ + internal::PlaceWindow(positioner, child_size, anchor_rect(), + {parent_position, parent_size}, display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, AttachesToRightEdgeGivenAnchorRectNearBothSides) { + positioner = {.anchor_rect = rectangle_near_both_sides, + .parent_anchor = Anchor::top_right, + .child_anchor = Anchor::top_left}; + + auto const expected_position{on_right_edge()}; + + auto const child_rect{ + internal::PlaceWindow(positioner, child_size, anchor_rect(), + {parent_position, parent_size}, display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, AttachesToBottomEdgeGivenAnchorRectAwayFromBottom) { + positioner = {.anchor_rect = rectangle_away_from_bottom, + .parent_anchor = Anchor::bottom_left, + .child_anchor = Anchor::top_left}; + + auto const expected_position{on_bottom_edge()}; + + auto const child_rect{ + internal::PlaceWindow(positioner, child_size, anchor_rect(), + {parent_position, parent_size}, display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, AttachesToTopEdgeGivenAnchorRectNearBottom) { + positioner = {.anchor_rect = rectangle_near_bottom, + .parent_anchor = Anchor::top_left, + .child_anchor = Anchor::bottom_left}; + + auto const expected_position{on_top_edge()}; + + auto const child_rect{ + internal::PlaceWindow(positioner, child_size, anchor_rect(), + {parent_position, parent_size}, display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, AttachesToBottomEdgeGivenAnchorRectNearBothSides) { + positioner = {.anchor_rect = rectangle_near_both_sides, + .parent_anchor = Anchor::bottom_left, + .child_anchor = Anchor::top_left}; + + auto const expected_position{on_bottom_edge()}; + + auto const child_rect{ + internal::PlaceWindow(positioner, child_size, anchor_rect(), + {parent_position, parent_size}, display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, + AttachesToTopEdgeGivenAnchorRectNearBothSidesAndBottom) { + positioner = {.anchor_rect = rectangle_near_both_sides_and_bottom, + .parent_anchor = Anchor::top_left, + .child_anchor = Anchor::bottom_left}; + + auto const expected_position{on_top_edge()}; + + auto const child_rect{ + internal::PlaceWindow(positioner, child_size, anchor_rect(), + {parent_position, parent_size}, display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, AttachesToRightEdgeGivenAnchorRectNearAllSides) { + positioner = {.anchor_rect = rectangle_near_all_sides, + .parent_anchor = Anchor::top_right, + .child_anchor = Anchor::top_left}; + + auto const expected_position{on_right_edge()}; + + auto const child_rect{ + internal::PlaceWindow(positioner, child_size, anchor_rect(), + {parent_position, parent_size}, display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +namespace { +Anchor const all_anchors[]{ + Anchor::top_left, Anchor::top, Anchor::top_right, + Anchor::left, Anchor::center, Anchor::right, + Anchor::bottom_left, Anchor::bottom, Anchor::bottom_right, +}; + +auto position_of(Anchor anchor, Rectangle rectangle) -> Point { + switch (anchor) { + case Anchor::top_left: + return rectangle.top_left; + case Anchor::top: + return rectangle.top_left + Point{rectangle.size.width / 2, 0}; + case Anchor::top_right: + return rectangle.top_left + Point{rectangle.size.width, 0}; + case Anchor::left: + return rectangle.top_left + Point{0, rectangle.size.height / 2}; + case Anchor::center: + return rectangle.top_left + + Point{rectangle.size.width / 2, rectangle.size.height / 2}; + case Anchor::right: + return rectangle.top_left + + Point{rectangle.size.width, rectangle.size.height / 2}; + case Anchor::bottom_left: + return rectangle.top_left + Point{0, rectangle.size.height}; + case Anchor::bottom: + return rectangle.top_left + + Point{rectangle.size.width / 2, rectangle.size.height}; + case Anchor::bottom_right: + return rectangle.top_left + static_cast(rectangle.size); + default: + std::cerr << "Unknown anchor value: " << static_cast(anchor) << '\n'; + std::abort(); + } +} +} // namespace + +TEST_F(WindowPlacementTest, CanAttachByEveryAnchorGivenNoConstraintAdjustment) { + positioner.anchor_rect = Rectangle{{100, 50}, {20, 20}}; + positioner.constraint_adjustment = Constraint{}; + + for (auto const rect_anchor : all_anchors) { + positioner.parent_anchor = rect_anchor; + + auto const anchor_position{position_of(rect_anchor, anchor_rect())}; + + for (auto const window_anchor : all_anchors) { + positioner.child_anchor = window_anchor; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area)}; + + EXPECT_EQ(position_of(window_anchor, child_rect), anchor_position); + } + } +} + +TEST_F(WindowPlacementTest, + PlacementIsFlippedGivenAnchorRectNearRightSideAndOffset) { + auto const x_offset{42}; + auto const y_offset{13}; + + positioner.anchor_rect = rectangle_near_rhs; + positioner.constraint_adjustment = Constraint::flip_x; + positioner.offset = Point{x_offset, y_offset}; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::top_right; + + auto const expected_position{on_left_edge() + Point{-1 * x_offset, y_offset}}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, + PlacementIsFlippedGivenAnchorRectNearBottomAndOffset) { + auto const x_offset{42}; + auto const y_offset{13}; + + positioner.anchor_rect = rectangle_near_bottom; + positioner.constraint_adjustment = Constraint::flip_y; + positioner.offset = Point{x_offset, y_offset}; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_left; + + auto const expected_position{on_top_edge() + Point{x_offset, -1 * y_offset}}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, + PlacementIsFlippedBothWaysGivenAnchorRectNearBottomRightAndOffset) { + auto const x_offset{42}; + auto const y_offset{13}; + + positioner.anchor_rect = rectangle_near_both_bottom_right; + positioner.constraint_adjustment = Constraint::flip_any; + positioner.offset = Point{x_offset, y_offset}; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_right; + + auto const expected_position{anchor_rect().top_left - + static_cast(child_size) - + Point{x_offset, y_offset}}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, PlacementCanSlideInXGivenAnchorRectNearRightSide) { + positioner.anchor_rect = rectangle_near_rhs; + positioner.constraint_adjustment = Constraint::slide_x; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::top_right; + + Point const expected_position{ + (display_area.top_left.x + display_area.size.width) - child_size.width, + anchor_rect().top_left.y}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, PlacementCanSlideInXGivenAnchorRectNearLeftSide) { + Rectangle const rectangle_near_left_side{{0, 20}, {20, 20}}; + + positioner.anchor_rect = rectangle_near_left_side; + positioner.constraint_adjustment = Constraint::slide_x; + positioner.child_anchor = Anchor::top_right; + positioner.parent_anchor = Anchor::top_left; + + Point const expected_position{display_area.top_left.x, + anchor_rect().top_left.y}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, PlacementCanSlideInYGivenAnchorRectNearBottom) { + positioner.anchor_rect = rectangle_near_bottom; + positioner.constraint_adjustment = Constraint::slide_y; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_left; + + Point const expected_position{ + anchor_rect().top_left.x, + (display_area.top_left.y + display_area.size.height) - child_size.height}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, PlacementCanSlideInYGivenAnchorRectNearTop) { + positioner.anchor_rect = rectangle_near_all_sides; + positioner.constraint_adjustment = Constraint::slide_y; + positioner.child_anchor = Anchor::bottom_left; + positioner.parent_anchor = Anchor::top_left; + + Point const expected_position{anchor_rect().top_left.x, + display_area.top_left.y}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, + PlacementCanSlideInXAndYGivenAnchorRectNearBottomRightAndOffset) { + positioner.anchor_rect = rectangle_near_both_bottom_right; + positioner.constraint_adjustment = Constraint::slide_any; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_left; + + auto const expected_position{ + (display_area.top_left + static_cast(display_area.size)) - + static_cast(child_size)}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, PlacementCanResizeInXGivenAnchorRectNearRightSide) { + positioner.anchor_rect = rectangle_near_rhs; + positioner.constraint_adjustment = Constraint::resize_x; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::top_right; + + auto const expected_position{anchor_rect().top_left + + Point{anchor_rect().size.width, 0}}; + Size const expected_size{ + (display_area.top_left.x + display_area.size.width) - + (anchor_rect().top_left.x + anchor_rect().size.width), + child_size.height}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, expected_size); +} + +TEST_F(WindowPlacementTest, PlacementCanResizeInXGivenAnchorRectNearLeftSide) { + Rectangle const rectangle_near_left_side{{0, 20}, {20, 20}}; + + positioner.anchor_rect = rectangle_near_left_side; + positioner.constraint_adjustment = Constraint::resize_x; + positioner.child_anchor = Anchor::top_right; + positioner.parent_anchor = Anchor::top_left; + + Point const expected_position{display_area.top_left.x, + anchor_rect().top_left.y}; + Size const expected_size{anchor_rect().top_left.x - display_area.top_left.x, + child_size.height}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, expected_size); +} + +TEST_F(WindowPlacementTest, PlacementCanResizeInYGivenAnchorRectNearBottom) { + positioner.anchor_rect = rectangle_near_bottom; + positioner.constraint_adjustment = Constraint::resize_y; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_left; + + auto const expected_position{anchor_rect().top_left + + Point{0, anchor_rect().size.height}}; + Size const expected_size{ + child_size.width, + (display_area.top_left.y + display_area.size.height) - + (anchor_rect().top_left.y + anchor_rect().size.height)}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, expected_size); +} + +TEST_F(WindowPlacementTest, PlacementCanResizeInYGivenAnchorRectNearTop) { + positioner.anchor_rect = rectangle_near_all_sides; + positioner.constraint_adjustment = Constraint::resize_y; + positioner.child_anchor = Anchor::bottom_left; + positioner.parent_anchor = Anchor::top_left; + + Point const expected_position{anchor_rect().top_left.x, + display_area.top_left.y}; + Size const expected_size{child_size.width, + anchor_rect().top_left.y - display_area.top_left.y}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, expected_size); +} + +TEST_F(WindowPlacementTest, + PlacementCanResizeInXAndYGivenAnchorRectNearBottomRightAndOffset) { + positioner.anchor_rect = rectangle_near_both_bottom_right; + positioner.constraint_adjustment = Constraint::resize_any; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_right; + + auto const expected_position{anchor_rect().top_left + + static_cast(anchor_rect().size)}; + Size const expected_size{ + (display_area.top_left.x + display_area.size.width) - expected_position.x, + (display_area.top_left.y + display_area.size.height) - + expected_position.y}; + + auto const child_rect{internal::PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area)}; + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, expected_size); +} + +} // namespace flutter diff --git a/shell/platform/windows/client_wrapper/flutter_window_controller.cc b/shell/platform/windows/client_wrapper/flutter_window_controller.cc index 4585f38c27fcf..18978444f12fc 100644 --- a/shell/platform/windows/client_wrapper/flutter_window_controller.cc +++ b/shell/platform/windows/client_wrapper/flutter_window_controller.cc @@ -143,9 +143,12 @@ void FlutterWindowController::SetEngine(std::shared_ptr engine) { }); } -auto FlutterWindowController::CreateFlutterWindow(std::wstring const& title, - WindowSize const& size, - WindowArchetype archetype) +auto FlutterWindowController::CreateFlutterWindow( + std::wstring const& title, + WindowSize const& size, + WindowArchetype archetype, + std::optional positioner, + std::optional parent_view_id) -> std::optional { std::unique_lock lock(mutex_); if (!engine_) { @@ -155,9 +158,15 @@ auto FlutterWindowController::CreateFlutterWindow(std::wstring const& title, auto window{std::make_unique(engine_, win32_)}; + std::optional const parent_hwnd{ + parent_view_id.has_value() && + windows_.find(parent_view_id.value()) != windows_.end() + ? std::optional{windows_[parent_view_id.value()]->GetHandle()} + : std::nullopt}; + lock.unlock(); - if (!window->Create(title, size, archetype)) { + if (!window->Create(title, size, archetype, parent_hwnd, positioner)) { return std::nullopt; } @@ -171,12 +180,12 @@ auto FlutterWindowController::CreateFlutterWindow(std::wstring const& title, auto const view_id{window->GetFlutterViewId()}; windows_[view_id] = std::move(window); - SendOnWindowCreated(view_id, std::nullopt); + SendOnWindowCreated(view_id, parent_view_id); WindowMetadata result{.view_id = view_id, .archetype = archetype, .size = GetWindowSize(view_id), - .parent_id = std::nullopt}; + .parent_id = parent_view_id}; return result; } @@ -209,6 +218,8 @@ void FlutterWindowController::MethodCallHandler(MethodCall<> const& call, MethodResult<>& result) { if (call.method_name() == "createWindow") { HandleCreateWindow(WindowArchetype::regular, call, result); + } else if (call.method_name() == "createPopup") { + HandleCreateWindow(WindowArchetype::popup, call, result); } else if (call.method_name() == "destroyWindow") { HandleDestroyWindow(call, result); } else { @@ -248,6 +259,39 @@ auto FlutterWindowController::MessageHandler(HWND hwnd, } } return 0; + case WM_ACTIVATE: + if (wparam != WA_INACTIVE) { + if (auto* const window{Win32Window::GetThisFromHandle(hwnd)}) { + if (window->GetArchetype() != WindowArchetype::popup) { + // If a non-popup window is activated, close popups for all windows + std::unique_lock lock(mutex_); + auto it{windows_.begin()}; + while (it != windows_.end()) { + lock.unlock(); + auto const num_popups_closed{it->second->CloseChildPopups()}; + lock.lock(); + if (num_popups_closed > 0) { + it = windows_.begin(); + } else { + ++it; + } + } + } else { + // If a popup window is activated, close its child popups + window->CloseChildPopups(); + } + } + } + break; + case WM_ACTIVATEAPP: + if (wparam == FALSE) { + if (auto* const window{Win32Window::GetThisFromHandle(hwnd)}) { + // Close child popups from all windows if a window + // belonging to a different application is being activated + window->CloseChildPopups(); + } + } + break; case WM_SIZE: { std::lock_guard lock{mutex_}; auto const it{std::find_if(windows_.begin(), windows_.end(), @@ -334,9 +378,100 @@ void FlutterWindowController::HandleCreateWindow(WindowArchetype archetype, return; } + std::optional positioner; + std::optional anchor_rect; + + if (archetype == WindowArchetype::popup) { + if (auto const anchor_rect_it{map->find(EncodableValue("anchorRect"))}; + anchor_rect_it != map->end()) { + if (!anchor_rect_it->second.IsNull()) { + auto const anchor_rect_list{ + GetListValuesForKeyOrSendError("anchorRect", map, result)}; + if (!anchor_rect_list) { + return; + } + anchor_rect = + WindowRectangle{{anchor_rect_list->at(0), anchor_rect_list->at(1)}, + {anchor_rect_list->at(2), anchor_rect_list->at(3)}}; + } + } else { + result.Error(kErrorCodeInvalidValue, + "Map does not contain required 'anchorRect' key."); + return; + } + + auto const positioner_parent_anchor{GetSingleValueForKeyOrSendError( + "positionerParentAnchor", map, result)}; + if (!positioner_parent_anchor) { + return; + } + auto const positioner_child_anchor{GetSingleValueForKeyOrSendError( + "positionerChildAnchor", map, result)}; + if (!positioner_child_anchor) { + return; + } + auto const child_anchor{ + static_cast(positioner_child_anchor.value())}; + + auto const positioner_offset_list{GetListValuesForKeyOrSendError( + "positionerOffset", map, result)}; + if (!positioner_offset_list) { + return; + } + auto const positioner_constraint_adjustment{ + GetSingleValueForKeyOrSendError("positionerConstraintAdjustment", + map, result)}; + if (!positioner_constraint_adjustment) { + return; + } + positioner = WindowPositioner{ + .anchor_rect = anchor_rect, + .parent_anchor = static_cast( + positioner_parent_anchor.value()), + .child_anchor = child_anchor, + .offset = {positioner_offset_list->at(0), + positioner_offset_list->at(1)}, + .constraint_adjustment = + static_cast( + positioner_constraint_adjustment.value())}; + } + + std::optional parent_view_id; + if (archetype == WindowArchetype::popup) { + if (auto const parent_it{map->find(EncodableValue("parent"))}; + parent_it != map->end()) { + if (parent_it->second.IsNull()) { + result.Error(kErrorCodeInvalidValue, + "Value for 'parent' key must not be null."); + return; + } else { + if (auto const* const parent{std::get_if(&parent_it->second)}) { + parent_view_id = *parent >= 0 ? std::optional(*parent) + : std::nullopt; + if (!parent_view_id.has_value() && + archetype == WindowArchetype::popup) { + result.Error(kErrorCodeInvalidValue, + "Value for 'parent' key (" + + std::to_string(parent_view_id.value()) + + ") must be nonnegative."); + return; + } + } else { + result.Error(kErrorCodeInvalidValue, + "Value for 'parent' key must be of type int."); + return; + } + } + } else { + result.Error(kErrorCodeInvalidValue, + "Map does not contain required 'parent' key."); + return; + } + } + if (auto const data_opt{CreateFlutterWindow( title, {.width = size_list->at(0), .height = size_list->at(1)}, - archetype)}) { + archetype, positioner, parent_view_id)}) { auto const& data{data_opt.value()}; result.Success(EncodableValue(EncodableMap{ {EncodableValue("viewId"), EncodableValue(data.view_id)}, diff --git a/shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc b/shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc index fbea55efb506c..2e90d4898e905 100644 --- a/shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc +++ b/shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc @@ -128,6 +128,8 @@ TEST_F(FlutterWindowControllerTest, CreateRegularWindow) { auto const title{L"window"}; WindowSize const size{800, 600}; auto const archetype{WindowArchetype::regular}; + std::optional const positioner; + std::optional const parent_view_id; EXPECT_CALL(*mock_win32_, CreateWindowEx(0, _, StrEq(title), WS_OVERLAPPEDWINDOW, @@ -135,8 +137,8 @@ TEST_F(FlutterWindowControllerTest, CreateRegularWindow) { Gt(size.height), IsNull(), _, _, _)) .Times(1); - auto const result{ - mock_controller_->CreateFlutterWindow(title, size, archetype)}; + auto const result{mock_controller_->CreateFlutterWindow( + title, size, archetype, positioner, parent_view_id)}; ASSERT_TRUE(result.has_value()); EXPECT_EQ(result->view_id, 1); @@ -153,8 +155,8 @@ TEST_F(FlutterWindowControllerTest, DestroyWindow) { WindowSize const size{800, 600}; auto const archetype{WindowArchetype::regular}; - auto const create_result{ - mock_controller_->CreateFlutterWindow(title, size, archetype)}; + auto const create_result{mock_controller_->CreateFlutterWindow( + title, size, archetype, std::nullopt, std::nullopt)}; ASSERT_TRUE(create_result.has_value()); @@ -172,8 +174,8 @@ TEST_F(FlutterWindowControllerTest, DestroyWindowWithInvalidView) { WindowSize const size{800, 600}; auto const archetype{WindowArchetype::regular}; - auto const create_result{ - mock_controller_->CreateFlutterWindow(title, size, archetype)}; + auto const create_result{mock_controller_->CreateFlutterWindow( + title, size, archetype, std::nullopt, std::nullopt)}; ASSERT_TRUE(create_result.has_value()); @@ -196,8 +198,8 @@ TEST_F(FlutterWindowControllerTest, SendOnWindowCreated) { EXPECT_CALL(*mock_controller_, SendOnWindowCreated(1, Eq(std::nullopt))) .Times(1); - auto const create_result{ - mock_controller_->CreateFlutterWindow(title, size, archetype)}; + auto const create_result{mock_controller_->CreateFlutterWindow( + title, size, archetype, std::nullopt, std::nullopt)}; } TEST_F(FlutterWindowControllerTest, SendOnWindowDestroyed) { @@ -209,8 +211,8 @@ TEST_F(FlutterWindowControllerTest, SendOnWindowDestroyed) { WindowSize const size{800, 600}; auto const archetype{WindowArchetype::regular}; - auto const create_result{ - mock_controller_->CreateFlutterWindow(title, size, archetype)}; + auto const create_result{mock_controller_->CreateFlutterWindow( + title, size, archetype, std::nullopt, std::nullopt)}; ASSERT_TRUE(create_result.has_value()); @@ -228,8 +230,8 @@ TEST_F(FlutterWindowControllerTest, SendOnWindowChangedWhenWindowIsResized) { WindowSize const size{800, 600}; auto const archetype{WindowArchetype::regular}; - auto const create_result{ - mock_controller_->CreateFlutterWindow(title, size, archetype)}; + auto const create_result{mock_controller_->CreateFlutterWindow( + title, size, archetype, std::nullopt, std::nullopt)}; EXPECT_CALL(*mock_controller_, SendOnWindowChanged(1)).Times(1); @@ -271,8 +273,8 @@ TEST_F(FlutterWindowControllerTest, DestroyWindowUsingMethodCall) { auto const title{L"window"}; WindowSize size{800, 600}; auto const archetype{WindowArchetype::regular}; - auto create_result{ - mock_controller_->CreateFlutterWindow(title, size, archetype)}; + auto create_result{mock_controller_->CreateFlutterWindow( + title, size, archetype, std::nullopt, std::nullopt)}; ASSERT_TRUE(create_result.has_value()); diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h index 81355e8048985..ecaf0ce467ffb 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h +++ b/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h @@ -22,7 +22,9 @@ class FlutterWindowController { void SetEngine(std::shared_ptr engine); auto CreateFlutterWindow(std::wstring const& title, WindowSize const& size, - WindowArchetype archetype) + WindowArchetype archetype, + std::optional positioner, + std::optional parent_view_id) -> std::optional; auto DestroyFlutterWindow(FlutterViewId view_id) -> bool; diff --git a/shell/platform/windows/client_wrapper/include/flutter/win32_window.h b/shell/platform/windows/client_wrapper/include/flutter/win32_window.h index 5df1fee8758e4..75d8053f81067 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/win32_window.h +++ b/shell/platform/windows/client_wrapper/include/flutter/win32_window.h @@ -44,12 +44,16 @@ class Win32Window { protected: // Creates a native Win32 window. |title| is the window title string. // |client_size| specifies the requested size of the client rectangle (i.e., - // the size of the view). The window style is determined by |archetype|. - // After successful creation, |OnCreate| is called, and its result is - // returned. Otherwise, the return value is false. + // the size of the view). The window style is determined by |archetype|. For + // |FlutterWindowArchetype::popup|, both |parent| and |positioner| must be + // provided; |positioner| is used only for this archetype. After successful + // creation, |OnCreate| is called, and its result is returned. Otherwise, the + // return value is false. auto Create(std::wstring const& title, WindowSize const& client_size, - WindowArchetype archetype) -> bool; + WindowArchetype archetype, + std::optional parent, + std::optional positioner) -> bool; // Release OS resources associated with window. void Destroy(); @@ -91,6 +95,13 @@ class Win32Window { // The window's archetype (e.g., regular, dialog, popup). WindowArchetype archetype_{WindowArchetype::regular}; + // Windows that have this window as their parent or owner. + std::set children_; + + // The number of popups in |children_|, used to quickly check whether this + // window has any popups. + size_t num_child_popups_{0}; + // Indicates whether closing this window will quit the application. bool quit_on_close_{false}; @@ -99,6 +110,14 @@ class Win32Window { // Handle for hosted child content window. HWND child_content_{nullptr}; + + // Controls whether the non-client area can be redrawn as inactive. + // Enabled by default, but temporarily disabled during child popup destruction + // to prevent flickering. + bool enable_redraw_non_client_as_inactive_{true}; + + // Closes the popups of this window and returns the number of popups closed. + auto CloseChildPopups() -> std::size_t; }; } // namespace flutter diff --git a/shell/platform/windows/client_wrapper/win32_window.cc b/shell/platform/windows/client_wrapper/win32_window.cc index bfdb0f6a0a311..0e95cc9817791 100644 --- a/shell/platform/windows/client_wrapper/win32_window.cc +++ b/shell/platform/windows/client_wrapper/win32_window.cc @@ -53,6 +53,38 @@ auto GetLastErrorAsString() -> std::string { return oss.str(); } +// Estimates the size of the window frame, in physical coordinates, based on +// the given |window_size| (in physical coordinates) and the specified +// |window_style|, |extended_window_style|, and parent window |parent_hwnd|. +auto GetFrameSizeForWindowSize(flutter::WindowSize const& window_size, + DWORD window_style, + DWORD extended_window_style, + HWND parent_hwnd) -> flutter::WindowSize { + RECT frame_rect{0, 0, static_cast(window_size.width), + static_cast(window_size.height)}; + + WNDCLASS window_class{0}; + window_class.lpfnWndProc = DefWindowProc; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.lpszClassName = L"FLUTTER_WIN32_WINDOW_TEMPORARY"; + RegisterClass(&window_class); + + window_style &= ~WS_VISIBLE; + if (auto const window{CreateWindowEx( + extended_window_style, window_class.lpszClassName, L"", window_style, + CW_USEDEFAULT, CW_USEDEFAULT, window_size.width, window_size.height, + parent_hwnd, nullptr, GetModuleHandle(nullptr), nullptr)}) { + DwmGetWindowAttribute(window, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rect, + sizeof(frame_rect)); + DestroyWindow(window); + } + + UnregisterClass(window_class.lpszClassName, nullptr); + + return {static_cast(frame_rect.right - frame_rect.left), + static_cast(frame_rect.bottom - frame_rect.top)}; +} + // 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 @@ -258,7 +290,9 @@ auto Win32Window::GetArchetype() const -> WindowArchetype { auto Win32Window::Create(std::wstring const& title, WindowSize const& client_size, - WindowArchetype archetype) -> bool { + WindowArchetype archetype, + std::optional parent, + std::optional positioner) -> bool { std::lock_guard lock(gActiveWindowMutex); archetype_ = archetype; @@ -280,7 +314,15 @@ auto Win32Window::Create(std::wstring const& title, // TODO break; case WindowArchetype::popup: - // TODO + window_style |= WS_POPUP; + if (auto* const parent_window{ + GetThisFromHandle(parent.value_or(nullptr))}) { + if (parent_window->child_content_ != nullptr) { + SetFocus(parent_window->child_content_); + } + parent_window->children_.insert(this); + ++parent_window->num_child_popups_; + } break; case WindowArchetype::tip: // TODO @@ -292,10 +334,83 @@ auto Win32Window::Create(std::wstring const& title, } // Window rectangle in physical coordinates. - // Default positioning values (CW_USEDEFAULT) are used. + // Default positioning values (CW_USEDEFAULT) are used + // if the window has no parent or positioner. auto const window_rect{[&]() -> WindowRectangle { auto const window_size{GetWindowSizeForClientSize( - client_size, window_style, extended_window_style, nullptr)}; + client_size, window_style, extended_window_style, + parent.value_or(nullptr))}; + if (parent && positioner) { + auto const frame_size{GetFrameSizeForWindowSize( + window_size, window_style, extended_window_style, parent.value())}; + + // The rectangle of the parent's client area, in physical coordinates + auto const parent_rect{[](HWND parent_window) -> WindowRectangle { + RECT client_rect; + GetClientRect(parent_window, &client_rect); + POINT top_left{client_rect.left, client_rect.top}; + ClientToScreen(parent_window, &top_left); + POINT bottom_right{client_rect.right, client_rect.bottom}; + ClientToScreen(parent_window, &bottom_right); + return {{top_left.x, top_left.y}, + {bottom_right.x - top_left.x, bottom_right.y - top_left.y}}; + }(parent.value())}; + + // The anchor rectangle, in physical coordinates + auto const anchor_rect{[](WindowPositioner const& positioner, + HWND parent_window, + WindowRectangle const& parent_rect) + -> WindowRectangle { + if (positioner.anchor_rect) { + auto const dpr{FlutterDesktopGetDpiForHWND(parent_window) / + static_cast(USER_DEFAULT_SCREEN_DPI)}; + return { + {parent_rect.top_left.x + + static_cast(positioner.anchor_rect->top_left.x * dpr), + parent_rect.top_left.y + + static_cast(positioner.anchor_rect->top_left.y * dpr)}, + {static_cast(positioner.anchor_rect->size.width * dpr), + static_cast(positioner.anchor_rect->size.height * dpr)}}; + } else { + // If the anchor rect specified in the positioner is std::nullopt, + // return an anchor rect that is equal to the window frame area + RECT frame_rect; + DwmGetWindowAttribute(parent_window, DWMWA_EXTENDED_FRAME_BOUNDS, + &frame_rect, sizeof(frame_rect)); + return {{frame_rect.left, frame_rect.top}, + {frame_rect.right - frame_rect.left, + frame_rect.bottom - frame_rect.top}}; + } + }(positioner.value(), parent.value(), parent_rect)}; + + // Rectangle of the monitor that has the largest area of intersection + // with the anchor rectangle, in physical coordinates + auto const output_rect{ + [](RECT anchor_rect) + -> WindowRectangle { + auto* monitor{ + MonitorFromRect(&anchor_rect, MONITOR_DEFAULTTONEAREST)}; + MONITORINFO mi; + mi.cbSize = sizeof(MONITORINFO); + auto const bounds{GetMonitorInfo(monitor, &mi) ? mi.rcWork + : RECT{0, 0, 0, 0}}; + return {{bounds.left, bounds.top}, + {bounds.right - bounds.left, bounds.bottom - bounds.top}}; + }({.left = static_cast(anchor_rect.top_left.x), + .top = static_cast(anchor_rect.top_left.y), + .right = static_cast(anchor_rect.top_left.x + + anchor_rect.size.width), + .bottom = static_cast(anchor_rect.top_left.y + + anchor_rect.size.height)})}; + + auto const rect{internal::PlaceWindow( + positioner.value(), frame_size, anchor_rect, + positioner->anchor_rect ? parent_rect : anchor_rect, output_rect)}; + + return {rect.top_left, + {rect.size.width + window_size.width - frame_size.width, + rect.size.height + window_size.height - frame_size.height}}; + } return {{CW_USEDEFAULT, CW_USEDEFAULT}, window_size}; }()}; @@ -322,8 +437,8 @@ auto Win32Window::Create(std::wstring const& title, window_handle_ = win32_->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); + window_rect.size.height, parent.value_or(nullptr), nullptr, + GetModuleHandle(nullptr), this); if (!window_handle_) { auto const error_message{GetLastErrorAsString()}; @@ -412,6 +527,17 @@ auto Win32Window::MessageHandler(HWND hwnd, } return 0; + case WM_NCACTIVATE: + if (wparam == FALSE && archetype_ != WindowArchetype::popup) { + if (!enable_redraw_non_client_as_inactive_ || num_child_popups_ > 0) { + // If an inactive title bar is to be drawn, and this is a top-level + // window with popups, force the title bar to be drawn in its active + // colors + return TRUE; + } + } + break; + case WM_MOUSEACTIVATE: if (child_content_ != nullptr) { SetFocus(child_content_); @@ -434,7 +560,35 @@ auto Win32Window::OnCreate() -> bool { return true; } -void Win32Window::OnDestroy() {} +void Win32Window::OnDestroy() { + switch (archetype_) { + case WindowArchetype::regular: + break; + case WindowArchetype::floating_regular: + break; + case WindowArchetype::dialog: + break; + case WindowArchetype::satellite: + break; + case WindowArchetype::popup: + if (auto* const parent_window_handle{GetParent(window_handle_)}) { + if (auto* const parent_window{ + GetThisFromHandle(parent_window_handle)}) { + parent_window->children_.erase(this); + assert(parent_window->num_child_popups_ > 0); + --parent_window->num_child_popups_; + } + } + break; + case WindowArchetype::tip: + break; + default: + std::cerr << "Unhandled window archetype encountered in " + "Win32Window::OnDestroy: " + << static_cast(archetype_) << "\n"; + std::abort(); + } +} // static auto CALLBACK Win32Window::WndProc(HWND hwnd, @@ -459,4 +613,50 @@ auto CALLBACK Win32Window::WndProc(HWND hwnd, return DefWindowProc(hwnd, message, wparam, lparam); } +auto Win32Window::CloseChildPopups() -> std::size_t { + if (num_child_popups_ == 0) { + return 0; + } + + std::set popups; + for (auto* const child : children_) { + if (child->archetype_ == WindowArchetype::popup) { + popups.insert(child); + } + } + + for (auto it{children_.begin()}; it != children_.end();) { + if ((*it)->archetype_ == WindowArchetype::popup) { + it = children_.erase(it); + } else { + ++it; + } + } + + auto const previous_num_child_popups{num_child_popups_}; + + for (auto* popup : popups) { + auto const parent_handle{GetParent(popup->window_handle_)}; + if (auto* const parent{GetThisFromHandle(parent_handle)}) { + // Popups' parents are drawn with active colors even though they are + // actually inactive. When a popup is destroyed, the parent might be + // redrawn as inactive (reflecting its true state) before being redrawn as + // active. To prevent flickering during this transition, disable + // redrawing the non-client area as inactive. + parent->enable_redraw_non_client_as_inactive_ = false; + DestroyWindow(popup->GetHandle()); + parent->enable_redraw_non_client_as_inactive_ = true; + + // Repaint parent window to make sure its title bar is painted with the + // color based on its actual activation state + if (parent->num_child_popups_ == 0) { + SetWindowPos(parent_handle, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); + } + } + } + + return previous_num_child_popups - num_child_popups_; +} + } // namespace flutter From 61f03b36a6b652f94be050191e0c99cfb9d2c5af Mon Sep 17 00:00:00 2001 From: Harlen Batagelo Date: Tue, 22 Oct 2024 14:12:48 -0300 Subject: [PATCH 3/7] Add support for dialogs --- .../flutter_window_controller.cc | 13 +- .../include/flutter/win32_window.h | 18 +- .../windows/client_wrapper/win32_window.cc | 230 ++++++++++++------ 3 files changed, 182 insertions(+), 79 deletions(-) diff --git a/shell/platform/windows/client_wrapper/flutter_window_controller.cc b/shell/platform/windows/client_wrapper/flutter_window_controller.cc index 18978444f12fc..85cf7d43e4624 100644 --- a/shell/platform/windows/client_wrapper/flutter_window_controller.cc +++ b/shell/platform/windows/client_wrapper/flutter_window_controller.cc @@ -218,6 +218,8 @@ void FlutterWindowController::MethodCallHandler(MethodCall<> const& call, MethodResult<>& result) { if (call.method_name() == "createWindow") { HandleCreateWindow(WindowArchetype::regular, call, result); + } else if (call.method_name() == "createDialog") { + HandleCreateWindow(WindowArchetype::dialog, call, result); } else if (call.method_name() == "createPopup") { HandleCreateWindow(WindowArchetype::popup, call, result); } else if (call.method_name() == "destroyWindow") { @@ -437,13 +439,16 @@ void FlutterWindowController::HandleCreateWindow(WindowArchetype archetype, } std::optional parent_view_id; - if (archetype == WindowArchetype::popup) { + if (archetype == WindowArchetype::dialog || + archetype == WindowArchetype::popup) { if (auto const parent_it{map->find(EncodableValue("parent"))}; parent_it != map->end()) { if (parent_it->second.IsNull()) { - result.Error(kErrorCodeInvalidValue, - "Value for 'parent' key must not be null."); - return; + if (archetype != WindowArchetype::dialog) { + result.Error(kErrorCodeInvalidValue, + "Value for 'parent' key must not be null."); + return; + } } else { if (auto const* const parent{std::get_if(&parent_it->second)}) { parent_view_id = *parent >= 0 ? std::optional(*parent) diff --git a/shell/platform/windows/client_wrapper/include/flutter/win32_window.h b/shell/platform/windows/client_wrapper/include/flutter/win32_window.h index 75d8053f81067..fabc26e7f602b 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/win32_window.h +++ b/shell/platform/windows/client_wrapper/include/flutter/win32_window.h @@ -46,9 +46,11 @@ class Win32Window { // |client_size| specifies the requested size of the client rectangle (i.e., // the size of the view). The window style is determined by |archetype|. For // |FlutterWindowArchetype::popup|, both |parent| and |positioner| must be - // provided; |positioner| is used only for this archetype. After successful - // creation, |OnCreate| is called, and its result is returned. Otherwise, the - // return value is false. + // provided; |positioner| is used only for this archetype. For + // |FlutterWindowArchetype::dialog|, a modal dialog is created if |parent| is + // specified; otherwise, the dialog is modeless. After successful creation, + // |OnCreate| is called, and its result is returned. Otherwise, the return + // value is false. auto Create(std::wstring const& title, WindowSize const& client_size, WindowArchetype archetype, @@ -118,6 +120,16 @@ class Win32Window { // Closes the popups of this window and returns the number of popups closed. auto CloseChildPopups() -> std::size_t; + + // Enables or disables this window and all its descendants. + void EnableWindowAndDescendants(bool enable); + + // Enforces modal behavior by enabling the deepest dialog in the subtree + // rooted at the top-level window, along with its descendants, while + // disabling all other windows in the subtree. This ensures that the dialog + // and its children remain active and interactive. If no dialog is found, + // all windows in the subtree are enabled. + void UpdateModalState(); }; } // namespace flutter diff --git a/shell/platform/windows/client_wrapper/win32_window.cc b/shell/platform/windows/client_wrapper/win32_window.cc index 0e95cc9817791..fd4003751fe39 100644 --- a/shell/platform/windows/client_wrapper/win32_window.cc +++ b/shell/platform/windows/client_wrapper/win32_window.cc @@ -308,7 +308,22 @@ auto Win32Window::Create(std::wstring const& title, // TODO break; case WindowArchetype::dialog: - // TODO + window_style |= WS_OVERLAPPED | WS_CAPTION; + extended_window_style |= WS_EX_DLGMODALFRAME; + if (!parent) { + // If the dialog has no parent, add a minimize box and a system menu + // (which includes a close button) + window_style |= WS_MINIMIZEBOX | WS_SYSMENU; + } else { + // If the parent window has the WS_EX_TOOLWINDOW style, apply the same + // style to the dialog + if (GetWindowLongPtr(parent.value(), GWL_EXSTYLE) & WS_EX_TOOLWINDOW) { + extended_window_style |= WS_EX_TOOLWINDOW; + } + if (auto* const parent_window{GetThisFromHandle(parent.value())}) { + parent_window->children_.insert(this); + } + } break; case WindowArchetype::satellite: // TODO @@ -335,81 +350,98 @@ auto Win32Window::Create(std::wstring const& title, // Window rectangle in physical coordinates. // Default positioning values (CW_USEDEFAULT) are used - // if the window has no parent or positioner. + // if the window has no parent or positioner. Parented dialogs will be + // centered in the parent's frame. auto const window_rect{[&]() -> WindowRectangle { auto const window_size{GetWindowSizeForClientSize( client_size, window_style, extended_window_style, parent.value_or(nullptr))}; - if (parent && positioner) { - auto const frame_size{GetFrameSizeForWindowSize( - window_size, window_style, extended_window_style, parent.value())}; - - // The rectangle of the parent's client area, in physical coordinates - auto const parent_rect{[](HWND parent_window) -> WindowRectangle { - RECT client_rect; - GetClientRect(parent_window, &client_rect); - POINT top_left{client_rect.left, client_rect.top}; - ClientToScreen(parent_window, &top_left); - POINT bottom_right{client_rect.right, client_rect.bottom}; - ClientToScreen(parent_window, &bottom_right); - return {{top_left.x, top_left.y}, - {bottom_right.x - top_left.x, bottom_right.y - top_left.y}}; - }(parent.value())}; - - // The anchor rectangle, in physical coordinates - auto const anchor_rect{[](WindowPositioner const& positioner, - HWND parent_window, - WindowRectangle const& parent_rect) - -> WindowRectangle { - if (positioner.anchor_rect) { - auto const dpr{FlutterDesktopGetDpiForHWND(parent_window) / - static_cast(USER_DEFAULT_SCREEN_DPI)}; - return { - {parent_rect.top_left.x + - static_cast(positioner.anchor_rect->top_left.x * dpr), - parent_rect.top_left.y + - static_cast(positioner.anchor_rect->top_left.y * dpr)}, - {static_cast(positioner.anchor_rect->size.width * dpr), - static_cast(positioner.anchor_rect->size.height * dpr)}}; - } else { - // If the anchor rect specified in the positioner is std::nullopt, - // return an anchor rect that is equal to the window frame area - RECT frame_rect; - DwmGetWindowAttribute(parent_window, DWMWA_EXTENDED_FRAME_BOUNDS, - &frame_rect, sizeof(frame_rect)); - return {{frame_rect.left, frame_rect.top}, - {frame_rect.right - frame_rect.left, - frame_rect.bottom - frame_rect.top}}; - } - }(positioner.value(), parent.value(), parent_rect)}; - - // Rectangle of the monitor that has the largest area of intersection - // with the anchor rectangle, in physical coordinates - auto const output_rect{ - [](RECT anchor_rect) - -> WindowRectangle { - auto* monitor{ - MonitorFromRect(&anchor_rect, MONITOR_DEFAULTTONEAREST)}; - MONITORINFO mi; - mi.cbSize = sizeof(MONITORINFO); - auto const bounds{GetMonitorInfo(monitor, &mi) ? mi.rcWork - : RECT{0, 0, 0, 0}}; - return {{bounds.left, bounds.top}, - {bounds.right - bounds.left, bounds.bottom - bounds.top}}; - }({.left = static_cast(anchor_rect.top_left.x), - .top = static_cast(anchor_rect.top_left.y), - .right = static_cast(anchor_rect.top_left.x + - anchor_rect.size.width), - .bottom = static_cast(anchor_rect.top_left.y + - anchor_rect.size.height)})}; - - auto const rect{internal::PlaceWindow( - positioner.value(), frame_size, anchor_rect, - positioner->anchor_rect ? parent_rect : anchor_rect, output_rect)}; - - return {rect.top_left, - {rect.size.width + window_size.width - frame_size.width, - rect.size.height + window_size.height - frame_size.height}}; + if (parent) { + if (positioner) { + auto const frame_size{GetFrameSizeForWindowSize( + window_size, window_style, extended_window_style, parent.value())}; + + // The rectangle of the parent's client area, in physical coordinates + auto const parent_rect{[](HWND parent_window) -> WindowRectangle { + RECT client_rect; + GetClientRect(parent_window, &client_rect); + POINT top_left{client_rect.left, client_rect.top}; + ClientToScreen(parent_window, &top_left); + POINT bottom_right{client_rect.right, client_rect.bottom}; + ClientToScreen(parent_window, &bottom_right); + return {{top_left.x, top_left.y}, + {bottom_right.x - top_left.x, bottom_right.y - top_left.y}}; + }(parent.value())}; + + // The anchor rectangle, in physical coordinates + auto const anchor_rect{[](WindowPositioner const& positioner, + HWND parent_window, + WindowRectangle const& parent_rect) + -> WindowRectangle { + if (positioner.anchor_rect) { + auto const dpr{FlutterDesktopGetDpiForHWND(parent_window) / + static_cast(USER_DEFAULT_SCREEN_DPI)}; + return { + {parent_rect.top_left.x + + static_cast(positioner.anchor_rect->top_left.x * dpr), + parent_rect.top_left.y + + static_cast(positioner.anchor_rect->top_left.y * + dpr)}, + {static_cast(positioner.anchor_rect->size.width * dpr), + static_cast(positioner.anchor_rect->size.height * dpr)}}; + } else { + // If the anchor rect specified in the positioner is std::nullopt, + // return an anchor rect that is equal to the window frame area + RECT frame_rect; + DwmGetWindowAttribute(parent_window, DWMWA_EXTENDED_FRAME_BOUNDS, + &frame_rect, sizeof(frame_rect)); + return {{frame_rect.left, frame_rect.top}, + {frame_rect.right - frame_rect.left, + frame_rect.bottom - frame_rect.top}}; + } + }(positioner.value(), parent.value(), parent_rect)}; + + // Rectangle of the monitor that has the largest area of intersection + // with the anchor rectangle, in physical coordinates + auto const output_rect{ + [](RECT anchor_rect) + -> WindowRectangle { + auto* monitor{ + MonitorFromRect(&anchor_rect, MONITOR_DEFAULTTONEAREST)}; + MONITORINFO mi; + mi.cbSize = sizeof(MONITORINFO); + auto const bounds{ + GetMonitorInfo(monitor, &mi) ? mi.rcWork : RECT{0, 0, 0, 0}}; + return {{bounds.left, bounds.top}, + {bounds.right - bounds.left, bounds.bottom - bounds.top}}; + }({.left = static_cast(anchor_rect.top_left.x), + .top = static_cast(anchor_rect.top_left.y), + .right = static_cast(anchor_rect.top_left.x + + anchor_rect.size.width), + .bottom = static_cast(anchor_rect.top_left.y + + anchor_rect.size.height)})}; + + auto const rect{internal::PlaceWindow( + positioner.value(), frame_size, anchor_rect, + positioner->anchor_rect ? parent_rect : anchor_rect, output_rect)}; + + return {rect.top_left, + {rect.size.width + window_size.width - frame_size.width, + rect.size.height + window_size.height - frame_size.height}}; + } else if (archetype == WindowArchetype::dialog) { + // Center parented dialog in the parent frame + RECT parent_frame; + DwmGetWindowAttribute(parent.value(), DWMWA_EXTENDED_FRAME_BOUNDS, + &parent_frame, sizeof(parent_frame)); + WindowPoint const top_left{ + static_cast( + (parent_frame.left + parent_frame.right - window_size.width) * + 0.5), + static_cast( + (parent_frame.top + parent_frame.bottom - window_size.height) * + 0.5)}; + return {top_left, window_size}; + } } return {{CW_USEDEFAULT, CW_USEDEFAULT}, window_size}; }()}; @@ -464,6 +496,10 @@ auto Win32Window::Create(std::wstring const& title, UpdateTheme(window_handle_); + if (archetype == WindowArchetype::dialog && parent) { + UpdateModalState(); + } + gActiveWindowCount++; ShowWindow(window_handle_, SW_SHOW); @@ -567,6 +603,14 @@ void Win32Window::OnDestroy() { case WindowArchetype::floating_regular: break; case WindowArchetype::dialog: + if (auto* const owner_window_handle{ + GetWindow(window_handle_, GW_OWNER)}) { + if (auto* const owner_window{GetThisFromHandle(owner_window_handle)}) { + owner_window->children_.erase(this); + } + UpdateModalState(); + SetFocus(owner_window_handle); + } break; case WindowArchetype::satellite: break; @@ -659,4 +703,46 @@ auto Win32Window::CloseChildPopups() -> std::size_t { return previous_num_child_popups - num_child_popups_; } +void Win32Window::EnableWindowAndDescendants(bool enable) { + EnableWindow(window_handle_, enable); + for (auto* const child : children_) { + child->EnableWindowAndDescendants(enable); + } +} + +void Win32Window::UpdateModalState() { + auto const find_deepest_dialog{ + [](Win32Window* window, auto&& self) -> Win32Window* { + Win32Window* deepest_dialog{nullptr}; + if (window->archetype_ == WindowArchetype::dialog) { + deepest_dialog = window; + } + for (auto* const child : window->children_) { + if (auto* const child_deepest_dialog{self(child, self)}) { + deepest_dialog = child_deepest_dialog; + } + } + return deepest_dialog; + }}; + + auto const get_parent_or_owner{[](HWND window) -> HWND { + auto const parent{GetParent(window)}; + return parent ? parent : GetWindow(window, GW_OWNER); + }}; + + auto* root_ancestor_handle{window_handle_}; + while (auto* next{get_parent_or_owner(root_ancestor_handle)}) { + root_ancestor_handle = next; + } + if (auto* const root_ancestor{GetThisFromHandle(root_ancestor_handle)}) { + if (auto* const deepest_dialog{ + find_deepest_dialog(root_ancestor, find_deepest_dialog)}) { + root_ancestor->EnableWindowAndDescendants(false); + deepest_dialog->EnableWindowAndDescendants(true); + } else { + root_ancestor->EnableWindowAndDescendants(true); + } + } +} + } // namespace flutter From c099e6ed257b3667d9a31c877e4e641fc43cc81a Mon Sep 17 00:00:00 2001 From: Harlen Batagelo Date: Tue, 22 Oct 2024 14:21:18 -0300 Subject: [PATCH 4/7] Add support for satellites --- .../flutter_window_controller.cc | 103 +++++++++++++++++- .../flutter/flutter_window_controller.h | 13 +++ .../include/flutter/win32_window.h | 18 ++- .../windows/client_wrapper/win32_window.cc | 83 +++++++++++++- 4 files changed, 207 insertions(+), 10 deletions(-) diff --git a/shell/platform/windows/client_wrapper/flutter_window_controller.cc b/shell/platform/windows/client_wrapper/flutter_window_controller.cc index 85cf7d43e4624..104d16b200905 100644 --- a/shell/platform/windows/client_wrapper/flutter_window_controller.cc +++ b/shell/platform/windows/client_wrapper/flutter_window_controller.cc @@ -104,6 +104,11 @@ auto ArchetypeToWideString(flutter::WindowArchetype archetype) -> std::wstring { std::abort(); } +auto GetParentOrOwner(HWND window) -> HWND { + auto const parent{GetParent(window)}; + return parent ? parent : GetWindow(window, GW_OWNER); +} + } // namespace namespace flutter { @@ -196,6 +201,16 @@ auto FlutterWindowController::DestroyFlutterWindow(FlutterViewId view_id) auto it{windows_.find(view_id)}; if (it != windows_.end()) { auto* const window{it->second.get()}; + auto const window_handle{window->GetHandle()}; + + if (window->GetArchetype() == WindowArchetype::dialog && + GetWindow(window_handle, GW_OWNER)) { + // Temporarily disable satellite hiding. This prevents satellites from + // flickering because of briefly hiding and showing between the + // destruction of a modal dialog and the transfer of focus to the owner + // window. + disable_satellite_hiding_ = window_handle; + } lock.unlock(); @@ -220,6 +235,8 @@ void FlutterWindowController::MethodCallHandler(MethodCall<> const& call, HandleCreateWindow(WindowArchetype::regular, call, result); } else if (call.method_name() == "createDialog") { HandleCreateWindow(WindowArchetype::dialog, call, result); + } else if (call.method_name() == "createSatellite") { + HandleCreateWindow(WindowArchetype::satellite, call, result); } else if (call.method_name() == "createPopup") { HandleCreateWindow(WindowArchetype::popup, call, result); } else if (call.method_name() == "destroyWindow") { @@ -258,6 +275,12 @@ auto FlutterWindowController::MessageHandler(HWND hwnd, } SendOnWindowDestroyed(view_id); + + if (disable_satellite_hiding_ == hwnd) { + // Re-enable satellite hiding by clearing the window handle now that + // the window is fully destroyed + disable_satellite_hiding_ = nullptr; + } } } return 0; @@ -283,14 +306,16 @@ auto FlutterWindowController::MessageHandler(HWND hwnd, window->CloseChildPopups(); } } + ShowWindowAndAncestorsSatellites(hwnd); } break; case WM_ACTIVATEAPP: if (wparam == FALSE) { if (auto* const window{Win32Window::GetThisFromHandle(hwnd)}) { - // Close child popups from all windows if a window + // Close child popups and hide satellites from all windows if a window // belonging to a different application is being activated window->CloseChildPopups(); + HideWindowsSatellites(nullptr); } } break; @@ -383,7 +408,8 @@ void FlutterWindowController::HandleCreateWindow(WindowArchetype archetype, std::optional positioner; std::optional anchor_rect; - if (archetype == WindowArchetype::popup) { + if (archetype == WindowArchetype::satellite || + archetype == WindowArchetype::popup) { if (auto const anchor_rect_it{map->find(EncodableValue("anchorRect"))}; anchor_rect_it != map->end()) { if (!anchor_rect_it->second.IsNull()) { @@ -440,6 +466,7 @@ void FlutterWindowController::HandleCreateWindow(WindowArchetype archetype, std::optional parent_view_id; if (archetype == WindowArchetype::dialog || + archetype == WindowArchetype::satellite || archetype == WindowArchetype::popup) { if (auto const parent_it{map->find(EncodableValue("parent"))}; parent_it != map->end()) { @@ -454,7 +481,8 @@ void FlutterWindowController::HandleCreateWindow(WindowArchetype archetype, parent_view_id = *parent >= 0 ? std::optional(*parent) : std::nullopt; if (!parent_view_id.has_value() && - archetype == WindowArchetype::popup) { + (archetype == WindowArchetype::satellite || + archetype == WindowArchetype::popup)) { result.Error(kErrorCodeInvalidValue, "Value for 'parent' key (" + std::to_string(parent_view_id.value()) + @@ -544,4 +572,73 @@ WindowSize FlutterWindowController::GetWindowSize( return {static_cast(width), static_cast(height)}; } +void FlutterWindowController::HideWindowsSatellites(HWND opt_out_hwnd) { + if (disable_satellite_hiding_) { + return; + } + + // Helper function to check whether |hwnd| is a descendant of |ancestor|. + auto const is_descendant_of{[](HWND hwnd, HWND ancestor) -> bool { + auto current{ancestor}; + while (current) { + current = GetParentOrOwner(current); + if (current == hwnd) { + return true; + } + } + return false; + }}; + + // Helper function to check whether |window| has a child dialog. + auto const has_dialog{[](Win32Window* window) -> bool { + for (auto* const child : window->GetChildren()) { + if (child->GetArchetype() == WindowArchetype::dialog) { + return true; + } + } + return false; + }}; + + std::lock_guard const lock(mutex_); + for (auto const& [_, window] : windows_) { + if (window->GetHandle() == opt_out_hwnd || + is_descendant_of(window->GetHandle(), opt_out_hwnd)) { + continue; + } + + for (auto* const child : window->GetChildren()) { + if (child->GetArchetype() != WindowArchetype::satellite) { + continue; + } + if (!has_dialog(child)) { + ShowWindow(child->GetHandle(), SW_HIDE); + } + } + } +} + +void FlutterWindowController::ShowWindowAndAncestorsSatellites(HWND hwnd) { + if (disable_satellite_hiding_) { + return; + } + + auto current{hwnd}; + while (current) { + for (auto* const child : + Win32Window::GetThisFromHandle(current)->GetChildren()) { + if (child->GetArchetype() == WindowArchetype::satellite) { + ShowWindow(child->GetHandle(), SW_SHOWNOACTIVATE); + } + } + current = GetParentOrOwner(current); + } + + // Hide satellites of all other top-level windows + if (auto* const window{Win32Window::GetThisFromHandle(hwnd)}) { + if (window->GetArchetype() != WindowArchetype::satellite) { + HideWindowsSatellites(hwnd); + } + } +} + } // namespace flutter diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h index ecaf0ce467ffb..cbe5099d4d35b 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h +++ b/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h @@ -58,6 +58,19 @@ class FlutterWindowController { MethodCall<> const& call, MethodResult<>& result); void HandleDestroyWindow(MethodCall<> const& call, MethodResult<>& result); + // Hides all satellite windows in the application, except those that are + // descendants of |opt_out_hwnd| or have a dialog as a child. By default, + // |opt_out_hwnd| is null, so no window is excluded. + void HideWindowsSatellites(HWND opt_out_hwnd = nullptr); + // Shows the satellite windows of |hwnd| and of its ancestors. + void ShowWindowAndAncestorsSatellites(HWND hwnd); + + // Controls whether satellites are hidden when their top-level window + // and all its children become inactive. If null, satellite hiding + // is enabled. If not null, it contains the handle of the window that + // disabled the hiding, and it will be reset when the window if fully + // destroyed. + HWND disable_satellite_hiding_{nullptr}; mutable std::mutex mutex_; std::shared_ptr win32_; diff --git a/shell/platform/windows/client_wrapper/include/flutter/win32_window.h b/shell/platform/windows/client_wrapper/include/flutter/win32_window.h index fabc26e7f602b..69f02b0605ed5 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/win32_window.h +++ b/shell/platform/windows/client_wrapper/include/flutter/win32_window.h @@ -41,16 +41,19 @@ class Win32Window { // Returns the current window archetype. auto GetArchetype() const -> WindowArchetype; + // Returns the child windows. + auto GetChildren() const -> std::set const&; + protected: // Creates a native Win32 window. |title| is the window title string. // |client_size| specifies the requested size of the client rectangle (i.e., // the size of the view). The window style is determined by |archetype|. For - // |FlutterWindowArchetype::popup|, both |parent| and |positioner| must be - // provided; |positioner| is used only for this archetype. For - // |FlutterWindowArchetype::dialog|, a modal dialog is created if |parent| is - // specified; otherwise, the dialog is modeless. After successful creation, - // |OnCreate| is called, and its result is returned. Otherwise, the return - // value is false. + // |FlutterWindowArchetype::satellite| and |FlutterWindowArchetype::popup|, + // both |parent| and |positioner| must be provided; |positioner| is used only + // for these archetypes. For |FlutterWindowArchetype::dialog|, a modal dialog + // is created if |parent| is specified; otherwise, the dialog is modeless. + // After successful creation, |OnCreate| is called, and its result is + // returned. Otherwise, the return value is false. auto Create(std::wstring const& title, WindowSize const& client_size, WindowArchetype archetype, @@ -113,6 +116,9 @@ class Win32Window { // Handle for hosted child content window. HWND child_content_{nullptr}; + // Offset between this window's position and its owner's position. + POINT offset_from_owner_{0, 0}; + // Controls whether the non-client area can be redrawn as inactive. // Enabled by default, but temporarily disabled during child popup destruction // to prevent flickering. diff --git a/shell/platform/windows/client_wrapper/win32_window.cc b/shell/platform/windows/client_wrapper/win32_window.cc index fd4003751fe39..f9c702e0a9282 100644 --- a/shell/platform/windows/client_wrapper/win32_window.cc +++ b/shell/platform/windows/client_wrapper/win32_window.cc @@ -135,6 +135,22 @@ auto GetWindowSizeForClientSize(flutter::WindowSize const& client_size, static_cast(rect.bottom - rect.top)}; } +// Calculates the offset from the top-left corner of |from| to the top-left +// corner of |to|. If either window handle is null or if the window positions +// cannot be retrieved, the offset will be (0, 0). +auto GetOffsetBetweenWindows(HWND from, HWND to) -> POINT { + POINT offset{0, 0}; + if (to && from) { + RECT to_rect; + RECT from_rect; + if (GetWindowRect(to, &to_rect) && GetWindowRect(from, &from_rect)) { + offset.x = to_rect.left - from_rect.left; + offset.y = to_rect.top - from_rect.top; + } + } + return offset; +} + // 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. @@ -288,6 +304,10 @@ auto Win32Window::GetArchetype() const -> WindowArchetype { return archetype_; } +auto Win32Window::GetChildren() const -> std::set const& { + return children_; +} + auto Win32Window::Create(std::wstring const& title, WindowSize const& client_size, WindowArchetype archetype, @@ -326,7 +346,18 @@ auto Win32Window::Create(std::wstring const& title, } break; case WindowArchetype::satellite: - // TODO + window_style |= WS_OVERLAPPEDWINDOW & ~WS_MAXIMIZEBOX; + extended_window_style |= WS_EX_TOOLWINDOW; + if (auto* const parent_window{ + GetThisFromHandle(parent.value_or(nullptr))}) { + if (parent_window->child_content_ != nullptr) { + SetFocus(parent_window->child_content_); + } + parent_window->children_.insert(this); + } else { + std::cerr << "The parent of a satellite must not be null.\n"; + std::abort(); + } break; case WindowArchetype::popup: window_style |= WS_POPUP; @@ -494,6 +525,13 @@ auto Win32Window::Create(std::wstring const& title, window_rc.top - top_dropshadow_height, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + if (parent) { + if (auto* const owner_window{GetWindow(window_handle_, GW_OWNER)}) { + offset_from_owner_ = + GetOffsetBetweenWindows(owner_window, window_handle_); + } + } + UpdateTheme(window_handle_); if (archetype == WindowArchetype::dialog && parent) { @@ -547,6 +585,21 @@ auto Win32Window::MessageHandler(HWND hwnd, return 0; } case WM_SIZE: { + if (wparam == SIZE_MAXIMIZED) { + // Hide satellites of the maximized window + for (auto* const child : children_) { + if (child->archetype_ == WindowArchetype::satellite) { + ShowWindow(child->GetHandle(), SW_HIDE); + } + } + } else if (wparam == SIZE_RESTORED) { + // Show satellites of the restored window + for (auto* const child : children_) { + if (child->archetype_ == WindowArchetype::satellite) { + ShowWindow(child->GetHandle(), SW_SHOWNOACTIVATE); + } + } + } if (child_content_ != nullptr) { // Resize and reposition the child content window auto const client_rect{GetClientArea()}; @@ -574,6 +627,28 @@ auto Win32Window::MessageHandler(HWND hwnd, } break; + case WM_MOVE: { + if (auto* const owner_window{GetWindow(window_handle_, GW_OWNER)}) { + offset_from_owner_ = + GetOffsetBetweenWindows(owner_window, window_handle_); + } + + // Move satellites attached to this window + RECT window_rect; + GetWindowRect(hwnd, &window_rect); + for (auto* const child : children_) { + if (child->archetype_ == WindowArchetype::satellite) { + RECT rect_satellite; + GetWindowRect(child->GetHandle(), &rect_satellite); + MoveWindow(child->GetHandle(), + window_rect.left + child->offset_from_owner_.x, + window_rect.top + child->offset_from_owner_.y, + rect_satellite.right - rect_satellite.left, + rect_satellite.bottom - rect_satellite.top, FALSE); + } + } + } break; + case WM_MOUSEACTIVATE: if (child_content_ != nullptr) { SetFocus(child_content_); @@ -613,6 +688,12 @@ void Win32Window::OnDestroy() { } break; case WindowArchetype::satellite: + if (auto* const owner_window_handle{ + GetWindow(window_handle_, GW_OWNER)}) { + if (auto* const owner_window{GetThisFromHandle(owner_window_handle)}) { + owner_window->children_.erase(this); + } + } break; case WindowArchetype::popup: if (auto* const parent_window_handle{GetParent(window_handle_)}) { From ab8e11324f3ec4e1523173bfa6497be1409f8197 Mon Sep 17 00:00:00 2001 From: Harlen Batagelo Date: Tue, 17 Dec 2024 14:54:00 -0300 Subject: [PATCH 5/7] Refactor multi-window support --- ci/licenses_golden/excluded_files | 4 +- ci/licenses_golden/licenses_flutter | 9 +- common/settings.h | 4 + shell/common/switches.cc | 11 + shell/common/switches.h | 4 + shell/platform/common/BUILD.gn | 11 +- shell/platform/common/client_wrapper/BUILD.gn | 1 - .../client_wrapper/core_wrapper_files.gni | 2 - .../client_wrapper/windowing_unittests.cc | 645 ------------- .../common/{client_wrapper => }/windowing.cc | 99 +- .../include/flutter => }/windowing.h | 58 +- shell/platform/common/windowing_unittests.cc | 528 ++++++++++ shell/platform/windows/BUILD.gn | 8 + .../platform/windows/client_wrapper/BUILD.gn | 14 - .../windows/client_wrapper/flutter_engine.cc | 5 +- .../client_wrapper/flutter_view_controller.cc | 20 +- .../flutter_view_controller_unittests.cc | 14 +- .../client_wrapper/flutter_win32_window.cc | 77 -- .../flutter_window_controller.cc | 644 ------------- .../flutter_window_controller_unittests.cc | 296 ------ .../include/flutter/flutter_engine.h | 10 +- .../include/flutter/flutter_view_controller.h | 13 - .../include/flutter/flutter_win32_window.h | 40 - .../flutter/flutter_window_controller.h | 84 -- .../include/flutter/win32_window.h | 143 --- .../include/flutter/win32_wrapper.h | 34 - .../testing/stub_flutter_windows_api.cc | 23 - .../testing/stub_flutter_windows_api.h | 12 - .../windows/client_wrapper/win32_window.cc | 829 ---------------- shell/platform/windows/flutter_host_window.cc | 908 ++++++++++++++++++ shell/platform/windows/flutter_host_window.h | 143 +++ .../windows/flutter_host_window_controller.cc | 341 +++++++ .../windows/flutter_host_window_controller.h | 128 +++ ...lutter_host_window_controller_unittests.cc | 200 ++++ .../windows/flutter_windows_engine.cc | 10 + .../platform/windows/flutter_windows_engine.h | 11 + .../windows/flutter_windows_internal.h | 25 + .../platform/windows/public/flutter_windows.h | 25 - shell/platform/windows/windowing_handler.cc | 339 +++++++ shell/platform/windows/windowing_handler.h | 46 + .../windows/windowing_handler_unittests.cc | 151 +++ 41 files changed, 2969 insertions(+), 3000 deletions(-) delete mode 100644 shell/platform/common/client_wrapper/windowing_unittests.cc rename shell/platform/common/{client_wrapper => }/windowing.cc (77%) rename shell/platform/common/{client_wrapper/include/flutter => }/windowing.h (77%) create mode 100644 shell/platform/common/windowing_unittests.cc delete mode 100644 shell/platform/windows/client_wrapper/flutter_win32_window.cc delete mode 100644 shell/platform/windows/client_wrapper/flutter_window_controller.cc delete mode 100644 shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc delete mode 100644 shell/platform/windows/client_wrapper/include/flutter/flutter_win32_window.h delete mode 100644 shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h delete mode 100644 shell/platform/windows/client_wrapper/include/flutter/win32_window.h delete mode 100644 shell/platform/windows/client_wrapper/include/flutter/win32_wrapper.h delete mode 100644 shell/platform/windows/client_wrapper/win32_window.cc create mode 100644 shell/platform/windows/flutter_host_window.cc create mode 100644 shell/platform/windows/flutter_host_window.h create mode 100644 shell/platform/windows/flutter_host_window_controller.cc create mode 100644 shell/platform/windows/flutter_host_window_controller.h create mode 100644 shell/platform/windows/flutter_host_window_controller_unittests.cc create mode 100644 shell/platform/windows/windowing_handler.cc create mode 100644 shell/platform/windows/windowing_handler.h create mode 100644 shell/platform/windows/windowing_handler_unittests.cc diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index d5897249b3ae7..fc27f01cb807b 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -325,7 +325,6 @@ ../../../flutter/shell/platform/common/client_wrapper/standard_method_codec_unittests.cc ../../../flutter/shell/platform/common/client_wrapper/testing ../../../flutter/shell/platform/common/client_wrapper/texture_registrar_unittests.cc -../../../flutter/shell/platform/common/client_wrapper/windowing_unittests.cc ../../../flutter/shell/platform/common/engine_switches_unittests.cc ../../../flutter/shell/platform/common/flutter_platform_node_delegate_unittests.cc ../../../flutter/shell/platform/common/geometry_unittests.cc @@ -336,6 +335,7 @@ ../../../flutter/shell/platform/common/text_editing_delta_unittests.cc ../../../flutter/shell/platform/common/text_input_model_unittests.cc ../../../flutter/shell/platform/common/text_range_unittests.cc +../../../flutter/shell/platform/common/windowing_unittests.cc ../../../flutter/shell/platform/darwin/Doxyfile ../../../flutter/shell/platform/darwin/common/availability_version_check_unittests.cc ../../../flutter/shell/platform/darwin/common/framework/Source/flutter_codecs_unittest.mm @@ -409,6 +409,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 @@ -429,6 +430,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 6f989d5173dff..8fd68561b46f0 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -47425,7 +47425,6 @@ FILE: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/stan FILE: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/standard_message_codec.h FILE: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/standard_method_codec.h FILE: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/texture_registrar.h -FILE: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/windowing.h FILE: ../../../flutter/shell/platform/common/client_wrapper/plugin_registrar.cc FILE: ../../../flutter/shell/platform/common/client_wrapper/standard_codec.cc FILE: ../../../flutter/shell/platform/common/client_wrapper/texture_registrar_impl.h @@ -47455,6 +47454,8 @@ 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.cc +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 @@ -48198,6 +48199,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 @@ -48247,6 +48252,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..40e145ac0ca37 100644 --- a/shell/platform/common/BUILD.gn +++ b/shell/platform/common/BUILD.gn @@ -142,9 +142,13 @@ source_set("common_cpp_core") { public = [ "geometry.h", "path_utils.h", + "windowing.h", ] - sources = [ "path_utils.cc" ] + sources = [ + "path_utils.cc", + "windowing.cc", + ] public_configs = [ "//flutter:config" ] } @@ -157,7 +161,10 @@ if (enable_unittests) { executable("common_cpp_core_unittests") { testonly = true - sources = [ "path_utils_unittests.cc" ] + sources = [ + "path_utils_unittests.cc", + "windowing_unittests.cc", + ] deps = [ ":common_cpp_core", diff --git a/shell/platform/common/client_wrapper/BUILD.gn b/shell/platform/common/client_wrapper/BUILD.gn index 5fb070ccae6b5..91e7120b65734 100644 --- a/shell/platform/common/client_wrapper/BUILD.gn +++ b/shell/platform/common/client_wrapper/BUILD.gn @@ -51,7 +51,6 @@ executable("client_wrapper_unittests") { "testing/test_codec_extensions.cc", "testing/test_codec_extensions.h", "texture_registrar_unittests.cc", - "windowing_unittests.cc", ] deps = [ diff --git a/shell/platform/common/client_wrapper/core_wrapper_files.gni b/shell/platform/common/client_wrapper/core_wrapper_files.gni index f123787a024d0..c2ee524e0f117 100644 --- a/shell/platform/common/client_wrapper/core_wrapper_files.gni +++ b/shell/platform/common/client_wrapper/core_wrapper_files.gni @@ -25,7 +25,6 @@ core_cpp_client_wrapper_includes = "include/flutter/standard_message_codec.h", "include/flutter/standard_method_codec.h", "include/flutter/texture_registrar.h", - "include/flutter/windowing.h", ], "abspath") @@ -47,7 +46,6 @@ core_cpp_client_wrapper_sources = get_path_info([ "core_implementations.cc", "plugin_registrar.cc", "standard_codec.cc", - "windowing.cc", ], "abspath") diff --git a/shell/platform/common/client_wrapper/windowing_unittests.cc b/shell/platform/common/client_wrapper/windowing_unittests.cc deleted file mode 100644 index b9f3448dbf8b7..0000000000000 --- a/shell/platform/common/client_wrapper/windowing_unittests.cc +++ /dev/null @@ -1,645 +0,0 @@ -// 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/common/client_wrapper/include/flutter/windowing.h" - -#include "gtest/gtest.h" - -namespace flutter { - -using Positioner = WindowPositioner; -using Anchor = Positioner::Anchor; -using Constraint = Positioner::ConstraintAdjustment; -using Rectangle = WindowRectangle; -using Point = WindowPoint; -using Size = WindowSize; - -std::ostream& operator<<(std::ostream& os, Point const& point) { - return os << "(x: " << point.x << ", y: " << point.y << ")"; -} - -std::ostream& operator<<(std::ostream& os, Size const& size) { - return os << "(width: " << size.width << ", height: " << size.height << ")"; -} - -std::ostream& operator<<(std::ostream& os, Rectangle const& rect) { - return os << "(x: " << rect.top_left.x << ", y: " << rect.top_left.y - << ", width: " << rect.size.width - << ", height: " << rect.size.height << ")"; -} - -namespace { - -struct WindowPlacementTest : testing::Test { - struct ClientAnchorsToParentConfig { - Rectangle const display_area{{0, 0}, {800, 600}}; - Size const parent_size{400, 300}; - Size const child_size{100, 50}; - Point const parent_position{ - (display_area.size.width - parent_size.width) / 2, - (display_area.size.height - parent_size.height) / 2}; - } client_anchors_to_parent_config; - - Rectangle const display_area{{0, 0}, {640, 480}}; - Size const parent_size{600, 400}; - Size const child_size{300, 300}; - Rectangle const rectangle_away_from_rhs{{20, 20}, {20, 20}}; - Rectangle const rectangle_near_rhs{{590, 20}, {10, 20}}; - Rectangle const rectangle_away_from_bottom{{20, 20}, {20, 20}}; - Rectangle const rectangle_near_bottom{{20, 380}, {20, 20}}; - Rectangle const rectangle_near_both_sides{{0, 20}, {600, 20}}; - Rectangle const rectangle_near_both_sides_and_bottom{{0, 380}, {600, 20}}; - Rectangle const rectangle_near_all_sides{{0, 20}, {600, 380}}; - Rectangle const rectangle_near_both_bottom_right{{400, 380}, {200, 20}}; - Point const parent_position{ - (display_area.size.width - parent_size.width) / 2, - (display_area.size.height - parent_size.height) / 2}; - - Positioner positioner; - - auto anchor_rect() -> Rectangle { - auto rectangle{positioner.anchor_rect.value()}; - return {rectangle.top_left + parent_position, rectangle.size}; - } - - auto parent_rect() -> Rectangle { return {parent_position, parent_size}; } - - auto on_top_edge() -> Point { - return anchor_rect().top_left - Point{0, child_size.height}; - } - - auto on_right_edge() -> Point { - auto const rect{anchor_rect()}; - return rect.top_left + Point{rect.size.width, 0}; - } - - auto on_left_edge() -> Point { - return anchor_rect().top_left - Point{child_size.width, 0}; - } - - auto on_bottom_edge() -> Point { - auto const rect{anchor_rect()}; - return rect.top_left + Point{0, rect.size.height}; - } -}; - -} // namespace - -TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenRectAnchorRightOfParent) { - auto const& display_area{client_anchors_to_parent_config.display_area}; - auto const& parent_size{client_anchors_to_parent_config.parent_size}; - auto const& child_size{client_anchors_to_parent_config.child_size}; - auto const& parent_position{client_anchors_to_parent_config.parent_position}; - - auto const rect_size{10}; - Rectangle const overlapping_right{ - parent_position + - Point{parent_size.width - rect_size / 2, parent_size.height / 2}, - {rect_size, rect_size}}; - - Positioner const positioner{.anchor_rect = overlapping_right, - .parent_anchor = Anchor::top_right, - .child_anchor = Anchor::top_left, - .constraint_adjustment = static_cast( - static_cast(Constraint::slide_y) | - static_cast(Constraint::resize_x))}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, positioner.anchor_rect.value(), - {parent_position, parent_size}, display_area)}; - - auto const expected_position{ - parent_position + Point{parent_size.width, parent_size.height / 2}}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, child_size); -} - -TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenRectAnchorAboveParent) { - auto const& display_area{client_anchors_to_parent_config.display_area}; - auto const& parent_size{client_anchors_to_parent_config.parent_size}; - auto const& child_size{client_anchors_to_parent_config.child_size}; - auto const& parent_position{client_anchors_to_parent_config.parent_position}; - - auto const rect_size{10}; - Rectangle const overlapping_above{ - parent_position + Point{parent_size.width / 2, -rect_size / 2}, - {rect_size, rect_size}}; - - Positioner const positioner{.anchor_rect = overlapping_above, - .parent_anchor = Anchor::top_right, - .child_anchor = Anchor::bottom_right, - .constraint_adjustment = Constraint::slide_x}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, positioner.anchor_rect.value(), - {parent_position, parent_size}, display_area)}; - - auto const expected_position{parent_position + - Point{parent_size.width / 2 + rect_size, 0} - - static_cast(child_size)}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, child_size); -} - -TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenOffsetRightOfParent) { - auto const& display_area{client_anchors_to_parent_config.display_area}; - auto const& parent_size{client_anchors_to_parent_config.parent_size}; - auto const& child_size{client_anchors_to_parent_config.child_size}; - auto const& parent_position{client_anchors_to_parent_config.parent_position}; - - auto const rect_size{10}; - Rectangle const mid_right{ - parent_position + - Point{parent_size.width - rect_size, parent_size.height / 2}, - {rect_size, rect_size}}; - - Positioner const positioner{.anchor_rect = mid_right, - .parent_anchor = Anchor::top_right, - .child_anchor = Anchor::top_left, - .offset = Point{rect_size, 0}, - .constraint_adjustment = static_cast( - static_cast(Constraint::slide_y) | - static_cast(Constraint::resize_x))}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, positioner.anchor_rect.value(), - {parent_position, parent_size}, display_area)}; - - auto const expected_position{ - parent_position + Point{parent_size.width, parent_size.height / 2}}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, child_size); -} - -TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenOffsetAboveParent) { - auto const& display_area{client_anchors_to_parent_config.display_area}; - auto const& parent_size{client_anchors_to_parent_config.parent_size}; - auto const& child_size{client_anchors_to_parent_config.child_size}; - auto const& parent_position{client_anchors_to_parent_config.parent_position}; - - auto const rect_size{10}; - Rectangle const mid_top{parent_position + Point{parent_size.width / 2, 0}, - {rect_size, rect_size}}; - - Positioner const positioner{.anchor_rect = mid_top, - .parent_anchor = Anchor::top_right, - .child_anchor = Anchor::bottom_right, - .offset = Point{0, -rect_size}, - .constraint_adjustment = Constraint::slide_x}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, positioner.anchor_rect.value(), - {parent_position, parent_size}, display_area)}; - - auto const expected_position{parent_position + - Point{parent_size.width / 2 + rect_size, 0} - - static_cast(child_size)}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, child_size); -} - -TEST_F(WindowPlacementTest, - ClientAnchorsToParentGivenRectAndOffsetBelowLeftParent) { - auto const& display_area{client_anchors_to_parent_config.display_area}; - auto const& parent_size{client_anchors_to_parent_config.parent_size}; - auto const& child_size{client_anchors_to_parent_config.child_size}; - auto const& parent_position{client_anchors_to_parent_config.parent_position}; - - auto const rect_size{10}; - Rectangle const below_left{ - parent_position + Point{-rect_size, parent_size.height}, - {rect_size, rect_size}}; - - Positioner const positioner{.anchor_rect = below_left, - .parent_anchor = Anchor::bottom_left, - .child_anchor = Anchor::top_right, - .offset = Point{-rect_size, rect_size}, - .constraint_adjustment = Constraint::resize_any}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, positioner.anchor_rect.value(), - {parent_position, parent_size}, display_area)}; - - auto const expected_position{parent_position + Point{0, parent_size.height} - - Point{child_size.width, 0}}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, child_size); -} - -TEST_F(WindowPlacementTest, - AttachesToRightEdgeGivenAnchorRectAwayFromRightSide) { - positioner = {.anchor_rect = rectangle_away_from_rhs, - .parent_anchor = Anchor::top_right, - .child_anchor = Anchor::top_left}; - - auto const expected_position{on_right_edge()}; - - auto const child_rect{ - internal::PlaceWindow(positioner, child_size, anchor_rect(), - {parent_position, parent_size}, display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, AttachesToLeftEdgeGivenAnchorRectNearRightSide) { - positioner = {.anchor_rect = rectangle_near_rhs, - .parent_anchor = Anchor::top_left, - .child_anchor = Anchor::top_right}; - - auto const expected_position{on_left_edge()}; - - auto const child_rect{ - internal::PlaceWindow(positioner, child_size, anchor_rect(), - {parent_position, parent_size}, display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, AttachesToRightEdgeGivenAnchorRectNearBothSides) { - positioner = {.anchor_rect = rectangle_near_both_sides, - .parent_anchor = Anchor::top_right, - .child_anchor = Anchor::top_left}; - - auto const expected_position{on_right_edge()}; - - auto const child_rect{ - internal::PlaceWindow(positioner, child_size, anchor_rect(), - {parent_position, parent_size}, display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, AttachesToBottomEdgeGivenAnchorRectAwayFromBottom) { - positioner = {.anchor_rect = rectangle_away_from_bottom, - .parent_anchor = Anchor::bottom_left, - .child_anchor = Anchor::top_left}; - - auto const expected_position{on_bottom_edge()}; - - auto const child_rect{ - internal::PlaceWindow(positioner, child_size, anchor_rect(), - {parent_position, parent_size}, display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, AttachesToTopEdgeGivenAnchorRectNearBottom) { - positioner = {.anchor_rect = rectangle_near_bottom, - .parent_anchor = Anchor::top_left, - .child_anchor = Anchor::bottom_left}; - - auto const expected_position{on_top_edge()}; - - auto const child_rect{ - internal::PlaceWindow(positioner, child_size, anchor_rect(), - {parent_position, parent_size}, display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, AttachesToBottomEdgeGivenAnchorRectNearBothSides) { - positioner = {.anchor_rect = rectangle_near_both_sides, - .parent_anchor = Anchor::bottom_left, - .child_anchor = Anchor::top_left}; - - auto const expected_position{on_bottom_edge()}; - - auto const child_rect{ - internal::PlaceWindow(positioner, child_size, anchor_rect(), - {parent_position, parent_size}, display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, - AttachesToTopEdgeGivenAnchorRectNearBothSidesAndBottom) { - positioner = {.anchor_rect = rectangle_near_both_sides_and_bottom, - .parent_anchor = Anchor::top_left, - .child_anchor = Anchor::bottom_left}; - - auto const expected_position{on_top_edge()}; - - auto const child_rect{ - internal::PlaceWindow(positioner, child_size, anchor_rect(), - {parent_position, parent_size}, display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, AttachesToRightEdgeGivenAnchorRectNearAllSides) { - positioner = {.anchor_rect = rectangle_near_all_sides, - .parent_anchor = Anchor::top_right, - .child_anchor = Anchor::top_left}; - - auto const expected_position{on_right_edge()}; - - auto const child_rect{ - internal::PlaceWindow(positioner, child_size, anchor_rect(), - {parent_position, parent_size}, display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -namespace { -Anchor const all_anchors[]{ - Anchor::top_left, Anchor::top, Anchor::top_right, - Anchor::left, Anchor::center, Anchor::right, - Anchor::bottom_left, Anchor::bottom, Anchor::bottom_right, -}; - -auto position_of(Anchor anchor, Rectangle rectangle) -> Point { - switch (anchor) { - case Anchor::top_left: - return rectangle.top_left; - case Anchor::top: - return rectangle.top_left + Point{rectangle.size.width / 2, 0}; - case Anchor::top_right: - return rectangle.top_left + Point{rectangle.size.width, 0}; - case Anchor::left: - return rectangle.top_left + Point{0, rectangle.size.height / 2}; - case Anchor::center: - return rectangle.top_left + - Point{rectangle.size.width / 2, rectangle.size.height / 2}; - case Anchor::right: - return rectangle.top_left + - Point{rectangle.size.width, rectangle.size.height / 2}; - case Anchor::bottom_left: - return rectangle.top_left + Point{0, rectangle.size.height}; - case Anchor::bottom: - return rectangle.top_left + - Point{rectangle.size.width / 2, rectangle.size.height}; - case Anchor::bottom_right: - return rectangle.top_left + static_cast(rectangle.size); - default: - std::cerr << "Unknown anchor value: " << static_cast(anchor) << '\n'; - std::abort(); - } -} -} // namespace - -TEST_F(WindowPlacementTest, CanAttachByEveryAnchorGivenNoConstraintAdjustment) { - positioner.anchor_rect = Rectangle{{100, 50}, {20, 20}}; - positioner.constraint_adjustment = Constraint{}; - - for (auto const rect_anchor : all_anchors) { - positioner.parent_anchor = rect_anchor; - - auto const anchor_position{position_of(rect_anchor, anchor_rect())}; - - for (auto const window_anchor : all_anchors) { - positioner.child_anchor = window_anchor; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(position_of(window_anchor, child_rect), anchor_position); - } - } -} - -TEST_F(WindowPlacementTest, - PlacementIsFlippedGivenAnchorRectNearRightSideAndOffset) { - auto const x_offset{42}; - auto const y_offset{13}; - - positioner.anchor_rect = rectangle_near_rhs; - positioner.constraint_adjustment = Constraint::flip_x; - positioner.offset = Point{x_offset, y_offset}; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::top_right; - - auto const expected_position{on_left_edge() + Point{-1 * x_offset, y_offset}}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, - PlacementIsFlippedGivenAnchorRectNearBottomAndOffset) { - auto const x_offset{42}; - auto const y_offset{13}; - - positioner.anchor_rect = rectangle_near_bottom; - positioner.constraint_adjustment = Constraint::flip_y; - positioner.offset = Point{x_offset, y_offset}; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::bottom_left; - - auto const expected_position{on_top_edge() + Point{x_offset, -1 * y_offset}}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, - PlacementIsFlippedBothWaysGivenAnchorRectNearBottomRightAndOffset) { - auto const x_offset{42}; - auto const y_offset{13}; - - positioner.anchor_rect = rectangle_near_both_bottom_right; - positioner.constraint_adjustment = Constraint::flip_any; - positioner.offset = Point{x_offset, y_offset}; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::bottom_right; - - auto const expected_position{anchor_rect().top_left - - static_cast(child_size) - - Point{x_offset, y_offset}}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, PlacementCanSlideInXGivenAnchorRectNearRightSide) { - positioner.anchor_rect = rectangle_near_rhs; - positioner.constraint_adjustment = Constraint::slide_x; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::top_right; - - Point const expected_position{ - (display_area.top_left.x + display_area.size.width) - child_size.width, - anchor_rect().top_left.y}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, PlacementCanSlideInXGivenAnchorRectNearLeftSide) { - Rectangle const rectangle_near_left_side{{0, 20}, {20, 20}}; - - positioner.anchor_rect = rectangle_near_left_side; - positioner.constraint_adjustment = Constraint::slide_x; - positioner.child_anchor = Anchor::top_right; - positioner.parent_anchor = Anchor::top_left; - - Point const expected_position{display_area.top_left.x, - anchor_rect().top_left.y}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, PlacementCanSlideInYGivenAnchorRectNearBottom) { - positioner.anchor_rect = rectangle_near_bottom; - positioner.constraint_adjustment = Constraint::slide_y; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::bottom_left; - - Point const expected_position{ - anchor_rect().top_left.x, - (display_area.top_left.y + display_area.size.height) - child_size.height}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, PlacementCanSlideInYGivenAnchorRectNearTop) { - positioner.anchor_rect = rectangle_near_all_sides; - positioner.constraint_adjustment = Constraint::slide_y; - positioner.child_anchor = Anchor::bottom_left; - positioner.parent_anchor = Anchor::top_left; - - Point const expected_position{anchor_rect().top_left.x, - display_area.top_left.y}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, - PlacementCanSlideInXAndYGivenAnchorRectNearBottomRightAndOffset) { - positioner.anchor_rect = rectangle_near_both_bottom_right; - positioner.constraint_adjustment = Constraint::slide_any; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::bottom_left; - - auto const expected_position{ - (display_area.top_left + static_cast(display_area.size)) - - static_cast(child_size)}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, PlacementCanResizeInXGivenAnchorRectNearRightSide) { - positioner.anchor_rect = rectangle_near_rhs; - positioner.constraint_adjustment = Constraint::resize_x; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::top_right; - - auto const expected_position{anchor_rect().top_left + - Point{anchor_rect().size.width, 0}}; - Size const expected_size{ - (display_area.top_left.x + display_area.size.width) - - (anchor_rect().top_left.x + anchor_rect().size.width), - child_size.height}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, expected_size); -} - -TEST_F(WindowPlacementTest, PlacementCanResizeInXGivenAnchorRectNearLeftSide) { - Rectangle const rectangle_near_left_side{{0, 20}, {20, 20}}; - - positioner.anchor_rect = rectangle_near_left_side; - positioner.constraint_adjustment = Constraint::resize_x; - positioner.child_anchor = Anchor::top_right; - positioner.parent_anchor = Anchor::top_left; - - Point const expected_position{display_area.top_left.x, - anchor_rect().top_left.y}; - Size const expected_size{anchor_rect().top_left.x - display_area.top_left.x, - child_size.height}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, expected_size); -} - -TEST_F(WindowPlacementTest, PlacementCanResizeInYGivenAnchorRectNearBottom) { - positioner.anchor_rect = rectangle_near_bottom; - positioner.constraint_adjustment = Constraint::resize_y; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::bottom_left; - - auto const expected_position{anchor_rect().top_left + - Point{0, anchor_rect().size.height}}; - Size const expected_size{ - child_size.width, - (display_area.top_left.y + display_area.size.height) - - (anchor_rect().top_left.y + anchor_rect().size.height)}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, expected_size); -} - -TEST_F(WindowPlacementTest, PlacementCanResizeInYGivenAnchorRectNearTop) { - positioner.anchor_rect = rectangle_near_all_sides; - positioner.constraint_adjustment = Constraint::resize_y; - positioner.child_anchor = Anchor::bottom_left; - positioner.parent_anchor = Anchor::top_left; - - Point const expected_position{anchor_rect().top_left.x, - display_area.top_left.y}; - Size const expected_size{child_size.width, - anchor_rect().top_left.y - display_area.top_left.y}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, expected_size); -} - -TEST_F(WindowPlacementTest, - PlacementCanResizeInXAndYGivenAnchorRectNearBottomRightAndOffset) { - positioner.anchor_rect = rectangle_near_both_bottom_right; - positioner.constraint_adjustment = Constraint::resize_any; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::bottom_right; - - auto const expected_position{anchor_rect().top_left + - static_cast(anchor_rect().size)}; - Size const expected_size{ - (display_area.top_left.x + display_area.size.width) - expected_position.x, - (display_area.top_left.y + display_area.size.height) - - expected_position.y}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, expected_size); -} - -} // namespace flutter diff --git a/shell/platform/common/client_wrapper/windowing.cc b/shell/platform/common/windowing.cc similarity index 77% rename from shell/platform/common/client_wrapper/windowing.cc rename to shell/platform/common/windowing.cc index df30c38e13387..63881ae5d6f55 100644 --- a/shell/platform/common/client_wrapper/windowing.cc +++ b/shell/platform/common/windowing.cc @@ -2,17 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "flutter/shell/platform/common/windowing.h" + #include #include -#include "include/flutter/windowing.h" - namespace flutter { namespace { -auto offset_for(WindowSize const& size, - WindowPositioner::Anchor anchor) -> WindowPoint { +WindowPoint offset_for(WindowSize const& size, + WindowPositioner::Anchor anchor) { switch (anchor) { case WindowPositioner::Anchor::top_left: return {0, 0}; @@ -38,8 +38,8 @@ auto offset_for(WindowSize const& size, } } -auto anchor_position_for(WindowRectangle const& rect, - WindowPositioner::Anchor anchor) -> WindowPoint { +WindowPoint anchor_position_for(WindowRectangle const& rect, + WindowPositioner::Anchor anchor) { switch (anchor) { case WindowPositioner::Anchor::top_left: return rect.top_left; @@ -66,14 +66,12 @@ auto anchor_position_for(WindowRectangle const& rect, } } -auto constrain_to(WindowRectangle const& r, - WindowPoint const& p) -> WindowPoint { +WindowPoint constrain_to(WindowRectangle const& r, WindowPoint const& p) { return {std::clamp(p.x, r.top_left.x, r.top_left.x + r.size.width), std::clamp(p.y, r.top_left.y, r.top_left.y + r.size.height)}; } -auto flip_anchor_x(WindowPositioner::Anchor anchor) - -> WindowPositioner::Anchor { +WindowPositioner::Anchor flip_anchor_x(WindowPositioner::Anchor anchor) { switch (anchor) { case WindowPositioner::Anchor::top_left: return WindowPositioner::Anchor::top_right; @@ -92,8 +90,7 @@ auto flip_anchor_x(WindowPositioner::Anchor anchor) } } -auto flip_anchor_y(WindowPositioner::Anchor anchor) - -> WindowPositioner::Anchor { +WindowPositioner::Anchor flip_anchor_y(WindowPositioner::Anchor anchor) { switch (anchor) { case WindowPositioner::Anchor::top_left: return WindowPositioner::Anchor::bottom_left; @@ -112,31 +109,29 @@ auto flip_anchor_y(WindowPositioner::Anchor anchor) } } -auto flip_offset_x(WindowPoint const& p) -> WindowPoint { +WindowPoint flip_offset_x(WindowPoint const& p) { return {-1 * p.x, p.y}; } -auto flip_offset_y(WindowPoint const& p) -> WindowPoint { +WindowPoint flip_offset_y(WindowPoint const& p) { return {p.x, -1 * p.y}; } } // namespace -namespace internal { - -auto PlaceWindow(WindowPositioner const& positioner, - WindowSize child_size, - WindowRectangle const& anchor_rect, - WindowRectangle const& parent_rect, - WindowRectangle const& output_rect) -> WindowRectangle { +WindowRectangle PlaceWindow(WindowPositioner const& positioner, + WindowSize child_size, + WindowRectangle const& anchor_rect, + WindowRectangle const& parent_rect, + WindowRectangle const& output_rect) { WindowRectangle default_result; { - auto const result{ + WindowPoint const result = constrain_to(parent_rect, anchor_position_for( anchor_rect, positioner.parent_anchor) + positioner.offset) + - offset_for(child_size, positioner.child_anchor)}; + offset_for(child_size, positioner.child_anchor); if (output_rect.contains({result, child_size})) { return WindowRectangle{result, child_size}; @@ -147,12 +142,12 @@ auto PlaceWindow(WindowPositioner const& positioner, if (static_cast(positioner.constraint_adjustment) & static_cast(WindowPositioner::ConstraintAdjustment::flip_x)) { - auto const result{ + WindowPoint const result = constrain_to(parent_rect, anchor_position_for( anchor_rect, flip_anchor_x(positioner.parent_anchor)) + flip_offset_x(positioner.offset)) + - offset_for(child_size, flip_anchor_x(positioner.child_anchor))}; + offset_for(child_size, flip_anchor_x(positioner.child_anchor)); if (output_rect.contains({result, child_size})) { return WindowRectangle{result, child_size}; @@ -161,12 +156,12 @@ auto PlaceWindow(WindowPositioner const& positioner, if (static_cast(positioner.constraint_adjustment) & static_cast(WindowPositioner::ConstraintAdjustment::flip_y)) { - auto const result{ + WindowPoint const result = constrain_to(parent_rect, anchor_position_for( anchor_rect, flip_anchor_y(positioner.parent_anchor)) + flip_offset_y(positioner.offset)) + - offset_for(child_size, flip_anchor_y(positioner.child_anchor))}; + offset_for(child_size, flip_anchor_y(positioner.child_anchor)); if (output_rect.contains({result, child_size})) { return WindowRectangle{result, child_size}; @@ -177,14 +172,14 @@ auto PlaceWindow(WindowPositioner const& positioner, static_cast(WindowPositioner::ConstraintAdjustment::flip_x) && static_cast(positioner.constraint_adjustment) & static_cast(WindowPositioner::ConstraintAdjustment::flip_y)) { - auto const result{ + WindowPoint const result = constrain_to( parent_rect, anchor_position_for(anchor_rect, flip_anchor_x(flip_anchor_y( positioner.parent_anchor))) + flip_offset_x(flip_offset_y(positioner.offset))) + offset_for(child_size, - flip_anchor_x(flip_anchor_y(positioner.child_anchor)))}; + flip_anchor_x(flip_anchor_y(positioner.child_anchor))); if (output_rect.contains({result, child_size})) { return WindowRectangle{result, child_size}; @@ -192,18 +187,18 @@ auto PlaceWindow(WindowPositioner const& positioner, } { - auto result{constrain_to( - parent_rect, - anchor_position_for(anchor_rect, positioner.parent_anchor) + - positioner.offset) + - offset_for(child_size, positioner.child_anchor)}; + WindowPoint result = + constrain_to(parent_rect, anchor_position_for( + anchor_rect, positioner.parent_anchor) + + positioner.offset) + + offset_for(child_size, positioner.child_anchor); if (static_cast(positioner.constraint_adjustment) & static_cast(WindowPositioner::ConstraintAdjustment::slide_x)) { - auto const left_overhang{result.x - output_rect.top_left.x}; - auto const right_overhang{ + int const left_overhang = result.x - output_rect.top_left.x; + int const right_overhang = (result.x + child_size.width) - - (output_rect.top_left.x + output_rect.size.width)}; + (output_rect.top_left.x + output_rect.size.width); if (left_overhang < 0) { result.x -= left_overhang; @@ -214,10 +209,10 @@ auto PlaceWindow(WindowPositioner const& positioner, if (static_cast(positioner.constraint_adjustment) & static_cast(WindowPositioner::ConstraintAdjustment::slide_y)) { - auto const top_overhang{result.y - output_rect.top_left.y}; - auto const bot_overhang{ + int const top_overhang = result.y - output_rect.top_left.y; + int const bot_overhang = (result.y + child_size.height) - - (output_rect.top_left.y + output_rect.size.height)}; + (output_rect.top_left.y + output_rect.size.height); if (top_overhang < 0) { result.y -= top_overhang; @@ -232,18 +227,18 @@ auto PlaceWindow(WindowPositioner const& positioner, } { - auto result{constrain_to( - parent_rect, - anchor_position_for(anchor_rect, positioner.parent_anchor) + - positioner.offset) + - offset_for(child_size, positioner.child_anchor)}; + WindowPoint result = + constrain_to(parent_rect, anchor_position_for( + anchor_rect, positioner.parent_anchor) + + positioner.offset) + + offset_for(child_size, positioner.child_anchor); if (static_cast(positioner.constraint_adjustment) & static_cast(WindowPositioner::ConstraintAdjustment::resize_x)) { - auto const left_overhang{result.x - output_rect.top_left.x}; - auto const right_overhang{ + int const left_overhang = result.x - output_rect.top_left.x; + int const right_overhang = (result.x + child_size.width) - - (output_rect.top_left.x + output_rect.size.width)}; + (output_rect.top_left.x + output_rect.size.width); if (left_overhang < 0) { result.x -= left_overhang; @@ -257,10 +252,10 @@ auto PlaceWindow(WindowPositioner const& positioner, if (static_cast(positioner.constraint_adjustment) & static_cast(WindowPositioner::ConstraintAdjustment::resize_y)) { - auto const top_overhang{result.y - output_rect.top_left.y}; - auto const bot_overhang{ + int const top_overhang = result.y - output_rect.top_left.y; + int const bot_overhang = (result.y + child_size.height) - - (output_rect.top_left.y + output_rect.size.height)}; + (output_rect.top_left.y + output_rect.size.height); if (top_overhang < 0) { result.y -= top_overhang; @@ -280,6 +275,4 @@ auto PlaceWindow(WindowPositioner const& positioner, return default_result; } -} // namespace internal - } // namespace flutter diff --git a/shell/platform/common/client_wrapper/include/flutter/windowing.h b/shell/platform/common/windowing.h similarity index 77% rename from shell/platform/common/client_wrapper/include/flutter/windowing.h rename to shell/platform/common/windowing.h index c25d05c1cda49..7e224ac41f6de 100644 --- a/shell/platform/common/client_wrapper/include/flutter/windowing.h +++ b/shell/platform/common/windowing.h @@ -2,28 +2,26 @@ // 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_CLIENT_WRAPPER_INCLUDE_FLUTTER_WINDOWING_H_ -#define FLUTTER_SHELL_PLATFORM_COMMON_CLIENT_WRAPPER_INCLUDE_FLUTTER_WINDOWING_H_ +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_WINDOWING_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_WINDOWING_H_ #include namespace flutter { -// The unique identifier for a view. +// A unique identifier for a view. using FlutterViewId = int64_t; -// A point (x, y) in 2D space for window positioning. +// A point in 2D space for window positioning using integer coordinates. struct WindowPoint { - int x{0}; - int y{0}; + int x = 0; + int y = 0; - friend auto operator+(WindowPoint const& lhs, - WindowPoint const& rhs) -> WindowPoint { + friend WindowPoint operator+(WindowPoint const& lhs, WindowPoint const& rhs) { return {lhs.x + rhs.x, lhs.y + rhs.y}; } - friend auto operator-(WindowPoint const& lhs, - WindowPoint const& rhs) -> WindowPoint { + friend WindowPoint operator-(WindowPoint const& lhs, WindowPoint const& rhs) { return {lhs.x - rhs.x, lhs.y - rhs.y}; } @@ -32,10 +30,10 @@ struct WindowPoint { } }; -// A size (width, height) in 2D space. +// A 2D size using integer dimensions. struct WindowSize { - int width{0}; - int height{0}; + int width = 0; + int height = 0; explicit operator WindowPoint() const { return {width, height}; } @@ -52,7 +50,7 @@ struct WindowRectangle { // 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 - auto contains(WindowRectangle const& rect) const -> bool { + 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 && @@ -103,9 +101,9 @@ struct WindowPositioner { // rectangle. std::optional anchor_rect; // Specifies which anchor of the parent window to align to. - Anchor parent_anchor{Anchor::center}; + Anchor parent_anchor = Anchor::center; // Specifies which anchor of the child window to align with the parent. - Anchor child_anchor{Anchor::center}; + Anchor child_anchor = Anchor::center; // Offset relative to the position of the anchor on the anchor rectangle and // the anchor on the child. WindowPoint offset; @@ -118,24 +116,20 @@ struct WindowPositioner { enum class WindowArchetype { // Regular top-level window. regular, - // A window that is on a layer above regular windows and is not dockable. - floating_regular, // Dialog window. dialog, - // Satellite window attached to a regular, floating_regular or dialog window. + // Satellite window attached to a regular or dialog window. satellite, // Popup. popup, - // Tooltip. - tip, }; // 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 (e.g., regular, dialog, popup, etc). - WindowArchetype archetype{WindowArchetype::regular}; + 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 @@ -143,8 +137,6 @@ struct WindowMetadata { std::optional parent_id; }; -namespace internal { - // Computes the screen-space rectangle for a child window placed according to // the given |positioner|. |child_size| is the frame size of the child window. // |anchor_rect| is the rectangle relative to which the child window is placed. @@ -153,14 +145,12 @@ namespace internal { // are in physical coordinates. Note: WindowPositioner::anchor_rect is not used // in this function; use |anchor_rect| to set the anchor rectangle for the // child. -auto PlaceWindow(WindowPositioner const& positioner, - WindowSize child_size, - WindowRectangle const& anchor_rect, - WindowRectangle const& parent_rect, - WindowRectangle const& output_rect) -> WindowRectangle; - -} // namespace internal +WindowRectangle PlaceWindow(WindowPositioner const& positioner, + WindowSize child_size, + WindowRectangle const& anchor_rect, + WindowRectangle const& parent_rect, + WindowRectangle const& output_rect); } // namespace flutter -#endif // FLUTTER_SHELL_PLATFORM_COMMON_CLIENT_WRAPPER_INCLUDE_FLUTTER_WINDOWING_H_ +#endif // FLUTTER_SHELL_PLATFORM_COMMON_WINDOWING_H_ diff --git a/shell/platform/common/windowing_unittests.cc b/shell/platform/common/windowing_unittests.cc new file mode 100644 index 0000000000000..cc21cd8cf8f94 --- /dev/null +++ b/shell/platform/common/windowing_unittests.cc @@ -0,0 +1,528 @@ +// 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/common/windowing.h" + +#include + +#include "flutter/fml/logging.h" +#include "gtest/gtest.h" + +namespace flutter { + +namespace { + +using Positioner = WindowPositioner; +using Anchor = Positioner::Anchor; +using Constraint = Positioner::ConstraintAdjustment; +using Rectangle = WindowRectangle; +using Point = WindowPoint; +using Size = WindowSize; + +struct WindowPlacementTest + : ::testing::TestWithParam> { + struct ClientAnchorsToParentConfig { + Rectangle const display_area = {{0, 0}, {800, 600}}; + Size const parent_size = {400, 300}; + Size const child_size = {100, 50}; + Point const parent_position = { + (display_area.size.width - parent_size.width) / 2, + (display_area.size.height - parent_size.height) / 2}; + } client_anchors_to_parent_config; + + Rectangle const display_area = {{0, 0}, {640, 480}}; + Size const parent_size = {600, 400}; + Size const child_size = {300, 300}; + Rectangle const rectangle_away_from_rhs = {{20, 20}, {20, 20}}; + Rectangle const rectangle_near_rhs = {{590, 20}, {10, 20}}; + Rectangle const rectangle_away_from_bottom = {{20, 20}, {20, 20}}; + Rectangle const rectangle_near_bottom = {{20, 380}, {20, 20}}; + Rectangle const rectangle_near_both_sides = {{0, 20}, {600, 20}}; + Rectangle const rectangle_near_both_sides_and_bottom = {{0, 380}, {600, 20}}; + Rectangle const rectangle_near_all_sides = {{0, 20}, {600, 380}}; + Rectangle const rectangle_near_both_bottom_right = {{400, 380}, {200, 20}}; + Point const parent_position = { + (display_area.size.width - parent_size.width) / 2, + (display_area.size.height - parent_size.height) / 2}; + + Positioner positioner; + + Rectangle anchor_rect() { + Rectangle rectangle{positioner.anchor_rect.value()}; + return {rectangle.top_left + parent_position, rectangle.size}; + } + + Rectangle parent_rect() { return {parent_position, parent_size}; } + + Point on_top_edge() { + return anchor_rect().top_left - Point{0, child_size.height}; + } + + Point on_left_edge() { + return anchor_rect().top_left - Point{child_size.width, 0}; + } +}; + +std::vector> all_anchor_combinations() { + std::array const all_anchors = { + Anchor::top_left, Anchor::top, Anchor::top_right, + Anchor::left, Anchor::center, Anchor::right, + Anchor::bottom_left, Anchor::bottom, Anchor::bottom_right, + }; + std::vector> combinations; + combinations.reserve(all_anchors.size() * all_anchors.size()); + + for (Anchor const parent_anchor : all_anchors) { + for (Anchor const child_anchor : all_anchors) { + combinations.push_back(std::make_tuple(parent_anchor, child_anchor)); + } + } + return combinations; +} + +} // namespace + +TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenRectAnchorRightOfParent) { + Rectangle const& display_area = client_anchors_to_parent_config.display_area; + Size const& parent_size = client_anchors_to_parent_config.parent_size; + Size const& child_size = client_anchors_to_parent_config.child_size; + Point const& parent_position = + client_anchors_to_parent_config.parent_position; + + int const rect_size = 10; + Rectangle const overlapping_right = { + parent_position + + Point{parent_size.width - rect_size / 2, parent_size.height / 2}, + {rect_size, rect_size}}; + + Positioner const positioner = { + .anchor_rect = overlapping_right, + .parent_anchor = Anchor::top_right, + .child_anchor = Anchor::top_left, + .constraint_adjustment = + static_cast(static_cast(Constraint::slide_y) | + static_cast(Constraint::resize_x))}; + + WindowRectangle const child_rect = + PlaceWindow(positioner, child_size, positioner.anchor_rect.value(), + {parent_position, parent_size}, display_area); + + Point const expected_position = + parent_position + Point{parent_size.width, parent_size.height / 2}; + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, child_size); +} + +TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenRectAnchorAboveParent) { + Rectangle const& display_area = client_anchors_to_parent_config.display_area; + Size const& parent_size = client_anchors_to_parent_config.parent_size; + Size const& child_size = client_anchors_to_parent_config.child_size; + Point const& parent_position = + client_anchors_to_parent_config.parent_position; + + int const rect_size = 10; + Rectangle const overlapping_above = { + parent_position + Point{parent_size.width / 2, -rect_size / 2}, + {rect_size, rect_size}}; + + Positioner const positioner = {.anchor_rect = overlapping_above, + .parent_anchor = Anchor::top_right, + .child_anchor = Anchor::bottom_right, + .constraint_adjustment = Constraint::slide_x}; + + WindowRectangle const child_rect = + PlaceWindow(positioner, child_size, positioner.anchor_rect.value(), + {parent_position, parent_size}, display_area); + + Point const expected_position = parent_position + + Point{parent_size.width / 2 + rect_size, 0} - + static_cast(child_size); + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, child_size); +} + +TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenOffsetRightOfParent) { + Rectangle const& display_area = client_anchors_to_parent_config.display_area; + Size const& parent_size = client_anchors_to_parent_config.parent_size; + Size const& child_size = client_anchors_to_parent_config.child_size; + Point const& parent_position = + client_anchors_to_parent_config.parent_position; + + int const rect_size = 10; + Rectangle const mid_right = { + parent_position + + Point{parent_size.width - rect_size, parent_size.height / 2}, + {rect_size, rect_size}}; + + Positioner const positioner = { + .anchor_rect = mid_right, + .parent_anchor = Anchor::top_right, + .child_anchor = Anchor::top_left, + .offset = Point{rect_size, 0}, + .constraint_adjustment = + static_cast(static_cast(Constraint::slide_y) | + static_cast(Constraint::resize_x))}; + + WindowRectangle const child_rect = + PlaceWindow(positioner, child_size, positioner.anchor_rect.value(), + {parent_position, parent_size}, display_area); + + Point const expected_position = + parent_position + Point{parent_size.width, parent_size.height / 2}; + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, child_size); +} + +TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenOffsetAboveParent) { + Rectangle const& display_area = client_anchors_to_parent_config.display_area; + Size const& parent_size = client_anchors_to_parent_config.parent_size; + Size const& child_size = client_anchors_to_parent_config.child_size; + Point const& parent_position = + client_anchors_to_parent_config.parent_position; + + int const rect_size = 10; + Rectangle const mid_top = {parent_position + Point{parent_size.width / 2, 0}, + {rect_size, rect_size}}; + + Positioner const positioner = {.anchor_rect = mid_top, + .parent_anchor = Anchor::top_right, + .child_anchor = Anchor::bottom_right, + .offset = Point{0, -rect_size}, + .constraint_adjustment = Constraint::slide_x}; + + WindowRectangle const child_rect = + PlaceWindow(positioner, child_size, positioner.anchor_rect.value(), + {parent_position, parent_size}, display_area); + + Point const expected_position = parent_position + + Point{parent_size.width / 2 + rect_size, 0} - + static_cast(child_size); + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, child_size); +} + +TEST_F(WindowPlacementTest, + ClientAnchorsToParentGivenRectAndOffsetBelowLeftParent) { + Rectangle const& display_area = client_anchors_to_parent_config.display_area; + Size const& parent_size = client_anchors_to_parent_config.parent_size; + Size const& child_size = client_anchors_to_parent_config.child_size; + Point const& parent_position = + client_anchors_to_parent_config.parent_position; + + int const rect_size = 10; + Rectangle const below_left = { + parent_position + Point{-rect_size, parent_size.height}, + {rect_size, rect_size}}; + + Positioner const positioner = { + .anchor_rect = below_left, + .parent_anchor = Anchor::bottom_left, + .child_anchor = Anchor::top_right, + .offset = Point{-rect_size, rect_size}, + .constraint_adjustment = Constraint::resize_any}; + + WindowRectangle const child_rect = + PlaceWindow(positioner, child_size, positioner.anchor_rect.value(), + {parent_position, parent_size}, display_area); + + Point const expected_position = parent_position + + Point{0, parent_size.height} - + Point{child_size.width, 0}; + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, child_size); +} + +TEST_P(WindowPlacementTest, CanAttachByEveryAnchorGivenNoConstraintAdjustment) { + positioner.anchor_rect = Rectangle{{100, 50}, {20, 20}}; + positioner.constraint_adjustment = Constraint{}; + std::tie(positioner.parent_anchor, positioner.child_anchor) = GetParam(); + + auto const position_of = [](Anchor anchor, Rectangle rectangle) -> Point { + switch (anchor) { + case Anchor::top_left: + return rectangle.top_left; + case Anchor::top: + return rectangle.top_left + Point{rectangle.size.width / 2, 0}; + case Anchor::top_right: + return rectangle.top_left + Point{rectangle.size.width, 0}; + case Anchor::left: + return rectangle.top_left + Point{0, rectangle.size.height / 2}; + case Anchor::center: + return rectangle.top_left + + Point{rectangle.size.width / 2, rectangle.size.height / 2}; + case Anchor::right: + return rectangle.top_left + + Point{rectangle.size.width, rectangle.size.height / 2}; + case Anchor::bottom_left: + return rectangle.top_left + Point{0, rectangle.size.height}; + case Anchor::bottom: + return rectangle.top_left + + Point{rectangle.size.width / 2, rectangle.size.height}; + case Anchor::bottom_right: + return rectangle.top_left + static_cast(rectangle.size); + default: + FML_UNREACHABLE(); + } + }; + + Point const anchor_position = + position_of(positioner.parent_anchor, anchor_rect()); + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(position_of(positioner.child_anchor, child_rect), anchor_position); +} + +INSTANTIATE_TEST_SUITE_P(AnchorCombinations, + WindowPlacementTest, + ::testing::ValuesIn(all_anchor_combinations())); + +TEST_F(WindowPlacementTest, + PlacementIsFlippedGivenAnchorRectNearRightSideAndOffset) { + int const x_offset = 42; + int const y_offset = 13; + + positioner.anchor_rect = rectangle_near_rhs; + positioner.constraint_adjustment = Constraint::flip_x; + positioner.offset = Point{x_offset, y_offset}; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::top_right; + + Point const expected_position = + on_left_edge() + Point{-1 * x_offset, y_offset}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, + PlacementIsFlippedGivenAnchorRectNearBottomAndOffset) { + int const x_offset = 42; + int const y_offset = 13; + + positioner.anchor_rect = rectangle_near_bottom; + positioner.constraint_adjustment = Constraint::flip_y; + positioner.offset = Point{x_offset, y_offset}; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_left; + + Point const expected_position = + on_top_edge() + Point{x_offset, -1 * y_offset}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, + PlacementIsFlippedBothWaysGivenAnchorRectNearBottomRightAndOffset) { + int const x_offset = 42; + int const y_offset = 13; + + positioner.anchor_rect = rectangle_near_both_bottom_right; + positioner.constraint_adjustment = Constraint::flip_any; + positioner.offset = Point{x_offset, y_offset}; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_right; + + Point const expected_position = anchor_rect().top_left - + static_cast(child_size) - + Point{x_offset, y_offset}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, PlacementCanSlideInXGivenAnchorRectNearRightSide) { + positioner.anchor_rect = rectangle_near_rhs; + positioner.constraint_adjustment = Constraint::slide_x; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::top_right; + + Point const expected_position = { + (display_area.top_left.x + display_area.size.width) - child_size.width, + anchor_rect().top_left.y}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, PlacementCanSlideInXGivenAnchorRectNearLeftSide) { + Rectangle const rectangle_near_left_side = {{0, 20}, {20, 20}}; + + positioner.anchor_rect = rectangle_near_left_side; + positioner.constraint_adjustment = Constraint::slide_x; + positioner.child_anchor = Anchor::top_right; + positioner.parent_anchor = Anchor::top_left; + + Point const expected_position = {display_area.top_left.x, + anchor_rect().top_left.y}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, PlacementCanSlideInYGivenAnchorRectNearBottom) { + positioner.anchor_rect = rectangle_near_bottom; + positioner.constraint_adjustment = Constraint::slide_y; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_left; + + Point const expected_position = { + anchor_rect().top_left.x, + (display_area.top_left.y + display_area.size.height) - child_size.height}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, PlacementCanSlideInYGivenAnchorRectNearTop) { + positioner.anchor_rect = rectangle_near_all_sides; + positioner.constraint_adjustment = Constraint::slide_y; + positioner.child_anchor = Anchor::bottom_left; + positioner.parent_anchor = Anchor::top_left; + + Point const expected_position = {anchor_rect().top_left.x, + display_area.top_left.y}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, + PlacementCanSlideInXAndYGivenAnchorRectNearBottomRightAndOffset) { + positioner.anchor_rect = rectangle_near_both_bottom_right; + positioner.constraint_adjustment = Constraint::slide_any; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_left; + + Point const expected_position = { + (display_area.top_left + static_cast(display_area.size)) - + static_cast(child_size)}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, PlacementCanResizeInXGivenAnchorRectNearRightSide) { + positioner.anchor_rect = rectangle_near_rhs; + positioner.constraint_adjustment = Constraint::resize_x; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::top_right; + + Point const expected_position = + anchor_rect().top_left + Point{anchor_rect().size.width, 0}; + Size const expected_size = { + (display_area.top_left.x + display_area.size.width) - + (anchor_rect().top_left.x + anchor_rect().size.width), + child_size.height}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, expected_size); +} + +TEST_F(WindowPlacementTest, PlacementCanResizeInXGivenAnchorRectNearLeftSide) { + Rectangle const rectangle_near_left_side = {{0, 20}, {20, 20}}; + + positioner.anchor_rect = rectangle_near_left_side; + positioner.constraint_adjustment = Constraint::resize_x; + positioner.child_anchor = Anchor::top_right; + positioner.parent_anchor = Anchor::top_left; + + Point const expected_position = {display_area.top_left.x, + anchor_rect().top_left.y}; + Size const expected_size = { + anchor_rect().top_left.x - display_area.top_left.x, child_size.height}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, expected_size); +} + +TEST_F(WindowPlacementTest, PlacementCanResizeInYGivenAnchorRectNearBottom) { + positioner.anchor_rect = rectangle_near_bottom; + positioner.constraint_adjustment = Constraint::resize_y; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_left; + + Point const expected_position = + anchor_rect().top_left + Point{0, anchor_rect().size.height}; + Size const expected_size = { + child_size.width, + (display_area.top_left.y + display_area.size.height) - + (anchor_rect().top_left.y + anchor_rect().size.height)}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, expected_size); +} + +TEST_F(WindowPlacementTest, PlacementCanResizeInYGivenAnchorRectNearTop) { + positioner.anchor_rect = rectangle_near_all_sides; + positioner.constraint_adjustment = Constraint::resize_y; + positioner.child_anchor = Anchor::bottom_left; + positioner.parent_anchor = Anchor::top_left; + + Point const expected_position = {anchor_rect().top_left.x, + display_area.top_left.y}; + Size const expected_size = { + child_size.width, anchor_rect().top_left.y - display_area.top_left.y}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, expected_size); +} + +TEST_F(WindowPlacementTest, + PlacementCanResizeInXAndYGivenAnchorRectNearBottomRightAndOffset) { + positioner.anchor_rect = rectangle_near_both_bottom_right; + positioner.constraint_adjustment = Constraint::resize_any; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_right; + + Point const expected_position = + anchor_rect().top_left + static_cast(anchor_rect().size); + Size const expected_size = { + (display_area.top_left.x + display_area.size.width) - expected_position.x, + (display_area.top_left.y + display_area.size.height) - + expected_position.y}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, expected_size); +} + +} // namespace flutter 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/client_wrapper/BUILD.gn b/shell/platform/windows/client_wrapper/BUILD.gn index aaf2abf2ef97d..1d40c3c1314fb 100644 --- a/shell/platform/windows/client_wrapper/BUILD.gn +++ b/shell/platform/windows/client_wrapper/BUILD.gn @@ -11,19 +11,12 @@ _wrapper_includes = [ "include/flutter/flutter_engine.h", "include/flutter/flutter_view_controller.h", "include/flutter/flutter_view.h", - "include/flutter/flutter_win32_window.h", - "include/flutter/flutter_window_controller.h", "include/flutter/plugin_registrar_windows.h", - "include/flutter/win32_window.h", - "include/flutter/win32_wrapper.h", ] _wrapper_sources = [ "flutter_engine.cc", "flutter_view_controller.cc", - "flutter_win32_window.cc", - "flutter_window_controller.cc", - "win32_window.cc", ] # This code will be merged into .../common/client_wrapper for client use, @@ -47,8 +40,6 @@ source_set("client_wrapper_windows") { "//flutter/shell/platform/windows:flutter_windows_headers", ] - libs = [ "dwmapi.lib" ] - configs += [ "//flutter/shell/platform/common:desktop_library_implementation" ] @@ -88,7 +79,6 @@ executable("client_wrapper_windows_unittests") { "flutter_engine_unittests.cc", "flutter_view_controller_unittests.cc", "flutter_view_unittests.cc", - "flutter_window_controller_unittests.cc", "plugin_registrar_windows_unittests.cc", ] @@ -99,7 +89,6 @@ executable("client_wrapper_windows_unittests") { ":client_wrapper_library_stubs_windows", ":client_wrapper_windows", ":client_wrapper_windows_fixtures", - "//flutter/shell/platform/common/client_wrapper", "//flutter/shell/platform/common/client_wrapper:client_wrapper_library_stubs", "//flutter/testing", @@ -121,9 +110,6 @@ client_wrapper_file_archive_list = [ win_client_wrapper_file_archive_list = [ "flutter_engine.cc", "flutter_view_controller.cc", - "flutter_win32_window.cc", - "flutter_window_controller.cc", - "win32_window.cc", ] zip_bundle("client_wrapper_archive") { diff --git a/shell/platform/windows/client_wrapper/flutter_engine.cc b/shell/platform/windows/client_wrapper/flutter_engine.cc index 3cbe59622fc4f..7860947aa068b 100644 --- a/shell/platform/windows/client_wrapper/flutter_engine.cc +++ b/shell/platform/windows/client_wrapper/flutter_engine.cc @@ -63,7 +63,7 @@ bool FlutterEngine::Run(const char* entry_point) { } void FlutterEngine::ShutDown() { - if (engine_) { + if (engine_ && owns_engine_) { FlutterDesktopEngineDestroy(engine_); } engine_ = nullptr; @@ -113,7 +113,8 @@ std::optional FlutterEngine::ProcessExternalWindowMessage( return std::nullopt; } -FlutterDesktopEngineRef FlutterEngine::engine() const { +FlutterDesktopEngineRef FlutterEngine::RelinquishEngine() { + owns_engine_ = false; return engine_; } diff --git a/shell/platform/windows/client_wrapper/flutter_view_controller.cc b/shell/platform/windows/client_wrapper/flutter_view_controller.cc index 1dbc5a0dfa96f..98c65e10c27bc 100644 --- a/shell/platform/windows/client_wrapper/flutter_view_controller.cc +++ b/shell/platform/windows/client_wrapper/flutter_view_controller.cc @@ -11,22 +11,10 @@ namespace flutter { FlutterViewController::FlutterViewController(int width, int height, - const DartProject& project) - : FlutterViewController(width, - height, - std::make_shared(project)) {} - -FlutterViewController::FlutterViewController( - int width, - int height, - std::shared_ptr engine) { - FlutterDesktopViewControllerProperties properties = {}; - properties.width = width; - properties.height = height; - - engine_ = std::move(engine); - controller_ = - FlutterDesktopEngineCreateViewController(engine_->engine(), &properties); + const DartProject& project) { + engine_ = std::make_shared(project); + controller_ = FlutterDesktopViewControllerCreate(width, height, + engine_->RelinquishEngine()); if (!controller_) { std::cerr << "Failed to create view controller." << std::endl; return; diff --git a/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc b/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc index f79842f280978..837c2e13e583d 100644 --- a/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc +++ b/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc @@ -17,8 +17,10 @@ namespace { class TestWindowsApi : public testing::StubFlutterWindowsApi { public: // |flutter::testing::StubFlutterWindowsApi| - FlutterDesktopViewControllerRef EngineCreateViewController( - const FlutterDesktopViewControllerProperties* properties) override { + FlutterDesktopViewControllerRef ViewControllerCreate( + int width, + int height, + FlutterDesktopEngineRef engine) override { return reinterpret_cast(2); } @@ -61,13 +63,11 @@ TEST(FlutterViewControllerTest, CreateDestroy) { testing::ScopedStubFlutterWindowsApi scoped_api_stub( std::make_unique()); auto test_api = static_cast(scoped_api_stub.stub()); - - // Create and destroy a view controller. - // This should also create and destroy an engine. { FlutterViewController controller(100, 100, project); } - EXPECT_TRUE(test_api->view_controller_destroyed()); - EXPECT_TRUE(test_api->engine_destroyed()); + // Per the C API, once a view controller has taken ownership of an engine + // the engine destruction method should not be called. + EXPECT_FALSE(test_api->engine_destroyed()); } TEST(FlutterViewControllerTest, GetViewId) { diff --git a/shell/platform/windows/client_wrapper/flutter_win32_window.cc b/shell/platform/windows/client_wrapper/flutter_win32_window.cc deleted file mode 100644 index 227508b8c571a..0000000000000 --- a/shell/platform/windows/client_wrapper/flutter_win32_window.cc +++ /dev/null @@ -1,77 +0,0 @@ -#include "include/flutter/flutter_win32_window.h" - -#include - -namespace flutter { - -FlutterWin32Window::FlutterWin32Window(std::shared_ptr engine) - : engine_{std::move(engine)} {} - -FlutterWin32Window::FlutterWin32Window(std::shared_ptr engine, - std::shared_ptr wrapper) - : engine_{std::move(engine)}, Win32Window{std::move(wrapper)} {} - -auto FlutterWin32Window::GetFlutterViewId() const -> FlutterViewId { - return view_controller_->view_id(); -}; - -auto FlutterWin32Window::OnCreate() -> bool { - if (!Win32Window::OnCreate()) { - return false; - } - - auto const client_rect{GetClientArea()}; - - // The size here must match the window dimensions to avoid unnecessary surface - // creation / destruction in the startup path. - view_controller_ = std::make_unique( - client_rect.right - client_rect.left, - client_rect.bottom - client_rect.top, engine_); - // Ensure that basic setup of the controller was successful. - if (!view_controller_->view()) { - return false; - } - - SetChildContent(view_controller_->view()->GetNativeWindow()); - - // 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. - return true; -} - -void FlutterWin32Window::OnDestroy() { - if (view_controller_) { - view_controller_ = nullptr; - } - - Win32Window::OnDestroy(); -} - -auto FlutterWin32Window::MessageHandler(HWND hwnd, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) -> LRESULT { - // Give Flutter, including plugins, an opportunity to handle window messages. - if (view_controller_) { - auto const result{view_controller_->HandleTopLevelWindowProc( - hwnd, message, wparam, lparam)}; - if (result) { - return *result; - } - } - - switch (message) { - case WM_FONTCHANGE: - engine_->ReloadSystemFonts(); - break; - default: - break; - } - - return Win32Window::MessageHandler(hwnd, message, wparam, lparam); -} - -} // namespace flutter diff --git a/shell/platform/windows/client_wrapper/flutter_window_controller.cc b/shell/platform/windows/client_wrapper/flutter_window_controller.cc deleted file mode 100644 index 104d16b200905..0000000000000 --- a/shell/platform/windows/client_wrapper/flutter_window_controller.cc +++ /dev/null @@ -1,644 +0,0 @@ -#include "include/flutter/flutter_window_controller.h" - -#include "include/flutter/encodable_value.h" -#include "include/flutter/flutter_win32_window.h" -#include "include/flutter/standard_method_codec.h" - -#include -#include - -#include - -namespace { - -auto const* const kChannel{"flutter/windowing"}; -auto const* const kErrorCodeInvalidValue{"INVALID_VALUE"}; -auto const* const kErrorCodeUnavailable{"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 -auto GetSingleValueForKeyOrSendError(std::string const& key, - flutter::EncodableMap const* map, - flutter::MethodResult<>& result) - -> std::optional { - 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(kErrorCodeInvalidValue, "Value for '" + key + - "' key must be of type '" + - typeid(T).name() + "'."); - } - } else { - result.Error(kErrorCodeInvalidValue, - "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 -auto GetListValuesForKeyOrSendError(std::string const& key, - flutter::EncodableMap const* map, - flutter::MethodResult<>& result) - -> std::optional> { - 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(kErrorCodeInvalidValue, - "Array for '" + key + "' key must have " + - std::to_string(Size) + " values."); - return std::nullopt; - } - std::vector decoded_values; - for (auto const& value : *array) { - if (std::holds_alternative(value)) { - decoded_values.push_back(std::get(value)); - } else { - result.Error(kErrorCodeInvalidValue, - "Array for '" + key + - "' key must only have values of type '" + - typeid(T).name() + "'."); - return std::nullopt; - } - } - return decoded_values; - } else { - result.Error(kErrorCodeInvalidValue, - "Value for '" + key + "' key must be an array."); - } - } else { - result.Error(kErrorCodeInvalidValue, - "Map does not contain required '" + key + "' key."); - } - return std::nullopt; -} - -// Converts a |flutter::WindowArchetype| to its corresponding wide string -// representation. -auto ArchetypeToWideString(flutter::WindowArchetype archetype) -> std::wstring { - switch (archetype) { - case flutter::WindowArchetype::regular: - return L"regular"; - case flutter::WindowArchetype::floating_regular: - return L"floating_regular"; - case flutter::WindowArchetype::dialog: - return L"dialog"; - case flutter::WindowArchetype::satellite: - return L"satellite"; - case flutter::WindowArchetype::popup: - return L"popup"; - case flutter::WindowArchetype::tip: - return L"tip"; - } - std::cerr - << "Unhandled window archetype encountered in archetypeToWideString: " - << static_cast(archetype) << "\n"; - std::abort(); -} - -auto GetParentOrOwner(HWND window) -> HWND { - auto const parent{GetParent(window)}; - return parent ? parent : GetWindow(window, GW_OWNER); -} - -} // namespace - -namespace flutter { - -FlutterWindowController::~FlutterWindowController() { - { - std::lock_guard lock(mutex_); - if (channel_) { - channel_->SetMethodCallHandler(nullptr); - } - } - DestroyWindows(); -} - -void FlutterWindowController::DestroyWindows() { - std::unique_lock lock(mutex_); - std::vector view_ids; - view_ids.reserve(windows_.size()); - for (auto const& [view_id, _] : windows_) { - view_ids.push_back(view_id); - } - lock.unlock(); - for (auto const& view_id : view_ids) { - DestroyFlutterWindow(view_id); - } -} - -void FlutterWindowController::SetEngine(std::shared_ptr engine) { - DestroyWindows(); - std::lock_guard const lock(mutex_); - engine_ = std::move(engine); - channel_ = std::make_unique>( - engine_->messenger(), kChannel, &StandardMethodCodec::GetInstance()); - channel_->SetMethodCallHandler( - [this](MethodCall<> const& call, std::unique_ptr> result) { - MethodCallHandler(call, *result); - }); -} - -auto FlutterWindowController::CreateFlutterWindow( - std::wstring const& title, - WindowSize const& size, - WindowArchetype archetype, - std::optional positioner, - std::optional parent_view_id) - -> std::optional { - std::unique_lock lock(mutex_); - if (!engine_) { - std::cerr << "Cannot create window without an engine.\n"; - return std::nullopt; - } - - auto window{std::make_unique(engine_, win32_)}; - - std::optional const parent_hwnd{ - parent_view_id.has_value() && - windows_.find(parent_view_id.value()) != windows_.end() - ? std::optional{windows_[parent_view_id.value()]->GetHandle()} - : std::nullopt}; - - lock.unlock(); - - if (!window->Create(title, size, archetype, parent_hwnd, positioner)) { - return std::nullopt; - } - - lock.lock(); - - // Assume first window is the main window - if (windows_.empty()) { - window->SetQuitOnClose(true); - } - - auto const view_id{window->GetFlutterViewId()}; - windows_[view_id] = std::move(window); - - SendOnWindowCreated(view_id, parent_view_id); - - WindowMetadata result{.view_id = view_id, - .archetype = archetype, - .size = GetWindowSize(view_id), - .parent_id = parent_view_id}; - - return result; -} - -auto FlutterWindowController::DestroyFlutterWindow(FlutterViewId view_id) - -> bool { - std::unique_lock lock(mutex_); - auto it{windows_.find(view_id)}; - if (it != windows_.end()) { - auto* const window{it->second.get()}; - auto const window_handle{window->GetHandle()}; - - if (window->GetArchetype() == WindowArchetype::dialog && - GetWindow(window_handle, GW_OWNER)) { - // Temporarily disable satellite hiding. This prevents satellites from - // flickering because of briefly hiding and showing between the - // destruction of a modal dialog and the transfer of focus to the owner - // window. - disable_satellite_hiding_ = window_handle; - } - - lock.unlock(); - - // |window| will be removed from |windows_| when WM_NCDESTROY is handled - win32_->DestroyWindow(window->GetHandle()); - - return true; - } - return false; -} - -FlutterWindowController::FlutterWindowController() - : win32_{std::make_shared()} {} - -FlutterWindowController::FlutterWindowController( - std::shared_ptr wrapper) - : win32_{std::move(wrapper)} {} - -void FlutterWindowController::MethodCallHandler(MethodCall<> const& call, - MethodResult<>& result) { - if (call.method_name() == "createWindow") { - HandleCreateWindow(WindowArchetype::regular, call, result); - } else if (call.method_name() == "createDialog") { - HandleCreateWindow(WindowArchetype::dialog, call, result); - } else if (call.method_name() == "createSatellite") { - HandleCreateWindow(WindowArchetype::satellite, call, result); - } else if (call.method_name() == "createPopup") { - HandleCreateWindow(WindowArchetype::popup, call, result); - } else if (call.method_name() == "destroyWindow") { - HandleDestroyWindow(call, result); - } else { - result.NotImplemented(); - } -} - -auto FlutterWindowController::MessageHandler(HWND hwnd, - UINT message, - WPARAM wparam, - LPARAM lparam) -> LRESULT { - switch (message) { - case WM_NCDESTROY: { - std::unique_lock lock{mutex_}; - auto const it{std::find_if(windows_.begin(), windows_.end(), - [hwnd](auto const& window) { - return window.second->GetHandle() == hwnd; - })}; - if (it != windows_.end()) { - auto const view_id{it->first}; - auto const quit_on_close{it->second.get()->GetQuitOnClose()}; - - windows_.erase(it); - - if (quit_on_close) { - auto it2{windows_.begin()}; - while (it2 != windows_.end()) { - auto const& that{it2->second}; - lock.unlock(); - DestroyWindow(that->GetHandle()); - lock.lock(); - it2 = windows_.begin(); - } - } - - SendOnWindowDestroyed(view_id); - - if (disable_satellite_hiding_ == hwnd) { - // Re-enable satellite hiding by clearing the window handle now that - // the window is fully destroyed - disable_satellite_hiding_ = nullptr; - } - } - } - return 0; - case WM_ACTIVATE: - if (wparam != WA_INACTIVE) { - if (auto* const window{Win32Window::GetThisFromHandle(hwnd)}) { - if (window->GetArchetype() != WindowArchetype::popup) { - // If a non-popup window is activated, close popups for all windows - std::unique_lock lock(mutex_); - auto it{windows_.begin()}; - while (it != windows_.end()) { - lock.unlock(); - auto const num_popups_closed{it->second->CloseChildPopups()}; - lock.lock(); - if (num_popups_closed > 0) { - it = windows_.begin(); - } else { - ++it; - } - } - } else { - // If a popup window is activated, close its child popups - window->CloseChildPopups(); - } - } - ShowWindowAndAncestorsSatellites(hwnd); - } - break; - case WM_ACTIVATEAPP: - if (wparam == FALSE) { - if (auto* const window{Win32Window::GetThisFromHandle(hwnd)}) { - // Close child popups and hide satellites from all windows if a window - // belonging to a different application is being activated - window->CloseChildPopups(); - HideWindowsSatellites(nullptr); - } - } - break; - case WM_SIZE: { - std::lock_guard lock{mutex_}; - auto const it{std::find_if(windows_.begin(), windows_.end(), - [hwnd](auto const& window) { - return window.second->GetHandle() == hwnd; - })}; - if (it != windows_.end()) { - auto const view_id{it->first}; - SendOnWindowChanged(view_id); - } - } break; - default: - break; - } - - if (auto* const window{Win32Window::GetThisFromHandle(hwnd)}) { - return window->MessageHandler(hwnd, message, wparam, lparam); - } - return DefWindowProc(hwnd, message, wparam, lparam); -} - -void FlutterWindowController::SendOnWindowCreated( - FlutterViewId view_id, - std::optional parent_view_id) const { - if (channel_) { - channel_->InvokeMethod( - "onWindowCreated", - std::make_unique(EncodableMap{ - {EncodableValue("viewId"), EncodableValue(view_id)}, - {EncodableValue("parentViewId"), - parent_view_id ? EncodableValue(parent_view_id.value()) - : EncodableValue()}})); - } -} - -void FlutterWindowController::SendOnWindowDestroyed( - FlutterViewId view_id) const { - if (channel_) { - channel_->InvokeMethod( - "onWindowDestroyed", - std::make_unique(EncodableMap{ - {EncodableValue("viewId"), EncodableValue(view_id)}, - })); - } -} - -void FlutterWindowController::SendOnWindowChanged(FlutterViewId view_id) const { - if (channel_) { - auto const size{GetWindowSize(view_id)}; - channel_->InvokeMethod( - "onWindowChanged", - std::make_unique(EncodableMap{ - {EncodableValue("viewId"), EncodableValue(view_id)}, - {EncodableValue("size"), - EncodableValue(EncodableList{EncodableValue(size.width), - EncodableValue(size.height)})}, - {EncodableValue("relativePosition"), EncodableValue()}, // TODO - {EncodableValue("isMoving"), EncodableValue()}})); // TODO - } -} - -void FlutterWindowController::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(kErrorCodeInvalidValue, "Method call argument is not a map."); - return; - } - - std::wstring const title{ArchetypeToWideString(archetype)}; - - auto const size_list{ - GetListValuesForKeyOrSendError("size", map, result)}; - if (!size_list) { - return; - } - if (size_list->at(0) < 0 || size_list->at(1) < 0) { - result.Error(kErrorCodeInvalidValue, - "Values for 'size' key (" + std::to_string(size_list->at(0)) + - ", " + std::to_string(size_list->at(1)) + - ") must be nonnegative."); - return; - } - - std::optional positioner; - std::optional anchor_rect; - - if (archetype == WindowArchetype::satellite || - archetype == WindowArchetype::popup) { - if (auto const anchor_rect_it{map->find(EncodableValue("anchorRect"))}; - anchor_rect_it != map->end()) { - if (!anchor_rect_it->second.IsNull()) { - auto const anchor_rect_list{ - GetListValuesForKeyOrSendError("anchorRect", map, result)}; - if (!anchor_rect_list) { - return; - } - anchor_rect = - WindowRectangle{{anchor_rect_list->at(0), anchor_rect_list->at(1)}, - {anchor_rect_list->at(2), anchor_rect_list->at(3)}}; - } - } else { - result.Error(kErrorCodeInvalidValue, - "Map does not contain required 'anchorRect' key."); - return; - } - - auto const positioner_parent_anchor{GetSingleValueForKeyOrSendError( - "positionerParentAnchor", map, result)}; - if (!positioner_parent_anchor) { - return; - } - auto const positioner_child_anchor{GetSingleValueForKeyOrSendError( - "positionerChildAnchor", map, result)}; - if (!positioner_child_anchor) { - return; - } - auto const child_anchor{ - static_cast(positioner_child_anchor.value())}; - - auto const positioner_offset_list{GetListValuesForKeyOrSendError( - "positionerOffset", map, result)}; - if (!positioner_offset_list) { - return; - } - auto const positioner_constraint_adjustment{ - GetSingleValueForKeyOrSendError("positionerConstraintAdjustment", - map, result)}; - if (!positioner_constraint_adjustment) { - return; - } - positioner = WindowPositioner{ - .anchor_rect = anchor_rect, - .parent_anchor = static_cast( - positioner_parent_anchor.value()), - .child_anchor = child_anchor, - .offset = {positioner_offset_list->at(0), - positioner_offset_list->at(1)}, - .constraint_adjustment = - static_cast( - positioner_constraint_adjustment.value())}; - } - - std::optional parent_view_id; - if (archetype == WindowArchetype::dialog || - archetype == WindowArchetype::satellite || - archetype == WindowArchetype::popup) { - if (auto const parent_it{map->find(EncodableValue("parent"))}; - parent_it != map->end()) { - if (parent_it->second.IsNull()) { - if (archetype != WindowArchetype::dialog) { - result.Error(kErrorCodeInvalidValue, - "Value for 'parent' key must not be null."); - return; - } - } else { - if (auto const* const parent{std::get_if(&parent_it->second)}) { - parent_view_id = *parent >= 0 ? std::optional(*parent) - : std::nullopt; - if (!parent_view_id.has_value() && - (archetype == WindowArchetype::satellite || - archetype == WindowArchetype::popup)) { - result.Error(kErrorCodeInvalidValue, - "Value for 'parent' key (" + - std::to_string(parent_view_id.value()) + - ") must be nonnegative."); - return; - } - } else { - result.Error(kErrorCodeInvalidValue, - "Value for 'parent' key must be of type int."); - return; - } - } - } else { - result.Error(kErrorCodeInvalidValue, - "Map does not contain required 'parent' key."); - return; - } - } - - if (auto const data_opt{CreateFlutterWindow( - title, {.width = size_list->at(0), .height = size_list->at(1)}, - archetype, positioner, parent_view_id)}) { - auto const& data{data_opt.value()}; - result.Success(EncodableValue(EncodableMap{ - {EncodableValue("viewId"), EncodableValue(data.view_id)}, - {EncodableValue("archetype"), - EncodableValue(static_cast(data.archetype))}, - {EncodableValue("size"), - EncodableValue(EncodableList{EncodableValue(data.size.width), - EncodableValue(data.size.height)})}, - {EncodableValue("parentViewId"), - data.parent_id ? EncodableValue(data.parent_id.value()) - : EncodableValue()}})); - } else { - result.Error(kErrorCodeUnavailable, "Can't create window."); - } -} - -void FlutterWindowController::HandleDestroyWindow(MethodCall<> const& call, - MethodResult<>& result) { - auto const* const arguments{call.arguments()}; - auto const* const map{std::get_if(arguments)}; - if (!map) { - result.Error(kErrorCodeInvalidValue, "Method call argument is not a map."); - return; - } - - auto const view_id{ - GetSingleValueForKeyOrSendError("viewId", map, result)}; - if (!view_id) { - return; - } - if (view_id.value() < 0) { - result.Error(kErrorCodeInvalidValue, "Value for 'viewId' (" + - std::to_string(view_id.value()) + - ") cannot be negative."); - return; - } - - if (!DestroyFlutterWindow(view_id.value())) { - result.Error(kErrorCodeInvalidValue, "Can't find window with 'viewId' (" + - std::to_string(view_id.value()) + - ")."); - return; - } - - result.Success(); -} - -WindowSize FlutterWindowController::GetWindowSize( - flutter::FlutterViewId view_id) const { - auto* const hwnd{windows_.at(view_id)->GetHandle()}; - 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 FlutterWindowController::HideWindowsSatellites(HWND opt_out_hwnd) { - if (disable_satellite_hiding_) { - return; - } - - // Helper function to check whether |hwnd| is a descendant of |ancestor|. - auto const is_descendant_of{[](HWND hwnd, HWND ancestor) -> bool { - auto current{ancestor}; - while (current) { - current = GetParentOrOwner(current); - if (current == hwnd) { - return true; - } - } - return false; - }}; - - // Helper function to check whether |window| has a child dialog. - auto const has_dialog{[](Win32Window* window) -> bool { - for (auto* const child : window->GetChildren()) { - if (child->GetArchetype() == WindowArchetype::dialog) { - return true; - } - } - return false; - }}; - - std::lock_guard const lock(mutex_); - for (auto const& [_, window] : windows_) { - if (window->GetHandle() == opt_out_hwnd || - is_descendant_of(window->GetHandle(), opt_out_hwnd)) { - continue; - } - - for (auto* const child : window->GetChildren()) { - if (child->GetArchetype() != WindowArchetype::satellite) { - continue; - } - if (!has_dialog(child)) { - ShowWindow(child->GetHandle(), SW_HIDE); - } - } - } -} - -void FlutterWindowController::ShowWindowAndAncestorsSatellites(HWND hwnd) { - if (disable_satellite_hiding_) { - return; - } - - auto current{hwnd}; - while (current) { - for (auto* const child : - Win32Window::GetThisFromHandle(current)->GetChildren()) { - if (child->GetArchetype() == WindowArchetype::satellite) { - ShowWindow(child->GetHandle(), SW_SHOWNOACTIVATE); - } - } - current = GetParentOrOwner(current); - } - - // Hide satellites of all other top-level windows - if (auto* const window{Win32Window::GetThisFromHandle(hwnd)}) { - if (window->GetArchetype() != WindowArchetype::satellite) { - HideWindowsSatellites(hwnd); - } - } -} - -} // namespace flutter diff --git a/shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc b/shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc deleted file mode 100644 index 2e90d4898e905..0000000000000 --- a/shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc +++ /dev/null @@ -1,296 +0,0 @@ -// 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/common/client_wrapper/include/flutter/encodable_value.h" -#include "flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_win32_window.h" -#include "flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h" -#include "flutter/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h" - -#include "gmock/gmock.h" -#include "gtest/gtest.h" - -using ::testing::_; -using ::testing::AnyNumber; -using ::testing::Eq; -using ::testing::Gt; -using ::testing::IsNull; -using ::testing::Mock; -using ::testing::NiceMock; -using ::testing::Return; -using ::testing::StrEq; - -namespace flutter { - -namespace { - -HWND const k_hwnd{reinterpret_cast(-1)}; - -// Stub implementation to validate calls to the API. -class TestWindowsApi : public testing::StubFlutterWindowsApi { - public: - // |flutter::testing::StubFlutterWindowsApi| - FlutterDesktopViewControllerRef EngineCreateViewController( - const FlutterDesktopViewControllerProperties* properties) override { - return reinterpret_cast(2); - } -}; - -// Mocked classes -class MockWin32Wrapper : public Win32Wrapper { - public: - MOCK_METHOD(HWND, - CreateWindowEx, - (DWORD dwExStyle, - LPCWSTR lpClassName, - LPCWSTR lpWindowName, - DWORD dwStyle, - int X, - int Y, - int nWidth, - int nHeight, - HWND hWndParent, - HMENU hMenu, - HINSTANCE hInstance, - LPVOID lpParam), - (override)); - MOCK_METHOD(BOOL, DestroyWindow, (HWND hWnd), (override)); -}; - -class MockMethodResult : public MethodResult<> { - public: - MOCK_METHOD(void, - SuccessInternal, - (EncodableValue const* result), - (override)); - MOCK_METHOD(void, - ErrorInternal, - (std::string const& error_code, - std::string const& error_message, - EncodableValue const* error_details), - (override)); - MOCK_METHOD(void, NotImplementedInternal, (), (override)); -}; - -class MockFlutterWindowController : public FlutterWindowController { - public: - using FlutterWindowController::MessageHandler; - using FlutterWindowController::MethodCallHandler; - - MockFlutterWindowController(std::shared_ptr wrapper) - : FlutterWindowController(std::move(wrapper)) {} - - MOCK_METHOD(void, - SendOnWindowCreated, - (FlutterViewId view_id, - std::optional parent_view_id), - (override, const)); - MOCK_METHOD(void, - SendOnWindowDestroyed, - (FlutterViewId view_id), - (override, const)); - MOCK_METHOD(void, - SendOnWindowChanged, - (FlutterViewId view_id), - (override, const)); -}; - -// Test fixture -class FlutterWindowControllerTest : public ::testing::Test { - protected: - void SetUp() override { - DartProject project(L"test"); - engine_ = std::make_shared(project); - mock_win32_ = std::make_shared>(); - mock_controller_ = - std::make_unique>(mock_win32_); - mock_controller_->SetEngine(engine_); - - ON_CALL(*mock_win32_, CreateWindowEx).WillByDefault(Return(k_hwnd)); - ON_CALL(*mock_win32_, DestroyWindow).WillByDefault([&](HWND hwnd) { - mock_controller_->MessageHandler(hwnd, WM_NCDESTROY, 0, 0); - return TRUE; - }); - } - - std::shared_ptr engine_; - std::shared_ptr> mock_win32_; - std::unique_ptr> mock_controller_; -}; - -} // namespace - -TEST_F(FlutterWindowControllerTest, CreateRegularWindow) { - testing::ScopedStubFlutterWindowsApi scoped_api_stub( - std::make_unique()); - auto test_api{static_cast(scoped_api_stub.stub())}; - - auto const title{L"window"}; - WindowSize const size{800, 600}; - auto const archetype{WindowArchetype::regular}; - std::optional const positioner; - std::optional const parent_view_id; - - EXPECT_CALL(*mock_win32_, - CreateWindowEx(0, _, StrEq(title), WS_OVERLAPPEDWINDOW, - CW_USEDEFAULT, CW_USEDEFAULT, Gt(size.width), - Gt(size.height), IsNull(), _, _, _)) - .Times(1); - - auto const result{mock_controller_->CreateFlutterWindow( - title, size, archetype, positioner, parent_view_id)}; - - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(result->view_id, 1); - EXPECT_FALSE(result->parent_id.has_value()); - EXPECT_EQ(result->archetype, archetype); -} - -TEST_F(FlutterWindowControllerTest, DestroyWindow) { - testing::ScopedStubFlutterWindowsApi scoped_api_stub( - std::make_unique()); - auto test_api{static_cast(scoped_api_stub.stub())}; - - auto const title{L"window"}; - WindowSize const size{800, 600}; - auto const archetype{WindowArchetype::regular}; - - auto const create_result{mock_controller_->CreateFlutterWindow( - title, size, archetype, std::nullopt, std::nullopt)}; - - ASSERT_TRUE(create_result.has_value()); - - EXPECT_CALL(*mock_win32_, DestroyWindow(k_hwnd)).Times(1); - - EXPECT_TRUE(mock_controller_->DestroyFlutterWindow(1)); -} - -TEST_F(FlutterWindowControllerTest, DestroyWindowWithInvalidView) { - testing::ScopedStubFlutterWindowsApi scoped_api_stub( - std::make_unique()); - auto test_api{static_cast(scoped_api_stub.stub())}; - - auto const title{L"window"}; - WindowSize const size{800, 600}; - auto const archetype{WindowArchetype::regular}; - - auto const create_result{mock_controller_->CreateFlutterWindow( - title, size, archetype, std::nullopt, std::nullopt)}; - - ASSERT_TRUE(create_result.has_value()); - - EXPECT_CALL(*mock_win32_, DestroyWindow(k_hwnd)).Times(0); - - EXPECT_FALSE(mock_controller_->DestroyFlutterWindow(9999)); - - Mock::VerifyAndClearExpectations(mock_win32_.get()); -} - -TEST_F(FlutterWindowControllerTest, SendOnWindowCreated) { - testing::ScopedStubFlutterWindowsApi scoped_api_stub( - std::make_unique()); - auto test_api{static_cast(scoped_api_stub.stub())}; - - auto const title{L"window"}; - WindowSize const size{800, 600}; - auto const archetype{WindowArchetype::regular}; - - EXPECT_CALL(*mock_controller_, SendOnWindowCreated(1, Eq(std::nullopt))) - .Times(1); - - auto const create_result{mock_controller_->CreateFlutterWindow( - title, size, archetype, std::nullopt, std::nullopt)}; -} - -TEST_F(FlutterWindowControllerTest, SendOnWindowDestroyed) { - testing::ScopedStubFlutterWindowsApi scoped_api_stub( - std::make_unique()); - auto test_api{static_cast(scoped_api_stub.stub())}; - - auto const title{L"window"}; - WindowSize const size{800, 600}; - auto const archetype{WindowArchetype::regular}; - - auto const create_result{mock_controller_->CreateFlutterWindow( - title, size, archetype, std::nullopt, std::nullopt)}; - - ASSERT_TRUE(create_result.has_value()); - - EXPECT_CALL(*mock_controller_, SendOnWindowDestroyed).Times(1); - - mock_controller_->DestroyFlutterWindow(1); -} - -TEST_F(FlutterWindowControllerTest, SendOnWindowChangedWhenWindowIsResized) { - testing::ScopedStubFlutterWindowsApi scoped_api_stub( - std::make_unique()); - auto test_api{static_cast(scoped_api_stub.stub())}; - - auto const title{L"window"}; - WindowSize const size{800, 600}; - auto const archetype{WindowArchetype::regular}; - - auto const create_result{mock_controller_->CreateFlutterWindow( - title, size, archetype, std::nullopt, std::nullopt)}; - - EXPECT_CALL(*mock_controller_, SendOnWindowChanged(1)).Times(1); - - mock_controller_->MessageHandler(k_hwnd, WM_SIZE, 0, 0); -} - -TEST_F(FlutterWindowControllerTest, CreateRegularWindowUsingMethodCall) { - testing::ScopedStubFlutterWindowsApi scoped_api_stub( - std::make_unique()); - auto test_api{static_cast(scoped_api_stub.stub())}; - - WindowSize const size{800, 600}; - - EncodableMap const arguments{ - {EncodableValue("size"), - EncodableValue(EncodableList{EncodableValue(size.width), - EncodableValue(size.height)})}, - }; - MethodCall<> call("createWindow", - std::make_unique(arguments)); - - NiceMock mock_result; - - EXPECT_CALL(mock_result, SuccessInternal(_)).Times(1); - EXPECT_CALL(*mock_win32_, - CreateWindowEx(0, _, StrEq(L"regular"), WS_OVERLAPPEDWINDOW, - CW_USEDEFAULT, CW_USEDEFAULT, Gt(size.width), - Gt(size.height), IsNull(), _, _, _)) - .Times(1); - - mock_controller_->MethodCallHandler(call, mock_result); -} - -TEST_F(FlutterWindowControllerTest, DestroyWindowUsingMethodCall) { - testing::ScopedStubFlutterWindowsApi scoped_api_stub( - std::make_unique()); - auto test_api{static_cast(scoped_api_stub.stub())}; - - auto const title{L"window"}; - WindowSize size{800, 600}; - auto const archetype{WindowArchetype::regular}; - auto create_result{mock_controller_->CreateFlutterWindow( - title, size, archetype, std::nullopt, std::nullopt)}; - - ASSERT_TRUE(create_result.has_value()); - - EncodableMap const arguments{ - {EncodableValue("viewId"), - EncodableValue(static_cast(create_result->view_id))}, - }; - MethodCall<> call("destroyWindow", - std::make_unique(arguments)); - - NiceMock mock_result; - - EXPECT_CALL(mock_result, SuccessInternal(_)).Times(1); - EXPECT_CALL(*mock_win32_, DestroyWindow(k_hwnd)).Times(1); - - mock_controller_->MethodCallHandler(call, mock_result); -} - -} // namespace flutter diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h index 89ca2d188b46f..0369db35a14fc 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h +++ b/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h @@ -98,8 +98,11 @@ class FlutterEngine : public PluginRegistry { // For access to the engine handle. friend class FlutterViewController; - // Get the handle for interacting with the C API's engine reference. - FlutterDesktopEngineRef engine() const; + // Gives up ownership of |engine_|, but keeps a weak reference to it. + // + // This is intended to be used by FlutterViewController, since the underlying + // C API for view controllers takes over engine ownership. + FlutterDesktopEngineRef RelinquishEngine(); // Handle for interacting with the C API's engine reference. FlutterDesktopEngineRef engine_ = nullptr; @@ -107,6 +110,9 @@ class FlutterEngine : public PluginRegistry { // Messenger for communicating with the engine. std::unique_ptr messenger_; + // Whether or not this wrapper owns |engine_|. + bool owns_engine_ = true; + // Whether |Run| has been called successfully. // // This is used to improve error messages. This can be false while the engine diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h index b26e017a6760a..4007534a5d73e 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h +++ b/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h @@ -32,16 +32,6 @@ class FlutterViewController { // |dart_project| will be used to configure the engine backing this view. FlutterViewController(int width, int height, const DartProject& project); - // Creates a FlutterView that can be parented into a Windows View hierarchy - // either using HWNDs. - // - // This creates the view on an existing FlutterEngine. - // - // |dart_project| will be used to configure the engine backing this view. - FlutterViewController(int width, - int height, - std::shared_ptr engine); - virtual ~FlutterViewController(); // Prevent copying. @@ -54,9 +44,6 @@ class FlutterViewController { // Returns the engine running Flutter content in this view. FlutterEngine* engine() const { return engine_.get(); } - // Returns the engine running Flutter content in this view. - std::shared_ptr shared_engine() const { return engine_; } - // Returns the view managed by this controller. FlutterView* view() const { return view_.get(); } diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_win32_window.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_win32_window.h deleted file mode 100644 index b91d50fef7c75..0000000000000 --- a/shell/platform/windows/client_wrapper/include/flutter/flutter_win32_window.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WIN32_WINDOW_H_ -#define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WIN32_WINDOW_H_ - -#include "flutter_view_controller.h" - -#include "win32_window.h" - -namespace flutter { - -// A window that does nothing but host a Flutter view. -class FlutterWin32Window : public Win32Window { - public: - // Creates a new FlutterWin32Window hosting a Flutter view running |engine|. - explicit FlutterWin32Window(std::shared_ptr engine); - FlutterWin32Window(std::shared_ptr engine, - std::shared_ptr wrapper); - ~FlutterWin32Window() override = default; - - auto GetFlutterViewId() const -> FlutterViewId; - - protected: - // Win32Window: - auto OnCreate() -> bool override; - void OnDestroy() override; - auto MessageHandler(HWND hwnd, - UINT message, - WPARAM wparam, - LPARAM lparam) -> LRESULT override; - - private: - // The engine this window is attached to. - std::shared_ptr engine_; - - // The Flutter instance hosted by this window. - std::unique_ptr view_controller_; -}; - -} // namespace flutter - -#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WIN32_WINDOW_H_ diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h deleted file mode 100644 index cbe5099d4d35b..0000000000000 --- a/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h +++ /dev/null @@ -1,84 +0,0 @@ -#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WINDOW_CONTROLLER_H_ -#define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WINDOW_CONTROLLER_H_ - -#include - -#include "flutter_engine.h" -#include "method_channel.h" -#include "win32_wrapper.h" -#include "windowing.h" - -namespace flutter { - -// A singleton controller for Flutter windows. -class FlutterWindowController { - public: - virtual ~FlutterWindowController(); - - // Prevent copying. - FlutterWindowController(FlutterWindowController const&) = delete; - FlutterWindowController& operator=(FlutterWindowController const&) = delete; - - void SetEngine(std::shared_ptr engine); - auto CreateFlutterWindow(std::wstring const& title, - WindowSize const& size, - WindowArchetype archetype, - std::optional positioner, - std::optional parent_view_id) - -> std::optional; - auto DestroyFlutterWindow(FlutterViewId view_id) -> bool; - - static FlutterWindowController& GetInstance() { - static FlutterWindowController instance; - return instance; - } - - protected: - FlutterWindowController(); - FlutterWindowController(std::shared_ptr wrapper); - - void MethodCallHandler(MethodCall<> const& call, MethodResult<>& result); - auto MessageHandler(HWND hwnd, - UINT message, - WPARAM wparam, - LPARAM lparam) -> LRESULT; - - virtual void SendOnWindowCreated( - FlutterViewId view_id, - std::optional parent_view_id) const; - virtual void SendOnWindowDestroyed(FlutterViewId view_id) const; - virtual void SendOnWindowChanged(FlutterViewId view_id) const; - - private: - friend class Win32Window; - - void DestroyWindows(); - auto GetWindowSize(FlutterViewId view_id) const -> WindowSize; - void HandleCreateWindow(WindowArchetype archetype, - MethodCall<> const& call, - MethodResult<>& result); - void HandleDestroyWindow(MethodCall<> const& call, MethodResult<>& result); - // Hides all satellite windows in the application, except those that are - // descendants of |opt_out_hwnd| or have a dialog as a child. By default, - // |opt_out_hwnd| is null, so no window is excluded. - void HideWindowsSatellites(HWND opt_out_hwnd = nullptr); - // Shows the satellite windows of |hwnd| and of its ancestors. - void ShowWindowAndAncestorsSatellites(HWND hwnd); - - // Controls whether satellites are hidden when their top-level window - // and all its children become inactive. If null, satellite hiding - // is enabled. If not null, it contains the handle of the window that - // disabled the hiding, and it will be reset when the window if fully - // destroyed. - HWND disable_satellite_hiding_{nullptr}; - - mutable std::mutex mutex_; - std::shared_ptr win32_; - std::unique_ptr> channel_; - std::shared_ptr engine_; - std::unordered_map> windows_; -}; - -} // namespace flutter - -#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WINDOW_CONTROLLER_H_ diff --git a/shell/platform/windows/client_wrapper/include/flutter/win32_window.h b/shell/platform/windows/client_wrapper/include/flutter/win32_window.h deleted file mode 100644 index 69f02b0605ed5..0000000000000 --- a/shell/platform/windows/client_wrapper/include/flutter/win32_window.h +++ /dev/null @@ -1,143 +0,0 @@ -#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WINDOW_H_ -#define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WINDOW_H_ - -#include "win32_wrapper.h" -#include "windowing.h" - -#include - -#include -#include -#include -#include - -namespace flutter { - -// A class abstraction for a high DPI-aware Win32 Window. Intended to be -// inherited from by classes that wish to specialize with custom -// rendering and input handling. -class Win32Window { - public: - Win32Window(); - explicit Win32Window(std::shared_ptr wrapper); - virtual ~Win32Window(); - - // Retrieves a class instance pointer for |hwnd|. - static auto GetThisFromHandle(HWND hwnd) -> Win32Window*; - - // Returns the backing window handle to enable clients to set icon and other - // window properties. Returns nullptr if the window has been destroyed. - auto GetHandle() const -> HWND; - - // If |quit_on_close| is true, closing this window will quit the application. - void SetQuitOnClose(bool quit_on_close); - - // Returns true if closing this window will cause the application to quit. - auto GetQuitOnClose() const -> bool; - - // Returns the bounds of the current client area. - auto GetClientArea() const -> RECT; - - // Returns the current window archetype. - auto GetArchetype() const -> WindowArchetype; - - // Returns the child windows. - auto GetChildren() const -> std::set const&; - - protected: - // Creates a native Win32 window. |title| is the window title string. - // |client_size| specifies the requested size of the client rectangle (i.e., - // the size of the view). The window style is determined by |archetype|. For - // |FlutterWindowArchetype::satellite| and |FlutterWindowArchetype::popup|, - // both |parent| and |positioner| must be provided; |positioner| is used only - // for these archetypes. For |FlutterWindowArchetype::dialog|, a modal dialog - // is created if |parent| is specified; otherwise, the dialog is modeless. - // After successful creation, |OnCreate| is called, and its result is - // returned. Otherwise, the return value is false. - auto Create(std::wstring const& title, - WindowSize const& client_size, - WindowArchetype archetype, - std::optional parent, - std::optional positioner) -> bool; - - // Release OS resources associated with window. - void Destroy(); - - // Inserts |content| into the window tree. - void SetChildContent(HWND content); - - // Processes and route salient window messages for mouse handling, - // size change and DPI. Delegates handling of these to member overloads that - // inheriting classes can handle. - virtual auto MessageHandler(HWND hwnd, - UINT message, - WPARAM wparam, - LPARAM lparam) -> LRESULT; - - // Called when Create is called, allowing subclass window-related setup. - // Subclasses should return false if setup fails. - virtual auto OnCreate() -> bool; - - // Called when Destroy is called. - virtual void OnDestroy(); - - private: - friend class FlutterWindowController; - - // 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. All other messages are handled by the - // controller's MessageHandler. - static auto CALLBACK WndProc(HWND hwnd, - UINT message, - WPARAM wparam, - LPARAM lparam) -> LRESULT; - - // Wrapper for Win32 API calls. - std::shared_ptr win32_; - - // The window's archetype (e.g., regular, dialog, popup). - WindowArchetype archetype_{WindowArchetype::regular}; - - // Windows that have this window as their parent or owner. - std::set children_; - - // The number of popups in |children_|, used to quickly check whether this - // window has any popups. - size_t num_child_popups_{0}; - - // Indicates whether closing this window will quit the application. - bool quit_on_close_{false}; - - // Handle for the top-level window. - HWND window_handle_{nullptr}; - - // Handle for hosted child content window. - HWND child_content_{nullptr}; - - // Offset between this window's position and its owner's position. - POINT offset_from_owner_{0, 0}; - - // Controls whether the non-client area can be redrawn as inactive. - // Enabled by default, but temporarily disabled during child popup destruction - // to prevent flickering. - bool enable_redraw_non_client_as_inactive_{true}; - - // Closes the popups of this window and returns the number of popups closed. - auto CloseChildPopups() -> std::size_t; - - // Enables or disables this window and all its descendants. - void EnableWindowAndDescendants(bool enable); - - // Enforces modal behavior by enabling the deepest dialog in the subtree - // rooted at the top-level window, along with its descendants, while - // disabling all other windows in the subtree. This ensures that the dialog - // and its children remain active and interactive. If no dialog is found, - // all windows in the subtree are enabled. - void UpdateModalState(); -}; - -} // namespace flutter - -#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WINDOW_H_ diff --git a/shell/platform/windows/client_wrapper/include/flutter/win32_wrapper.h b/shell/platform/windows/client_wrapper/include/flutter/win32_wrapper.h deleted file mode 100644 index 5e81e2130f64f..0000000000000 --- a/shell/platform/windows/client_wrapper/include/flutter/win32_wrapper.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WRAPPER_H_ -#define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WRAPPER_H_ - -#include - -namespace flutter { - -// Wraps Win32 API calls to enable mock-based testing. -class Win32Wrapper { - public: - virtual ~Win32Wrapper() = default; - - virtual HWND CreateWindowEx(DWORD dwExStyle, - LPCWSTR lpClassName, - LPCWSTR lpWindowName, - DWORD dwStyle, - int X, - int Y, - int nWidth, - int nHeight, - HWND hWndParent, - HMENU hMenu, - HINSTANCE hInstance, - LPVOID lpParam) { - return ::CreateWindowEx(dwExStyle, lpClassName, lpWindowName, dwStyle, X, Y, - nWidth, nHeight, hWndParent, hMenu, hInstance, - lpParam); - } - virtual BOOL DestroyWindow(HWND hWnd) { return ::DestroyWindow(hWnd); } -}; - -} // namespace flutter - -#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WRAPPER_H_ diff --git a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc index b5428bc1d26b1..f51d7f14ad879 100644 --- a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc +++ b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc @@ -114,15 +114,6 @@ bool FlutterDesktopEngineRun(FlutterDesktopEngineRef engine, return true; } -FlutterDesktopViewControllerRef FlutterDesktopEngineCreateViewController( - FlutterDesktopEngineRef engine, - const FlutterDesktopViewControllerProperties* properties) { - if (s_stub_implementation) { - return s_stub_implementation->EngineCreateViewController(properties); - } - return nullptr; -} - uint64_t FlutterDesktopEngineProcessMessages(FlutterDesktopEngineRef engine) { if (s_stub_implementation) { return s_stub_implementation->EngineProcessMessages(); @@ -237,17 +228,3 @@ void FlutterDesktopPluginRegistrarUnregisterTopLevelWindowProcDelegate( ->PluginRegistrarUnregisterTopLevelWindowProcDelegate(delegate); } } - -UINT FlutterDesktopGetDpiForMonitor(HMONITOR monitor) { - if (s_stub_implementation) { - return s_stub_implementation->GetDpiForMonitor(monitor); - } - return 96; -} - -UINT FlutterDesktopGetDpiForHWND(HWND hwnd) { - if (s_stub_implementation) { - return s_stub_implementation->GetDpiForHWND(hwnd); - } - return 96; -} diff --git a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h index a9780a8c3a53c..8f3eb0905ac7a 100644 --- a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h +++ b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h @@ -63,12 +63,6 @@ class StubFlutterWindowsApi { // Called for FlutterDesktopEngineRun. virtual bool EngineRun(const char* entry_point) { return true; } - // Called for FlutterDesktopEngineCreateViewController. - virtual FlutterDesktopViewControllerRef EngineCreateViewController( - const FlutterDesktopViewControllerProperties* properties) { - return nullptr; - } - // Called for FlutterDesktopEngineProcessMessages. virtual uint64_t EngineProcessMessages() { return 0; } @@ -121,12 +115,6 @@ class StubFlutterWindowsApi { LRESULT* result) { return false; } - - // Called for FlutterDesktopGetDpiForMonitor. - virtual UINT GetDpiForMonitor(HMONITOR monitor) { return 96; } - - // Called for FlutterDesktopGetDpiForHWND. - virtual UINT GetDpiForHWND(HWND hwnd) { return 96; } }; // A test helper that owns a stub implementation, making it the test stub for diff --git a/shell/platform/windows/client_wrapper/win32_window.cc b/shell/platform/windows/client_wrapper/win32_window.cc deleted file mode 100644 index f9c702e0a9282..0000000000000 --- a/shell/platform/windows/client_wrapper/win32_window.cc +++ /dev/null @@ -1,829 +0,0 @@ -#include "include/flutter/win32_window.h" -#include "include/flutter/flutter_window_controller.h" - -#include "flutter_windows.h" - -#include -#include -#include -#include -#include - -#include - -namespace { - -auto const* const kWindowClassName{L"FLUTTER_WIN32_WINDOW"}; - -// The number of Win32Window objects that currently exist. -static int gActiveWindowCount{0}; -// A mutex for thread-safe use of the window count. -static std::mutex gActiveWindowMutex; - -// Retrieves the calling thread's last-error code message as a string, -// or a fallback message if the error message cannot be formatted. -auto GetLastErrorAsString() -> std::string { - LPWSTR message_buffer{nullptr}; - - if (auto 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 (auto 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() << '\n'; - return oss.str(); -} - -// Estimates the size of the window frame, in physical coordinates, based on -// the given |window_size| (in physical coordinates) and the specified -// |window_style|, |extended_window_style|, and parent window |parent_hwnd|. -auto GetFrameSizeForWindowSize(flutter::WindowSize const& window_size, - DWORD window_style, - DWORD extended_window_style, - HWND parent_hwnd) -> flutter::WindowSize { - RECT frame_rect{0, 0, static_cast(window_size.width), - static_cast(window_size.height)}; - - WNDCLASS window_class{0}; - window_class.lpfnWndProc = DefWindowProc; - window_class.hInstance = GetModuleHandle(nullptr); - window_class.lpszClassName = L"FLUTTER_WIN32_WINDOW_TEMPORARY"; - RegisterClass(&window_class); - - window_style &= ~WS_VISIBLE; - if (auto const window{CreateWindowEx( - extended_window_style, window_class.lpszClassName, L"", window_style, - CW_USEDEFAULT, CW_USEDEFAULT, window_size.width, window_size.height, - parent_hwnd, nullptr, GetModuleHandle(nullptr), nullptr)}) { - DwmGetWindowAttribute(window, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rect, - sizeof(frame_rect)); - DestroyWindow(window); - } - - UnregisterClass(window_class.lpszClassName, nullptr); - - return {static_cast(frame_rect.right - frame_rect.left), - static_cast(frame_rect.bottom - frame_rect.top)}; -} - -// 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 drop-shadow effects. -auto GetWindowSizeForClientSize(flutter::WindowSize const& client_size, - DWORD window_style, - DWORD extended_window_style, - HWND parent_hwnd) -> flutter::WindowSize { - auto const dpi{FlutterDesktopGetDpiForHWND(parent_hwnd)}; - auto 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 { - std::cerr << "Failed to run AdjustWindowRectExForDpi: " - << GetLastErrorAsString() << '\n'; - } - } else { - std::cerr << "Failed to retrieve AdjustWindowRectExForDpi address from " - "User32.dll.\n"; - } - FreeLibrary(user32_module); - } else { - std::cerr << "Failed to load User32.dll.\n"; - } - - if (!AdjustWindowRectEx(&rect, window_style, FALSE, extended_window_style)) { - std::cerr << "Failed to run AdjustWindowRectEx: " << GetLastErrorAsString() - << '\n'; - } - return {static_cast(rect.right - rect.left), - static_cast(rect.bottom - rect.top)}; -} - -// Calculates the offset from the top-left corner of |from| to the top-left -// corner of |to|. If either window handle is null or if the window positions -// cannot be retrieved, the offset will be (0, 0). -auto GetOffsetBetweenWindows(HWND from, HWND to) -> POINT { - POINT offset{0, 0}; - if (to && from) { - RECT to_rect; - RECT from_rect; - if (GetWindowRect(to, &to_rect) && GetWindowRect(from, &from_rect)) { - offset.x = to_rect.left - from_rect.left; - offset.y = to_rect.top - from_rect.top; - } - } - return offset; -} - -// 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); -} - -/// 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)); - } -} - -auto IsClassRegistered(LPCWSTR class_name) -> bool { - WNDCLASSEX window_class{}; - return GetClassInfoEx(GetModuleHandle(nullptr), class_name, &window_class) != - 0; -} - -} // namespace - -namespace flutter { - -Win32Window::Win32Window() : win32_{std::make_shared()} {} - -Win32Window::Win32Window(std::shared_ptr wrapper) - : win32_{std::move(wrapper)} {} - -Win32Window::~Win32Window() { - std::lock_guard lock(gActiveWindowMutex); - if (--gActiveWindowCount == 0) { - UnregisterClass(kWindowClassName, GetModuleHandle(nullptr)); - } -} - -auto Win32Window::GetThisFromHandle(HWND hwnd) -> Win32Window* { - return reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); -} - -auto Win32Window::GetHandle() const -> HWND { - return window_handle_; -} - -void Win32Window::SetQuitOnClose(bool quit_on_close) { - quit_on_close_ = quit_on_close; -} - -auto Win32Window::GetQuitOnClose() const -> bool { - return quit_on_close_; -} - -auto Win32Window::GetClientArea() const -> RECT { - RECT client_rect; - GetClientRect(window_handle_, &client_rect); - return client_rect; -} - -auto Win32Window::GetArchetype() const -> WindowArchetype { - return archetype_; -} - -auto Win32Window::GetChildren() const -> std::set const& { - return children_; -} - -auto Win32Window::Create(std::wstring const& title, - WindowSize const& client_size, - WindowArchetype archetype, - std::optional parent, - std::optional positioner) -> bool { - std::lock_guard lock(gActiveWindowMutex); - - archetype_ = archetype; - - DWORD window_style{}; - DWORD extended_window_style{}; - - switch (archetype) { - case WindowArchetype::regular: - window_style |= WS_OVERLAPPEDWINDOW; - break; - case WindowArchetype::floating_regular: - // TODO - break; - case WindowArchetype::dialog: - window_style |= WS_OVERLAPPED | WS_CAPTION; - extended_window_style |= WS_EX_DLGMODALFRAME; - if (!parent) { - // If the dialog has no parent, add a minimize box and a system menu - // (which includes a close button) - window_style |= WS_MINIMIZEBOX | WS_SYSMENU; - } else { - // If the parent window has the WS_EX_TOOLWINDOW style, apply the same - // style to the dialog - if (GetWindowLongPtr(parent.value(), GWL_EXSTYLE) & WS_EX_TOOLWINDOW) { - extended_window_style |= WS_EX_TOOLWINDOW; - } - if (auto* const parent_window{GetThisFromHandle(parent.value())}) { - parent_window->children_.insert(this); - } - } - break; - case WindowArchetype::satellite: - window_style |= WS_OVERLAPPEDWINDOW & ~WS_MAXIMIZEBOX; - extended_window_style |= WS_EX_TOOLWINDOW; - if (auto* const parent_window{ - GetThisFromHandle(parent.value_or(nullptr))}) { - if (parent_window->child_content_ != nullptr) { - SetFocus(parent_window->child_content_); - } - parent_window->children_.insert(this); - } else { - std::cerr << "The parent of a satellite must not be null.\n"; - std::abort(); - } - break; - case WindowArchetype::popup: - window_style |= WS_POPUP; - if (auto* const parent_window{ - GetThisFromHandle(parent.value_or(nullptr))}) { - if (parent_window->child_content_ != nullptr) { - SetFocus(parent_window->child_content_); - } - parent_window->children_.insert(this); - ++parent_window->num_child_popups_; - } - break; - case WindowArchetype::tip: - // TODO - break; - default: - std::cerr << "Unhandled window archetype: " << static_cast(archetype) - << "\n"; - std::abort(); - } - - // Window rectangle in physical coordinates. - // Default positioning values (CW_USEDEFAULT) are used - // if the window has no parent or positioner. Parented dialogs will be - // centered in the parent's frame. - auto const window_rect{[&]() -> WindowRectangle { - auto const window_size{GetWindowSizeForClientSize( - client_size, window_style, extended_window_style, - parent.value_or(nullptr))}; - if (parent) { - if (positioner) { - auto const frame_size{GetFrameSizeForWindowSize( - window_size, window_style, extended_window_style, parent.value())}; - - // The rectangle of the parent's client area, in physical coordinates - auto const parent_rect{[](HWND parent_window) -> WindowRectangle { - RECT client_rect; - GetClientRect(parent_window, &client_rect); - POINT top_left{client_rect.left, client_rect.top}; - ClientToScreen(parent_window, &top_left); - POINT bottom_right{client_rect.right, client_rect.bottom}; - ClientToScreen(parent_window, &bottom_right); - return {{top_left.x, top_left.y}, - {bottom_right.x - top_left.x, bottom_right.y - top_left.y}}; - }(parent.value())}; - - // The anchor rectangle, in physical coordinates - auto const anchor_rect{[](WindowPositioner const& positioner, - HWND parent_window, - WindowRectangle const& parent_rect) - -> WindowRectangle { - if (positioner.anchor_rect) { - auto const dpr{FlutterDesktopGetDpiForHWND(parent_window) / - static_cast(USER_DEFAULT_SCREEN_DPI)}; - return { - {parent_rect.top_left.x + - static_cast(positioner.anchor_rect->top_left.x * dpr), - parent_rect.top_left.y + - static_cast(positioner.anchor_rect->top_left.y * - dpr)}, - {static_cast(positioner.anchor_rect->size.width * dpr), - static_cast(positioner.anchor_rect->size.height * dpr)}}; - } else { - // If the anchor rect specified in the positioner is std::nullopt, - // return an anchor rect that is equal to the window frame area - RECT frame_rect; - DwmGetWindowAttribute(parent_window, DWMWA_EXTENDED_FRAME_BOUNDS, - &frame_rect, sizeof(frame_rect)); - return {{frame_rect.left, frame_rect.top}, - {frame_rect.right - frame_rect.left, - frame_rect.bottom - frame_rect.top}}; - } - }(positioner.value(), parent.value(), parent_rect)}; - - // Rectangle of the monitor that has the largest area of intersection - // with the anchor rectangle, in physical coordinates - auto const output_rect{ - [](RECT anchor_rect) - -> WindowRectangle { - auto* monitor{ - MonitorFromRect(&anchor_rect, MONITOR_DEFAULTTONEAREST)}; - MONITORINFO mi; - mi.cbSize = sizeof(MONITORINFO); - auto const bounds{ - GetMonitorInfo(monitor, &mi) ? mi.rcWork : RECT{0, 0, 0, 0}}; - return {{bounds.left, bounds.top}, - {bounds.right - bounds.left, bounds.bottom - bounds.top}}; - }({.left = static_cast(anchor_rect.top_left.x), - .top = static_cast(anchor_rect.top_left.y), - .right = static_cast(anchor_rect.top_left.x + - anchor_rect.size.width), - .bottom = static_cast(anchor_rect.top_left.y + - anchor_rect.size.height)})}; - - auto const rect{internal::PlaceWindow( - positioner.value(), frame_size, anchor_rect, - positioner->anchor_rect ? parent_rect : anchor_rect, output_rect)}; - - return {rect.top_left, - {rect.size.width + window_size.width - frame_size.width, - rect.size.height + window_size.height - frame_size.height}}; - } else if (archetype == WindowArchetype::dialog) { - // Center parented dialog in the parent frame - RECT parent_frame; - DwmGetWindowAttribute(parent.value(), DWMWA_EXTENDED_FRAME_BOUNDS, - &parent_frame, sizeof(parent_frame)); - WindowPoint const top_left{ - static_cast( - (parent_frame.left + parent_frame.right - window_size.width) * - 0.5), - static_cast( - (parent_frame.top + parent_frame.bottom - window_size.height) * - 0.5)}; - return {top_left, window_size}; - } - } - return {{CW_USEDEFAULT, CW_USEDEFAULT}, window_size}; - }()}; - - 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 = Win32Window::WndProc; - window_class.cbClsExtra = 0; - window_class.cbWndExtra = 0; - window_class.hInstance = GetModuleHandle(nullptr); - window_class.hIcon = - LoadIcon(window_class.hInstance, MAKEINTRESOURCE(idi_app_icon)); - window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); - window_class.hbrBackground = 0; - window_class.lpszMenuName = nullptr; - window_class.lpszClassName = kWindowClassName; - window_class.hIconSm = nullptr; - - RegisterClassEx(&window_class); - } - - window_handle_ = win32_->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, parent.value_or(nullptr), nullptr, - GetModuleHandle(nullptr), this); - - if (!window_handle_) { - auto const error_message{GetLastErrorAsString()}; - std::cerr << "Cannot create window due to a CreateWindowEx error: " - << error_message.c_str() << '\n'; - return false; - } - - // 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(window_handle_, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rc, - sizeof(frame_rc)); - RECT window_rc; - GetWindowRect(window_handle_, &window_rc); - auto const left_dropshadow_width{frame_rc.left - window_rc.left}; - auto const top_dropshadow_height{window_rc.top - frame_rc.top}; - SetWindowPos(window_handle_, nullptr, window_rc.left - left_dropshadow_width, - window_rc.top - top_dropshadow_height, 0, 0, - SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); - - if (parent) { - if (auto* const owner_window{GetWindow(window_handle_, GW_OWNER)}) { - offset_from_owner_ = - GetOffsetBetweenWindows(owner_window, window_handle_); - } - } - - UpdateTheme(window_handle_); - - if (archetype == WindowArchetype::dialog && parent) { - UpdateModalState(); - } - - gActiveWindowCount++; - - ShowWindow(window_handle_, SW_SHOW); - - return OnCreate(); -} - -void Win32Window::Destroy() { - OnDestroy(); -} - -void Win32Window::SetChildContent(HWND content) { - child_content_ = content; - SetParent(content, window_handle_); - auto const client_rect{GetClientArea()}; - - MoveWindow(content, client_rect.left, client_rect.top, - client_rect.right - client_rect.left, - client_rect.bottom - client_rect.top, true); - - SetFocus(child_content_); -} - -auto Win32Window::MessageHandler(HWND hwnd, - UINT message, - WPARAM wparam, - LPARAM lparam) -> LRESULT { - switch (message) { - case WM_DESTROY: - Destroy(); - if (quit_on_close_) { - PostQuitMessage(0); - } - return 0; - - case WM_DPICHANGED: { - auto* const new_scaled_window_rect{reinterpret_cast(lparam)}; - auto const width{new_scaled_window_rect->right - - new_scaled_window_rect->left}; - auto 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 (wparam == SIZE_MAXIMIZED) { - // Hide satellites of the maximized window - for (auto* const child : children_) { - if (child->archetype_ == WindowArchetype::satellite) { - ShowWindow(child->GetHandle(), SW_HIDE); - } - } - } else if (wparam == SIZE_RESTORED) { - // Show satellites of the restored window - for (auto* const child : children_) { - if (child->archetype_ == WindowArchetype::satellite) { - ShowWindow(child->GetHandle(), SW_SHOWNOACTIVATE); - } - } - } - if (child_content_ != nullptr) { - // Resize and reposition the child content window - auto const client_rect{GetClientArea()}; - 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: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return 0; - - case WM_NCACTIVATE: - if (wparam == FALSE && archetype_ != WindowArchetype::popup) { - if (!enable_redraw_non_client_as_inactive_ || num_child_popups_ > 0) { - // If an inactive title bar is to be drawn, and this is a top-level - // window with popups, force the title bar to be drawn in its active - // colors - return TRUE; - } - } - break; - - case WM_MOVE: { - if (auto* const owner_window{GetWindow(window_handle_, GW_OWNER)}) { - offset_from_owner_ = - GetOffsetBetweenWindows(owner_window, window_handle_); - } - - // Move satellites attached to this window - RECT window_rect; - GetWindowRect(hwnd, &window_rect); - for (auto* const child : children_) { - if (child->archetype_ == WindowArchetype::satellite) { - RECT rect_satellite; - GetWindowRect(child->GetHandle(), &rect_satellite); - MoveWindow(child->GetHandle(), - window_rect.left + child->offset_from_owner_.x, - window_rect.top + child->offset_from_owner_.y, - rect_satellite.right - rect_satellite.left, - rect_satellite.bottom - rect_satellite.top, FALSE); - } - } - } break; - - case WM_MOUSEACTIVATE: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return MA_ACTIVATE; - - case WM_DWMCOLORIZATIONCOLORCHANGED: - UpdateTheme(hwnd); - return 0; - - default: - break; - } - - return DefWindowProc(window_handle_, message, wparam, lparam); -} - -auto Win32Window::OnCreate() -> bool { - // No-op; provided for subclasses. - return true; -} - -void Win32Window::OnDestroy() { - switch (archetype_) { - case WindowArchetype::regular: - break; - case WindowArchetype::floating_regular: - break; - case WindowArchetype::dialog: - if (auto* const owner_window_handle{ - GetWindow(window_handle_, GW_OWNER)}) { - if (auto* const owner_window{GetThisFromHandle(owner_window_handle)}) { - owner_window->children_.erase(this); - } - UpdateModalState(); - SetFocus(owner_window_handle); - } - break; - case WindowArchetype::satellite: - if (auto* const owner_window_handle{ - GetWindow(window_handle_, GW_OWNER)}) { - if (auto* const owner_window{GetThisFromHandle(owner_window_handle)}) { - owner_window->children_.erase(this); - } - } - break; - case WindowArchetype::popup: - if (auto* const parent_window_handle{GetParent(window_handle_)}) { - if (auto* const parent_window{ - GetThisFromHandle(parent_window_handle)}) { - parent_window->children_.erase(this); - assert(parent_window->num_child_popups_ > 0); - --parent_window->num_child_popups_; - } - } - break; - case WindowArchetype::tip: - break; - default: - std::cerr << "Unhandled window archetype encountered in " - "Win32Window::OnDestroy: " - << static_cast(archetype_) << "\n"; - std::abort(); - } -} - -// static -auto CALLBACK Win32Window::WndProc(HWND hwnd, - UINT message, - WPARAM wparam, - LPARAM lparam) -> LRESULT { - 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 (auto* const window{GetThisFromHandle(hwnd)}) { - return FlutterWindowController::GetInstance().MessageHandler( - hwnd, message, wparam, lparam); - } - - return DefWindowProc(hwnd, message, wparam, lparam); -} - -auto Win32Window::CloseChildPopups() -> std::size_t { - if (num_child_popups_ == 0) { - return 0; - } - - std::set popups; - for (auto* const child : children_) { - if (child->archetype_ == WindowArchetype::popup) { - popups.insert(child); - } - } - - for (auto it{children_.begin()}; it != children_.end();) { - if ((*it)->archetype_ == WindowArchetype::popup) { - it = children_.erase(it); - } else { - ++it; - } - } - - auto const previous_num_child_popups{num_child_popups_}; - - for (auto* popup : popups) { - auto const parent_handle{GetParent(popup->window_handle_)}; - if (auto* const parent{GetThisFromHandle(parent_handle)}) { - // Popups' parents are drawn with active colors even though they are - // actually inactive. When a popup is destroyed, the parent might be - // redrawn as inactive (reflecting its true state) before being redrawn as - // active. To prevent flickering during this transition, disable - // redrawing the non-client area as inactive. - parent->enable_redraw_non_client_as_inactive_ = false; - DestroyWindow(popup->GetHandle()); - parent->enable_redraw_non_client_as_inactive_ = true; - - // Repaint parent window to make sure its title bar is painted with the - // color based on its actual activation state - if (parent->num_child_popups_ == 0) { - SetWindowPos(parent_handle, nullptr, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); - } - } - } - - return previous_num_child_popups - num_child_popups_; -} - -void Win32Window::EnableWindowAndDescendants(bool enable) { - EnableWindow(window_handle_, enable); - for (auto* const child : children_) { - child->EnableWindowAndDescendants(enable); - } -} - -void Win32Window::UpdateModalState() { - auto const find_deepest_dialog{ - [](Win32Window* window, auto&& self) -> Win32Window* { - Win32Window* deepest_dialog{nullptr}; - if (window->archetype_ == WindowArchetype::dialog) { - deepest_dialog = window; - } - for (auto* const child : window->children_) { - if (auto* const child_deepest_dialog{self(child, self)}) { - deepest_dialog = child_deepest_dialog; - } - } - return deepest_dialog; - }}; - - auto const get_parent_or_owner{[](HWND window) -> HWND { - auto const parent{GetParent(window)}; - return parent ? parent : GetWindow(window, GW_OWNER); - }}; - - auto* root_ancestor_handle{window_handle_}; - while (auto* next{get_parent_or_owner(root_ancestor_handle)}) { - root_ancestor_handle = next; - } - if (auto* const root_ancestor{GetThisFromHandle(root_ancestor_handle)}) { - if (auto* const deepest_dialog{ - find_deepest_dialog(root_ancestor, find_deepest_dialog)}) { - root_ancestor->EnableWindowAndDescendants(false); - deepest_dialog->EnableWindowAndDescendants(true); - } else { - root_ancestor->EnableWindowAndDescendants(true); - } - } -} - -} // namespace flutter diff --git a/shell/platform/windows/flutter_host_window.cc b/shell/platform/windows/flutter_host_window.cc new file mode 100644 index 0000000000000..2e6d9d0820953 --- /dev/null +++ b/shell/platform/windows/flutter_host_window.cc @@ -0,0 +1,908 @@ +// 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); +} + +// Computes the screen-space anchor rectangle for a window being positioned +// with |positioner|, having |owner_hwnd| as owner, and |owner_rect| +// as the owner's client rectangle, also in screen space. If the positioner +// specifies an anchor rectangle (in logical coordinates), its coordinates are +// scaled using the owner's DPI and offset relative to |owner_rect|. +// Otherwise, the function defaults to using the window frame of |owner_hwnd| +// as the anchor rectangle. +flutter::WindowRectangle GetAnchorRectInScreenSpace( + flutter::WindowPositioner const& positioner, + HWND owner_hwnd, + flutter::WindowRectangle const& owner_rect) { + if (positioner.anchor_rect) { + double const dpr = flutter::GetDpiForHWND(owner_hwnd) / + static_cast(USER_DEFAULT_SCREEN_DPI); + return {{owner_rect.top_left.x + + static_cast(positioner.anchor_rect->top_left.x * dpr), + owner_rect.top_left.y + + static_cast(positioner.anchor_rect->top_left.y * dpr)}, + {static_cast(positioner.anchor_rect->size.width * dpr), + static_cast(positioner.anchor_rect->size.height * dpr)}}; + } else { + // If the anchor rectangle specified in the positioner is std::nullopt, + // return an anchor rectangle that is equal to the owner's frame. + RECT frame_rect; + DwmGetWindowAttribute(owner_hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rect, + sizeof(frame_rect)); + return {{frame_rect.left, frame_rect.top}, + {frame_rect.right - frame_rect.left, + frame_rect.bottom - frame_rect.top}}; + } +} + +// Calculates the client area of |hwnd| in screen space. +flutter::WindowRectangle GetClientRectInScreenSpace(HWND hwnd) { + RECT client_rect; + GetClientRect(hwnd, &client_rect); + POINT top_left = {0, 0}; + ClientToScreen(hwnd, &top_left); + POINT bottom_right = {client_rect.right, client_rect.bottom}; + ClientToScreen(hwnd, &bottom_right); + return {{top_left.x, top_left.y}, + {bottom_right.x - top_left.x, bottom_right.y - top_left.y}}; +} + +// Calculates the size of the window frame in physical coordinates, based on +// the given |window_size| (also in physical coordinates) and the specified +// |window_style|, |extended_window_style|, and owner window |owner_hwnd|. +flutter::WindowSize GetFrameSizeForWindowSize( + flutter::WindowSize const& window_size, + DWORD window_style, + DWORD extended_window_style, + HWND owner_hwnd) { + RECT frame_rect = {0, 0, static_cast(window_size.width), + static_cast(window_size.height)}; + + HINSTANCE hInstance = GetModuleHandle(nullptr); + WNDCLASS window_class = {}; + window_class.lpfnWndProc = DefWindowProc; + window_class.hInstance = hInstance; + window_class.lpszClassName = L"FLUTTER_HOST_WINDOW_TEMPORARY"; + RegisterClass(&window_class); + + window_style &= ~WS_VISIBLE; + if (HWND const window = CreateWindowEx( + extended_window_style, window_class.lpszClassName, L"", window_style, + CW_USEDEFAULT, CW_USEDEFAULT, window_size.width, window_size.height, + owner_hwnd, nullptr, hInstance, nullptr)) { + DwmGetWindowAttribute(window, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rect, + sizeof(frame_rect)); + DestroyWindow(window); + } + + UnregisterClass(window_class.lpszClassName, hInstance); + + return {static_cast(frame_rect.right - frame_rect.left), + static_cast(frame_rect.bottom - frame_rect.top)}; +} + +// 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 offset from the top-left corner of |from| to the top-left +// corner of |to|. If either window handle is null or if the window positions +// cannot be retrieved, the offset will be (0, 0). +POINT GetOffsetBetweenWindows(HWND from, HWND to) { + POINT offset = {0, 0}; + if (to && from) { + RECT to_rect; + RECT from_rect; + if (GetWindowRect(to, &to_rect) && GetWindowRect(from, &from_rect)) { + offset.x = to_rect.left - from_rect.left; + offset.y = to_rect.top - from_rect.top; + } + } + return offset; +} + +// Calculates the rectangle of the monitor that has the largest area of +// intersection with |rect|, in physical coordinates. +flutter::WindowRectangle GetOutputRect(RECT rect) { + HMONITOR monitor = MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST); + MONITORINFO mi; + mi.cbSize = sizeof(MONITORINFO); + RECT const bounds = + GetMonitorInfo(monitor, &mi) ? mi.rcWork : RECT{0, 0, 0, 0}; + return {{bounds.left, bounds.top}, + {bounds.right - bounds.left, bounds.bottom - bounds.top}}; +} + +// 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, + std::optional owner, + std::optional positioner) + : 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: + if (owner.has_value()) { + FML_LOG(ERROR) << "A regular window cannot have an owner."; + return; + } + if (positioner.has_value()) { + FML_LOG(ERROR) << "A regular window cannot have a positioner."; + return; + } + window_style |= WS_OVERLAPPEDWINDOW; + break; + case WindowArchetype::dialog: + if (positioner.has_value()) { + FML_LOG(ERROR) << "A dialog cannot have a positioner."; + return; + } + window_style |= WS_OVERLAPPED | WS_CAPTION; + extended_window_style |= WS_EX_DLGMODALFRAME; + if (!owner) { + // If the dialog has no owner, add a minimize box and a system menu. + window_style |= WS_MINIMIZEBOX | WS_SYSMENU; + } else { + // If the owner window has WS_EX_TOOLWINDOW style, apply the same + // style to the dialog. + if (GetWindowLongPtr(owner.value(), GWL_EXSTYLE) & WS_EX_TOOLWINDOW) { + extended_window_style |= WS_EX_TOOLWINDOW; + } + } + break; + case WindowArchetype::satellite: + if (!positioner.has_value()) { + FML_LOG(ERROR) << "A satellite window requires a positioner."; + return; + } + if (!owner.has_value()) { + FML_LOG(ERROR) << "A satellite window must have an owner."; + return; + } + window_style |= WS_OVERLAPPEDWINDOW & ~WS_MAXIMIZEBOX; + extended_window_style |= WS_EX_TOOLWINDOW; + break; + case WindowArchetype::popup: + if (!positioner.has_value()) { + FML_LOG(ERROR) << "A popup window requires a positioner."; + return; + } + if (!owner.has_value()) { + FML_LOG(ERROR) << "A popup window must have an owner."; + return; + } + window_style |= WS_POPUP; + 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. Owned dialogs will be + // centered in the owner's frame. + WindowRectangle const window_rect = [&]() -> WindowRectangle { + WindowSize const window_size = GetWindowSizeForClientSize( + preferred_client_size, window_style, extended_window_style, + owner.value_or(nullptr)); + if (owner) { + if (positioner) { + // Calculate the window rectangle according to a positioner and + // the owner's rectangle. + WindowSize const frame_size = GetFrameSizeForWindowSize( + window_size, window_style, extended_window_style, owner.value()); + + WindowRectangle const owner_rect = + GetClientRectInScreenSpace(owner.value()); + + WindowRectangle const anchor_rect = GetAnchorRectInScreenSpace( + positioner.value(), owner.value(), owner_rect); + + WindowRectangle const output_rect = GetOutputRect( + {.left = static_cast(anchor_rect.top_left.x), + .top = static_cast(anchor_rect.top_left.y), + .right = static_cast(anchor_rect.top_left.x + + anchor_rect.size.width), + .bottom = static_cast(anchor_rect.top_left.y + + anchor_rect.size.height)}); + + WindowRectangle const rect = PlaceWindow( + positioner.value(), frame_size, anchor_rect, + positioner->anchor_rect ? owner_rect : anchor_rect, output_rect); + + return {rect.top_left, + {rect.size.width + window_size.width - frame_size.width, + rect.size.height + window_size.height - frame_size.height}}; + } else if (archetype == WindowArchetype::dialog) { + // Center owned dialog in the owner's frame. + RECT owner_frame; + DwmGetWindowAttribute(owner.value(), DWMWA_EXTENDED_FRAME_BOUNDS, + &owner_frame, sizeof(owner_frame)); + WindowPoint const top_left = { + static_cast( + (owner_frame.left + owner_frame.right - window_size.width) * + 0.5), + static_cast( + (owner_frame.top + owner_frame.bottom - window_size.height) * + 0.5)}; + return {top_left, window_size}; + } + } + 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, owner.value_or(nullptr), nullptr, + GetModuleHandle(nullptr), this); + + if (!hwnd) { + FML_LOG(ERROR) << "Cannot create window: " << GetLastErrorAsString(); + return; + } + + // If this is a modeless dialog, remove the close button from the system menu. + if (archetype == WindowArchetype::dialog && !owner) { + if (HMENU hMenu = GetSystemMenu(hwnd, FALSE)) { + DeleteMenu(hMenu, SC_CLOSE, MF_BYCOMMAND); + } + } + + // 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); + + if (owner) { + if (HWND const owner_window = GetWindow(hwnd, GW_OWNER)) { + offset_from_owner_ = GetOffsetBetweenWindows(owner_window, hwnd); + } + } + + // 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; + } + + // Update the properties of the owner window, if it exists. + if (FlutterHostWindow* const owner_window = + GetThisFromHandle(owner.value_or(nullptr))) { + owner_window->owned_windows_.insert(this); + + if (archetype == WindowArchetype::popup) { + ++owner_window->num_owned_popups_; + } + } + + UpdateTheme(hwnd); + + if (archetype == WindowArchetype::dialog && owner) { + UpdateModalState(); + } + + 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::set const& FlutterHostWindow::GetOwnedWindows() const { + return owned_windows_; +} + +std::optional FlutterHostWindow::GetFlutterViewId() const { + if (!view_controller_ || !view_controller_->view()) { + return std::nullopt; + } + return view_controller_->view()->view_id(); +}; + +FlutterHostWindow* FlutterHostWindow::GetOwnerWindow() const { + if (HWND const owner_window_handle = GetWindow(GetWindowHandle(), GW_OWNER)) { + return GetThisFromHandle(owner_window_handle); + } + return nullptr; +}; + +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); +} + +std::size_t FlutterHostWindow::CloseOwnedPopups() { + if (num_owned_popups_ == 0) { + return 0; + } + + std::set popups; + for (FlutterHostWindow* const owned : owned_windows_) { + if (owned->archetype_ == WindowArchetype::popup) { + popups.insert(owned); + } + } + + for (auto it = owned_windows_.begin(); it != owned_windows_.end();) { + if ((*it)->archetype_ == WindowArchetype::popup) { + it = owned_windows_.erase(it); + } else { + ++it; + } + } + + std::size_t const previous_num_owned_popups = num_owned_popups_; + + for (FlutterHostWindow* popup : popups) { + HWND const owner_handle = GetWindow(popup->window_handle_, GW_OWNER); + if (FlutterHostWindow* const owner = GetThisFromHandle(owner_handle)) { + // Popups' owners are drawn with active colors even though they are + // actually inactive. When a popup is destroyed, the owner might be + // redrawn as inactive (reflecting its true state) before being redrawn as + // active. To prevent flickering during this transition, disable + // redrawing the non-client area as inactive. + owner->enable_redraw_non_client_as_inactive_ = false; + DestroyWindow(popup->GetWindowHandle()); + owner->enable_redraw_non_client_as_inactive_ = true; + + // Repaint owner window to make sure its title bar is painted with the + // color based on its actual activation state. + if (owner->num_owned_popups_ == 0) { + SetWindowPos(owner_handle, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); + } + } + } + + return previous_num_owned_popups - num_owned_popups_; +} + +void FlutterHostWindow::EnableWindowAndDescendants(bool enable) { + EnableWindow(window_handle_, enable); + + for (FlutterHostWindow* const owned : owned_windows_) { + owned->EnableWindowAndDescendants(enable); + } +} + +FlutterHostWindow* FlutterHostWindow::FindFirstEnabledDescendant() const { + if (IsWindowEnabled(GetWindowHandle())) { + return const_cast(this); + } + + for (FlutterHostWindow* const owned : GetOwnedWindows()) { + if (FlutterHostWindow* const result = owned->FindFirstEnabledDescendant()) { + return result; + } + } + + return nullptr; +} + +LRESULT FlutterHostWindow::HandleMessage(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) { + switch (message) { + case WM_DESTROY: + if (window_handle_) { + switch (archetype_) { + case WindowArchetype::regular: + break; + case WindowArchetype::dialog: + if (FlutterHostWindow* const owner_window = GetOwnerWindow()) { + owner_window->owned_windows_.erase(this); + UpdateModalState(); + FocusViewOf(owner_window); + } + break; + case WindowArchetype::satellite: + if (FlutterHostWindow* const owner_window = GetOwnerWindow()) { + owner_window->owned_windows_.erase(this); + FocusViewOf(owner_window); + } + break; + case WindowArchetype::popup: + if (FlutterHostWindow* const owner_window = GetOwnerWindow()) { + owner_window->owned_windows_.erase(this); + assert(owner_window->num_owned_popups_ > 0); + --owner_window->num_owned_popups_; + FocusViewOf(owner_window); + } + break; + default: + FML_UNREACHABLE(); + } + if (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 (wparam == SIZE_MAXIMIZED) { + // Hide the satellites of the maximized window + for (FlutterHostWindow* const owned : owned_windows_) { + if (owned->archetype_ == WindowArchetype::satellite) { + ShowWindow(owned->GetWindowHandle(), SW_HIDE); + } + } + } else if (wparam == SIZE_RESTORED) { + // Show the satellites of the restored window + for (FlutterHostWindow* const owned : owned_windows_) { + if (owned->archetype_ == WindowArchetype::satellite) { + ShowWindow(owned->GetWindowHandle(), SW_SHOWNOACTIVATE); + } + } + } + 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: + // Prevent disabled window from being activated using the task switcher + if (!IsWindowEnabled(hwnd) && LOWORD(wparam) != WA_INACTIVE) { + // Redirect focus and activation to the first enabled descendant + if (FlutterHostWindow* enabled_descendant = + FindFirstEnabledDescendant()) { + SetActiveWindow(enabled_descendant->GetWindowHandle()); + } + return 0; + } + FocusViewOf(this); + return 0; + + case WM_NCACTIVATE: + if (wparam == FALSE && archetype_ != WindowArchetype::popup) { + if (!enable_redraw_non_client_as_inactive_ || num_owned_popups_ > 0) { + // If an inactive title bar is to be drawn, and this is a top-level + // window with popups, force the title bar to be drawn in its active + // colors. + return TRUE; + } + } + break; + + case WM_MOVE: { + if (HWND const owner_window = GetWindow(hwnd, GW_OWNER)) { + offset_from_owner_ = GetOffsetBetweenWindows(owner_window, hwnd); + } + + // Move the satellites attached to this window. + RECT window_rect; + GetWindowRect(hwnd, &window_rect); + for (FlutterHostWindow* const owned : owned_windows_) { + if (owned->archetype_ == WindowArchetype::satellite) { + RECT rect_satellite; + GetWindowRect(owned->GetWindowHandle(), &rect_satellite); + MoveWindow(owned->GetWindowHandle(), + window_rect.left + owned->offset_from_owner_.x, + window_rect.top + owned->offset_from_owner_.y, + rect_satellite.right - rect_satellite.left, + rect_satellite.bottom - rect_satellite.top, FALSE); + } + } + } break; + + 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); +} + +void FlutterHostWindow::UpdateModalState() { + auto const find_deepest_dialog = [](FlutterHostWindow* window, + auto&& self) -> FlutterHostWindow* { + FlutterHostWindow* deepest_dialog = nullptr; + if (window->archetype_ == WindowArchetype::dialog) { + deepest_dialog = window; + } + for (FlutterHostWindow* const owned : window->owned_windows_) { + if (FlutterHostWindow* const owned_deepest_dialog = self(owned, self)) { + deepest_dialog = owned_deepest_dialog; + } + } + return deepest_dialog; + }; + + HWND root_ancestor_handle = window_handle_; + while (HWND next = GetWindow(root_ancestor_handle, GW_OWNER)) { + root_ancestor_handle = next; + } + if (FlutterHostWindow* const root_ancestor = + GetThisFromHandle(root_ancestor_handle)) { + if (FlutterHostWindow* const deepest_dialog = + find_deepest_dialog(root_ancestor, find_deepest_dialog)) { + root_ancestor->EnableWindowAndDescendants(false); + deepest_dialog->EnableWindowAndDescendants(true); + } else { + root_ancestor->EnableWindowAndDescendants(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..748cb6343ea45 --- /dev/null +++ b/shell/platform/windows/flutter_host_window.h @@ -0,0 +1,143 @@ +// 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|. For + // |WindowArchetype::satellite| and |WindowArchetype::popup|, both |owner| + // and |positioner| must be provided, with |positioner| used only for these + // archetypes. For |WindowArchetype::dialog|, a modal dialog is created if + // |owner| is provided; otherwise, it is modeless. For + // |WindowArchetype::regular|, |positioner| and |owner| must be std::nullopt. + // 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, + std::optional owner, + std::optional positioner); + 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 owned windows. + std::set const& GetOwnedWindows() const; + + // Returns the hosted Flutter view's ID or std::nullopt if not created. + std::optional GetFlutterViewId() const; + + // Returns the owner window, or nullptr if this is a top-level window. + FlutterHostWindow* GetOwnerWindow() 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); + + // Closes this window's popups and returns the count of closed popups. + std::size_t CloseOwnedPopups(); + + // Enables/disables this window and all its descendants. + void EnableWindowAndDescendants(bool enable); + + // Finds the first enabled descendant window. If the current window itself is + // enabled, returns the current window. + FlutterHostWindow* FindFirstEnabledDescendant() const; + + // 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); + + // Enforces modal behavior by enabling the deepest dialog in the subtree + // rooted at the top-level window, along with its descendants, while + // disabling all other windows in the subtree. This ensures that the dialog + // and its owned windows remain active and interactive. If no dialog is found, + // enables all windows in the subtree. + void UpdateModalState(); + + // 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; + + // Windows that have this window as their owner window. + std::set owned_windows_; + + // The number of popups in |owned_windows_| (for quick popup existence + // checks). + std::size_t num_owned_popups_ = 0; + + // 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; + + // Offset between this window's position and its owner's. + POINT offset_from_owner_ = {0, 0}; + + // Whether the non-client area can be redrawn as inactive. Temporarily + // disabled during owned popup destruction to prevent flickering. + bool enable_redraw_non_client_as_inactive_ = true; + + 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..789983818fccf --- /dev/null +++ b/shell/platform/windows/flutter_host_window_controller.cc @@ -0,0 +1,341 @@ +// 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, + std::optional positioner, + std::optional parent_view_id) { + std::optional const owner_hwnd = + parent_view_id.has_value() && + windows_.find(parent_view_id.value()) != windows_.end() + ? std::optional{windows_[parent_view_id.value()] + ->GetWindowHandle()} + : std::nullopt; + + auto window = std::make_unique( + this, title, preferred_size, archetype, owner_hwnd, positioner); + 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, parent_view_id); + + WindowMetadata result = {.view_id = view_id, + .archetype = archetype, + .size = GetWindowSize(view_id), + .parent_id = parent_view_id}; + + 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(); + + if (window->GetArchetype() == WindowArchetype::dialog && + GetWindow(window_handle, GW_OWNER)) { + // Temporarily disable satellite hiding. This prevents satellites from + // flickering because of briefly hiding and showing between the + // destruction of a modal dialog and the transfer of focus to the owner + // window. + disable_satellite_hiding_ = window_handle; + } + + // |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 (disable_satellite_hiding_ == hwnd) { + // Re-enable satellite hiding by clearing the window handle now that + // the window is fully destroyed. + disable_satellite_hiding_ = nullptr; + } + + if (quit_on_close) { + DestroyAllWindows(); + } + } + } + return 0; + case WM_ACTIVATE: + if (wparam != WA_INACTIVE) { + if (FlutterHostWindow* const window = + FlutterHostWindow::GetThisFromHandle(hwnd)) { + if (window->GetArchetype() != WindowArchetype::popup) { + // If a non-popup window is activated, close popups for all windows. + auto it = windows_.begin(); + while (it != windows_.end()) { + std::size_t const num_popups_closed = + it->second->CloseOwnedPopups(); + if (num_popups_closed > 0) { + it = windows_.begin(); + } else { + ++it; + } + } + } else { + // If a popup window is activated, close its owned popups. + window->CloseOwnedPopups(); + } + } + ShowWindowAndAncestorsSatellites(hwnd); + } + break; + case WM_ACTIVATEAPP: + if (wparam == FALSE) { + if (FlutterHostWindow* const window = + FlutterHostWindow::GetThisFromHandle(hwnd)) { + // Close owned popups and hide satellites from all windows if a window + // belonging to a different application is being activated. + window->CloseOwnedPopups(); + HideWindowsSatellites(nullptr); + } + } + break; + 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::HideWindowsSatellites(HWND opt_out_hwnd) { + if (disable_satellite_hiding_) { + return; + } + + // Helper function to check whether |hwnd| is a descendant of |ancestor|. + auto const is_descendant_of = [](HWND hwnd, HWND ancestor) -> bool { + HWND current = ancestor; + while (current) { + current = GetWindow(current, GW_OWNER); + if (current == hwnd) { + return true; + } + } + return false; + }; + + // Helper function to check whether |window| owns a dialog. + auto const has_dialog = [](FlutterHostWindow* window) -> bool { + for (auto* const owned : window->GetOwnedWindows()) { + if (owned->GetArchetype() == WindowArchetype::dialog) { + return true; + } + } + return false; + }; + + for (auto const& [_, window] : windows_) { + if (window->GetWindowHandle() == opt_out_hwnd || + is_descendant_of(window->GetWindowHandle(), opt_out_hwnd)) { + continue; + } + + for (FlutterHostWindow* const owned : window->GetOwnedWindows()) { + if (owned->GetArchetype() != WindowArchetype::satellite) { + continue; + } + if (!has_dialog(owned)) { + ShowWindow(owned->GetWindowHandle(), SW_HIDE); + } + } + } +} + +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)}, + })); + } +} + +void FlutterHostWindowController::ShowWindowAndAncestorsSatellites(HWND hwnd) { + HWND current = hwnd; + while (current) { + for (FlutterHostWindow* const owned : + FlutterHostWindow::GetThisFromHandle(current)->GetOwnedWindows()) { + if (owned->GetArchetype() == WindowArchetype::satellite) { + ShowWindow(owned->GetWindowHandle(), SW_SHOWNOACTIVATE); + } + } + current = GetWindow(current, GW_OWNER); + } + + // Hide satellites of all other top-level windows. + if (!disable_satellite_hiding_) { + if (FlutterHostWindow* const window = + FlutterHostWindow::GetThisFromHandle(hwnd)) { + if (window->GetArchetype() != WindowArchetype::satellite) { + HideWindowsSatellites(hwnd); + } + } + } +} + +} // 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..94b87e1547eb7 --- /dev/null +++ b/shell/platform/windows/flutter_host_window_controller.h @@ -0,0 +1,128 @@ +// 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|. For |WindowArchetype::satellite| and + // |WindowArchetype::popup|, both |parent_view_id| and |positioner| must be + // provided; |positioner| is used only for these archetypes. For + // |WindowArchetype::dialog|, a modal dialog is created if |parent_view_id| is + // specified; otherwise, the dialog is modeless. For + // |WindowArchetype::regular|, |positioner| and |parent_view_id| should be + // std::nullopt. When |parent_view_id| is specified, the |FlutterHostWindow| + // that hosts the view with ID |parent_view_id| will become the owner window + // of the |FlutterHostWindow| created by this function. + // + // 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, + std::optional positioner, + std::optional parent_view_id); + + // 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; + + // Hides all satellite windows managed by this controller, except those that + // are descendants of |opt_out_hwnd| or own a dialog. If |opt_out_hwnd| is + // nullptr (default), only satellites that own a dialog are excluded from + // being hidden. + void HideWindowsSatellites(HWND opt_out_hwnd = nullptr); + + // 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; + + // Shows the satellite windows of |hwnd| and of its ancestors. + void ShowWindowAndAncestorsSatellites(HWND hwnd); + + // 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_; + + // Controls whether satellites can be hidden when there is no active window + // in the window subtree starting from the satellite's top-level window. If + // set to nullptr, satellite hiding is enabled. If set to a non-null value, + // satellite hiding remains disabled until the window represented by this + // handle is destroyed. After the window is destroyed, this is reset to + // nullptr. + HWND disable_satellite_hiding_ = nullptr; + + 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..278545cc8e657 --- /dev/null +++ b/shell/platform/windows/flutter_host_window_controller_unittests.cc @@ -0,0 +1,200 @@ +// 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, + std::nullopt, std::nullopt); + + // 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, + std::nullopt, std::nullopt); + 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/flutter_windows_internal.h b/shell/platform/windows/flutter_windows_internal.h index 47b98e983a48a..bb1e6f767905f 100644 --- a/shell/platform/windows/flutter_windows_internal.h +++ b/shell/platform/windows/flutter_windows_internal.h @@ -14,6 +14,31 @@ extern "C" { // Declare functions that are currently in-progress and shall be exposed to the // public facing API upon completion. +// Properties for configuring a Flutter view controller. +typedef struct { + // The view's initial width. + int width; + + // The view's initial height. + int height; +} FlutterDesktopViewControllerProperties; + +// Creates a view for the given engine. +// +// The |engine| will be started if it is not already running. +// +// The caller owns the returned reference, and is responsible for calling +// |FlutterDesktopViewControllerDestroy|. Returns a null pointer in the event of +// an error. +// +// Unlike |FlutterDesktopViewControllerCreate|, this does *not* take ownership +// of |engine| and |FlutterDesktopEngineDestroy| must be called to destroy +// the engine. +FLUTTER_EXPORT FlutterDesktopViewControllerRef +FlutterDesktopEngineCreateViewController( + FlutterDesktopEngineRef engine, + const FlutterDesktopViewControllerProperties* properties); + typedef int64_t PlatformViewId; typedef struct { diff --git a/shell/platform/windows/public/flutter_windows.h b/shell/platform/windows/public/flutter_windows.h index d7b2a30520b04..80d78766f9383 100644 --- a/shell/platform/windows/public/flutter_windows.h +++ b/shell/platform/windows/public/flutter_windows.h @@ -70,15 +70,6 @@ typedef struct { } FlutterDesktopEngineProperties; -// Properties for configuring a Flutter view controller. -typedef struct { - // The view's initial width. - int width; - - // The view's initial height. - int height; -} FlutterDesktopViewControllerProperties; - // ========== View Controller ========== // Creates a view that hosts and displays the given engine instance. @@ -174,22 +165,6 @@ FLUTTER_EXPORT bool FlutterDesktopEngineDestroy(FlutterDesktopEngineRef engine); FLUTTER_EXPORT bool FlutterDesktopEngineRun(FlutterDesktopEngineRef engine, const char* entry_point); -// Creates a view for the given engine. -// -// The |engine| will be started if it is not already running. -// -// The caller owns the returned reference, and is responsible for calling -// |FlutterDesktopViewControllerDestroy|. Returns a null pointer in the event of -// an error. -// -// Unlike |FlutterDesktopViewControllerCreate|, this does *not* take ownership -// of |engine| and |FlutterDesktopEngineDestroy| must be called to destroy -// the engine. -FLUTTER_EXPORT FlutterDesktopViewControllerRef -FlutterDesktopEngineCreateViewController( - FlutterDesktopEngineRef engine, - const FlutterDesktopViewControllerProperties* properties); - // DEPRECATED: This is no longer necessary to call, Flutter will take care of // processing engine messages transparently through DispatchMessage. // diff --git a/shell/platform/windows/windowing_handler.cc b/shell/platform/windows/windowing_handler.cc new file mode 100644 index 0000000000000..0752dd4f2cbb1 --- /dev/null +++ b/shell/platform/windows/windowing_handler.cc @@ -0,0 +1,339 @@ +// 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"; +constexpr char kCreateDialogMethod[] = "createDialog"; +constexpr char kCreateSatelliteMethod[] = "createSatellite"; +constexpr char kCreatePopupMethod[] = "createPopup"; + +// 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"; + case flutter::WindowArchetype::dialog: + return L"dialog"; + case flutter::WindowArchetype::satellite: + return L"satellite"; + case flutter::WindowArchetype::popup: + return L"popup"; + } + 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 == kCreateDialogMethod) { + HandleCreateWindow(WindowArchetype::dialog, method_call, *result); + } else if (method == kCreateSatelliteMethod) { + HandleCreateWindow(WindowArchetype::satellite, method_call, *result); + } else if (method == kCreatePopupMethod) { + HandleCreateWindow(WindowArchetype::popup, 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; + } + + std::optional positioner; + std::optional anchor_rect; + + if (archetype == WindowArchetype::satellite || + archetype == WindowArchetype::popup) { + if (auto const anchor_rect_it = map->find(EncodableValue(kAnchorRectKey)); + anchor_rect_it != map->end()) { + if (!anchor_rect_it->second.IsNull()) { + auto const anchor_rect_list = + GetListValuesForKeyOrSendError(kAnchorRectKey, map, result); + if (!anchor_rect_list) { + return; + } + anchor_rect = + WindowRectangle{{anchor_rect_list->at(0), anchor_rect_list->at(1)}, + {anchor_rect_list->at(2), anchor_rect_list->at(3)}}; + } + } else { + result.Error(kInvalidValueError, "Map does not contain required '" + + std::string(kAnchorRectKey) + + "' key."); + return; + } + + auto const positioner_parent_anchor = GetSingleValueForKeyOrSendError( + kPositionerParentAnchorKey, map, result); + if (!positioner_parent_anchor) { + return; + } + auto const positioner_child_anchor = GetSingleValueForKeyOrSendError( + kPositionerChildAnchorKey, map, result); + if (!positioner_child_anchor) { + return; + } + auto const child_anchor = + static_cast(positioner_child_anchor.value()); + + auto const positioner_offset_list = GetListValuesForKeyOrSendError( + kPositionerOffsetKey, map, result); + if (!positioner_offset_list) { + return; + } + auto const positioner_constraint_adjustment = + GetSingleValueForKeyOrSendError(kPositionerConstraintAdjustmentKey, + map, result); + if (!positioner_constraint_adjustment) { + return; + } + positioner = WindowPositioner{ + .anchor_rect = anchor_rect, + .parent_anchor = static_cast( + positioner_parent_anchor.value()), + .child_anchor = child_anchor, + .offset = {positioner_offset_list->at(0), + positioner_offset_list->at(1)}, + .constraint_adjustment = + static_cast( + positioner_constraint_adjustment.value())}; + } + + std::optional parent_view_id; + if (archetype == WindowArchetype::dialog || + archetype == WindowArchetype::satellite || + archetype == WindowArchetype::popup) { + if (auto const parent_it = map->find(EncodableValue(kParentKey)); + parent_it != map->end()) { + if (parent_it->second.IsNull()) { + if (archetype != WindowArchetype::dialog) { + result.Error( + kInvalidValueError, + "Value for '" + std::string(kParentKey) + "' must not be null."); + return; + } + } else { + if (auto const* const parent = std::get_if(&parent_it->second)) { + parent_view_id = *parent >= 0 ? std::optional(*parent) + : std::nullopt; + if (!parent_view_id.has_value() && + (archetype == WindowArchetype::satellite || + archetype == WindowArchetype::popup)) { + result.Error(kInvalidValueError, + "Value for '" + std::string(kParentKey) + "' (" + + std::to_string(parent_view_id.value()) + + ") must be nonnegative."); + return; + } + } else { + result.Error(kInvalidValueError, "Value for '" + + std::string(kParentKey) + + "' must be of type int."); + return; + } + } + } else { + result.Error(kInvalidValueError, "Map does not contain required '" + + std::string(kParentKey) + "' key."); + return; + } + } + + if (std::optional const data_opt = + controller_->CreateHostWindow( + title, {.width = size_list->at(0), .height = size_list->at(1)}, + archetype, positioner, parent_view_id)) { + 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..be2db4ceed3da --- /dev/null +++ b/shell/platform/windows/windowing_handler_unittests.cc @@ -0,0 +1,151 @@ +// 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, + std::optional positioner, + std::optional parent_view_id), + (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, + Eq(std::nullopt), Eq(std::nullopt))) + .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 From 534a4bfa17319f23e9832cf8e0dd85000458216c Mon Sep 17 00:00:00 2001 From: Harlen Batagelo Date: Wed, 18 Dec 2024 17:39:03 -0300 Subject: [PATCH 6/7] Add tests for popups, dialogs, and satellites --- ...lutter_host_window_controller_unittests.cc | 339 ++++++++++++++++++ shell/platform/windows/windowing_handler.cc | 6 +- .../windows/windowing_handler_unittests.cc | 202 ++++++++++- 3 files changed, 540 insertions(+), 7 deletions(-) diff --git a/shell/platform/windows/flutter_host_window_controller_unittests.cc b/shell/platform/windows/flutter_host_window_controller_unittests.cc index 278545cc8e657..04f175894891a 100644 --- a/shell/platform/windows/flutter_host_window_controller_unittests.cc +++ b/shell/platform/windows/flutter_host_window_controller_unittests.cc @@ -139,6 +139,345 @@ TEST_F(FlutterHostWindowControllerTest, CreateRegularWindow) { EXPECT_EQ(client_rect.bottom - client_rect.top, size.height); } +TEST_F(FlutterHostWindowControllerTest, CreatePopup) { + // Create a top-level window first. + std::optional const parent_result = + host_window_controller()->CreateHostWindow(L"parent", {800, 600}, + WindowArchetype::regular, + std::nullopt, std::nullopt); + ASSERT_NE(parent_result, std::nullopt); + + 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 present and valid. + 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_EQ(*value_parentViewId, parent_result->view_id); + } + }); + + // Define parameters for the popup to be created. + WindowSize const size = {200, 200}; + wchar_t const* const title = L"popup"; + WindowArchetype const archetype = WindowArchetype::popup; + WindowPositioner const positioner = WindowPositioner{ + .anchor_rect = std::optional( + {.top_left = {0, 0}, .size = {size.width, size.height}}), + .parent_anchor = WindowPositioner::Anchor::center, + .child_anchor = WindowPositioner::Anchor::center, + .offset = {0, 0}, + .constraint_adjustment = WindowPositioner::ConstraintAdjustment::none}; + + // Create the windowing handler with the test messenger. + WindowingHandler windowing_handler(&messenger, host_window_controller()); + + // Create popup parented to top-level window. + std::optional const result = + host_window_controller()->CreateHostWindow( + title, size, archetype, positioner, parent_result->view_id); + + // 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_EQ(result->parent_id.value(), parent_result->view_id); + + // Verify the popup 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, CreateModalDialog) { + // Create a top-level window first. + std::optional const parent_result = + host_window_controller()->CreateHostWindow(L"parent", {800, 600}, + WindowArchetype::regular, + std::nullopt, std::nullopt); + ASSERT_NE(parent_result, std::nullopt); + + 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 present and valid. + 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_EQ(*value_parentViewId, parent_result->view_id); + } + }); + + // Define parameters for the popup to be created. + WindowSize const size = {400, 300}; + wchar_t const* const title = L"modal_dialog"; + WindowArchetype const archetype = WindowArchetype::dialog; + + // Create the windowing handler with the test messenger. + WindowingHandler windowing_handler(&messenger, host_window_controller()); + + // Create dialog parented to top-level window. + std::optional const result = + host_window_controller()->CreateHostWindow( + title, size, archetype, std::nullopt, parent_result->view_id); + + // 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_EQ(result->parent_id.value(), parent_result->view_id); + + // Verify the dialog 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, CreateModelessDialog) { + 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); + } + }); + + // Define parameters for the popup to be created. + WindowSize const size = {400, 300}; + wchar_t const* const title = L"modeless_dialog"; + WindowArchetype const archetype = WindowArchetype::dialog; + + // Create the windowing handler with the test messenger. + WindowingHandler windowing_handler(&messenger, host_window_controller()); + + // Create dialog without a parent. + std::optional const result = + host_window_controller()->CreateHostWindow(title, size, archetype, + std::nullopt, std::nullopt); + + // 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 dialog 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, CreateSatellite) { + // Create a top-level window first. + std::optional const parent_result = + host_window_controller()->CreateHostWindow(L"parent", {800, 600}, + WindowArchetype::regular, + std::nullopt, std::nullopt); + ASSERT_NE(parent_result, std::nullopt); + + 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 present and valid. + 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_EQ(*value_parentViewId, parent_result->view_id); + } + }); + + // Define parameters for the satellite to be created. + WindowSize const size = {200, 300}; + wchar_t const* const title = L"satellite"; + WindowArchetype const archetype = WindowArchetype::satellite; + WindowPositioner const positioner = WindowPositioner{ + .anchor_rect = std::optional( + {.top_left = {0, 0}, .size = {size.width, size.height}}), + .parent_anchor = WindowPositioner::Anchor::center, + .child_anchor = WindowPositioner::Anchor::center, + .offset = {0, 0}, + .constraint_adjustment = WindowPositioner::ConstraintAdjustment::none}; + + // Create the windowing handler with the test messenger. + WindowingHandler windowing_handler(&messenger, host_window_controller()); + + // Create popup parented to top-level window. + std::optional const result = + host_window_controller()->CreateHostWindow( + title, size, archetype, positioner, parent_result->view_id); + + // 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_EQ(result->parent_id.value(), parent_result->view_id); + + // Verify the satellite 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; diff --git a/shell/platform/windows/windowing_handler.cc b/shell/platform/windows/windowing_handler.cc index 0752dd4f2cbb1..25221f406fc6f 100644 --- a/shell/platform/windows/windowing_handler.cc +++ b/shell/platform/windows/windowing_handler.cc @@ -214,6 +214,9 @@ void WindowingHandler::HandleCreateWindow(WindowArchetype archetype, if (!positioner_parent_anchor) { return; } + auto const parent_anchor = + static_cast(positioner_parent_anchor.value()); + auto const positioner_child_anchor = GetSingleValueForKeyOrSendError( kPositionerChildAnchorKey, map, result); if (!positioner_child_anchor) { @@ -235,8 +238,7 @@ void WindowingHandler::HandleCreateWindow(WindowArchetype archetype, } positioner = WindowPositioner{ .anchor_rect = anchor_rect, - .parent_anchor = static_cast( - positioner_parent_anchor.value()), + .parent_anchor = parent_anchor, .child_anchor = child_anchor, .offset = {positioner_offset_list->at(0), positioner_offset_list->at(1)}, diff --git a/shell/platform/windows/windowing_handler_unittests.cc b/shell/platform/windows/windowing_handler_unittests.cc index be2db4ceed3da..6f468dc08280a 100644 --- a/shell/platform/windows/windowing_handler_unittests.cc +++ b/shell/platform/windows/windowing_handler_unittests.cc @@ -20,13 +20,27 @@ namespace { using ::testing::_; using ::testing::Eq; using ::testing::NiceMock; +using ::testing::Optional; using ::testing::Return; using ::testing::StrEq; -static constexpr char kChannelName[] = "flutter/windowing"; +constexpr char kChannelName[] = "flutter/windowing"; -static constexpr char kCreateWindowMethod[] = "createWindow"; -static constexpr char kDestroyWindowMethod[] = "destroyWindow"; +constexpr char kCreateWindowMethod[] = "createWindow"; +constexpr char kCreateDialogMethod[] = "createDialog"; +constexpr char kCreateSatelliteMethod[] = "createSatellite"; +constexpr char kCreatePopupMethod[] = "createPopup"; +constexpr char kDestroyWindowMethod[] = "destroyWindow"; + +constexpr char kAnchorRectKey[] = "anchorRect"; +constexpr char kParentKey[] = "parent"; +constexpr char kPositionerChildAnchorKey[] = "positionerChildAnchor"; +constexpr char kPositionerConstraintAdjustmentKey[] = + "positionerConstraintAdjustment"; +constexpr char kPositionerOffsetKey[] = "positionerOffset"; +constexpr char kPositionerParentAnchorKey[] = "positionerParentAnchor"; +constexpr char kSizeKey[] = "size"; +constexpr char kViewIdKey[] = "viewId"; void SimulateWindowingMessage(TestBinaryMessenger* messenger, const std::string& method_name, @@ -64,6 +78,20 @@ class MockFlutterHostWindowController : public FlutterHostWindowController { FML_DISALLOW_COPY_AND_ASSIGN(MockFlutterHostWindowController); }; +bool operator==(WindowPositioner const& lhs, WindowPositioner const& rhs) { + return lhs.anchor_rect == rhs.anchor_rect && + lhs.parent_anchor == rhs.parent_anchor && + lhs.child_anchor == rhs.child_anchor && lhs.offset == rhs.offset && + lhs.constraint_adjustment == rhs.constraint_adjustment; +} + +MATCHER_P(WindowPositionerEq, expected, "WindowPositioner matches expected") { + return arg.anchor_rect == expected.anchor_rect && + arg.parent_anchor == expected.parent_anchor && + arg.child_anchor == expected.child_anchor && + arg.offset == expected.offset && + arg.constraint_adjustment == expected.constraint_adjustment; +} } // namespace class WindowingHandlerTest : public WindowsTest { @@ -102,7 +130,7 @@ TEST_F(WindowingHandlerTest, HandleCreateRegularWindow) { WindowSize const size = {800, 600}; EncodableMap const arguments = { - {EncodableValue("size"), + {EncodableValue(kSizeKey), EncodableValue(EncodableList{EncodableValue(size.width), EncodableValue(size.height)})}, }; @@ -125,12 +153,176 @@ TEST_F(WindowingHandlerTest, HandleCreateRegularWindow) { EXPECT_TRUE(success); } +TEST_F(WindowingHandlerTest, HandleCreatePopup) { + TestBinaryMessenger messenger; + WindowingHandler windowing_handler(&messenger, controller()); + + WindowSize const size = {200, 200}; + std::optional const parent_view_id = 0; + WindowPositioner const positioner = WindowPositioner{ + .anchor_rect = std::optional( + {.top_left = {0, 0}, .size = {size.width, size.height}}), + .parent_anchor = WindowPositioner::Anchor::center, + .child_anchor = WindowPositioner::Anchor::center, + .offset = {0, 0}, + .constraint_adjustment = WindowPositioner::ConstraintAdjustment::none}; + EncodableMap const arguments = { + {EncodableValue(kSizeKey), + EncodableValue(EncodableList{EncodableValue(size.width), + EncodableValue(size.height)})}, + {EncodableValue(kAnchorRectKey), + EncodableValue( + EncodableList{EncodableValue(positioner.anchor_rect->top_left.x), + EncodableValue(positioner.anchor_rect->top_left.y), + EncodableValue(positioner.anchor_rect->size.width), + EncodableValue(positioner.anchor_rect->size.height)})}, + {EncodableValue(kPositionerParentAnchorKey), + EncodableValue(static_cast(positioner.parent_anchor))}, + {EncodableValue(kPositionerChildAnchorKey), + EncodableValue(static_cast(positioner.child_anchor))}, + {EncodableValue(kPositionerOffsetKey), + EncodableValue(EncodableList{EncodableValue(positioner.offset.x), + EncodableValue(positioner.offset.y)})}, + {EncodableValue(kPositionerConstraintAdjustmentKey), + EncodableValue(static_cast(positioner.constraint_adjustment))}, + {EncodableValue(kParentKey), + EncodableValue(static_cast(parent_view_id.value()))}}; + + bool success = false; + MethodResultFunctions<> result_handler( + [&success](const EncodableValue* result) { success = true; }, nullptr, + nullptr); + + EXPECT_CALL(*controller(), + CreateHostWindow(StrEq(L"popup"), size, WindowArchetype::popup, + Optional(WindowPositionerEq(positioner)), + parent_view_id)) + .Times(1); + + SimulateWindowingMessage(&messenger, kCreatePopupMethod, + std::make_unique(arguments), + &result_handler); + + EXPECT_TRUE(success); +} + +TEST_F(WindowingHandlerTest, HandleCreateModalDialog) { + TestBinaryMessenger messenger; + WindowingHandler windowing_handler(&messenger, controller()); + + WindowSize const size = {400, 300}; + std::optional const parent_view_id = 0; + EncodableMap const arguments = { + {EncodableValue(kSizeKey), + EncodableValue(EncodableList{EncodableValue(size.width), + EncodableValue(size.height)})}, + {EncodableValue(kParentKey), + EncodableValue(static_cast(parent_view_id.value()))}}; + + bool success = false; + MethodResultFunctions<> result_handler( + [&success](const EncodableValue* result) { success = true; }, nullptr, + nullptr); + + EXPECT_CALL(*controller(), + CreateHostWindow(StrEq(L"dialog"), size, WindowArchetype::dialog, + Eq(std::nullopt), parent_view_id)) + .Times(1); + + SimulateWindowingMessage(&messenger, kCreateDialogMethod, + std::make_unique(arguments), + &result_handler); + + EXPECT_TRUE(success); +} + +TEST_F(WindowingHandlerTest, HandleCreateModelessDialog) { + TestBinaryMessenger messenger; + WindowingHandler windowing_handler(&messenger, controller()); + + WindowSize const size = {400, 300}; + EncodableMap const arguments = { + {EncodableValue(kSizeKey), + EncodableValue(EncodableList{EncodableValue(size.width), + EncodableValue(size.height)})}, + {EncodableValue(kParentKey), EncodableValue()}}; + + bool success = false; + MethodResultFunctions<> result_handler( + [&success](const EncodableValue* result) { success = true; }, nullptr, + nullptr); + + EXPECT_CALL(*controller(), + CreateHostWindow(StrEq(L"dialog"), size, WindowArchetype::dialog, + Eq(std::nullopt), Eq(std::nullopt))) + .Times(1); + + SimulateWindowingMessage(&messenger, kCreateDialogMethod, + std::make_unique(arguments), + &result_handler); + + EXPECT_TRUE(success); +} + +TEST_F(WindowingHandlerTest, HandleCreateSatellite) { + TestBinaryMessenger messenger; + WindowingHandler windowing_handler(&messenger, controller()); + + WindowSize const size = {200, 300}; + std::optional const parent_view_id = 0; + WindowPositioner const positioner = WindowPositioner{ + .anchor_rect = std::optional( + {.top_left = {0, 0}, .size = {size.width, size.height}}), + .parent_anchor = WindowPositioner::Anchor::center, + .child_anchor = WindowPositioner::Anchor::center, + .offset = {0, 0}, + .constraint_adjustment = WindowPositioner::ConstraintAdjustment::none}; + EncodableMap const arguments = { + {EncodableValue(kSizeKey), + EncodableValue(EncodableList{EncodableValue(size.width), + EncodableValue(size.height)})}, + {EncodableValue(kAnchorRectKey), + EncodableValue( + EncodableList{EncodableValue(positioner.anchor_rect->top_left.x), + EncodableValue(positioner.anchor_rect->top_left.y), + EncodableValue(positioner.anchor_rect->size.width), + EncodableValue(positioner.anchor_rect->size.height)})}, + {EncodableValue(kPositionerParentAnchorKey), + EncodableValue(static_cast(positioner.parent_anchor))}, + {EncodableValue(kPositionerChildAnchorKey), + EncodableValue(static_cast(positioner.child_anchor))}, + {EncodableValue(kPositionerOffsetKey), + EncodableValue(EncodableList{EncodableValue(positioner.offset.x), + EncodableValue(positioner.offset.y)})}, + {EncodableValue(kPositionerConstraintAdjustmentKey), + EncodableValue(static_cast(positioner.constraint_adjustment))}, + {EncodableValue(kParentKey), + EncodableValue(static_cast(parent_view_id.value()))}}; + + bool success = false; + MethodResultFunctions<> result_handler( + [&success](const EncodableValue* result) { success = true; }, nullptr, + nullptr); + + EXPECT_CALL(*controller(), + CreateHostWindow( + StrEq(L"satellite"), size, WindowArchetype::satellite, + Optional(WindowPositionerEq(positioner)), parent_view_id)) + .Times(1); + + SimulateWindowingMessage(&messenger, kCreateSatelliteMethod, + 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)}, + {EncodableValue(kViewIdKey), EncodableValue(1)}, }; bool success = false; From cf7e288622f268a3853a4116f4361cf203db1350 Mon Sep 17 00:00:00 2001 From: Harlen Batagelo Date: Mon, 6 Jan 2025 09:53:50 -0300 Subject: [PATCH 7/7] Remove redundant static keyword --- .../flutter_host_window_controller_unittests.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/shell/platform/windows/flutter_host_window_controller_unittests.cc b/shell/platform/windows/flutter_host_window_controller_unittests.cc index 04f175894891a..fad1a560f7092 100644 --- a/shell/platform/windows/flutter_host_window_controller_unittests.cc +++ b/shell/platform/windows/flutter_host_window_controller_unittests.cc @@ -16,11 +16,11 @@ 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"; +constexpr char kChannelName[] = "flutter/windowing"; +constexpr char kOnWindowCreatedMethod[] = "onWindowCreated"; +constexpr char kOnWindowDestroyedMethod[] = "onWindowDestroyed"; +constexpr char kViewIdKey[] = "viewId"; +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.