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 fb51a53e49f41d13d5e31dd1754f8fedb8bce182 Mon Sep 17 00:00:00 2001 From: Robert Ancell Date: Fri, 25 Oct 2024 10:29:08 +0200 Subject: [PATCH 2/7] Add new files to licenses --- ci/licenses_golden/excluded_files | 1 + ci/licenses_golden/licenses_flutter | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index 1009dc88cc425..67782b4500cfa 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -398,6 +398,7 @@ ../../../flutter/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc ../../../flutter/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc ../../../flutter/shell/platform/windows/client_wrapper/flutter_view_unittests.cc +../../../flutter/shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc ../../../flutter/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc ../../../flutter/shell/platform/windows/client_wrapper/testing ../../../flutter/shell/platform/windows/compositor_opengl_unittests.cc diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 35835f6bc35ff..f30b33460c8c4 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -44417,6 +44417,7 @@ ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/st ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/standard_message_codec.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/standard_method_codec.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/texture_registrar.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/windowing.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/plugin_registrar.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/standard_codec.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/texture_registrar_impl.h + ../../../flutter/LICENSE @@ -48021,11 +48022,18 @@ FILE: ../../../flutter/shell/platform/windows/accessibility_plugin.cc FILE: ../../../flutter/shell/platform/windows/accessibility_plugin.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_engine.cc FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_view_controller.cc +FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_win32_window.cc +FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_window_controller.cc FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/dart_project.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_view.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h +FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_win32_window.h +FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/plugin_registrar_windows.h +FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/win32_window.h +FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/win32_wrapper.h +FILE: ../../../flutter/shell/platform/windows/client_wrapper/win32_window.cc FILE: ../../../flutter/shell/platform/windows/compositor.h FILE: ../../../flutter/shell/platform/windows/compositor_opengl.cc FILE: ../../../flutter/shell/platform/windows/compositor_opengl.h From 294081bd6149a2acb97888e3f6740108ea9e32f6 Mon Sep 17 00:00:00 2001 From: Harlen Batagelo Date: Tue, 10 Dec 2024 16:34:59 -0300 Subject: [PATCH 3/7] Refactor multi-window support --- ci/licenses_golden/excluded_files | 4 +- ci/licenses_golden/licenses_flutter | 17 +- common/settings.h | 4 + shell/common/switches.cc | 11 + shell/common/switches.h | 4 + shell/platform/common/BUILD.gn | 11 +- .../client_wrapper/core_wrapper_files.gni | 1 - shell/platform/common/windowing.cc | 278 ++++++ .../include/flutter => }/windowing.h | 54 +- 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 | 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 --------- shell/platform/windows/flutter_host_window.cc | 909 ++++++++++++++++++ shell/platform/windows/flutter_host_window.h | 143 +++ .../windows/flutter_host_window_controller.cc | 325 +++++++ .../windows/flutter_host_window_controller.h | 126 +++ ...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 | 322 +++++++ shell/platform/windows/windowing_handler.h | 46 + .../windows/windowing_handler_unittests.cc | 151 +++ 39 files changed, 3174 insertions(+), 1639 deletions(-) create mode 100644 shell/platform/common/windowing.cc rename shell/platform/common/{client_wrapper/include/flutter => }/windowing.h (73%) 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 8c9a0c930ca2a..4a840836bb21a 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -335,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 @@ -400,7 +401,6 @@ ../../../flutter/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc ../../../flutter/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc ../../../flutter/shell/platform/windows/client_wrapper/flutter_view_unittests.cc -../../../flutter/shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc ../../../flutter/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc ../../../flutter/shell/platform/windows/client_wrapper/testing ../../../flutter/shell/platform/windows/compositor_opengl_unittests.cc @@ -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 @@ -427,6 +428,7 @@ ../../../flutter/shell/platform/windows/task_runner_unittests.cc ../../../flutter/shell/platform/windows/testing ../../../flutter/shell/platform/windows/text_input_plugin_unittest.cc +../../../flutter/shell/platform/windows/windowing_handler_unittests.cc ../../../flutter/shell/platform/windows/window_proc_delegate_manager_unittests.cc ../../../flutter/shell/platform/windows/window_unittests.cc ../../../flutter/shell/platform/windows/windows_lifecycle_manager_unittests.cc diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index e24641f499e0f..8fd68561b46f0 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -44480,7 +44480,6 @@ ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/st ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/standard_message_codec.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/standard_method_codec.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/texture_registrar.h + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/windowing.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/plugin_registrar.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/standard_codec.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/client_wrapper/texture_registrar_impl.h + ../../../flutter/LICENSE @@ -47426,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 @@ -47456,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 @@ -48158,18 +48158,11 @@ FILE: ../../../flutter/shell/platform/windows/accessibility_plugin.cc FILE: ../../../flutter/shell/platform/windows/accessibility_plugin.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_engine.cc FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_view_controller.cc -FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_win32_window.cc -FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_window_controller.cc FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/dart_project.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_view.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h -FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_win32_window.h -FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/plugin_registrar_windows.h -FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/win32_window.h -FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/win32_wrapper.h -FILE: ../../../flutter/shell/platform/windows/client_wrapper/win32_window.cc FILE: ../../../flutter/shell/platform/windows/compositor.h FILE: ../../../flutter/shell/platform/windows/compositor_opengl.cc FILE: ../../../flutter/shell/platform/windows/compositor_opengl.h @@ -48206,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 @@ -48255,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/core_wrapper_files.gni b/shell/platform/common/client_wrapper/core_wrapper_files.gni index 013c758c7f83c..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") diff --git a/shell/platform/common/windowing.cc b/shell/platform/common/windowing.cc new file mode 100644 index 0000000000000..63881ae5d6f55 --- /dev/null +++ b/shell/platform/common/windowing.cc @@ -0,0 +1,278 @@ +// 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 + +namespace flutter { + +namespace { + +WindowPoint offset_for(WindowSize const& size, + WindowPositioner::Anchor anchor) { + 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(); + } +} + +WindowPoint anchor_position_for(WindowRectangle const& rect, + WindowPositioner::Anchor anchor) { + 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(); + } +} + +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)}; +} + +WindowPositioner::Anchor flip_anchor_x(WindowPositioner::Anchor 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; + } +} + +WindowPositioner::Anchor flip_anchor_y(WindowPositioner::Anchor 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; + } +} + +WindowPoint flip_offset_x(WindowPoint const& p) { + return {-1 * p.x, p.y}; +} + +WindowPoint flip_offset_y(WindowPoint const& p) { + return {p.x, -1 * p.y}; +} + +} // namespace + +WindowRectangle PlaceWindow(WindowPositioner const& positioner, + WindowSize child_size, + WindowRectangle const& anchor_rect, + WindowRectangle const& parent_rect, + WindowRectangle const& output_rect) { + WindowRectangle default_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); + + 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)) { + 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)); + + if (output_rect.contains({result, child_size})) { + return WindowRectangle{result, child_size}; + } + } + + if (static_cast(positioner.constraint_adjustment) & + static_cast(WindowPositioner::ConstraintAdjustment::flip_y)) { + 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)); + + 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)) { + 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))); + + if (output_rect.contains({result, child_size})) { + return WindowRectangle{result, child_size}; + } + } + + { + 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)) { + 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); + + 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)) { + 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); + + 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}; + } + } + + { + 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)) { + 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); + + 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)) { + 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); + + 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 flutter diff --git a/shell/platform/common/client_wrapper/include/flutter/windowing.h b/shell/platform/common/windowing.h similarity index 73% rename from shell/platform/common/client_wrapper/include/flutter/windowing.h rename to shell/platform/common/windowing.h index 72e3dd5cfaca7..5131a6eb2642b 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, // 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}; + FlutterViewId view_id = 0; // The type of the window (e.g., regular, dialog, popup, etc). - WindowArchetype archetype{WindowArchetype::regular}; + 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,6 +137,20 @@ struct WindowMetadata { std::optional parent_id; }; +// 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. +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 4585f38c27fcf..0000000000000 --- a/shell/platform/windows/client_wrapper/flutter_window_controller.cc +++ /dev/null @@ -1,407 +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(); -} - -} // 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 deleted file mode 100644 index fbea55efb506c..0000000000000 --- a/shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc +++ /dev/null @@ -1,294 +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}; - - 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 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 81355e8048985..0000000000000 --- a/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h +++ /dev/null @@ -1,69 +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; - 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 deleted file mode 100644 index 5df1fee8758e4..0000000000000 --- a/shell/platform/windows/client_wrapper/include/flutter/win32_window.h +++ /dev/null @@ -1,106 +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; - - 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 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 bfdb0f6a0a311..0000000000000 --- a/shell/platform/windows/client_wrapper/win32_window.cc +++ /dev/null @@ -1,462 +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(); -} - -// 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_host_window.cc b/shell/platform/windows/flutter_host_window.cc new file mode 100644 index 0000000000000..738fdff689860 --- /dev/null +++ b/shell/platform/windows/flutter_host_window.cc @@ -0,0 +1,909 @@ +// 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 { + +static 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 + // with a close button. + 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..0b3d2391bb773 --- /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. + // |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 |parent_view_id| 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 window in the descendant hierarchy. + FlutterHostWindow* FindFirstEnabledDescendant() const; + + // 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. + 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's archetype (e.g., regular, dialog, popup). + 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..b36718473879e --- /dev/null +++ b/shell/platform/windows/flutter_host_window_controller.cc @@ -0,0 +1,325 @@ +// 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 { + +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( + "onWindowChanged", + std::make_unique(EncodableMap{ + {EncodableValue("viewId"), EncodableValue(view_id)}, + {EncodableValue("size"), + EncodableValue(EncodableList{EncodableValue(size.width), + EncodableValue(size.height)})}, + {EncodableValue("relativePosition"), EncodableValue()}, + {EncodableValue("isMoving"), EncodableValue()}})); + } +} + +void FlutterHostWindowController::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 FlutterHostWindowController::SendOnWindowDestroyed( + FlutterViewId view_id) const { + if (channel_) { + channel_->InvokeMethod( + "onWindowDestroyed", + std::make_unique(EncodableMap{ + {EncodableValue("viewId"), 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..1d4202c6cae27 --- /dev/null +++ b/shell/platform/windows/flutter_host_window_controller.h @@ -0,0 +1,126 @@ +// 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|s. +// |FlutterWindowsEngine| uniquely owns an instance of this class and uses it in +// |WindowingHandler| to handle the methods and messages that enable +// multi-window support on Flutter. +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 to be called by |FlutterHostWindow| 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 dropshadow area. + WindowSize GetWindowSize(FlutterViewId view_id) const; + + // Hides all satellite windows in the application, except those that are + // descendants of |opt_out_hwnd| or have a dialog owns a window. If + // |opt_out_hwnd| is null (default), no windows are excluded. + 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 window messages. + std::shared_ptr> channel_; + + // The host windows managed by this controller. + std::map> windows_; + + // Controls whether satellites are hidden when their top-level window + // and all its owned windows 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; + + 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..21c76c59d14af --- /dev/null +++ b/shell/platform/windows/windowing_handler.cc @@ -0,0 +1,322 @@ +// 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. +static constexpr char kChannelName[] = "flutter/windowing"; + +// Methods for creating different types of windows. +static constexpr char kCreateWindowMethod[] = "createWindow"; +static constexpr char kCreateDialogMethod[] = "createDialog"; +static constexpr char kCreateSatelliteMethod[] = "createSatellite"; +static constexpr char kCreatePopupMethod[] = "createPopup"; + +// The method to destroy a window. +static constexpr char kDestroyWindowMethod[] = "destroyWindow"; + +// Error codes used for responses. +static constexpr char kErrorCodeInvalidValue[] = "INVALID_VALUE"; +static constexpr char 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 +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(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 +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(kErrorCodeInvalidValue, + "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(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. +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(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 (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("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 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(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 (!controller_->DestroyHostWindow(view_id.value())) { + result.Error(kErrorCodeInvalidValue, "Can't find window with 'viewId' (" + + 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 681ef2d831ac437f202c1c3e6566482c67efa1cc Mon Sep 17 00:00:00 2001 From: Harlen Batagelo Date: Wed, 11 Dec 2024 15:35:47 -0300 Subject: [PATCH 4/7] Fix comments, add constants for keys, and refactor error constants --- shell/platform/windows/flutter_host_window.cc | 2 +- shell/platform/windows/flutter_host_window.h | 7 +- .../windows/flutter_host_window_controller.cc | 36 +++-- .../windows/flutter_host_window_controller.h | 6 +- shell/platform/windows/windowing_handler.cc | 125 ++++++++++-------- 5 files changed, 104 insertions(+), 72 deletions(-) diff --git a/shell/platform/windows/flutter_host_window.cc b/shell/platform/windows/flutter_host_window.cc index 738fdff689860..1e19c64ffa4b9 100644 --- a/shell/platform/windows/flutter_host_window.cc +++ b/shell/platform/windows/flutter_host_window.cc @@ -13,7 +13,7 @@ namespace { -static constexpr wchar_t kWindowClassName[] = L"FLUTTER_HOST_WINDOW"; +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. diff --git a/shell/platform/windows/flutter_host_window.h b/shell/platform/windows/flutter_host_window.h index 0b3d2391bb773..0401c41baeaad 100644 --- a/shell/platform/windows/flutter_host_window.h +++ b/shell/platform/windows/flutter_host_window.h @@ -24,14 +24,13 @@ 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. - // |client_size| is the preferred size of the client rectangle in logical - // coordinates. The window style is defined by |archetype|. For + // |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 |parent_view_id| must be - // std::nullopt. + // |WindowArchetype::regular|, |positioner| and |owner| must be std::nullopt. // On success, a valid window handle can be retrieved via // |FlutterHostWindow::GetWindowHandle|. FlutterHostWindow(FlutterHostWindowController* controller, diff --git a/shell/platform/windows/flutter_host_window_controller.cc b/shell/platform/windows/flutter_host_window_controller.cc index b36718473879e..789983818fccf 100644 --- a/shell/platform/windows/flutter_host_window_controller.cc +++ b/shell/platform/windows/flutter_host_window_controller.cc @@ -10,6 +10,22 @@ 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) {} @@ -263,14 +279,14 @@ void FlutterHostWindowController::SendOnWindowChanged( if (channel_) { WindowSize const size = GetWindowSize(view_id); channel_->InvokeMethod( - "onWindowChanged", + kOnWindowChangedMethod, std::make_unique(EncodableMap{ - {EncodableValue("viewId"), EncodableValue(view_id)}, - {EncodableValue("size"), + {EncodableValue(kViewIdKey), EncodableValue(view_id)}, + {EncodableValue(kSizeKey), EncodableValue(EncodableList{EncodableValue(size.width), EncodableValue(size.height)})}, - {EncodableValue("relativePosition"), EncodableValue()}, - {EncodableValue("isMoving"), EncodableValue()}})); + {EncodableValue(kRelativePositionKey), EncodableValue()}, + {EncodableValue(kIsMovingKey), EncodableValue()}})); } } @@ -279,10 +295,10 @@ void FlutterHostWindowController::SendOnWindowCreated( std::optional parent_view_id) const { if (channel_) { channel_->InvokeMethod( - "onWindowCreated", + kOnWindowCreatedMethod, std::make_unique(EncodableMap{ - {EncodableValue("viewId"), EncodableValue(view_id)}, - {EncodableValue("parentViewId"), + {EncodableValue(kViewIdKey), EncodableValue(view_id)}, + {EncodableValue(kParentViewIdKey), parent_view_id ? EncodableValue(parent_view_id.value()) : EncodableValue()}})); } @@ -292,9 +308,9 @@ void FlutterHostWindowController::SendOnWindowDestroyed( FlutterViewId view_id) const { if (channel_) { channel_->InvokeMethod( - "onWindowDestroyed", + kOnWindowDestroyedMethod, std::make_unique(EncodableMap{ - {EncodableValue("viewId"), EncodableValue(view_id)}, + {EncodableValue(kViewIdKey), EncodableValue(view_id)}, })); } } diff --git a/shell/platform/windows/flutter_host_window_controller.h b/shell/platform/windows/flutter_host_window_controller.h index 1d4202c6cae27..71b78e6ad456a 100644 --- a/shell/platform/windows/flutter_host_window_controller.h +++ b/shell/platform/windows/flutter_host_window_controller.h @@ -86,7 +86,7 @@ class FlutterHostWindowController { // Hides all satellite windows in the application, except those that are // descendants of |opt_out_hwnd| or have a dialog owns a window. If - // |opt_out_hwnd| is null (default), no windows are excluded. + // |opt_out_hwnd| is nullptr (default), no windows are excluded. void HideWindowsSatellites(HWND opt_out_hwnd = nullptr); // Sends the "onWindowChanged" message to the Flutter engine. @@ -112,8 +112,8 @@ class FlutterHostWindowController { std::map> windows_; // Controls whether satellites are hidden when their top-level window - // and all its owned windows become inactive. If null, satellite hiding - // is enabled. If not null, it contains the handle of the window that + // and all its owned windows become inactive. If nullptr, satellite hiding + // is enabled. If not nullptr, 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; diff --git a/shell/platform/windows/windowing_handler.cc b/shell/platform/windows/windowing_handler.cc index 21c76c59d14af..0752dd4f2cbb1 100644 --- a/shell/platform/windows/windowing_handler.cc +++ b/shell/platform/windows/windowing_handler.cc @@ -10,20 +10,33 @@ namespace { // Name of the windowing channel. -static constexpr char kChannelName[] = "flutter/windowing"; +constexpr char kChannelName[] = "flutter/windowing"; // Methods for creating different types of windows. -static constexpr char kCreateWindowMethod[] = "createWindow"; -static constexpr char kCreateDialogMethod[] = "createDialog"; -static constexpr char kCreateSatelliteMethod[] = "createSatellite"; -static constexpr char kCreatePopupMethod[] = "createPopup"; +constexpr char kCreateWindowMethod[] = "createWindow"; +constexpr char kCreateDialogMethod[] = "createDialog"; +constexpr char kCreateSatelliteMethod[] = "createSatellite"; +constexpr char kCreatePopupMethod[] = "createPopup"; // The method to destroy a window. -static constexpr char kDestroyWindowMethod[] = "destroyWindow"; +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. -static constexpr char kErrorCodeInvalidValue[] = "INVALID_VALUE"; -static constexpr char kErrorCodeUnavailable[] = "UNAVAILABLE"; +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, @@ -38,12 +51,12 @@ std::optional GetSingleValueForKeyOrSendError( 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() + "'."); + result.Error(kInvalidValueError, "Value for '" + key + + "' key must be of type '" + + typeid(T).name() + "'."); } } else { - result.Error(kErrorCodeInvalidValue, + result.Error(kInvalidValueError, "Map does not contain required '" + key + "' key."); } return std::nullopt; @@ -62,9 +75,9 @@ std::optional> GetListValuesForKeyOrSendError( 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."); + result.Error(kInvalidValueError, "Array for '" + key + + "' key must have " + + std::to_string(Size) + " values."); return std::nullopt; } std::vector decoded_values; @@ -72,7 +85,7 @@ std::optional> GetListValuesForKeyOrSendError( if (std::holds_alternative(value)) { decoded_values.push_back(std::get(value)); } else { - result.Error(kErrorCodeInvalidValue, + result.Error(kInvalidValueError, "Array for '" + key + "' key must only have values of type '" + typeid(T).name() + "'."); @@ -81,11 +94,11 @@ std::optional> GetListValuesForKeyOrSendError( } return decoded_values; } else { - result.Error(kErrorCodeInvalidValue, + result.Error(kInvalidValueError, "Value for '" + key + "' key must be an array."); } } else { - result.Error(kErrorCodeInvalidValue, + result.Error(kInvalidValueError, "Map does not contain required '" + key + "' key."); } return std::nullopt; @@ -152,21 +165,22 @@ void WindowingHandler::HandleCreateWindow(WindowArchetype archetype, 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."); + result.Error(kInvalidValueError, "Method call argument is not a map."); return; } std::wstring const title = ArchetypeToWideString(archetype); auto const size_list = - GetListValuesForKeyOrSendError("size", map, result); + GetListValuesForKeyOrSendError(kSizeKey, 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)) + + 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; } @@ -176,11 +190,11 @@ void WindowingHandler::HandleCreateWindow(WindowArchetype archetype, if (archetype == WindowArchetype::satellite || archetype == WindowArchetype::popup) { - if (auto const anchor_rect_it = map->find(EncodableValue("anchorRect")); + 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("anchorRect", map, result); + GetListValuesForKeyOrSendError(kAnchorRectKey, map, result); if (!anchor_rect_list) { return; } @@ -189,31 +203,32 @@ void WindowingHandler::HandleCreateWindow(WindowArchetype archetype, {anchor_rect_list->at(2), anchor_rect_list->at(3)}}; } } else { - result.Error(kErrorCodeInvalidValue, - "Map does not contain required 'anchorRect' key."); + result.Error(kInvalidValueError, "Map does not contain required '" + + std::string(kAnchorRectKey) + + "' key."); return; } auto const positioner_parent_anchor = GetSingleValueForKeyOrSendError( - "positionerParentAnchor", map, result); + kPositionerParentAnchorKey, map, result); if (!positioner_parent_anchor) { return; } auto const positioner_child_anchor = GetSingleValueForKeyOrSendError( - "positionerChildAnchor", map, result); + kPositionerChildAnchorKey, 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); + auto const positioner_offset_list = GetListValuesForKeyOrSendError( + kPositionerOffsetKey, map, result); if (!positioner_offset_list) { return; } auto const positioner_constraint_adjustment = - GetSingleValueForKeyOrSendError("positionerConstraintAdjustment", + GetSingleValueForKeyOrSendError(kPositionerConstraintAdjustmentKey, map, result); if (!positioner_constraint_adjustment) { return; @@ -234,12 +249,13 @@ void WindowingHandler::HandleCreateWindow(WindowArchetype archetype, if (archetype == WindowArchetype::dialog || archetype == WindowArchetype::satellite || archetype == WindowArchetype::popup) { - if (auto const parent_it = map->find(EncodableValue("parent")); + if (auto const parent_it = map->find(EncodableValue(kParentKey)); parent_it != map->end()) { if (parent_it->second.IsNull()) { if (archetype != WindowArchetype::dialog) { - result.Error(kErrorCodeInvalidValue, - "Value for 'parent' key must not be null."); + result.Error( + kInvalidValueError, + "Value for '" + std::string(kParentKey) + "' must not be null."); return; } } else { @@ -249,21 +265,22 @@ void WindowingHandler::HandleCreateWindow(WindowArchetype archetype, if (!parent_view_id.has_value() && (archetype == WindowArchetype::satellite || archetype == WindowArchetype::popup)) { - result.Error(kErrorCodeInvalidValue, - "Value for 'parent' key (" + + result.Error(kInvalidValueError, + "Value for '" + std::string(kParentKey) + "' (" + std::to_string(parent_view_id.value()) + ") must be nonnegative."); return; } } else { - result.Error(kErrorCodeInvalidValue, - "Value for 'parent' key must be of type int."); + result.Error(kInvalidValueError, "Value for '" + + std::string(kParentKey) + + "' must be of type int."); return; } } } else { - result.Error(kErrorCodeInvalidValue, - "Map does not contain required 'parent' key."); + result.Error(kInvalidValueError, "Map does not contain required '" + + std::string(kParentKey) + "' key."); return; } } @@ -274,17 +291,17 @@ void WindowingHandler::HandleCreateWindow(WindowArchetype archetype, archetype, positioner, parent_view_id)) { WindowMetadata const& data = data_opt.value(); result.Success(EncodableValue(EncodableMap{ - {EncodableValue("viewId"), EncodableValue(data.view_id)}, - {EncodableValue("archetype"), + {EncodableValue(kViewIdKey), EncodableValue(data.view_id)}, + {EncodableValue(kArchetypeKey), EncodableValue(static_cast(data.archetype))}, - {EncodableValue("size"), + {EncodableValue(kSizeKey), EncodableValue(EncodableList{EncodableValue(data.size.width), EncodableValue(data.size.height)})}, - {EncodableValue("parentViewId"), + {EncodableValue(kParentViewIdKey), data.parent_id ? EncodableValue(data.parent_id.value()) : EncodableValue()}})); } else { - result.Error(kErrorCodeUnavailable, "Can't create window."); + result.Error(kUnavailableError, "Can't create window."); } } @@ -293,26 +310,26 @@ void WindowingHandler::HandleDestroyWindow(MethodCall<> const& call, 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."); + result.Error(kInvalidValueError, "Method call argument is not a map."); return; } auto const view_id = - GetSingleValueForKeyOrSendError("viewId", map, result); + GetSingleValueForKeyOrSendError(kViewIdKey, 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."); + 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(kErrorCodeInvalidValue, "Can't find window with 'viewId' (" + - std::to_string(view_id.value()) + - ")."); + result.Error(kInvalidValueError, + "Can't find window with '" + std::string(kViewIdKey) + "' (" + + std::to_string(view_id.value()) + ")."); return; } From c27b446f09567604e0d2497e109952884d5f3054 Mon Sep 17 00:00:00 2001 From: Harlen Batagelo Date: Thu, 12 Dec 2024 08:22:34 -0300 Subject: [PATCH 5/7] Rephrase comments to improve readability --- shell/platform/windows/flutter_host_window.cc | 15 ++++----- shell/platform/windows/flutter_host_window.h | 7 ++-- .../windows/flutter_host_window_controller.h | 32 ++++++++++--------- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/shell/platform/windows/flutter_host_window.cc b/shell/platform/windows/flutter_host_window.cc index 1e19c64ffa4b9..2e6d9d0820953 100644 --- a/shell/platform/windows/flutter_host_window.cc +++ b/shell/platform/windows/flutter_host_window.cc @@ -287,12 +287,12 @@ bool IsClassRegistered(LPCWSTR class_name) { 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 +// 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 @@ -356,8 +356,7 @@ FlutterHostWindow::FlutterHostWindow(FlutterHostWindowController* controller, 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 - // with a close button. + // 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 diff --git a/shell/platform/windows/flutter_host_window.h b/shell/platform/windows/flutter_host_window.h index 0401c41baeaad..748cb6343ea45 100644 --- a/shell/platform/windows/flutter_host_window.h +++ b/shell/platform/windows/flutter_host_window.h @@ -84,10 +84,11 @@ class FlutterHostWindow { // Enables/disables this window and all its descendants. void EnableWindowAndDescendants(bool enable); - // Finds the first enabled window in the descendant hierarchy. + // Finds the first enabled descendant window. If the current window itself is + // enabled, returns the current window. FlutterHostWindow* FindFirstEnabledDescendant() const; - // Processes and route salient window messages for mouse handling, + // 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); @@ -108,7 +109,7 @@ class FlutterHostWindow { // Controller for the view hosted by this window. std::unique_ptr view_controller_; - // The window's archetype (e.g., regular, dialog, popup). + // The window archetype. WindowArchetype archetype_ = WindowArchetype::regular; // Windows that have this window as their owner window. diff --git a/shell/platform/windows/flutter_host_window_controller.h b/shell/platform/windows/flutter_host_window_controller.h index 71b78e6ad456a..94b87e1547eb7 100644 --- a/shell/platform/windows/flutter_host_window_controller.h +++ b/shell/platform/windows/flutter_host_window_controller.h @@ -15,10 +15,10 @@ namespace flutter { class FlutterWindowsEngine; -// A controller class for managing |FlutterHostWindow|s. -// |FlutterWindowsEngine| uniquely owns an instance of this class and uses it in -// |WindowingHandler| to handle the methods and messages that enable -// multi-window support on Flutter. +// 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); @@ -63,7 +63,7 @@ class FlutterHostWindowController { // with ID |view_id|. FlutterHostWindow* GetHostWindow(FlutterViewId view_id) const; - // Message handler to be called by |FlutterHostWindow| to process window + // 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); @@ -81,12 +81,13 @@ class FlutterHostWindowController { // 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 dropshadow area. + // the dimensions of the drop-shadow area. WindowSize GetWindowSize(FlutterViewId view_id) const; - // Hides all satellite windows in the application, except those that are - // descendants of |opt_out_hwnd| or have a dialog owns a window. If - // |opt_out_hwnd| is nullptr (default), no windows are excluded. + // 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. @@ -105,17 +106,18 @@ class FlutterHostWindowController { // The Flutter engine that owns this controller. FlutterWindowsEngine* const engine_; - // The windowing channel through which the controller sends window messages. + // 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 are hidden when their top-level window - // and all its owned windows become inactive. If nullptr, satellite hiding - // is enabled. If not nullptr, it contains the handle of the window that - // disabled the hiding, and it will be reset when the window if fully - // destroyed. + // 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); From 0dee4017f2d290cda7cfa4b5903ed06a38f7bd50 Mon Sep 17 00:00:00 2001 From: Harlen Batagelo Date: Tue, 17 Dec 2024 08:57:07 -0300 Subject: [PATCH 6/7] Remove code unrelated to regular windows --- ci/licenses_golden/excluded_files | 1 - ci/licenses_golden/licenses_flutter | 1 - shell/platform/common/BUILD.gn | 10 +- shell/platform/common/windowing.cc | 278 --------- shell/platform/common/windowing.h | 71 +-- shell/platform/common/windowing_unittests.cc | 528 ------------------ shell/platform/windows/flutter_host_window.cc | 444 +-------------- shell/platform/windows/flutter_host_window.h | 48 +- .../windows/flutter_host_window_controller.cc | 136 +---- .../windows/flutter_host_window_controller.h | 31 +- ...lutter_host_window_controller_unittests.cc | 6 +- shell/platform/windows/windowing_handler.cc | 117 +--- .../windows/windowing_handler_unittests.cc | 10 +- 13 files changed, 28 insertions(+), 1653 deletions(-) delete mode 100644 shell/platform/common/windowing.cc delete mode 100644 shell/platform/common/windowing_unittests.cc diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index 4a840836bb21a..8318a40d14115 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -335,7 +335,6 @@ ../../../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 diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 8fd68561b46f0..95056b752043f 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -47454,7 +47454,6 @@ 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 diff --git a/shell/platform/common/BUILD.gn b/shell/platform/common/BUILD.gn index 40e145ac0ca37..f3b352e42ac4e 100644 --- a/shell/platform/common/BUILD.gn +++ b/shell/platform/common/BUILD.gn @@ -145,10 +145,7 @@ source_set("common_cpp_core") { "windowing.h", ] - sources = [ - "path_utils.cc", - "windowing.cc", - ] + sources = [ "path_utils.cc" ] public_configs = [ "//flutter:config" ] } @@ -161,10 +158,7 @@ if (enable_unittests) { executable("common_cpp_core_unittests") { testonly = true - sources = [ - "path_utils_unittests.cc", - "windowing_unittests.cc", - ] + sources = [ "path_utils_unittests.cc" ] deps = [ ":common_cpp_core", diff --git a/shell/platform/common/windowing.cc b/shell/platform/common/windowing.cc deleted file mode 100644 index 63881ae5d6f55..0000000000000 --- a/shell/platform/common/windowing.cc +++ /dev/null @@ -1,278 +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/windowing.h" - -#include -#include - -namespace flutter { - -namespace { - -WindowPoint offset_for(WindowSize const& size, - WindowPositioner::Anchor anchor) { - 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(); - } -} - -WindowPoint anchor_position_for(WindowRectangle const& rect, - WindowPositioner::Anchor anchor) { - 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(); - } -} - -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)}; -} - -WindowPositioner::Anchor flip_anchor_x(WindowPositioner::Anchor 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; - } -} - -WindowPositioner::Anchor flip_anchor_y(WindowPositioner::Anchor 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; - } -} - -WindowPoint flip_offset_x(WindowPoint const& p) { - return {-1 * p.x, p.y}; -} - -WindowPoint flip_offset_y(WindowPoint const& p) { - return {p.x, -1 * p.y}; -} - -} // namespace - -WindowRectangle PlaceWindow(WindowPositioner const& positioner, - WindowSize child_size, - WindowRectangle const& anchor_rect, - WindowRectangle const& parent_rect, - WindowRectangle const& output_rect) { - WindowRectangle default_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); - - 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)) { - 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)); - - if (output_rect.contains({result, child_size})) { - return WindowRectangle{result, child_size}; - } - } - - if (static_cast(positioner.constraint_adjustment) & - static_cast(WindowPositioner::ConstraintAdjustment::flip_y)) { - 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)); - - 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)) { - 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))); - - if (output_rect.contains({result, child_size})) { - return WindowRectangle{result, child_size}; - } - } - - { - 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)) { - 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); - - 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)) { - 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); - - 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}; - } - } - - { - 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)) { - 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); - - 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)) { - 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); - - 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 flutter diff --git a/shell/platform/common/windowing.h b/shell/platform/common/windowing.h index 5131a6eb2642b..3645ab6711965 100644 --- a/shell/platform/common/windowing.h +++ b/shell/platform/common/windowing.h @@ -63,72 +63,17 @@ struct WindowRectangle { } }; -// 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, - // Dialog window. - dialog, - // Satellite window attached to a regular, floating_regular or dialog window. - satellite, - // Popup. - popup, }; // 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). + // The type of the window. WindowArchetype archetype = WindowArchetype::regular; // Size of the created window, in logical coordinates. WindowSize size; @@ -137,20 +82,6 @@ struct WindowMetadata { std::optional parent_id; }; -// 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. -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_WINDOWING_H_ diff --git a/shell/platform/common/windowing_unittests.cc b/shell/platform/common/windowing_unittests.cc deleted file mode 100644 index cc21cd8cf8f94..0000000000000 --- a/shell/platform/common/windowing_unittests.cc +++ /dev/null @@ -1,528 +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/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/flutter_host_window.cc b/shell/platform/windows/flutter_host_window.cc index 2e6d9d0820953..1759021268358 100644 --- a/shell/platform/windows/flutter_host_window.cc +++ b/shell/platform/windows/flutter_host_window.cc @@ -87,84 +87,6 @@ void EnableTransparentWindowBackground(HWND hwnd) { 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() { @@ -198,34 +120,6 @@ std::string GetLastErrorAsString() { 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 @@ -327,9 +221,7 @@ namespace flutter { FlutterHostWindow::FlutterHostWindow(FlutterHostWindowController* controller, std::wstring const& title, WindowSize const& preferred_client_size, - WindowArchetype archetype, - std::optional owner, - std::optional positioner) + WindowArchetype archetype) : window_controller_(controller) { archetype_ = archetype; @@ -338,112 +230,18 @@ FlutterHostWindow::FlutterHostWindow(FlutterHostWindowController* controller, 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. + // if the window has no owner or positioner. WindowRectangle const window_rect = [&]() -> WindowRectangle { WindowSize const window_size = GetWindowSizeForClientSize( - preferred_client_size, window_style, extended_window_style, - 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}; - } - } + preferred_client_size, window_style, extended_window_style, nullptr); return {{CW_USEDEFAULT, CW_USEDEFAULT}, window_size}; }(); @@ -470,24 +268,17 @@ FlutterHostWindow::FlutterHostWindow(FlutterHostWindowController* controller, } // 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); + HWND hwnd = CreateWindowEx(extended_window_style, kWindowClassName, + title.c_str(), window_style, + window_rect.top_left.x, window_rect.top_left.y, + window_rect.size.width, window_rect.size.height, + nullptr, nullptr, GetModuleHandle(nullptr), this); if (!hwnd) { FML_LOG(ERROR) << "Cannot create window: " << GetLastErrorAsString(); return; } - // 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 @@ -503,12 +294,6 @@ FlutterHostWindow::FlutterHostWindow(FlutterHostWindowController* controller, 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); @@ -547,22 +332,8 @@ FlutterHostWindow::FlutterHostWindow(FlutterHostWindowController* 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. @@ -599,10 +370,6 @@ 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; @@ -610,13 +377,6 @@ std::optional FlutterHostWindow::GetFlutterViewId() const { 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_; } @@ -657,111 +417,14 @@ LRESULT FlutterHostWindow::WndProc(HWND hwnd, 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); - } + if (window_handle_ && quit_on_close_) { + PostQuitMessage(0); } return 0; @@ -778,21 +441,6 @@ LRESULT FlutterHostWindow::HandleMessage(HWND hwnd, } 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; @@ -805,50 +453,9 @@ LRESULT FlutterHostWindow::HandleMessage(HWND hwnd, } 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; @@ -874,35 +481,4 @@ void FlutterHostWindow::SetChildContent(HWND content) { 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 index 748cb6343ea45..7b6fdeecc2e91 100644 --- a/shell/platform/windows/flutter_host_window.h +++ b/shell/platform/windows/flutter_host_window.h @@ -25,20 +25,13 @@ class FlutterHostWindow { // 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. + // logical coordinates. The window style is defined by |archetype|. // On success, a valid window handle can be retrieved via // |FlutterHostWindow::GetWindowHandle|. FlutterHostWindow(FlutterHostWindowController* controller, std::wstring const& title, WindowSize const& preferred_client_size, - WindowArchetype archetype, - std::optional owner, - std::optional positioner); + WindowArchetype archetype); virtual ~FlutterHostWindow(); // Returns the instance pointer for |hwnd| or nulllptr if invalid. @@ -47,15 +40,9 @@ class FlutterHostWindow { // 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; @@ -78,16 +65,6 @@ class FlutterHostWindow { // 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. @@ -96,13 +73,6 @@ class FlutterHostWindow { // 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_; @@ -112,13 +82,6 @@ class FlutterHostWindow { // 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; @@ -128,13 +91,6 @@ class FlutterHostWindow { // 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); }; diff --git a/shell/platform/windows/flutter_host_window_controller.cc b/shell/platform/windows/flutter_host_window_controller.cc index 789983818fccf..1220d268069bd 100644 --- a/shell/platform/windows/flutter_host_window_controller.cc +++ b/shell/platform/windows/flutter_host_window_controller.cc @@ -37,18 +37,9 @@ FlutterHostWindowController::~FlutterHostWindowController() { 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); + WindowArchetype archetype) { + auto window = std::make_unique(this, title, preferred_size, + archetype); if (!window->GetWindowHandle()) { return std::nullopt; } @@ -61,12 +52,12 @@ std::optional FlutterHostWindowController::CreateHostWindow( FlutterViewId const view_id = window->GetFlutterViewId().value(); windows_[view_id] = std::move(window); - SendOnWindowCreated(view_id, parent_view_id); + SendOnWindowCreated(view_id, std::nullopt); WindowMetadata result = {.view_id = view_id, .archetype = archetype, .size = GetWindowSize(view_id), - .parent_id = parent_view_id}; + .parent_id = std::nullopt}; return result; } @@ -76,15 +67,6 @@ bool FlutterHostWindowController::DestroyHostWindow(FlutterViewId view_id) { 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); @@ -119,53 +101,12 @@ LRESULT FlutterHostWindowController::HandleMessage(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; - } - 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) { @@ -230,50 +171,6 @@ WindowSize FlutterHostWindowController::GetWindowSize( 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_) { @@ -315,27 +212,4 @@ void FlutterHostWindowController::SendOnWindowDestroyed( } } -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 index 94b87e1547eb7..3d73a838a4d8d 100644 --- a/shell/platform/windows/flutter_host_window_controller.h +++ b/shell/platform/windows/flutter_host_window_controller.h @@ -32,24 +32,14 @@ class FlutterHostWindowController { // |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. + // determined by |archetype|. // // Returns a |WindowMetadata| with the metadata of the window just created, or // std::nullopt if the window could not be created. virtual std::optional CreateHostWindow( std::wstring const& title, WindowSize const& preferred_size, - WindowArchetype archetype, - std::optional positioner, - std::optional parent_view_id); + WindowArchetype archetype); // Destroys the window that hosts the view with ID |view_id|. // @@ -84,12 +74,6 @@ class FlutterHostWindowController { // 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; @@ -100,9 +84,6 @@ class FlutterHostWindowController { // 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_; @@ -112,14 +93,6 @@ class FlutterHostWindowController { // 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); }; diff --git a/shell/platform/windows/flutter_host_window_controller_unittests.cc b/shell/platform/windows/flutter_host_window_controller_unittests.cc index 278545cc8e657..ee41fda6c17ac 100644 --- a/shell/platform/windows/flutter_host_window_controller_unittests.cc +++ b/shell/platform/windows/flutter_host_window_controller_unittests.cc @@ -115,8 +115,7 @@ TEST_F(FlutterHostWindowControllerTest, CreateRegularWindow) { // Create the window. std::optional const result = - host_window_controller()->CreateHostWindow(title, size, archetype, - std::nullopt, std::nullopt); + host_window_controller()->CreateHostWindow(title, size, archetype); // Verify the onWindowCreated callback was invoked. EXPECT_TRUE(called_onWindowCreated); @@ -183,8 +182,7 @@ TEST_F(FlutterHostWindowControllerTest, DestroyWindow) { // Create the window. std::optional const result = - host_window_controller()->CreateHostWindow(title, size, archetype, - std::nullopt, std::nullopt); + host_window_controller()->CreateHostWindow(title, size, archetype); ASSERT_TRUE(result.has_value()); // Destroy the window and ensure onWindowDestroyed was invoked. diff --git a/shell/platform/windows/windowing_handler.cc b/shell/platform/windows/windowing_handler.cc index 0752dd4f2cbb1..1682c54d29f21 100644 --- a/shell/platform/windows/windowing_handler.cc +++ b/shell/platform/windows/windowing_handler.cc @@ -14,9 +14,6 @@ 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"; @@ -110,12 +107,6 @@ 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(); } @@ -146,12 +137,6 @@ void WindowingHandler::HandleMethodCall( 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 { @@ -185,110 +170,10 @@ void WindowingHandler::HandleCreateWindow(WindowArchetype archetype, 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)) { + archetype)) { WindowMetadata const& data = data_opt.value(); result.Success(EncodableValue(EncodableMap{ {EncodableValue(kViewIdKey), EncodableValue(data.view_id)}, diff --git a/shell/platform/windows/windowing_handler_unittests.cc b/shell/platform/windows/windowing_handler_unittests.cc index be2db4ceed3da..06b2d1ee015c4 100644 --- a/shell/platform/windows/windowing_handler_unittests.cc +++ b/shell/platform/windows/windowing_handler_unittests.cc @@ -54,9 +54,7 @@ class MockFlutterHostWindowController : public FlutterHostWindowController { CreateHostWindow, (std::wstring const& title, WindowSize const& size, - WindowArchetype archetype, - std::optional positioner, - std::optional parent_view_id), + WindowArchetype archetype), (override)); MOCK_METHOD(bool, DestroyHostWindow, (FlutterViewId view_id), (override)); @@ -112,10 +110,8 @@ TEST_F(WindowingHandlerTest, HandleCreateRegularWindow) { [&success](const EncodableValue* result) { success = true; }, nullptr, nullptr); - EXPECT_CALL( - *controller(), - CreateHostWindow(StrEq(L"regular"), size, WindowArchetype::regular, - Eq(std::nullopt), Eq(std::nullopt))) + EXPECT_CALL(*controller(), CreateHostWindow(StrEq(L"regular"), size, + WindowArchetype::regular)) .Times(1); SimulateWindowingMessage(&messenger, kCreateWindowMethod, From 8739d84700ea8feaac095cf3c1797fc8c9ae70ff Mon Sep 17 00:00:00 2001 From: Harlen Batagelo Date: Tue, 17 Dec 2024 10:31:11 -0300 Subject: [PATCH 7/7] Reorder entries in excluded_files --- ci/licenses_golden/excluded_files | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index 8318a40d14115..28f2b2276540a 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -427,9 +427,9 @@ ../../../flutter/shell/platform/windows/task_runner_unittests.cc ../../../flutter/shell/platform/windows/testing ../../../flutter/shell/platform/windows/text_input_plugin_unittest.cc -../../../flutter/shell/platform/windows/windowing_handler_unittests.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