diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index fc87ccf46c344..fc27f01cb807b 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 @@ -408,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 @@ -428,6 +430,7 @@ ../../../flutter/shell/platform/windows/text_input_plugin_unittest.cc ../../../flutter/shell/platform/windows/window_proc_delegate_manager_unittests.cc ../../../flutter/shell/platform/windows/window_unittests.cc +../../../flutter/shell/platform/windows/windowing_handler_unittests.cc ../../../flutter/shell/platform/windows/windows_lifecycle_manager_unittests.cc ../../../flutter/shell/profiling/sampling_profiler_unittest.cc ../../../flutter/shell/testing diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 476cee8211866..8fd68561b46f0 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -47454,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 @@ -48197,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 @@ -48246,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/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/windowing.h b/shell/platform/common/windowing.h new file mode 100644 index 0000000000000..7e224ac41f6de --- /dev/null +++ b/shell/platform/common/windowing.h @@ -0,0 +1,156 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_WINDOWING_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_WINDOWING_H_ + +#include + +namespace flutter { + +// A unique identifier for a view. +using FlutterViewId = int64_t; + +// A point in 2D space for window positioning using integer coordinates. +struct WindowPoint { + int x = 0; + int y = 0; + + friend WindowPoint operator+(WindowPoint const& lhs, WindowPoint const& rhs) { + return {lhs.x + rhs.x, lhs.y + rhs.y}; + } + + friend WindowPoint operator-(WindowPoint const& lhs, WindowPoint const& rhs) { + return {lhs.x - rhs.x, lhs.y - rhs.y}; + } + + friend bool operator==(WindowPoint const& lhs, WindowPoint const& rhs) { + return lhs.x == rhs.x && lhs.y == rhs.y; + } +}; + +// A 2D size using integer dimensions. +struct WindowSize { + int width = 0; + int height = 0; + + explicit operator WindowPoint() const { return {width, height}; } + + friend bool operator==(WindowSize const& lhs, WindowSize const& rhs) { + return lhs.width == rhs.width && lhs.height == rhs.height; + } +}; + +// A rectangular area defined by a top-left point and size. +struct WindowRectangle { + WindowPoint top_left; + WindowSize size; + + // Checks if this rectangle fully contains |rect|. + // Note: An empty rectangle can still contain other empty rectangles, + // which are treated as points or lines of thickness zero + bool contains(WindowRectangle const& rect) const { + return rect.top_left.x >= top_left.x && + rect.top_left.x + rect.size.width <= top_left.x + size.width && + rect.top_left.y >= top_left.y && + rect.top_left.y + rect.size.height <= top_left.y + size.height; + } + + friend bool operator==(WindowRectangle const& lhs, + WindowRectangle const& rhs) { + return lhs.top_left == rhs.top_left && lhs.size == rhs.size; + } +}; + +// 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 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. + 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; +}; + +// 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 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/flutter_host_window.cc b/shell/platform/windows/flutter_host_window.cc new file mode 100644 index 0000000000000..2e6d9d0820953 --- /dev/null +++ b/shell/platform/windows/flutter_host_window.cc @@ -0,0 +1,908 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/windows/flutter_host_window.h" + +#include + +#include "flutter/shell/platform/windows/dpi_utils.h" +#include "flutter/shell/platform/windows/flutter_host_window_controller.h" +#include "flutter/shell/platform/windows/flutter_window.h" +#include "flutter/shell/platform/windows/flutter_windows_view_controller.h" + +namespace { + +constexpr wchar_t kWindowClassName[] = L"FLUTTER_HOST_WINDOW"; + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module +// so that the non-client area automatically responds to changes in DPI. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + + using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + + FreeLibrary(user32_module); +} + +// Dynamically loads |SetWindowCompositionAttribute| from the User32 module to +// make the window's background transparent. +void EnableTransparentWindowBackground(HWND hwnd) { + HMODULE const user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + + enum WINDOWCOMPOSITIONATTRIB { WCA_ACCENT_POLICY = 19 }; + + struct WINDOWCOMPOSITIONATTRIBDATA { + WINDOWCOMPOSITIONATTRIB Attrib; + PVOID pvData; + SIZE_T cbData; + }; + + using SetWindowCompositionAttribute = + BOOL(__stdcall*)(HWND, WINDOWCOMPOSITIONATTRIBDATA*); + + auto set_window_composition_attribute = + reinterpret_cast( + GetProcAddress(user32_module, "SetWindowCompositionAttribute")); + if (set_window_composition_attribute != nullptr) { + enum ACCENT_STATE { ACCENT_DISABLED = 0 }; + + struct ACCENT_POLICY { + ACCENT_STATE AccentState; + DWORD AccentFlags; + DWORD GradientColor; + DWORD AnimationId; + }; + + // Set the accent policy to disable window composition. + ACCENT_POLICY accent = {ACCENT_DISABLED, 2, static_cast(0), 0}; + WINDOWCOMPOSITIONATTRIBDATA data = {.Attrib = WCA_ACCENT_POLICY, + .pvData = &accent, + .cbData = sizeof(accent)}; + set_window_composition_attribute(hwnd, &data); + + // Extend the frame into the client area and set the window's system + // backdrop type for visual effects. + MARGINS const margins = {-1}; + ::DwmExtendFrameIntoClientArea(hwnd, &margins); + INT effect_value = 1; + ::DwmSetWindowAttribute(hwnd, DWMWA_SYSTEMBACKDROP_TYPE, &effect_value, + sizeof(BOOL)); + } + + FreeLibrary(user32_module); +} + +// Computes the screen-space anchor rectangle for a window being positioned +// with |positioner|, having |owner_hwnd| as owner, and |owner_rect| +// as the owner's client rectangle, also in screen space. If the positioner +// specifies an anchor rectangle (in logical coordinates), its coordinates are +// scaled using the owner's DPI and offset relative to |owner_rect|. +// Otherwise, the function defaults to using the window frame of |owner_hwnd| +// as the anchor rectangle. +flutter::WindowRectangle GetAnchorRectInScreenSpace( + flutter::WindowPositioner const& positioner, + HWND owner_hwnd, + flutter::WindowRectangle const& owner_rect) { + if (positioner.anchor_rect) { + double const dpr = flutter::GetDpiForHWND(owner_hwnd) / + static_cast(USER_DEFAULT_SCREEN_DPI); + return {{owner_rect.top_left.x + + static_cast(positioner.anchor_rect->top_left.x * dpr), + owner_rect.top_left.y + + static_cast(positioner.anchor_rect->top_left.y * dpr)}, + {static_cast(positioner.anchor_rect->size.width * dpr), + static_cast(positioner.anchor_rect->size.height * dpr)}}; + } else { + // If the anchor rectangle specified in the positioner is std::nullopt, + // return an anchor rectangle that is equal to the owner's frame. + RECT frame_rect; + DwmGetWindowAttribute(owner_hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rect, + sizeof(frame_rect)); + return {{frame_rect.left, frame_rect.top}, + {frame_rect.right - frame_rect.left, + frame_rect.bottom - frame_rect.top}}; + } +} + +// Calculates the client area of |hwnd| in screen space. +flutter::WindowRectangle GetClientRectInScreenSpace(HWND hwnd) { + RECT client_rect; + GetClientRect(hwnd, &client_rect); + POINT top_left = {0, 0}; + ClientToScreen(hwnd, &top_left); + POINT bottom_right = {client_rect.right, client_rect.bottom}; + ClientToScreen(hwnd, &bottom_right); + return {{top_left.x, top_left.y}, + {bottom_right.x - top_left.x, bottom_right.y - top_left.y}}; +} + +// Calculates the size of the window frame in physical coordinates, based on +// the given |window_size| (also in physical coordinates) and the specified +// |window_style|, |extended_window_style|, and owner window |owner_hwnd|. +flutter::WindowSize GetFrameSizeForWindowSize( + flutter::WindowSize const& window_size, + DWORD window_style, + DWORD extended_window_style, + HWND owner_hwnd) { + RECT frame_rect = {0, 0, static_cast(window_size.width), + static_cast(window_size.height)}; + + HINSTANCE hInstance = GetModuleHandle(nullptr); + WNDCLASS window_class = {}; + window_class.lpfnWndProc = DefWindowProc; + window_class.hInstance = hInstance; + window_class.lpszClassName = L"FLUTTER_HOST_WINDOW_TEMPORARY"; + RegisterClass(&window_class); + + window_style &= ~WS_VISIBLE; + if (HWND const window = CreateWindowEx( + extended_window_style, window_class.lpszClassName, L"", window_style, + CW_USEDEFAULT, CW_USEDEFAULT, window_size.width, window_size.height, + owner_hwnd, nullptr, hInstance, nullptr)) { + DwmGetWindowAttribute(window, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rect, + sizeof(frame_rect)); + DestroyWindow(window); + } + + UnregisterClass(window_class.lpszClassName, hInstance); + + return {static_cast(frame_rect.right - frame_rect.left), + static_cast(frame_rect.bottom - frame_rect.top)}; +} + +// Retrieves the calling thread's last-error code message as a string, +// or a fallback message if the error message cannot be formatted. +std::string GetLastErrorAsString() { + LPWSTR message_buffer = nullptr; + + if (DWORD const size = FormatMessage( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&message_buffer), 0, nullptr)) { + std::wstring const wide_message(message_buffer, size); + LocalFree(message_buffer); + message_buffer = nullptr; + + if (int const buffer_size = + WideCharToMultiByte(CP_UTF8, 0, wide_message.c_str(), -1, nullptr, + 0, nullptr, nullptr)) { + std::string message(buffer_size, 0); + WideCharToMultiByte(CP_UTF8, 0, wide_message.c_str(), -1, &message[0], + buffer_size, nullptr, nullptr); + return message; + } + } + + if (message_buffer) { + LocalFree(message_buffer); + } + std::ostringstream oss; + oss << "Format message failed with 0x" << std::hex << std::setfill('0') + << std::setw(8) << GetLastError(); + return oss.str(); +} + +// Calculates the offset from the top-left corner of |from| to the top-left +// corner of |to|. If either window handle is null or if the window positions +// cannot be retrieved, the offset will be (0, 0). +POINT GetOffsetBetweenWindows(HWND from, HWND to) { + POINT offset = {0, 0}; + if (to && from) { + RECT to_rect; + RECT from_rect; + if (GetWindowRect(to, &to_rect) && GetWindowRect(from, &from_rect)) { + offset.x = to_rect.left - from_rect.left; + offset.y = to_rect.top - from_rect.top; + } + } + return offset; +} + +// Calculates the rectangle of the monitor that has the largest area of +// intersection with |rect|, in physical coordinates. +flutter::WindowRectangle GetOutputRect(RECT rect) { + HMONITOR monitor = MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST); + MONITORINFO mi; + mi.cbSize = sizeof(MONITORINFO); + RECT const bounds = + GetMonitorInfo(monitor, &mi) ? mi.rcWork : RECT{0, 0, 0, 0}; + return {{bounds.left, bounds.top}, + {bounds.right - bounds.left, bounds.bottom - bounds.top}}; +} + +// Calculates the required window size, in physical coordinates, to +// accommodate the given |client_size|, in logical coordinates, for a window +// with the specified |window_style| and |extended_window_style|. The result +// accounts for window borders, non-client areas, and the drop-shadow area. +flutter::WindowSize GetWindowSizeForClientSize( + flutter::WindowSize const& client_size, + DWORD window_style, + DWORD extended_window_style, + HWND owner_hwnd) { + UINT const dpi = flutter::GetDpiForHWND(owner_hwnd); + double const scale_factor = + static_cast(dpi) / USER_DEFAULT_SCREEN_DPI; + RECT rect = {.left = 0, + .top = 0, + .right = static_cast(client_size.width * scale_factor), + .bottom = static_cast(client_size.height * scale_factor)}; + + HMODULE const user32_module = LoadLibraryA("User32.dll"); + if (user32_module) { + using AdjustWindowRectExForDpi = BOOL __stdcall( + LPRECT lpRect, DWORD dwStyle, BOOL bMenu, DWORD dwExStyle, UINT dpi); + + auto* const adjust_window_rect_ext_for_dpi = + reinterpret_cast( + GetProcAddress(user32_module, "AdjustWindowRectExForDpi")); + if (adjust_window_rect_ext_for_dpi) { + if (adjust_window_rect_ext_for_dpi(&rect, window_style, FALSE, + extended_window_style, dpi)) { + FreeLibrary(user32_module); + return {static_cast(rect.right - rect.left), + static_cast(rect.bottom - rect.top)}; + } else { + FML_LOG(WARNING) << "Failed to run AdjustWindowRectExForDpi: " + << GetLastErrorAsString(); + } + } else { + FML_LOG(WARNING) + << "Failed to retrieve AdjustWindowRectExForDpi address from " + "User32.dll."; + } + FreeLibrary(user32_module); + } else { + FML_LOG(WARNING) << "Failed to load User32.dll.\n"; + } + + if (!AdjustWindowRectEx(&rect, window_style, FALSE, extended_window_style)) { + FML_LOG(WARNING) << "Failed to run AdjustWindowRectEx: " + << GetLastErrorAsString(); + } + return {static_cast(rect.right - rect.left), + static_cast(rect.bottom - rect.top)}; +} + +// Checks whether the window class of name |class_name| is registered for the +// current application. +bool IsClassRegistered(LPCWSTR class_name) { + WNDCLASSEX window_class = {}; + return GetClassInfoEx(GetModuleHandle(nullptr), class_name, &window_class) != + 0; +} + +// Window attribute that enables dark mode window decorations. +// +// Redefined in case the developer's machine has a Windows SDK older than +// version 10.0.22000.0. +// See: +// https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +// Update the window frame's theme to match the system theme. +void UpdateTheme(HWND window) { + // Registry key for app theme preference. + const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; + const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + + // A value of 0 indicates apps should use dark mode. A non-zero or missing + // value indicates apps should use light mode. + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS const result = + RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD, nullptr, + &light_mode, &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} + +} // namespace + +namespace flutter { + +FlutterHostWindow::FlutterHostWindow(FlutterHostWindowController* controller, + std::wstring const& title, + WindowSize const& preferred_client_size, + WindowArchetype archetype, + std::optional owner, + std::optional positioner) + : window_controller_(controller) { + archetype_ = archetype; + + // Check preconditions and set window styles based on window type. + DWORD window_style = 0; + DWORD extended_window_style = 0; + switch (archetype) { + case WindowArchetype::regular: + if (owner.has_value()) { + FML_LOG(ERROR) << "A regular window cannot have an owner."; + return; + } + if (positioner.has_value()) { + FML_LOG(ERROR) << "A regular window cannot have a positioner."; + return; + } + window_style |= WS_OVERLAPPEDWINDOW; + break; + case WindowArchetype::dialog: + if (positioner.has_value()) { + FML_LOG(ERROR) << "A dialog cannot have a positioner."; + return; + } + window_style |= WS_OVERLAPPED | WS_CAPTION; + extended_window_style |= WS_EX_DLGMODALFRAME; + if (!owner) { + // If the dialog has no owner, add a minimize box and a system menu. + window_style |= WS_MINIMIZEBOX | WS_SYSMENU; + } else { + // If the owner window has WS_EX_TOOLWINDOW style, apply the same + // style to the dialog. + if (GetWindowLongPtr(owner.value(), GWL_EXSTYLE) & WS_EX_TOOLWINDOW) { + extended_window_style |= WS_EX_TOOLWINDOW; + } + } + break; + case WindowArchetype::satellite: + if (!positioner.has_value()) { + FML_LOG(ERROR) << "A satellite window requires a positioner."; + return; + } + if (!owner.has_value()) { + FML_LOG(ERROR) << "A satellite window must have an owner."; + return; + } + window_style |= WS_OVERLAPPEDWINDOW & ~WS_MAXIMIZEBOX; + extended_window_style |= WS_EX_TOOLWINDOW; + break; + case WindowArchetype::popup: + if (!positioner.has_value()) { + FML_LOG(ERROR) << "A popup window requires a positioner."; + return; + } + if (!owner.has_value()) { + FML_LOG(ERROR) << "A popup window must have an owner."; + return; + } + window_style |= WS_POPUP; + break; + default: + FML_UNREACHABLE(); + } + + // Calculate the screen space window rectangle for the new window. + // Default positioning values (CW_USEDEFAULT) are used + // if the window has no owner or positioner. Owned dialogs will be + // centered in the owner's frame. + WindowRectangle const window_rect = [&]() -> WindowRectangle { + WindowSize const window_size = GetWindowSizeForClientSize( + preferred_client_size, window_style, extended_window_style, + owner.value_or(nullptr)); + if (owner) { + if (positioner) { + // Calculate the window rectangle according to a positioner and + // the owner's rectangle. + WindowSize const frame_size = GetFrameSizeForWindowSize( + window_size, window_style, extended_window_style, owner.value()); + + WindowRectangle const owner_rect = + GetClientRectInScreenSpace(owner.value()); + + WindowRectangle const anchor_rect = GetAnchorRectInScreenSpace( + positioner.value(), owner.value(), owner_rect); + + WindowRectangle const output_rect = GetOutputRect( + {.left = static_cast(anchor_rect.top_left.x), + .top = static_cast(anchor_rect.top_left.y), + .right = static_cast(anchor_rect.top_left.x + + anchor_rect.size.width), + .bottom = static_cast(anchor_rect.top_left.y + + anchor_rect.size.height)}); + + WindowRectangle const rect = PlaceWindow( + positioner.value(), frame_size, anchor_rect, + positioner->anchor_rect ? owner_rect : anchor_rect, output_rect); + + return {rect.top_left, + {rect.size.width + window_size.width - frame_size.width, + rect.size.height + window_size.height - frame_size.height}}; + } else if (archetype == WindowArchetype::dialog) { + // Center owned dialog in the owner's frame. + RECT owner_frame; + DwmGetWindowAttribute(owner.value(), DWMWA_EXTENDED_FRAME_BOUNDS, + &owner_frame, sizeof(owner_frame)); + WindowPoint const top_left = { + static_cast( + (owner_frame.left + owner_frame.right - window_size.width) * + 0.5), + static_cast( + (owner_frame.top + owner_frame.bottom - window_size.height) * + 0.5)}; + return {top_left, window_size}; + } + } + return {{CW_USEDEFAULT, CW_USEDEFAULT}, window_size}; + }(); + + // Register the window class. + if (!IsClassRegistered(kWindowClassName)) { + auto const idi_app_icon = 101; + WNDCLASSEX window_class = {}; + window_class.cbSize = sizeof(WNDCLASSEX); + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.lpfnWndProc = FlutterHostWindow::WndProc; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(idi_app_icon)); + if (!window_class.hIcon) { + window_class.hIcon = LoadIcon(nullptr, IDI_APPLICATION); + } + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + + if (!RegisterClassEx(&window_class)) { + FML_LOG(ERROR) << "Cannot register window class " << kWindowClassName + << ": " << GetLastErrorAsString(); + } + } + + // Create the native window. + HWND hwnd = CreateWindowEx( + extended_window_style, kWindowClassName, title.c_str(), window_style, + window_rect.top_left.x, window_rect.top_left.y, window_rect.size.width, + window_rect.size.height, owner.value_or(nullptr), nullptr, + GetModuleHandle(nullptr), this); + + if (!hwnd) { + FML_LOG(ERROR) << "Cannot create window: " << GetLastErrorAsString(); + return; + } + + // If this is a modeless dialog, remove the close button from the system menu. + if (archetype == WindowArchetype::dialog && !owner) { + if (HMENU hMenu = GetSystemMenu(hwnd, FALSE)) { + DeleteMenu(hMenu, SC_CLOSE, MF_BYCOMMAND); + } + } + + // Adjust the window position so its origin aligns with the top-left corner + // of the window frame, not the window rectangle (which includes the + // drop-shadow). This adjustment must be done post-creation since the frame + // rectangle is only available after the window has been created. + RECT frame_rc; + DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rc, + sizeof(frame_rc)); + RECT window_rc; + GetWindowRect(hwnd, &window_rc); + LONG const left_dropshadow_width = frame_rc.left - window_rc.left; + LONG const top_dropshadow_height = window_rc.top - frame_rc.top; + SetWindowPos(hwnd, nullptr, window_rc.left - left_dropshadow_width, + window_rc.top - top_dropshadow_height, 0, 0, + SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + + if (owner) { + if (HWND const owner_window = GetWindow(hwnd, GW_OWNER)) { + offset_from_owner_ = GetOffsetBetweenWindows(owner_window, hwnd); + } + } + + // Set up the view. + RECT client_rect; + GetClientRect(hwnd, &client_rect); + int const width = client_rect.right - client_rect.left; + int const height = client_rect.bottom - client_rect.top; + + FlutterWindowsEngine* const engine = window_controller_->engine(); + auto view_window = std::make_unique( + width, height, engine->windows_proc_table()); + + std::unique_ptr view = + engine->CreateView(std::move(view_window)); + if (!view) { + FML_LOG(ERROR) << "Failed to create view"; + return; + } + + view_controller_ = + std::make_unique(nullptr, std::move(view)); + + // Launch the engine if it is not running already. + if (!engine->running() && !engine->Run()) { + FML_LOG(ERROR) << "Failed to launch engine"; + return; + } + // Must happen after engine is running. + view_controller_->view()->SendInitialBounds(); + // The Windows embedder listens to accessibility updates using the + // view's HWND. The embedder's accessibility features may be stale if + // the app was in headless mode. + view_controller_->engine()->UpdateAccessibilityFeatures(); + + // Ensure that basic setup of the view controller was successful. + if (!view_controller_->view()) { + FML_LOG(ERROR) << "Failed to set up the view controller"; + return; + } + + // Update the properties of the owner window, if it exists. + if (FlutterHostWindow* const owner_window = + GetThisFromHandle(owner.value_or(nullptr))) { + owner_window->owned_windows_.insert(this); + + if (archetype == WindowArchetype::popup) { + ++owner_window->num_owned_popups_; + } + } + + UpdateTheme(hwnd); + + if (archetype == WindowArchetype::dialog && owner) { + UpdateModalState(); + } + + SetChildContent(view_controller_->view()->GetWindowHandle()); + + // TODO(loicsharma): Hide the window until the first frame is rendered. + // Single window apps use the engine's next frame callback to show the window. + // This doesn't work for multi window apps as the engine cannot have multiple + // next frame callbacks. If multiple windows are created, only the last one + // will be shown. + ShowWindow(hwnd, SW_SHOW); + + window_handle_ = hwnd; +} + +FlutterHostWindow::~FlutterHostWindow() { + if (HWND const hwnd = window_handle_) { + window_handle_ = nullptr; + DestroyWindow(hwnd); + + // Unregisters the window class. It will fail silently if there are + // other windows using the class, as only the last window can + // successfully unregister the class. + if (!UnregisterClass(kWindowClassName, GetModuleHandle(nullptr))) { + // Clears the error information after the failed unregistering. + SetLastError(ERROR_SUCCESS); + } + } +} + +FlutterHostWindow* FlutterHostWindow::GetThisFromHandle(HWND hwnd) { + return reinterpret_cast( + GetWindowLongPtr(hwnd, GWLP_USERDATA)); +} + +WindowArchetype FlutterHostWindow::GetArchetype() const { + return archetype_; +} + +std::set const& FlutterHostWindow::GetOwnedWindows() const { + return owned_windows_; +} + +std::optional FlutterHostWindow::GetFlutterViewId() const { + if (!view_controller_ || !view_controller_->view()) { + return std::nullopt; + } + return view_controller_->view()->view_id(); +}; + +FlutterHostWindow* FlutterHostWindow::GetOwnerWindow() const { + if (HWND const owner_window_handle = GetWindow(GetWindowHandle(), GW_OWNER)) { + return GetThisFromHandle(owner_window_handle); + } + return nullptr; +}; + +HWND FlutterHostWindow::GetWindowHandle() const { + return window_handle_; +} + +void FlutterHostWindow::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool FlutterHostWindow::GetQuitOnClose() const { + return quit_on_close_; +} + +void FlutterHostWindow::FocusViewOf(FlutterHostWindow* window) { + if (window != nullptr && window->child_content_ != nullptr) { + SetFocus(window->child_content_); + } +}; + +LRESULT FlutterHostWindow::WndProc(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) { + if (message == WM_NCCREATE) { + auto* const create_struct = reinterpret_cast(lparam); + SetWindowLongPtr(hwnd, GWLP_USERDATA, + reinterpret_cast(create_struct->lpCreateParams)); + auto* const window = + static_cast(create_struct->lpCreateParams); + window->window_handle_ = hwnd; + + EnableFullDpiSupportIfAvailable(hwnd); + EnableTransparentWindowBackground(hwnd); + } else if (FlutterHostWindow* const window = GetThisFromHandle(hwnd)) { + return window->window_controller_->HandleMessage(hwnd, message, wparam, + lparam); + } + + return DefWindowProc(hwnd, message, wparam, lparam); +} + +std::size_t FlutterHostWindow::CloseOwnedPopups() { + if (num_owned_popups_ == 0) { + return 0; + } + + std::set popups; + for (FlutterHostWindow* const owned : owned_windows_) { + if (owned->archetype_ == WindowArchetype::popup) { + popups.insert(owned); + } + } + + for (auto it = owned_windows_.begin(); it != owned_windows_.end();) { + if ((*it)->archetype_ == WindowArchetype::popup) { + it = owned_windows_.erase(it); + } else { + ++it; + } + } + + std::size_t const previous_num_owned_popups = num_owned_popups_; + + for (FlutterHostWindow* popup : popups) { + HWND const owner_handle = GetWindow(popup->window_handle_, GW_OWNER); + if (FlutterHostWindow* const owner = GetThisFromHandle(owner_handle)) { + // Popups' owners are drawn with active colors even though they are + // actually inactive. When a popup is destroyed, the owner might be + // redrawn as inactive (reflecting its true state) before being redrawn as + // active. To prevent flickering during this transition, disable + // redrawing the non-client area as inactive. + owner->enable_redraw_non_client_as_inactive_ = false; + DestroyWindow(popup->GetWindowHandle()); + owner->enable_redraw_non_client_as_inactive_ = true; + + // Repaint owner window to make sure its title bar is painted with the + // color based on its actual activation state. + if (owner->num_owned_popups_ == 0) { + SetWindowPos(owner_handle, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); + } + } + } + + return previous_num_owned_popups - num_owned_popups_; +} + +void FlutterHostWindow::EnableWindowAndDescendants(bool enable) { + EnableWindow(window_handle_, enable); + + for (FlutterHostWindow* const owned : owned_windows_) { + owned->EnableWindowAndDescendants(enable); + } +} + +FlutterHostWindow* FlutterHostWindow::FindFirstEnabledDescendant() const { + if (IsWindowEnabled(GetWindowHandle())) { + return const_cast(this); + } + + for (FlutterHostWindow* const owned : GetOwnedWindows()) { + if (FlutterHostWindow* const result = owned->FindFirstEnabledDescendant()) { + return result; + } + } + + return nullptr; +} + +LRESULT FlutterHostWindow::HandleMessage(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) { + switch (message) { + case WM_DESTROY: + if (window_handle_) { + switch (archetype_) { + case WindowArchetype::regular: + break; + case WindowArchetype::dialog: + if (FlutterHostWindow* const owner_window = GetOwnerWindow()) { + owner_window->owned_windows_.erase(this); + UpdateModalState(); + FocusViewOf(owner_window); + } + break; + case WindowArchetype::satellite: + if (FlutterHostWindow* const owner_window = GetOwnerWindow()) { + owner_window->owned_windows_.erase(this); + FocusViewOf(owner_window); + } + break; + case WindowArchetype::popup: + if (FlutterHostWindow* const owner_window = GetOwnerWindow()) { + owner_window->owned_windows_.erase(this); + assert(owner_window->num_owned_popups_ > 0); + --owner_window->num_owned_popups_; + FocusViewOf(owner_window); + } + break; + default: + FML_UNREACHABLE(); + } + if (quit_on_close_) { + PostQuitMessage(0); + } + } + return 0; + + case WM_DPICHANGED: { + auto* const new_scaled_window_rect = reinterpret_cast(lparam); + LONG const width = + new_scaled_window_rect->right - new_scaled_window_rect->left; + LONG const height = + new_scaled_window_rect->bottom - new_scaled_window_rect->top; + SetWindowPos(hwnd, nullptr, new_scaled_window_rect->left, + new_scaled_window_rect->top, width, height, + SWP_NOZORDER | SWP_NOACTIVATE); + return 0; + } + + case WM_SIZE: { + if (wparam == SIZE_MAXIMIZED) { + // Hide the satellites of the maximized window + for (FlutterHostWindow* const owned : owned_windows_) { + if (owned->archetype_ == WindowArchetype::satellite) { + ShowWindow(owned->GetWindowHandle(), SW_HIDE); + } + } + } else if (wparam == SIZE_RESTORED) { + // Show the satellites of the restored window + for (FlutterHostWindow* const owned : owned_windows_) { + if (owned->archetype_ == WindowArchetype::satellite) { + ShowWindow(owned->GetWindowHandle(), SW_SHOWNOACTIVATE); + } + } + } + if (child_content_ != nullptr) { + // Resize and reposition the child content window + RECT client_rect; + GetClientRect(hwnd, &client_rect); + MoveWindow(child_content_, client_rect.left, client_rect.top, + client_rect.right - client_rect.left, + client_rect.bottom - client_rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + // Prevent disabled window from being activated using the task switcher + if (!IsWindowEnabled(hwnd) && LOWORD(wparam) != WA_INACTIVE) { + // Redirect focus and activation to the first enabled descendant + if (FlutterHostWindow* enabled_descendant = + FindFirstEnabledDescendant()) { + SetActiveWindow(enabled_descendant->GetWindowHandle()); + } + return 0; + } + FocusViewOf(this); + return 0; + + case WM_NCACTIVATE: + if (wparam == FALSE && archetype_ != WindowArchetype::popup) { + if (!enable_redraw_non_client_as_inactive_ || num_owned_popups_ > 0) { + // If an inactive title bar is to be drawn, and this is a top-level + // window with popups, force the title bar to be drawn in its active + // colors. + return TRUE; + } + } + break; + + case WM_MOVE: { + if (HWND const owner_window = GetWindow(hwnd, GW_OWNER)) { + offset_from_owner_ = GetOffsetBetweenWindows(owner_window, hwnd); + } + + // Move the satellites attached to this window. + RECT window_rect; + GetWindowRect(hwnd, &window_rect); + for (FlutterHostWindow* const owned : owned_windows_) { + if (owned->archetype_ == WindowArchetype::satellite) { + RECT rect_satellite; + GetWindowRect(owned->GetWindowHandle(), &rect_satellite); + MoveWindow(owned->GetWindowHandle(), + window_rect.left + owned->offset_from_owner_.x, + window_rect.top + owned->offset_from_owner_.y, + rect_satellite.right - rect_satellite.left, + rect_satellite.bottom - rect_satellite.top, FALSE); + } + } + } break; + + case WM_MOUSEACTIVATE: + FocusViewOf(this); + return MA_ACTIVATE; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + + default: + break; + } + + return DefWindowProc(hwnd, message, wparam, lparam); +} + +void FlutterHostWindow::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT client_rect; + GetClientRect(window_handle_, &client_rect); + MoveWindow(content, client_rect.left, client_rect.top, + client_rect.right - client_rect.left, + client_rect.bottom - client_rect.top, true); +} + +void FlutterHostWindow::UpdateModalState() { + auto const find_deepest_dialog = [](FlutterHostWindow* window, + auto&& self) -> FlutterHostWindow* { + FlutterHostWindow* deepest_dialog = nullptr; + if (window->archetype_ == WindowArchetype::dialog) { + deepest_dialog = window; + } + for (FlutterHostWindow* const owned : window->owned_windows_) { + if (FlutterHostWindow* const owned_deepest_dialog = self(owned, self)) { + deepest_dialog = owned_deepest_dialog; + } + } + return deepest_dialog; + }; + + HWND root_ancestor_handle = window_handle_; + while (HWND next = GetWindow(root_ancestor_handle, GW_OWNER)) { + root_ancestor_handle = next; + } + if (FlutterHostWindow* const root_ancestor = + GetThisFromHandle(root_ancestor_handle)) { + if (FlutterHostWindow* const deepest_dialog = + find_deepest_dialog(root_ancestor, find_deepest_dialog)) { + root_ancestor->EnableWindowAndDescendants(false); + deepest_dialog->EnableWindowAndDescendants(true); + } else { + root_ancestor->EnableWindowAndDescendants(true); + } + } +} + +} // namespace flutter diff --git a/shell/platform/windows/flutter_host_window.h b/shell/platform/windows/flutter_host_window.h new file mode 100644 index 0000000000000..748cb6343ea45 --- /dev/null +++ b/shell/platform/windows/flutter_host_window.h @@ -0,0 +1,143 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_HOST_WINDOW_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_HOST_WINDOW_H_ + +#include + +#include +#include +#include + +#include "flutter/fml/macros.h" +#include "flutter/shell/platform/common/windowing.h" + +namespace flutter { + +class FlutterHostWindowController; +class FlutterWindowsViewController; + +// A Win32 window that hosts a |FlutterWindow| in its client area. +class FlutterHostWindow { + public: + // Creates a native Win32 window with a child view confined to its client + // area. |controller| manages the window. |title| is the window title. + // |preferred_client_size| is the preferred size of the client rectangle in + // logical coordinates. The window style is defined by |archetype|. For + // |WindowArchetype::satellite| and |WindowArchetype::popup|, both |owner| + // and |positioner| must be provided, with |positioner| used only for these + // archetypes. For |WindowArchetype::dialog|, a modal dialog is created if + // |owner| is provided; otherwise, it is modeless. For + // |WindowArchetype::regular|, |positioner| and |owner| must be std::nullopt. + // On success, a valid window handle can be retrieved via + // |FlutterHostWindow::GetWindowHandle|. + FlutterHostWindow(FlutterHostWindowController* controller, + std::wstring const& title, + WindowSize const& preferred_client_size, + WindowArchetype archetype, + std::optional owner, + std::optional positioner); + virtual ~FlutterHostWindow(); + + // Returns the instance pointer for |hwnd| or nulllptr if invalid. + static FlutterHostWindow* GetThisFromHandle(HWND hwnd); + + // Returns the window archetype. + WindowArchetype GetArchetype() const; + + // Returns the owned windows. + std::set const& GetOwnedWindows() const; + + // Returns the hosted Flutter view's ID or std::nullopt if not created. + std::optional GetFlutterViewId() const; + + // Returns the owner window, or nullptr if this is a top-level window. + FlutterHostWindow* GetOwnerWindow() const; + + // Returns the backing window handle, or nullptr if the native window is not + // created or has already been destroyed. + HWND GetWindowHandle() const; + + // Sets whether closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Returns whether closing this window will quit the application. + bool GetQuitOnClose() const; + + private: + friend FlutterHostWindowController; + + // Set the focus to the child view window of |window|. + static void FocusViewOf(FlutterHostWindow* window); + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. Delegates other messages to the controller. + static LRESULT WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); + + // Closes this window's popups and returns the count of closed popups. + std::size_t CloseOwnedPopups(); + + // Enables/disables this window and all its descendants. + void EnableWindowAndDescendants(bool enable); + + // Finds the first enabled descendant window. If the current window itself is + // enabled, returns the current window. + FlutterHostWindow* FindFirstEnabledDescendant() const; + + // Processes and routes salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + LRESULT HandleMessage(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Enforces modal behavior by enabling the deepest dialog in the subtree + // rooted at the top-level window, along with its descendants, while + // disabling all other windows in the subtree. This ensures that the dialog + // and its owned windows remain active and interactive. If no dialog is found, + // enables all windows in the subtree. + void UpdateModalState(); + + // Controller for this window. + FlutterHostWindowController* const window_controller_; + + // Controller for the view hosted by this window. + std::unique_ptr view_controller_; + + // The window archetype. + WindowArchetype archetype_ = WindowArchetype::regular; + + // Windows that have this window as their owner window. + std::set owned_windows_; + + // The number of popups in |owned_windows_| (for quick popup existence + // checks). + std::size_t num_owned_popups_ = 0; + + // Indicates if closing this window will quit the application. + bool quit_on_close_ = false; + + // Backing handle for this window. + HWND window_handle_ = nullptr; + + // Backing handle for the hosted view window. + HWND child_content_ = nullptr; + + // Offset between this window's position and its owner's. + POINT offset_from_owner_ = {0, 0}; + + // Whether the non-client area can be redrawn as inactive. Temporarily + // disabled during owned popup destruction to prevent flickering. + bool enable_redraw_non_client_as_inactive_ = true; + + FML_DISALLOW_COPY_AND_ASSIGN(FlutterHostWindow); +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_HOST_WINDOW_H_ diff --git a/shell/platform/windows/flutter_host_window_controller.cc b/shell/platform/windows/flutter_host_window_controller.cc new file mode 100644 index 0000000000000..789983818fccf --- /dev/null +++ b/shell/platform/windows/flutter_host_window_controller.cc @@ -0,0 +1,341 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/windows/flutter_host_window_controller.h" + +#include + +#include "flutter/shell/platform/windows/flutter_windows_engine.h" + +namespace flutter { + +namespace { + +// Names of the messages sent by the controller in response to window events. +constexpr char kOnWindowChangedMethod[] = "onWindowChanged"; +constexpr char kOnWindowCreatedMethod[] = "onWindowCreated"; +constexpr char kOnWindowDestroyedMethod[] = "onWindowDestroyed"; + +// Keys used in the onWindow* messages sent through the channel. +constexpr char kIsMovingKey[] = "isMoving"; +constexpr char kParentViewIdKey[] = "parentViewId"; +constexpr char kRelativePositionKey[] = "relativePosition"; +constexpr char kSizeKey[] = "size"; +constexpr char kViewIdKey[] = "viewId"; + +} // namespace + +FlutterHostWindowController::FlutterHostWindowController( + FlutterWindowsEngine* engine) + : engine_(engine) {} + +FlutterHostWindowController::~FlutterHostWindowController() { + DestroyAllWindows(); +} + +std::optional FlutterHostWindowController::CreateHostWindow( + std::wstring const& title, + WindowSize const& preferred_size, + WindowArchetype archetype, + std::optional positioner, + std::optional parent_view_id) { + std::optional const owner_hwnd = + parent_view_id.has_value() && + windows_.find(parent_view_id.value()) != windows_.end() + ? std::optional{windows_[parent_view_id.value()] + ->GetWindowHandle()} + : std::nullopt; + + auto window = std::make_unique( + this, title, preferred_size, archetype, owner_hwnd, positioner); + if (!window->GetWindowHandle()) { + return std::nullopt; + } + + // Assume first window is the main window. + if (windows_.empty()) { + window->SetQuitOnClose(true); + } + + FlutterViewId const view_id = window->GetFlutterViewId().value(); + windows_[view_id] = std::move(window); + + SendOnWindowCreated(view_id, parent_view_id); + + WindowMetadata result = {.view_id = view_id, + .archetype = archetype, + .size = GetWindowSize(view_id), + .parent_id = parent_view_id}; + + return result; +} + +bool FlutterHostWindowController::DestroyHostWindow(FlutterViewId view_id) { + if (auto it = windows_.find(view_id); it != windows_.end()) { + FlutterHostWindow* const window = it->second.get(); + HWND const window_handle = window->GetWindowHandle(); + + if (window->GetArchetype() == WindowArchetype::dialog && + GetWindow(window_handle, GW_OWNER)) { + // Temporarily disable satellite hiding. This prevents satellites from + // flickering because of briefly hiding and showing between the + // destruction of a modal dialog and the transfer of focus to the owner + // window. + disable_satellite_hiding_ = window_handle; + } + + // |window| will be removed from |windows_| when WM_NCDESTROY is handled. + PostMessage(window->GetWindowHandle(), WM_CLOSE, 0, 0); + + return true; + } + return false; +} + +FlutterHostWindow* FlutterHostWindowController::GetHostWindow( + FlutterViewId view_id) const { + if (auto it = windows_.find(view_id); it != windows_.end()) { + return it->second.get(); + } + return nullptr; +} + +LRESULT FlutterHostWindowController::HandleMessage(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) { + switch (message) { + case WM_NCDESTROY: { + auto const it = std::find_if( + windows_.begin(), windows_.end(), [hwnd](auto const& window) { + return window.second->GetWindowHandle() == hwnd; + }); + if (it != windows_.end()) { + FlutterViewId const view_id = it->first; + bool const quit_on_close = it->second->GetQuitOnClose(); + + windows_.erase(it); + + SendOnWindowDestroyed(view_id); + + if (disable_satellite_hiding_ == hwnd) { + // Re-enable satellite hiding by clearing the window handle now that + // the window is fully destroyed. + disable_satellite_hiding_ = nullptr; + } + + if (quit_on_close) { + DestroyAllWindows(); + } + } + } + return 0; + case WM_ACTIVATE: + if (wparam != WA_INACTIVE) { + if (FlutterHostWindow* const window = + FlutterHostWindow::GetThisFromHandle(hwnd)) { + if (window->GetArchetype() != WindowArchetype::popup) { + // If a non-popup window is activated, close popups for all windows. + auto it = windows_.begin(); + while (it != windows_.end()) { + std::size_t const num_popups_closed = + it->second->CloseOwnedPopups(); + if (num_popups_closed > 0) { + it = windows_.begin(); + } else { + ++it; + } + } + } else { + // If a popup window is activated, close its owned popups. + window->CloseOwnedPopups(); + } + } + ShowWindowAndAncestorsSatellites(hwnd); + } + break; + case WM_ACTIVATEAPP: + if (wparam == FALSE) { + if (FlutterHostWindow* const window = + FlutterHostWindow::GetThisFromHandle(hwnd)) { + // Close owned popups and hide satellites from all windows if a window + // belonging to a different application is being activated. + window->CloseOwnedPopups(); + HideWindowsSatellites(nullptr); + } + } + break; + case WM_SIZE: { + auto const it = std::find_if( + windows_.begin(), windows_.end(), [hwnd](auto const& window) { + return window.second->GetWindowHandle() == hwnd; + }); + if (it != windows_.end()) { + FlutterViewId const view_id = it->first; + SendOnWindowChanged(view_id); + } + } break; + default: + break; + } + + if (FlutterHostWindow* const window = + FlutterHostWindow::GetThisFromHandle(hwnd)) { + return window->HandleMessage(hwnd, message, wparam, lparam); + } + return DefWindowProc(hwnd, message, wparam, lparam); +} + +void FlutterHostWindowController::SetMethodChannel( + std::shared_ptr> channel) { + channel_ = std::move(channel); +} + +FlutterWindowsEngine* FlutterHostWindowController::engine() const { + return engine_; +} + +void FlutterHostWindowController::DestroyAllWindows() { + if (!windows_.empty()) { + // Destroy windows in reverse order of creation. + for (auto it = std::prev(windows_.end()); + it != std::prev(windows_.begin());) { + auto current = it--; + auto const& [view_id, window] = *current; + if (window->GetWindowHandle()) { + DestroyHostWindow(view_id); + } + } + } +} + +WindowSize FlutterHostWindowController::GetWindowSize( + FlutterViewId view_id) const { + HWND const hwnd = windows_.at(view_id)->GetWindowHandle(); + RECT frame_rect; + DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rect, + sizeof(frame_rect)); + + // Convert to logical coordinates. + auto const dpr = FlutterDesktopGetDpiForHWND(hwnd) / + static_cast(USER_DEFAULT_SCREEN_DPI); + frame_rect.left = static_cast(frame_rect.left / dpr); + frame_rect.top = static_cast(frame_rect.top / dpr); + frame_rect.right = static_cast(frame_rect.right / dpr); + frame_rect.bottom = static_cast(frame_rect.bottom / dpr); + + auto const width = frame_rect.right - frame_rect.left; + auto const height = frame_rect.bottom - frame_rect.top; + return {static_cast(width), static_cast(height)}; +} + +void FlutterHostWindowController::HideWindowsSatellites(HWND opt_out_hwnd) { + if (disable_satellite_hiding_) { + return; + } + + // Helper function to check whether |hwnd| is a descendant of |ancestor|. + auto const is_descendant_of = [](HWND hwnd, HWND ancestor) -> bool { + HWND current = ancestor; + while (current) { + current = GetWindow(current, GW_OWNER); + if (current == hwnd) { + return true; + } + } + return false; + }; + + // Helper function to check whether |window| owns a dialog. + auto const has_dialog = [](FlutterHostWindow* window) -> bool { + for (auto* const owned : window->GetOwnedWindows()) { + if (owned->GetArchetype() == WindowArchetype::dialog) { + return true; + } + } + return false; + }; + + for (auto const& [_, window] : windows_) { + if (window->GetWindowHandle() == opt_out_hwnd || + is_descendant_of(window->GetWindowHandle(), opt_out_hwnd)) { + continue; + } + + for (FlutterHostWindow* const owned : window->GetOwnedWindows()) { + if (owned->GetArchetype() != WindowArchetype::satellite) { + continue; + } + if (!has_dialog(owned)) { + ShowWindow(owned->GetWindowHandle(), SW_HIDE); + } + } + } +} + +void FlutterHostWindowController::SendOnWindowChanged( + FlutterViewId view_id) const { + if (channel_) { + WindowSize const size = GetWindowSize(view_id); + channel_->InvokeMethod( + kOnWindowChangedMethod, + std::make_unique(EncodableMap{ + {EncodableValue(kViewIdKey), EncodableValue(view_id)}, + {EncodableValue(kSizeKey), + EncodableValue(EncodableList{EncodableValue(size.width), + EncodableValue(size.height)})}, + {EncodableValue(kRelativePositionKey), EncodableValue()}, + {EncodableValue(kIsMovingKey), EncodableValue()}})); + } +} + +void FlutterHostWindowController::SendOnWindowCreated( + FlutterViewId view_id, + std::optional parent_view_id) const { + if (channel_) { + channel_->InvokeMethod( + kOnWindowCreatedMethod, + std::make_unique(EncodableMap{ + {EncodableValue(kViewIdKey), EncodableValue(view_id)}, + {EncodableValue(kParentViewIdKey), + parent_view_id ? EncodableValue(parent_view_id.value()) + : EncodableValue()}})); + } +} + +void FlutterHostWindowController::SendOnWindowDestroyed( + FlutterViewId view_id) const { + if (channel_) { + channel_->InvokeMethod( + kOnWindowDestroyedMethod, + std::make_unique(EncodableMap{ + {EncodableValue(kViewIdKey), EncodableValue(view_id)}, + })); + } +} + +void FlutterHostWindowController::ShowWindowAndAncestorsSatellites(HWND hwnd) { + HWND current = hwnd; + while (current) { + for (FlutterHostWindow* const owned : + FlutterHostWindow::GetThisFromHandle(current)->GetOwnedWindows()) { + if (owned->GetArchetype() == WindowArchetype::satellite) { + ShowWindow(owned->GetWindowHandle(), SW_SHOWNOACTIVATE); + } + } + current = GetWindow(current, GW_OWNER); + } + + // Hide satellites of all other top-level windows. + if (!disable_satellite_hiding_) { + if (FlutterHostWindow* const window = + FlutterHostWindow::GetThisFromHandle(hwnd)) { + if (window->GetArchetype() != WindowArchetype::satellite) { + HideWindowsSatellites(hwnd); + } + } + } +} + +} // namespace flutter diff --git a/shell/platform/windows/flutter_host_window_controller.h b/shell/platform/windows/flutter_host_window_controller.h new file mode 100644 index 0000000000000..94b87e1547eb7 --- /dev/null +++ b/shell/platform/windows/flutter_host_window_controller.h @@ -0,0 +1,128 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_HOST_WINDOW_CONTROLLER_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_HOST_WINDOW_CONTROLLER_H_ + +#include + +#include "flutter/fml/macros.h" +#include "flutter/shell/platform/common/client_wrapper/include/flutter/method_channel.h" +#include "flutter/shell/platform/windows/flutter_host_window.h" + +namespace flutter { + +class FlutterWindowsEngine; + +// A controller class for managing |FlutterHostWindow| instances. +// A unique instance of this class is owned by |FlutterWindowsEngine| and used +// in |WindowingHandler| to handle methods and messages enabling multi-window +// support. +class FlutterHostWindowController { + public: + explicit FlutterHostWindowController(FlutterWindowsEngine* engine); + virtual ~FlutterHostWindowController(); + + // Creates a |FlutterHostWindow|, i.e., a native Win32 window with a + // |FlutterWindow| parented to it. The child |FlutterWindow| implements a + // Flutter view that is displayed in the client area of the + // |FlutterHostWindow|. + // + // |title| is the window title string. |preferred_size| is the preferred size + // of the client rectangle, i.e., the size expected for the child view, in + // logical coordinates. The actual size may differ. The window style is + // determined by |archetype|. For |WindowArchetype::satellite| and + // |WindowArchetype::popup|, both |parent_view_id| and |positioner| must be + // provided; |positioner| is used only for these archetypes. For + // |WindowArchetype::dialog|, a modal dialog is created if |parent_view_id| is + // specified; otherwise, the dialog is modeless. For + // |WindowArchetype::regular|, |positioner| and |parent_view_id| should be + // std::nullopt. When |parent_view_id| is specified, the |FlutterHostWindow| + // that hosts the view with ID |parent_view_id| will become the owner window + // of the |FlutterHostWindow| created by this function. + // + // Returns a |WindowMetadata| with the metadata of the window just created, or + // std::nullopt if the window could not be created. + virtual std::optional CreateHostWindow( + std::wstring const& title, + WindowSize const& preferred_size, + WindowArchetype archetype, + std::optional positioner, + std::optional parent_view_id); + + // Destroys the window that hosts the view with ID |view_id|. + // + // Returns false if the controller does not have a window hosting a view with + // ID |view_id|. + virtual bool DestroyHostWindow(FlutterViewId view_id); + + // Gets the window hosting the view with ID |view_id|. + // + // Returns nullptr if the controller does not have a window hosting a view + // with ID |view_id|. + FlutterHostWindow* GetHostWindow(FlutterViewId view_id) const; + + // Message handler called by |FlutterHostWindow::WndProc| to process window + // messages before delegating them to the host window. This allows the + // controller to process messages that affect the state of other host windows. + LRESULT HandleMessage(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); + + // Sets the method channel through which the controller will send the window + // events "onWindowCreated", "onWindowDestroyed", and "onWindowChanged". + void SetMethodChannel(std::shared_ptr> channel); + + // Gets the engine that owns this controller. + FlutterWindowsEngine* engine() const; + + private: + // Destroys all windows managed by this controller. + void DestroyAllWindows(); + + // Gets the size of the window hosting the view with ID |view_id|. This is the + // size the host window frame, in logical coordinates, and does not include + // the dimensions of the drop-shadow area. + WindowSize GetWindowSize(FlutterViewId view_id) const; + + // Hides all satellite windows managed by this controller, except those that + // are descendants of |opt_out_hwnd| or own a dialog. If |opt_out_hwnd| is + // nullptr (default), only satellites that own a dialog are excluded from + // being hidden. + void HideWindowsSatellites(HWND opt_out_hwnd = nullptr); + + // Sends the "onWindowChanged" message to the Flutter engine. + void SendOnWindowChanged(FlutterViewId view_id) const; + + // Sends the "onWindowCreated" message to the Flutter engine. + void SendOnWindowCreated(FlutterViewId view_id, + std::optional parent_view_id) const; + + // Sends the "onWindowDestroyed" message to the Flutter engine. + void SendOnWindowDestroyed(FlutterViewId view_id) const; + + // Shows the satellite windows of |hwnd| and of its ancestors. + void ShowWindowAndAncestorsSatellites(HWND hwnd); + + // The Flutter engine that owns this controller. + FlutterWindowsEngine* const engine_; + + // The windowing channel through which the controller sends messages. + std::shared_ptr> channel_; + + // The host windows managed by this controller. + std::map> windows_; + + // Controls whether satellites can be hidden when there is no active window + // in the window subtree starting from the satellite's top-level window. If + // set to nullptr, satellite hiding is enabled. If set to a non-null value, + // satellite hiding remains disabled until the window represented by this + // handle is destroyed. After the window is destroyed, this is reset to + // nullptr. + HWND disable_satellite_hiding_ = nullptr; + + FML_DISALLOW_COPY_AND_ASSIGN(FlutterHostWindowController); +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_HOST_WINDOW_CONTROLLER_H_ diff --git a/shell/platform/windows/flutter_host_window_controller_unittests.cc b/shell/platform/windows/flutter_host_window_controller_unittests.cc new file mode 100644 index 0000000000000..fad1a560f7092 --- /dev/null +++ b/shell/platform/windows/flutter_host_window_controller_unittests.cc @@ -0,0 +1,539 @@ +// 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 { + +constexpr char kChannelName[] = "flutter/windowing"; +constexpr char kOnWindowCreatedMethod[] = "onWindowCreated"; +constexpr char kOnWindowDestroyedMethod[] = "onWindowDestroyed"; +constexpr char kViewIdKey[] = "viewId"; +constexpr char kParentViewIdKey[] = "parentViewId"; + +// Process the next Win32 message if there is one. This can be used to +// pump the Windows platform thread task runner. +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, CreatePopup) { + // Create a top-level window first. + std::optional const parent_result = + host_window_controller()->CreateHostWindow(L"parent", {800, 600}, + WindowArchetype::regular, + std::nullopt, std::nullopt); + ASSERT_NE(parent_result, std::nullopt); + + bool called_onWindowCreated = false; + + // Test messenger with a handler for onWindowCreated. + TestBinaryMessenger messenger([&](const std::string& channel, + const uint8_t* message, size_t size, + BinaryReply reply) { + // Ensure the message is sent on the windowing channel. + ASSERT_EQ(channel, kChannelName); + + // Ensure the decoded method call is valid. + auto const method = StandardMethodCodec::GetInstance().DecodeMethodCall( + std::vector(message, message + size)); + ASSERT_NE(method, nullptr); + + // Handle the onWindowCreated method. + if (method->method_name() == kOnWindowCreatedMethod) { + called_onWindowCreated = true; + + // Validate the method arguments. + auto const& args = *method->arguments(); + ASSERT_TRUE(std::holds_alternative(args)); + auto const& args_map = std::get(args); + + // Ensure the viewId is present and valid. + auto const& it_viewId = args_map.find(EncodableValue(kViewIdKey)); + ASSERT_NE(it_viewId, args_map.end()); + auto const* value_viewId = std::get_if(&it_viewId->second); + ASSERT_NE(value_viewId, nullptr); + EXPECT_GE(*value_viewId, 0); + EXPECT_NE(engine()->view(*value_viewId), nullptr); + + // Ensure the parentViewId is present and valid. + auto const& it_parentViewId = + args_map.find(EncodableValue(kParentViewIdKey)); + ASSERT_NE(it_parentViewId, args_map.end()); + auto const* value_parentViewId = + std::get_if(&it_parentViewId->second); + EXPECT_EQ(*value_parentViewId, parent_result->view_id); + } + }); + + // Define parameters for the popup to be created. + WindowSize const size = {200, 200}; + wchar_t const* const title = L"popup"; + WindowArchetype const archetype = WindowArchetype::popup; + WindowPositioner const positioner = WindowPositioner{ + .anchor_rect = std::optional( + {.top_left = {0, 0}, .size = {size.width, size.height}}), + .parent_anchor = WindowPositioner::Anchor::center, + .child_anchor = WindowPositioner::Anchor::center, + .offset = {0, 0}, + .constraint_adjustment = WindowPositioner::ConstraintAdjustment::none}; + + // Create the windowing handler with the test messenger. + WindowingHandler windowing_handler(&messenger, host_window_controller()); + + // Create popup parented to top-level window. + std::optional const result = + host_window_controller()->CreateHostWindow( + title, size, archetype, positioner, parent_result->view_id); + + // Verify the onWindowCreated callback was invoked. + EXPECT_TRUE(called_onWindowCreated); + + // Validate the returned metadata. + ASSERT_TRUE(result.has_value()); + EXPECT_NE(engine()->view(result->view_id), nullptr); + EXPECT_EQ(result->archetype, archetype); + EXPECT_GE(result->size.width, size.width); + EXPECT_GE(result->size.height, size.height); + EXPECT_EQ(result->parent_id.value(), parent_result->view_id); + + // Verify the popup exists and the view has the expected dimensions. + FlutterHostWindow* const window = + host_window_controller()->GetHostWindow(result->view_id); + ASSERT_NE(window, nullptr); + RECT client_rect; + GetClientRect(window->GetWindowHandle(), &client_rect); + EXPECT_EQ(client_rect.right - client_rect.left, size.width); + EXPECT_EQ(client_rect.bottom - client_rect.top, size.height); +} + +TEST_F(FlutterHostWindowControllerTest, CreateModalDialog) { + // Create a top-level window first. + std::optional const parent_result = + host_window_controller()->CreateHostWindow(L"parent", {800, 600}, + WindowArchetype::regular, + std::nullopt, std::nullopt); + ASSERT_NE(parent_result, std::nullopt); + + bool called_onWindowCreated = false; + + // Test messenger with a handler for onWindowCreated. + TestBinaryMessenger messenger([&](const std::string& channel, + const uint8_t* message, size_t size, + BinaryReply reply) { + // Ensure the message is sent on the windowing channel. + ASSERT_EQ(channel, kChannelName); + + // Ensure the decoded method call is valid. + auto const method = StandardMethodCodec::GetInstance().DecodeMethodCall( + std::vector(message, message + size)); + ASSERT_NE(method, nullptr); + + // Handle the onWindowCreated method. + if (method->method_name() == kOnWindowCreatedMethod) { + called_onWindowCreated = true; + + // Validate the method arguments. + auto const& args = *method->arguments(); + ASSERT_TRUE(std::holds_alternative(args)); + auto const& args_map = std::get(args); + + // Ensure the viewId is present and valid. + auto const& it_viewId = args_map.find(EncodableValue(kViewIdKey)); + ASSERT_NE(it_viewId, args_map.end()); + auto const* value_viewId = std::get_if(&it_viewId->second); + ASSERT_NE(value_viewId, nullptr); + EXPECT_GE(*value_viewId, 0); + EXPECT_NE(engine()->view(*value_viewId), nullptr); + + // Ensure the parentViewId is present and valid. + auto const& it_parentViewId = + args_map.find(EncodableValue(kParentViewIdKey)); + ASSERT_NE(it_parentViewId, args_map.end()); + auto const* value_parentViewId = + std::get_if(&it_parentViewId->second); + EXPECT_EQ(*value_parentViewId, parent_result->view_id); + } + }); + + // Define parameters for the popup to be created. + WindowSize const size = {400, 300}; + wchar_t const* const title = L"modal_dialog"; + WindowArchetype const archetype = WindowArchetype::dialog; + + // Create the windowing handler with the test messenger. + WindowingHandler windowing_handler(&messenger, host_window_controller()); + + // Create dialog parented to top-level window. + std::optional const result = + host_window_controller()->CreateHostWindow( + title, size, archetype, std::nullopt, parent_result->view_id); + + // Verify the onWindowCreated callback was invoked. + EXPECT_TRUE(called_onWindowCreated); + + // Validate the returned metadata. + ASSERT_TRUE(result.has_value()); + EXPECT_NE(engine()->view(result->view_id), nullptr); + EXPECT_EQ(result->archetype, archetype); + EXPECT_GE(result->size.width, size.width); + EXPECT_GE(result->size.height, size.height); + EXPECT_EQ(result->parent_id.value(), parent_result->view_id); + + // Verify the dialog exists and the view has the expected dimensions. + FlutterHostWindow* const window = + host_window_controller()->GetHostWindow(result->view_id); + ASSERT_NE(window, nullptr); + RECT client_rect; + GetClientRect(window->GetWindowHandle(), &client_rect); + EXPECT_EQ(client_rect.right - client_rect.left, size.width); + EXPECT_EQ(client_rect.bottom - client_rect.top, size.height); +} + +TEST_F(FlutterHostWindowControllerTest, CreateModelessDialog) { + bool called_onWindowCreated = false; + + // Test messenger with a handler for onWindowCreated. + TestBinaryMessenger messenger([&](const std::string& channel, + const uint8_t* message, size_t size, + BinaryReply reply) { + // Ensure the message is sent on the windowing channel. + ASSERT_EQ(channel, kChannelName); + + // Ensure the decoded method call is valid. + auto const method = StandardMethodCodec::GetInstance().DecodeMethodCall( + std::vector(message, message + size)); + ASSERT_NE(method, nullptr); + + // Handle the onWindowCreated method. + if (method->method_name() == kOnWindowCreatedMethod) { + called_onWindowCreated = true; + + // Validate the method arguments. + auto const& args = *method->arguments(); + ASSERT_TRUE(std::holds_alternative(args)); + auto const& args_map = std::get(args); + + // Ensure the viewId is present and valid. + auto const& it_viewId = args_map.find(EncodableValue(kViewIdKey)); + ASSERT_NE(it_viewId, args_map.end()); + auto const* value_viewId = std::get_if(&it_viewId->second); + ASSERT_NE(value_viewId, nullptr); + EXPECT_GE(*value_viewId, 0); + EXPECT_NE(engine()->view(*value_viewId), nullptr); + + // Ensure the parentViewId is a std::monostate (indicating no parent). + auto const& it_parentViewId = + args_map.find(EncodableValue(kParentViewIdKey)); + ASSERT_NE(it_parentViewId, args_map.end()); + auto const* value_parentViewId = + std::get_if(&it_parentViewId->second); + EXPECT_NE(value_parentViewId, nullptr); + } + }); + + // Define parameters for the popup to be created. + WindowSize const size = {400, 300}; + wchar_t const* const title = L"modeless_dialog"; + WindowArchetype const archetype = WindowArchetype::dialog; + + // Create the windowing handler with the test messenger. + WindowingHandler windowing_handler(&messenger, host_window_controller()); + + // Create dialog without a parent. + std::optional const result = + host_window_controller()->CreateHostWindow(title, size, archetype, + std::nullopt, std::nullopt); + + // Verify the onWindowCreated callback was invoked. + EXPECT_TRUE(called_onWindowCreated); + + // Validate the returned metadata. + ASSERT_TRUE(result.has_value()); + EXPECT_NE(engine()->view(result->view_id), nullptr); + EXPECT_EQ(result->archetype, archetype); + EXPECT_GE(result->size.width, size.width); + EXPECT_GE(result->size.height, size.height); + EXPECT_FALSE(result->parent_id.has_value()); + + // Verify the dialog exists and the view has the expected dimensions. + FlutterHostWindow* const window = + host_window_controller()->GetHostWindow(result->view_id); + ASSERT_NE(window, nullptr); + RECT client_rect; + GetClientRect(window->GetWindowHandle(), &client_rect); + EXPECT_EQ(client_rect.right - client_rect.left, size.width); + EXPECT_EQ(client_rect.bottom - client_rect.top, size.height); +} + +TEST_F(FlutterHostWindowControllerTest, CreateSatellite) { + // Create a top-level window first. + std::optional const parent_result = + host_window_controller()->CreateHostWindow(L"parent", {800, 600}, + WindowArchetype::regular, + std::nullopt, std::nullopt); + ASSERT_NE(parent_result, std::nullopt); + + bool called_onWindowCreated = false; + + // Test messenger with a handler for onWindowCreated. + TestBinaryMessenger messenger([&](const std::string& channel, + const uint8_t* message, size_t size, + BinaryReply reply) { + // Ensure the message is sent on the windowing channel. + ASSERT_EQ(channel, kChannelName); + + // Ensure the decoded method call is valid. + auto const method = StandardMethodCodec::GetInstance().DecodeMethodCall( + std::vector(message, message + size)); + ASSERT_NE(method, nullptr); + + // Handle the onWindowCreated method. + if (method->method_name() == kOnWindowCreatedMethod) { + called_onWindowCreated = true; + + // Validate the method arguments. + auto const& args = *method->arguments(); + ASSERT_TRUE(std::holds_alternative(args)); + auto const& args_map = std::get(args); + + // Ensure the viewId is present and valid. + auto const& it_viewId = args_map.find(EncodableValue(kViewIdKey)); + ASSERT_NE(it_viewId, args_map.end()); + auto const* value_viewId = std::get_if(&it_viewId->second); + ASSERT_NE(value_viewId, nullptr); + EXPECT_GE(*value_viewId, 0); + EXPECT_NE(engine()->view(*value_viewId), nullptr); + + // Ensure the parentViewId is present and valid. + auto const& it_parentViewId = + args_map.find(EncodableValue(kParentViewIdKey)); + ASSERT_NE(it_parentViewId, args_map.end()); + auto const* value_parentViewId = + std::get_if(&it_parentViewId->second); + EXPECT_EQ(*value_parentViewId, parent_result->view_id); + } + }); + + // Define parameters for the satellite to be created. + WindowSize const size = {200, 300}; + wchar_t const* const title = L"satellite"; + WindowArchetype const archetype = WindowArchetype::satellite; + WindowPositioner const positioner = WindowPositioner{ + .anchor_rect = std::optional( + {.top_left = {0, 0}, .size = {size.width, size.height}}), + .parent_anchor = WindowPositioner::Anchor::center, + .child_anchor = WindowPositioner::Anchor::center, + .offset = {0, 0}, + .constraint_adjustment = WindowPositioner::ConstraintAdjustment::none}; + + // Create the windowing handler with the test messenger. + WindowingHandler windowing_handler(&messenger, host_window_controller()); + + // Create popup parented to top-level window. + std::optional const result = + host_window_controller()->CreateHostWindow( + title, size, archetype, positioner, parent_result->view_id); + + // Verify the onWindowCreated callback was invoked. + EXPECT_TRUE(called_onWindowCreated); + + // Validate the returned metadata. + ASSERT_TRUE(result.has_value()); + EXPECT_NE(engine()->view(result->view_id), nullptr); + EXPECT_EQ(result->archetype, archetype); + EXPECT_GE(result->size.width, size.width); + EXPECT_GE(result->size.height, size.height); + EXPECT_EQ(result->parent_id.value(), parent_result->view_id); + + // Verify the satellite exists and the view has the expected dimensions. + FlutterHostWindow* const window = + host_window_controller()->GetHostWindow(result->view_id); + ASSERT_NE(window, nullptr); + RECT client_rect; + GetClientRect(window->GetWindowHandle(), &client_rect); + EXPECT_EQ(client_rect.right - client_rect.left, size.width); + EXPECT_EQ(client_rect.bottom - client_rect.top, size.height); +} + +TEST_F(FlutterHostWindowControllerTest, DestroyWindow) { + bool done = false; + + // 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/windowing_handler.cc b/shell/platform/windows/windowing_handler.cc new file mode 100644 index 0000000000000..25221f406fc6f --- /dev/null +++ b/shell/platform/windows/windowing_handler.cc @@ -0,0 +1,341 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/windows/windowing_handler.h" + +#include "flutter/fml/logging.h" +#include "flutter/shell/platform/common/client_wrapper/include/flutter/standard_method_codec.h" + +namespace { + +// Name of the windowing channel. +constexpr char kChannelName[] = "flutter/windowing"; + +// Methods for creating different types of windows. +constexpr char kCreateWindowMethod[] = "createWindow"; +constexpr char kCreateDialogMethod[] = "createDialog"; +constexpr char kCreateSatelliteMethod[] = "createSatellite"; +constexpr char kCreatePopupMethod[] = "createPopup"; + +// The method to destroy a window. +constexpr char kDestroyWindowMethod[] = "destroyWindow"; + +// Keys used in method calls. +constexpr char kAnchorRectKey[] = "anchorRect"; +constexpr char kArchetypeKey[] = "archetype"; +constexpr char kParentKey[] = "parent"; +constexpr char kParentViewIdKey[] = "parentViewId"; +constexpr char kPositionerChildAnchorKey[] = "positionerChildAnchor"; +constexpr char kPositionerConstraintAdjustmentKey[] = + "positionerConstraintAdjustment"; +constexpr char kPositionerOffsetKey[] = "positionerOffset"; +constexpr char kPositionerParentAnchorKey[] = "positionerParentAnchor"; +constexpr char kSizeKey[] = "size"; +constexpr char kViewIdKey[] = "viewId"; + +// Error codes used for responses. +constexpr char kInvalidValueError[] = "Invalid Value"; +constexpr char kUnavailableError[] = "Unavailable"; + +// Retrieves the value associated with |key| from |map|, ensuring it matches +// the expected type |T|. Returns the value if found and correctly typed, +// otherwise logs an error in |result| and returns std::nullopt. +template +std::optional GetSingleValueForKeyOrSendError( + std::string const& key, + flutter::EncodableMap const* map, + flutter::MethodResult<>& result) { + if (auto const it = map->find(flutter::EncodableValue(key)); + it != map->end()) { + if (auto const* const value = std::get_if(&it->second)) { + return *value; + } else { + result.Error(kInvalidValueError, "Value for '" + key + + "' key must be of type '" + + typeid(T).name() + "'."); + } + } else { + result.Error(kInvalidValueError, + "Map does not contain required '" + key + "' key."); + } + return std::nullopt; +} + +// Retrieves a list of values associated with |key| from |map|, ensuring the +// list has |Size| elements, all of type |T|. Returns the list if found and +// valid, otherwise logs an error in |result| and returns std::nullopt. +template +std::optional> GetListValuesForKeyOrSendError( + std::string const& key, + flutter::EncodableMap const* map, + flutter::MethodResult<>& result) { + if (auto const it = map->find(flutter::EncodableValue(key)); + it != map->end()) { + if (auto const* const array = + std::get_if>(&it->second)) { + if (array->size() != Size) { + result.Error(kInvalidValueError, "Array for '" + key + + "' key must have " + + std::to_string(Size) + " values."); + return std::nullopt; + } + std::vector decoded_values; + for (flutter::EncodableValue const& value : *array) { + if (std::holds_alternative(value)) { + decoded_values.push_back(std::get(value)); + } else { + result.Error(kInvalidValueError, + "Array for '" + key + + "' key must only have values of type '" + + typeid(T).name() + "'."); + return std::nullopt; + } + } + return decoded_values; + } else { + result.Error(kInvalidValueError, + "Value for '" + key + "' key must be an array."); + } + } else { + result.Error(kInvalidValueError, + "Map does not contain required '" + key + "' key."); + } + return std::nullopt; +} + +// Converts a |flutter::WindowArchetype| to its corresponding wide string +// representation. +std::wstring ArchetypeToWideString(flutter::WindowArchetype archetype) { + switch (archetype) { + case flutter::WindowArchetype::regular: + return L"regular"; + case flutter::WindowArchetype::dialog: + return L"dialog"; + case flutter::WindowArchetype::satellite: + return L"satellite"; + case flutter::WindowArchetype::popup: + return L"popup"; + } + FML_UNREACHABLE(); +} + +} // namespace + +namespace flutter { + +WindowingHandler::WindowingHandler(BinaryMessenger* messenger, + FlutterHostWindowController* controller) + : channel_(std::make_shared>( + messenger, + kChannelName, + &StandardMethodCodec::GetInstance())), + controller_(controller) { + channel_->SetMethodCallHandler( + [this](const MethodCall& call, + std::unique_ptr> result) { + HandleMethodCall(call, std::move(result)); + }); + controller_->SetMethodChannel(channel_); +} + +void WindowingHandler::HandleMethodCall( + const MethodCall& method_call, + std::unique_ptr> result) { + const std::string& method = method_call.method_name(); + + if (method == kCreateWindowMethod) { + HandleCreateWindow(WindowArchetype::regular, method_call, *result); + } else if (method == kCreateDialogMethod) { + HandleCreateWindow(WindowArchetype::dialog, method_call, *result); + } else if (method == kCreateSatelliteMethod) { + HandleCreateWindow(WindowArchetype::satellite, method_call, *result); + } else if (method == kCreatePopupMethod) { + HandleCreateWindow(WindowArchetype::popup, method_call, *result); + } else if (method == kDestroyWindowMethod) { + HandleDestroyWindow(method_call, *result); + } else { + result->NotImplemented(); + } +} + +void WindowingHandler::HandleCreateWindow(WindowArchetype archetype, + MethodCall<> const& call, + MethodResult<>& result) { + auto const* const arguments = call.arguments(); + auto const* const map = std::get_if(arguments); + if (!map) { + result.Error(kInvalidValueError, "Method call argument is not a map."); + return; + } + + std::wstring const title = ArchetypeToWideString(archetype); + + auto const size_list = + GetListValuesForKeyOrSendError(kSizeKey, map, result); + if (!size_list) { + return; + } + if (size_list->at(0) < 0 || size_list->at(1) < 0) { + result.Error(kInvalidValueError, + "Values for '" + std::string(kSizeKey) + "' key (" + + std::to_string(size_list->at(0)) + ", " + + std::to_string(size_list->at(1)) + + ") must be nonnegative."); + return; + } + + std::optional positioner; + std::optional anchor_rect; + + if (archetype == WindowArchetype::satellite || + archetype == WindowArchetype::popup) { + if (auto const anchor_rect_it = map->find(EncodableValue(kAnchorRectKey)); + anchor_rect_it != map->end()) { + if (!anchor_rect_it->second.IsNull()) { + auto const anchor_rect_list = + GetListValuesForKeyOrSendError(kAnchorRectKey, map, result); + if (!anchor_rect_list) { + return; + } + anchor_rect = + WindowRectangle{{anchor_rect_list->at(0), anchor_rect_list->at(1)}, + {anchor_rect_list->at(2), anchor_rect_list->at(3)}}; + } + } else { + result.Error(kInvalidValueError, "Map does not contain required '" + + std::string(kAnchorRectKey) + + "' key."); + return; + } + + auto const positioner_parent_anchor = GetSingleValueForKeyOrSendError( + kPositionerParentAnchorKey, map, result); + if (!positioner_parent_anchor) { + return; + } + auto const parent_anchor = + static_cast(positioner_parent_anchor.value()); + + 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 = parent_anchor, + .child_anchor = child_anchor, + .offset = {positioner_offset_list->at(0), + positioner_offset_list->at(1)}, + .constraint_adjustment = + static_cast( + positioner_constraint_adjustment.value())}; + } + + std::optional parent_view_id; + if (archetype == WindowArchetype::dialog || + archetype == WindowArchetype::satellite || + archetype == WindowArchetype::popup) { + if (auto const parent_it = map->find(EncodableValue(kParentKey)); + parent_it != map->end()) { + if (parent_it->second.IsNull()) { + if (archetype != WindowArchetype::dialog) { + result.Error( + kInvalidValueError, + "Value for '" + std::string(kParentKey) + "' must not be null."); + return; + } + } else { + if (auto const* const parent = std::get_if(&parent_it->second)) { + parent_view_id = *parent >= 0 ? std::optional(*parent) + : std::nullopt; + if (!parent_view_id.has_value() && + (archetype == WindowArchetype::satellite || + archetype == WindowArchetype::popup)) { + result.Error(kInvalidValueError, + "Value for '" + std::string(kParentKey) + "' (" + + std::to_string(parent_view_id.value()) + + ") must be nonnegative."); + return; + } + } else { + result.Error(kInvalidValueError, "Value for '" + + std::string(kParentKey) + + "' must be of type int."); + return; + } + } + } else { + result.Error(kInvalidValueError, "Map does not contain required '" + + std::string(kParentKey) + "' key."); + return; + } + } + + if (std::optional const data_opt = + controller_->CreateHostWindow( + title, {.width = size_list->at(0), .height = size_list->at(1)}, + archetype, positioner, parent_view_id)) { + WindowMetadata const& data = data_opt.value(); + result.Success(EncodableValue(EncodableMap{ + {EncodableValue(kViewIdKey), EncodableValue(data.view_id)}, + {EncodableValue(kArchetypeKey), + EncodableValue(static_cast(data.archetype))}, + {EncodableValue(kSizeKey), + EncodableValue(EncodableList{EncodableValue(data.size.width), + EncodableValue(data.size.height)})}, + {EncodableValue(kParentViewIdKey), + data.parent_id ? EncodableValue(data.parent_id.value()) + : EncodableValue()}})); + } else { + result.Error(kUnavailableError, "Can't create window."); + } +} + +void WindowingHandler::HandleDestroyWindow(MethodCall<> const& call, + MethodResult<>& result) { + auto const* const arguments = call.arguments(); + auto const* const map = std::get_if(arguments); + if (!map) { + result.Error(kInvalidValueError, "Method call argument is not a map."); + return; + } + + auto const view_id = + GetSingleValueForKeyOrSendError(kViewIdKey, map, result); + if (!view_id) { + return; + } + if (view_id.value() < 0) { + result.Error(kInvalidValueError, + "Value for '" + std::string(kViewIdKey) + "' (" + + std::to_string(view_id.value()) + ") cannot be negative."); + return; + } + + if (!controller_->DestroyHostWindow(view_id.value())) { + result.Error(kInvalidValueError, + "Can't find window with '" + std::string(kViewIdKey) + "' (" + + std::to_string(view_id.value()) + ")."); + return; + } + + result.Success(); +} + +} // namespace flutter diff --git a/shell/platform/windows/windowing_handler.h b/shell/platform/windows/windowing_handler.h new file mode 100644 index 0000000000000..c527826eda50b --- /dev/null +++ b/shell/platform/windows/windowing_handler.h @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOWING_HANDLER_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOWING_HANDLER_H_ + +#include "flutter/fml/macros.h" +#include "flutter/shell/platform/common/client_wrapper/include/flutter/method_channel.h" +#include "flutter/shell/platform/windows/flutter_host_window_controller.h" + +namespace flutter { + +// Handler for the windowing channel. +class WindowingHandler { + public: + explicit WindowingHandler(flutter::BinaryMessenger* messenger, + flutter::FlutterHostWindowController* controller); + + private: + // Handler for method calls received on |channel_|. Messages are + // redirected to either HandleCreateWindow or HandleDestroyWindow. + void HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result); + + // Handles the creation of windows. + void HandleCreateWindow(flutter::WindowArchetype archetype, + flutter::MethodCall<> const& call, + flutter::MethodResult<>& result); + // Handles the destruction of windows. + void HandleDestroyWindow(flutter::MethodCall<> const& call, + flutter::MethodResult<>& result); + + // The MethodChannel used for communication with the Flutter engine. + std::shared_ptr> channel_; + + // The controller of the host windows. + flutter::FlutterHostWindowController* controller_; + + FML_DISALLOW_COPY_AND_ASSIGN(WindowingHandler); +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOWING_HANDLER_H_ diff --git a/shell/platform/windows/windowing_handler_unittests.cc b/shell/platform/windows/windowing_handler_unittests.cc new file mode 100644 index 0000000000000..6f468dc08280a --- /dev/null +++ b/shell/platform/windows/windowing_handler_unittests.cc @@ -0,0 +1,343 @@ +// 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::Optional; +using ::testing::Return; +using ::testing::StrEq; + +constexpr char kChannelName[] = "flutter/windowing"; + +constexpr char kCreateWindowMethod[] = "createWindow"; +constexpr char kCreateDialogMethod[] = "createDialog"; +constexpr char kCreateSatelliteMethod[] = "createSatellite"; +constexpr char kCreatePopupMethod[] = "createPopup"; +constexpr char kDestroyWindowMethod[] = "destroyWindow"; + +constexpr char kAnchorRectKey[] = "anchorRect"; +constexpr char kParentKey[] = "parent"; +constexpr char kPositionerChildAnchorKey[] = "positionerChildAnchor"; +constexpr char kPositionerConstraintAdjustmentKey[] = + "positionerConstraintAdjustment"; +constexpr char kPositionerOffsetKey[] = "positionerOffset"; +constexpr char kPositionerParentAnchorKey[] = "positionerParentAnchor"; +constexpr char kSizeKey[] = "size"; +constexpr char kViewIdKey[] = "viewId"; + +void SimulateWindowingMessage(TestBinaryMessenger* messenger, + const std::string& method_name, + 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); +}; + +bool operator==(WindowPositioner const& lhs, WindowPositioner const& rhs) { + return lhs.anchor_rect == rhs.anchor_rect && + lhs.parent_anchor == rhs.parent_anchor && + lhs.child_anchor == rhs.child_anchor && lhs.offset == rhs.offset && + lhs.constraint_adjustment == rhs.constraint_adjustment; +} + +MATCHER_P(WindowPositionerEq, expected, "WindowPositioner matches expected") { + return arg.anchor_rect == expected.anchor_rect && + arg.parent_anchor == expected.parent_anchor && + arg.child_anchor == expected.child_anchor && + arg.offset == expected.offset && + arg.constraint_adjustment == expected.constraint_adjustment; +} +} // namespace + +class WindowingHandlerTest : public WindowsTest { + 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(kSizeKey), + 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, HandleCreatePopup) { + TestBinaryMessenger messenger; + WindowingHandler windowing_handler(&messenger, controller()); + + WindowSize const size = {200, 200}; + std::optional const parent_view_id = 0; + WindowPositioner const positioner = WindowPositioner{ + .anchor_rect = std::optional( + {.top_left = {0, 0}, .size = {size.width, size.height}}), + .parent_anchor = WindowPositioner::Anchor::center, + .child_anchor = WindowPositioner::Anchor::center, + .offset = {0, 0}, + .constraint_adjustment = WindowPositioner::ConstraintAdjustment::none}; + EncodableMap const arguments = { + {EncodableValue(kSizeKey), + EncodableValue(EncodableList{EncodableValue(size.width), + EncodableValue(size.height)})}, + {EncodableValue(kAnchorRectKey), + EncodableValue( + EncodableList{EncodableValue(positioner.anchor_rect->top_left.x), + EncodableValue(positioner.anchor_rect->top_left.y), + EncodableValue(positioner.anchor_rect->size.width), + EncodableValue(positioner.anchor_rect->size.height)})}, + {EncodableValue(kPositionerParentAnchorKey), + EncodableValue(static_cast(positioner.parent_anchor))}, + {EncodableValue(kPositionerChildAnchorKey), + EncodableValue(static_cast(positioner.child_anchor))}, + {EncodableValue(kPositionerOffsetKey), + EncodableValue(EncodableList{EncodableValue(positioner.offset.x), + EncodableValue(positioner.offset.y)})}, + {EncodableValue(kPositionerConstraintAdjustmentKey), + EncodableValue(static_cast(positioner.constraint_adjustment))}, + {EncodableValue(kParentKey), + EncodableValue(static_cast(parent_view_id.value()))}}; + + bool success = false; + MethodResultFunctions<> result_handler( + [&success](const EncodableValue* result) { success = true; }, nullptr, + nullptr); + + EXPECT_CALL(*controller(), + CreateHostWindow(StrEq(L"popup"), size, WindowArchetype::popup, + Optional(WindowPositionerEq(positioner)), + parent_view_id)) + .Times(1); + + SimulateWindowingMessage(&messenger, kCreatePopupMethod, + std::make_unique(arguments), + &result_handler); + + EXPECT_TRUE(success); +} + +TEST_F(WindowingHandlerTest, HandleCreateModalDialog) { + TestBinaryMessenger messenger; + WindowingHandler windowing_handler(&messenger, controller()); + + WindowSize const size = {400, 300}; + std::optional const parent_view_id = 0; + EncodableMap const arguments = { + {EncodableValue(kSizeKey), + EncodableValue(EncodableList{EncodableValue(size.width), + EncodableValue(size.height)})}, + {EncodableValue(kParentKey), + EncodableValue(static_cast(parent_view_id.value()))}}; + + bool success = false; + MethodResultFunctions<> result_handler( + [&success](const EncodableValue* result) { success = true; }, nullptr, + nullptr); + + EXPECT_CALL(*controller(), + CreateHostWindow(StrEq(L"dialog"), size, WindowArchetype::dialog, + Eq(std::nullopt), parent_view_id)) + .Times(1); + + SimulateWindowingMessage(&messenger, kCreateDialogMethod, + std::make_unique(arguments), + &result_handler); + + EXPECT_TRUE(success); +} + +TEST_F(WindowingHandlerTest, HandleCreateModelessDialog) { + TestBinaryMessenger messenger; + WindowingHandler windowing_handler(&messenger, controller()); + + WindowSize const size = {400, 300}; + EncodableMap const arguments = { + {EncodableValue(kSizeKey), + EncodableValue(EncodableList{EncodableValue(size.width), + EncodableValue(size.height)})}, + {EncodableValue(kParentKey), EncodableValue()}}; + + bool success = false; + MethodResultFunctions<> result_handler( + [&success](const EncodableValue* result) { success = true; }, nullptr, + nullptr); + + EXPECT_CALL(*controller(), + CreateHostWindow(StrEq(L"dialog"), size, WindowArchetype::dialog, + Eq(std::nullopt), Eq(std::nullopt))) + .Times(1); + + SimulateWindowingMessage(&messenger, kCreateDialogMethod, + std::make_unique(arguments), + &result_handler); + + EXPECT_TRUE(success); +} + +TEST_F(WindowingHandlerTest, HandleCreateSatellite) { + TestBinaryMessenger messenger; + WindowingHandler windowing_handler(&messenger, controller()); + + WindowSize const size = {200, 300}; + std::optional const parent_view_id = 0; + WindowPositioner const positioner = WindowPositioner{ + .anchor_rect = std::optional( + {.top_left = {0, 0}, .size = {size.width, size.height}}), + .parent_anchor = WindowPositioner::Anchor::center, + .child_anchor = WindowPositioner::Anchor::center, + .offset = {0, 0}, + .constraint_adjustment = WindowPositioner::ConstraintAdjustment::none}; + EncodableMap const arguments = { + {EncodableValue(kSizeKey), + EncodableValue(EncodableList{EncodableValue(size.width), + EncodableValue(size.height)})}, + {EncodableValue(kAnchorRectKey), + EncodableValue( + EncodableList{EncodableValue(positioner.anchor_rect->top_left.x), + EncodableValue(positioner.anchor_rect->top_left.y), + EncodableValue(positioner.anchor_rect->size.width), + EncodableValue(positioner.anchor_rect->size.height)})}, + {EncodableValue(kPositionerParentAnchorKey), + EncodableValue(static_cast(positioner.parent_anchor))}, + {EncodableValue(kPositionerChildAnchorKey), + EncodableValue(static_cast(positioner.child_anchor))}, + {EncodableValue(kPositionerOffsetKey), + EncodableValue(EncodableList{EncodableValue(positioner.offset.x), + EncodableValue(positioner.offset.y)})}, + {EncodableValue(kPositionerConstraintAdjustmentKey), + EncodableValue(static_cast(positioner.constraint_adjustment))}, + {EncodableValue(kParentKey), + EncodableValue(static_cast(parent_view_id.value()))}}; + + bool success = false; + MethodResultFunctions<> result_handler( + [&success](const EncodableValue* result) { success = true; }, nullptr, + nullptr); + + EXPECT_CALL(*controller(), + CreateHostWindow( + StrEq(L"satellite"), size, WindowArchetype::satellite, + Optional(WindowPositionerEq(positioner)), parent_view_id)) + .Times(1); + + SimulateWindowingMessage(&messenger, kCreateSatelliteMethod, + std::make_unique(arguments), + &result_handler); + + EXPECT_TRUE(success); +} + +TEST_F(WindowingHandlerTest, HandleDestroyWindow) { + TestBinaryMessenger messenger; + WindowingHandler windowing_handler(&messenger, controller()); + + EncodableMap const arguments = { + {EncodableValue(kViewIdKey), 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