diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index d5897249b3ae7..4a840836bb21a 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -325,7 +325,6 @@ ../../../flutter/shell/platform/common/client_wrapper/standard_method_codec_unittests.cc ../../../flutter/shell/platform/common/client_wrapper/testing ../../../flutter/shell/platform/common/client_wrapper/texture_registrar_unittests.cc -../../../flutter/shell/platform/common/client_wrapper/windowing_unittests.cc ../../../flutter/shell/platform/common/engine_switches_unittests.cc ../../../flutter/shell/platform/common/flutter_platform_node_delegate_unittests.cc ../../../flutter/shell/platform/common/geometry_unittests.cc @@ -336,6 +335,7 @@ ../../../flutter/shell/platform/common/text_editing_delta_unittests.cc ../../../flutter/shell/platform/common/text_input_model_unittests.cc ../../../flutter/shell/platform/common/text_range_unittests.cc +../../../flutter/shell/platform/common/windowing_unittests.cc ../../../flutter/shell/platform/darwin/Doxyfile ../../../flutter/shell/platform/darwin/common/availability_version_check_unittests.cc ../../../flutter/shell/platform/darwin/common/framework/Source/flutter_codecs_unittest.mm @@ -409,6 +409,7 @@ ../../../flutter/shell/platform/windows/direct_manipulation_unittests.cc ../../../flutter/shell/platform/windows/dpi_utils_unittests.cc ../../../flutter/shell/platform/windows/fixtures +../../../flutter/shell/platform/windows/flutter_host_window_controller_unittests.cc ../../../flutter/shell/platform/windows/flutter_project_bundle_unittests.cc ../../../flutter/shell/platform/windows/flutter_window_unittests.cc ../../../flutter/shell/platform/windows/flutter_windows_engine_unittests.cc @@ -427,6 +428,7 @@ ../../../flutter/shell/platform/windows/task_runner_unittests.cc ../../../flutter/shell/platform/windows/testing ../../../flutter/shell/platform/windows/text_input_plugin_unittest.cc +../../../flutter/shell/platform/windows/windowing_handler_unittests.cc ../../../flutter/shell/platform/windows/window_proc_delegate_manager_unittests.cc ../../../flutter/shell/platform/windows/window_unittests.cc ../../../flutter/shell/platform/windows/windows_lifecycle_manager_unittests.cc diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 6f989d5173dff..8fd68561b46f0 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -47425,7 +47425,6 @@ FILE: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/stan FILE: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/standard_message_codec.h FILE: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/standard_method_codec.h FILE: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/texture_registrar.h -FILE: ../../../flutter/shell/platform/common/client_wrapper/include/flutter/windowing.h FILE: ../../../flutter/shell/platform/common/client_wrapper/plugin_registrar.cc FILE: ../../../flutter/shell/platform/common/client_wrapper/standard_codec.cc FILE: ../../../flutter/shell/platform/common/client_wrapper/texture_registrar_impl.h @@ -47455,6 +47454,8 @@ FILE: ../../../flutter/shell/platform/common/text_editing_delta.h FILE: ../../../flutter/shell/platform/common/text_input_model.cc FILE: ../../../flutter/shell/platform/common/text_input_model.h FILE: ../../../flutter/shell/platform/common/text_range.h +FILE: ../../../flutter/shell/platform/common/windowing.cc +FILE: ../../../flutter/shell/platform/common/windowing.h FILE: ../../../flutter/shell/platform/darwin/common/availability_version_check.cc FILE: ../../../flutter/shell/platform/darwin/common/availability_version_check.h FILE: ../../../flutter/shell/platform/darwin/common/buffer_conversions.h @@ -48198,6 +48199,10 @@ FILE: ../../../flutter/shell/platform/windows/flutter_platform_node_delegate_win FILE: ../../../flutter/shell/platform/windows/flutter_platform_node_delegate_windows.h FILE: ../../../flutter/shell/platform/windows/flutter_project_bundle.cc FILE: ../../../flutter/shell/platform/windows/flutter_project_bundle.h +FILE: ../../../flutter/shell/platform/windows/flutter_host_window.cc +FILE: ../../../flutter/shell/platform/windows/flutter_host_window.h +FILE: ../../../flutter/shell/platform/windows/flutter_host_window_controller.cc +FILE: ../../../flutter/shell/platform/windows/flutter_host_window_controller.h FILE: ../../../flutter/shell/platform/windows/flutter_window.cc FILE: ../../../flutter/shell/platform/windows/flutter_window.h FILE: ../../../flutter/shell/platform/windows/flutter_windows.cc @@ -48247,6 +48252,8 @@ FILE: ../../../flutter/shell/platform/windows/window_binding_handler_delegate.h FILE: ../../../flutter/shell/platform/windows/window_proc_delegate_manager.cc FILE: ../../../flutter/shell/platform/windows/window_proc_delegate_manager.h FILE: ../../../flutter/shell/platform/windows/window_state.h +FILE: ../../../flutter/shell/platform/windows/windowing_handler.cc +FILE: ../../../flutter/shell/platform/windows/windowing_handler.h FILE: ../../../flutter/shell/platform/windows/windows_lifecycle_manager.cc FILE: ../../../flutter/shell/platform/windows/windows_lifecycle_manager.h FILE: ../../../flutter/shell/platform/windows/windows_proc_table.cc diff --git a/common/settings.h b/common/settings.h index 617ac202adb81..507a1ebe656cd 100644 --- a/common/settings.h +++ b/common/settings.h @@ -367,6 +367,10 @@ struct Settings { // If true, the UI thread is the platform thread on supported // platforms. bool merged_platform_ui_thread = true; + + // Enable support for multiple windows. Ignored if not supported on the + // platform. + bool enable_multi_window = false; }; } // namespace flutter diff --git a/shell/common/switches.cc b/shell/common/switches.cc index 9aa9b1528f0d6..cb56d13fa6c32 100644 --- a/shell/common/switches.cc +++ b/shell/common/switches.cc @@ -532,6 +532,17 @@ Settings SettingsFromCommandLine(const fml::CommandLine& command_line) { settings.merged_platform_ui_thread = !command_line.HasOption( FlagForSwitch(Switch::DisableMergedPlatformUIThread)); +#if FML_OS_WIN + // Process the EnableMultiWindow switch on Windows. + { + std::string enable_multi_window_value; + if (command_line.GetOptionValue(FlagForSwitch(Switch::EnableMultiWindow), + &enable_multi_window_value)) { + settings.enable_multi_window = "true" == enable_multi_window_value; + } + } +#endif // FML_OS_WIN + return settings; } diff --git a/shell/common/switches.h b/shell/common/switches.h index 1c1fb595b35d8..b80528ce06fb5 100644 --- a/shell/common/switches.h +++ b/shell/common/switches.h @@ -300,6 +300,10 @@ DEF_SWITCH(DisableMergedPlatformUIThread, DEF_SWITCH(DisableAndroidSurfaceControl, "disable-surface-control", "Disable the SurfaceControl backed swapchain even when supported.") +DEF_SWITCH(EnableMultiWindow, + "enable-multi-window", + "Enable support for multiple windows. Ignored if not supported on " + "the platform.") DEF_SWITCHES_END void PrintUsage(const std::string& executable_name); diff --git a/shell/platform/common/BUILD.gn b/shell/platform/common/BUILD.gn index 5ebfb2236d2c5..40e145ac0ca37 100644 --- a/shell/platform/common/BUILD.gn +++ b/shell/platform/common/BUILD.gn @@ -142,9 +142,13 @@ source_set("common_cpp_core") { public = [ "geometry.h", "path_utils.h", + "windowing.h", ] - sources = [ "path_utils.cc" ] + sources = [ + "path_utils.cc", + "windowing.cc", + ] public_configs = [ "//flutter:config" ] } @@ -157,7 +161,10 @@ if (enable_unittests) { executable("common_cpp_core_unittests") { testonly = true - sources = [ "path_utils_unittests.cc" ] + sources = [ + "path_utils_unittests.cc", + "windowing_unittests.cc", + ] deps = [ ":common_cpp_core", diff --git a/shell/platform/common/client_wrapper/BUILD.gn b/shell/platform/common/client_wrapper/BUILD.gn index 5fb070ccae6b5..91e7120b65734 100644 --- a/shell/platform/common/client_wrapper/BUILD.gn +++ b/shell/platform/common/client_wrapper/BUILD.gn @@ -51,7 +51,6 @@ executable("client_wrapper_unittests") { "testing/test_codec_extensions.cc", "testing/test_codec_extensions.h", "texture_registrar_unittests.cc", - "windowing_unittests.cc", ] deps = [ diff --git a/shell/platform/common/client_wrapper/core_wrapper_files.gni b/shell/platform/common/client_wrapper/core_wrapper_files.gni index f123787a024d0..c2ee524e0f117 100644 --- a/shell/platform/common/client_wrapper/core_wrapper_files.gni +++ b/shell/platform/common/client_wrapper/core_wrapper_files.gni @@ -25,7 +25,6 @@ core_cpp_client_wrapper_includes = "include/flutter/standard_message_codec.h", "include/flutter/standard_method_codec.h", "include/flutter/texture_registrar.h", - "include/flutter/windowing.h", ], "abspath") @@ -47,7 +46,6 @@ core_cpp_client_wrapper_sources = get_path_info([ "core_implementations.cc", "plugin_registrar.cc", "standard_codec.cc", - "windowing.cc", ], "abspath") diff --git a/shell/platform/common/client_wrapper/windowing_unittests.cc b/shell/platform/common/client_wrapper/windowing_unittests.cc deleted file mode 100644 index b9f3448dbf8b7..0000000000000 --- a/shell/platform/common/client_wrapper/windowing_unittests.cc +++ /dev/null @@ -1,645 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "flutter/shell/platform/common/client_wrapper/include/flutter/windowing.h" - -#include "gtest/gtest.h" - -namespace flutter { - -using Positioner = WindowPositioner; -using Anchor = Positioner::Anchor; -using Constraint = Positioner::ConstraintAdjustment; -using Rectangle = WindowRectangle; -using Point = WindowPoint; -using Size = WindowSize; - -std::ostream& operator<<(std::ostream& os, Point const& point) { - return os << "(x: " << point.x << ", y: " << point.y << ")"; -} - -std::ostream& operator<<(std::ostream& os, Size const& size) { - return os << "(width: " << size.width << ", height: " << size.height << ")"; -} - -std::ostream& operator<<(std::ostream& os, Rectangle const& rect) { - return os << "(x: " << rect.top_left.x << ", y: " << rect.top_left.y - << ", width: " << rect.size.width - << ", height: " << rect.size.height << ")"; -} - -namespace { - -struct WindowPlacementTest : testing::Test { - struct ClientAnchorsToParentConfig { - Rectangle const display_area{{0, 0}, {800, 600}}; - Size const parent_size{400, 300}; - Size const child_size{100, 50}; - Point const parent_position{ - (display_area.size.width - parent_size.width) / 2, - (display_area.size.height - parent_size.height) / 2}; - } client_anchors_to_parent_config; - - Rectangle const display_area{{0, 0}, {640, 480}}; - Size const parent_size{600, 400}; - Size const child_size{300, 300}; - Rectangle const rectangle_away_from_rhs{{20, 20}, {20, 20}}; - Rectangle const rectangle_near_rhs{{590, 20}, {10, 20}}; - Rectangle const rectangle_away_from_bottom{{20, 20}, {20, 20}}; - Rectangle const rectangle_near_bottom{{20, 380}, {20, 20}}; - Rectangle const rectangle_near_both_sides{{0, 20}, {600, 20}}; - Rectangle const rectangle_near_both_sides_and_bottom{{0, 380}, {600, 20}}; - Rectangle const rectangle_near_all_sides{{0, 20}, {600, 380}}; - Rectangle const rectangle_near_both_bottom_right{{400, 380}, {200, 20}}; - Point const parent_position{ - (display_area.size.width - parent_size.width) / 2, - (display_area.size.height - parent_size.height) / 2}; - - Positioner positioner; - - auto anchor_rect() -> Rectangle { - auto rectangle{positioner.anchor_rect.value()}; - return {rectangle.top_left + parent_position, rectangle.size}; - } - - auto parent_rect() -> Rectangle { return {parent_position, parent_size}; } - - auto on_top_edge() -> Point { - return anchor_rect().top_left - Point{0, child_size.height}; - } - - auto on_right_edge() -> Point { - auto const rect{anchor_rect()}; - return rect.top_left + Point{rect.size.width, 0}; - } - - auto on_left_edge() -> Point { - return anchor_rect().top_left - Point{child_size.width, 0}; - } - - auto on_bottom_edge() -> Point { - auto const rect{anchor_rect()}; - return rect.top_left + Point{0, rect.size.height}; - } -}; - -} // namespace - -TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenRectAnchorRightOfParent) { - auto const& display_area{client_anchors_to_parent_config.display_area}; - auto const& parent_size{client_anchors_to_parent_config.parent_size}; - auto const& child_size{client_anchors_to_parent_config.child_size}; - auto const& parent_position{client_anchors_to_parent_config.parent_position}; - - auto const rect_size{10}; - Rectangle const overlapping_right{ - parent_position + - Point{parent_size.width - rect_size / 2, parent_size.height / 2}, - {rect_size, rect_size}}; - - Positioner const positioner{.anchor_rect = overlapping_right, - .parent_anchor = Anchor::top_right, - .child_anchor = Anchor::top_left, - .constraint_adjustment = static_cast( - static_cast(Constraint::slide_y) | - static_cast(Constraint::resize_x))}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, positioner.anchor_rect.value(), - {parent_position, parent_size}, display_area)}; - - auto const expected_position{ - parent_position + Point{parent_size.width, parent_size.height / 2}}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, child_size); -} - -TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenRectAnchorAboveParent) { - auto const& display_area{client_anchors_to_parent_config.display_area}; - auto const& parent_size{client_anchors_to_parent_config.parent_size}; - auto const& child_size{client_anchors_to_parent_config.child_size}; - auto const& parent_position{client_anchors_to_parent_config.parent_position}; - - auto const rect_size{10}; - Rectangle const overlapping_above{ - parent_position + Point{parent_size.width / 2, -rect_size / 2}, - {rect_size, rect_size}}; - - Positioner const positioner{.anchor_rect = overlapping_above, - .parent_anchor = Anchor::top_right, - .child_anchor = Anchor::bottom_right, - .constraint_adjustment = Constraint::slide_x}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, positioner.anchor_rect.value(), - {parent_position, parent_size}, display_area)}; - - auto const expected_position{parent_position + - Point{parent_size.width / 2 + rect_size, 0} - - static_cast(child_size)}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, child_size); -} - -TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenOffsetRightOfParent) { - auto const& display_area{client_anchors_to_parent_config.display_area}; - auto const& parent_size{client_anchors_to_parent_config.parent_size}; - auto const& child_size{client_anchors_to_parent_config.child_size}; - auto const& parent_position{client_anchors_to_parent_config.parent_position}; - - auto const rect_size{10}; - Rectangle const mid_right{ - parent_position + - Point{parent_size.width - rect_size, parent_size.height / 2}, - {rect_size, rect_size}}; - - Positioner const positioner{.anchor_rect = mid_right, - .parent_anchor = Anchor::top_right, - .child_anchor = Anchor::top_left, - .offset = Point{rect_size, 0}, - .constraint_adjustment = static_cast( - static_cast(Constraint::slide_y) | - static_cast(Constraint::resize_x))}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, positioner.anchor_rect.value(), - {parent_position, parent_size}, display_area)}; - - auto const expected_position{ - parent_position + Point{parent_size.width, parent_size.height / 2}}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, child_size); -} - -TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenOffsetAboveParent) { - auto const& display_area{client_anchors_to_parent_config.display_area}; - auto const& parent_size{client_anchors_to_parent_config.parent_size}; - auto const& child_size{client_anchors_to_parent_config.child_size}; - auto const& parent_position{client_anchors_to_parent_config.parent_position}; - - auto const rect_size{10}; - Rectangle const mid_top{parent_position + Point{parent_size.width / 2, 0}, - {rect_size, rect_size}}; - - Positioner const positioner{.anchor_rect = mid_top, - .parent_anchor = Anchor::top_right, - .child_anchor = Anchor::bottom_right, - .offset = Point{0, -rect_size}, - .constraint_adjustment = Constraint::slide_x}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, positioner.anchor_rect.value(), - {parent_position, parent_size}, display_area)}; - - auto const expected_position{parent_position + - Point{parent_size.width / 2 + rect_size, 0} - - static_cast(child_size)}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, child_size); -} - -TEST_F(WindowPlacementTest, - ClientAnchorsToParentGivenRectAndOffsetBelowLeftParent) { - auto const& display_area{client_anchors_to_parent_config.display_area}; - auto const& parent_size{client_anchors_to_parent_config.parent_size}; - auto const& child_size{client_anchors_to_parent_config.child_size}; - auto const& parent_position{client_anchors_to_parent_config.parent_position}; - - auto const rect_size{10}; - Rectangle const below_left{ - parent_position + Point{-rect_size, parent_size.height}, - {rect_size, rect_size}}; - - Positioner const positioner{.anchor_rect = below_left, - .parent_anchor = Anchor::bottom_left, - .child_anchor = Anchor::top_right, - .offset = Point{-rect_size, rect_size}, - .constraint_adjustment = Constraint::resize_any}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, positioner.anchor_rect.value(), - {parent_position, parent_size}, display_area)}; - - auto const expected_position{parent_position + Point{0, parent_size.height} - - Point{child_size.width, 0}}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, child_size); -} - -TEST_F(WindowPlacementTest, - AttachesToRightEdgeGivenAnchorRectAwayFromRightSide) { - positioner = {.anchor_rect = rectangle_away_from_rhs, - .parent_anchor = Anchor::top_right, - .child_anchor = Anchor::top_left}; - - auto const expected_position{on_right_edge()}; - - auto const child_rect{ - internal::PlaceWindow(positioner, child_size, anchor_rect(), - {parent_position, parent_size}, display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, AttachesToLeftEdgeGivenAnchorRectNearRightSide) { - positioner = {.anchor_rect = rectangle_near_rhs, - .parent_anchor = Anchor::top_left, - .child_anchor = Anchor::top_right}; - - auto const expected_position{on_left_edge()}; - - auto const child_rect{ - internal::PlaceWindow(positioner, child_size, anchor_rect(), - {parent_position, parent_size}, display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, AttachesToRightEdgeGivenAnchorRectNearBothSides) { - positioner = {.anchor_rect = rectangle_near_both_sides, - .parent_anchor = Anchor::top_right, - .child_anchor = Anchor::top_left}; - - auto const expected_position{on_right_edge()}; - - auto const child_rect{ - internal::PlaceWindow(positioner, child_size, anchor_rect(), - {parent_position, parent_size}, display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, AttachesToBottomEdgeGivenAnchorRectAwayFromBottom) { - positioner = {.anchor_rect = rectangle_away_from_bottom, - .parent_anchor = Anchor::bottom_left, - .child_anchor = Anchor::top_left}; - - auto const expected_position{on_bottom_edge()}; - - auto const child_rect{ - internal::PlaceWindow(positioner, child_size, anchor_rect(), - {parent_position, parent_size}, display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, AttachesToTopEdgeGivenAnchorRectNearBottom) { - positioner = {.anchor_rect = rectangle_near_bottom, - .parent_anchor = Anchor::top_left, - .child_anchor = Anchor::bottom_left}; - - auto const expected_position{on_top_edge()}; - - auto const child_rect{ - internal::PlaceWindow(positioner, child_size, anchor_rect(), - {parent_position, parent_size}, display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, AttachesToBottomEdgeGivenAnchorRectNearBothSides) { - positioner = {.anchor_rect = rectangle_near_both_sides, - .parent_anchor = Anchor::bottom_left, - .child_anchor = Anchor::top_left}; - - auto const expected_position{on_bottom_edge()}; - - auto const child_rect{ - internal::PlaceWindow(positioner, child_size, anchor_rect(), - {parent_position, parent_size}, display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, - AttachesToTopEdgeGivenAnchorRectNearBothSidesAndBottom) { - positioner = {.anchor_rect = rectangle_near_both_sides_and_bottom, - .parent_anchor = Anchor::top_left, - .child_anchor = Anchor::bottom_left}; - - auto const expected_position{on_top_edge()}; - - auto const child_rect{ - internal::PlaceWindow(positioner, child_size, anchor_rect(), - {parent_position, parent_size}, display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, AttachesToRightEdgeGivenAnchorRectNearAllSides) { - positioner = {.anchor_rect = rectangle_near_all_sides, - .parent_anchor = Anchor::top_right, - .child_anchor = Anchor::top_left}; - - auto const expected_position{on_right_edge()}; - - auto const child_rect{ - internal::PlaceWindow(positioner, child_size, anchor_rect(), - {parent_position, parent_size}, display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -namespace { -Anchor const all_anchors[]{ - Anchor::top_left, Anchor::top, Anchor::top_right, - Anchor::left, Anchor::center, Anchor::right, - Anchor::bottom_left, Anchor::bottom, Anchor::bottom_right, -}; - -auto position_of(Anchor anchor, Rectangle rectangle) -> Point { - switch (anchor) { - case Anchor::top_left: - return rectangle.top_left; - case Anchor::top: - return rectangle.top_left + Point{rectangle.size.width / 2, 0}; - case Anchor::top_right: - return rectangle.top_left + Point{rectangle.size.width, 0}; - case Anchor::left: - return rectangle.top_left + Point{0, rectangle.size.height / 2}; - case Anchor::center: - return rectangle.top_left + - Point{rectangle.size.width / 2, rectangle.size.height / 2}; - case Anchor::right: - return rectangle.top_left + - Point{rectangle.size.width, rectangle.size.height / 2}; - case Anchor::bottom_left: - return rectangle.top_left + Point{0, rectangle.size.height}; - case Anchor::bottom: - return rectangle.top_left + - Point{rectangle.size.width / 2, rectangle.size.height}; - case Anchor::bottom_right: - return rectangle.top_left + static_cast(rectangle.size); - default: - std::cerr << "Unknown anchor value: " << static_cast(anchor) << '\n'; - std::abort(); - } -} -} // namespace - -TEST_F(WindowPlacementTest, CanAttachByEveryAnchorGivenNoConstraintAdjustment) { - positioner.anchor_rect = Rectangle{{100, 50}, {20, 20}}; - positioner.constraint_adjustment = Constraint{}; - - for (auto const rect_anchor : all_anchors) { - positioner.parent_anchor = rect_anchor; - - auto const anchor_position{position_of(rect_anchor, anchor_rect())}; - - for (auto const window_anchor : all_anchors) { - positioner.child_anchor = window_anchor; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(position_of(window_anchor, child_rect), anchor_position); - } - } -} - -TEST_F(WindowPlacementTest, - PlacementIsFlippedGivenAnchorRectNearRightSideAndOffset) { - auto const x_offset{42}; - auto const y_offset{13}; - - positioner.anchor_rect = rectangle_near_rhs; - positioner.constraint_adjustment = Constraint::flip_x; - positioner.offset = Point{x_offset, y_offset}; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::top_right; - - auto const expected_position{on_left_edge() + Point{-1 * x_offset, y_offset}}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, - PlacementIsFlippedGivenAnchorRectNearBottomAndOffset) { - auto const x_offset{42}; - auto const y_offset{13}; - - positioner.anchor_rect = rectangle_near_bottom; - positioner.constraint_adjustment = Constraint::flip_y; - positioner.offset = Point{x_offset, y_offset}; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::bottom_left; - - auto const expected_position{on_top_edge() + Point{x_offset, -1 * y_offset}}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, - PlacementIsFlippedBothWaysGivenAnchorRectNearBottomRightAndOffset) { - auto const x_offset{42}; - auto const y_offset{13}; - - positioner.anchor_rect = rectangle_near_both_bottom_right; - positioner.constraint_adjustment = Constraint::flip_any; - positioner.offset = Point{x_offset, y_offset}; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::bottom_right; - - auto const expected_position{anchor_rect().top_left - - static_cast(child_size) - - Point{x_offset, y_offset}}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, PlacementCanSlideInXGivenAnchorRectNearRightSide) { - positioner.anchor_rect = rectangle_near_rhs; - positioner.constraint_adjustment = Constraint::slide_x; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::top_right; - - Point const expected_position{ - (display_area.top_left.x + display_area.size.width) - child_size.width, - anchor_rect().top_left.y}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, PlacementCanSlideInXGivenAnchorRectNearLeftSide) { - Rectangle const rectangle_near_left_side{{0, 20}, {20, 20}}; - - positioner.anchor_rect = rectangle_near_left_side; - positioner.constraint_adjustment = Constraint::slide_x; - positioner.child_anchor = Anchor::top_right; - positioner.parent_anchor = Anchor::top_left; - - Point const expected_position{display_area.top_left.x, - anchor_rect().top_left.y}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, PlacementCanSlideInYGivenAnchorRectNearBottom) { - positioner.anchor_rect = rectangle_near_bottom; - positioner.constraint_adjustment = Constraint::slide_y; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::bottom_left; - - Point const expected_position{ - anchor_rect().top_left.x, - (display_area.top_left.y + display_area.size.height) - child_size.height}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, PlacementCanSlideInYGivenAnchorRectNearTop) { - positioner.anchor_rect = rectangle_near_all_sides; - positioner.constraint_adjustment = Constraint::slide_y; - positioner.child_anchor = Anchor::bottom_left; - positioner.parent_anchor = Anchor::top_left; - - Point const expected_position{anchor_rect().top_left.x, - display_area.top_left.y}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, - PlacementCanSlideInXAndYGivenAnchorRectNearBottomRightAndOffset) { - positioner.anchor_rect = rectangle_near_both_bottom_right; - positioner.constraint_adjustment = Constraint::slide_any; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::bottom_left; - - auto const expected_position{ - (display_area.top_left + static_cast(display_area.size)) - - static_cast(child_size)}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); -} - -TEST_F(WindowPlacementTest, PlacementCanResizeInXGivenAnchorRectNearRightSide) { - positioner.anchor_rect = rectangle_near_rhs; - positioner.constraint_adjustment = Constraint::resize_x; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::top_right; - - auto const expected_position{anchor_rect().top_left + - Point{anchor_rect().size.width, 0}}; - Size const expected_size{ - (display_area.top_left.x + display_area.size.width) - - (anchor_rect().top_left.x + anchor_rect().size.width), - child_size.height}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, expected_size); -} - -TEST_F(WindowPlacementTest, PlacementCanResizeInXGivenAnchorRectNearLeftSide) { - Rectangle const rectangle_near_left_side{{0, 20}, {20, 20}}; - - positioner.anchor_rect = rectangle_near_left_side; - positioner.constraint_adjustment = Constraint::resize_x; - positioner.child_anchor = Anchor::top_right; - positioner.parent_anchor = Anchor::top_left; - - Point const expected_position{display_area.top_left.x, - anchor_rect().top_left.y}; - Size const expected_size{anchor_rect().top_left.x - display_area.top_left.x, - child_size.height}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, expected_size); -} - -TEST_F(WindowPlacementTest, PlacementCanResizeInYGivenAnchorRectNearBottom) { - positioner.anchor_rect = rectangle_near_bottom; - positioner.constraint_adjustment = Constraint::resize_y; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::bottom_left; - - auto const expected_position{anchor_rect().top_left + - Point{0, anchor_rect().size.height}}; - Size const expected_size{ - child_size.width, - (display_area.top_left.y + display_area.size.height) - - (anchor_rect().top_left.y + anchor_rect().size.height)}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, expected_size); -} - -TEST_F(WindowPlacementTest, PlacementCanResizeInYGivenAnchorRectNearTop) { - positioner.anchor_rect = rectangle_near_all_sides; - positioner.constraint_adjustment = Constraint::resize_y; - positioner.child_anchor = Anchor::bottom_left; - positioner.parent_anchor = Anchor::top_left; - - Point const expected_position{anchor_rect().top_left.x, - display_area.top_left.y}; - Size const expected_size{child_size.width, - anchor_rect().top_left.y - display_area.top_left.y}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, expected_size); -} - -TEST_F(WindowPlacementTest, - PlacementCanResizeInXAndYGivenAnchorRectNearBottomRightAndOffset) { - positioner.anchor_rect = rectangle_near_both_bottom_right; - positioner.constraint_adjustment = Constraint::resize_any; - positioner.child_anchor = Anchor::top_left; - positioner.parent_anchor = Anchor::bottom_right; - - auto const expected_position{anchor_rect().top_left + - static_cast(anchor_rect().size)}; - Size const expected_size{ - (display_area.top_left.x + display_area.size.width) - expected_position.x, - (display_area.top_left.y + display_area.size.height) - - expected_position.y}; - - auto const child_rect{internal::PlaceWindow( - positioner, child_size, anchor_rect(), parent_rect(), display_area)}; - - EXPECT_EQ(child_rect.top_left, expected_position); - EXPECT_EQ(child_rect.size, expected_size); -} - -} // namespace flutter diff --git a/shell/platform/common/windowing.cc b/shell/platform/common/windowing.cc new file mode 100644 index 0000000000000..63881ae5d6f55 --- /dev/null +++ b/shell/platform/common/windowing.cc @@ -0,0 +1,278 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/common/windowing.h" + +#include +#include + +namespace flutter { + +namespace { + +WindowPoint offset_for(WindowSize const& size, + WindowPositioner::Anchor anchor) { + switch (anchor) { + case WindowPositioner::Anchor::top_left: + return {0, 0}; + case WindowPositioner::Anchor::top: + return {-size.width / 2, 0}; + case WindowPositioner::Anchor::top_right: + return {-1 * size.width, 0}; + case WindowPositioner::Anchor::left: + return {0, -size.height / 2}; + case WindowPositioner::Anchor::center: + return {-size.width / 2, -size.height / 2}; + case WindowPositioner::Anchor::right: + return {-1 * size.width, -size.height / 2}; + case WindowPositioner::Anchor::bottom_left: + return {0, -1 * size.height}; + case WindowPositioner::Anchor::bottom: + return {-size.width / 2, -1 * size.height}; + case WindowPositioner::Anchor::bottom_right: + return {-1 * size.width, -1 * size.height}; + default: + std::cerr << "Unknown anchor value: " << static_cast(anchor) << '\n'; + std::abort(); + } +} + +WindowPoint anchor_position_for(WindowRectangle const& rect, + WindowPositioner::Anchor anchor) { + switch (anchor) { + case WindowPositioner::Anchor::top_left: + return rect.top_left; + case WindowPositioner::Anchor::top: + return rect.top_left + WindowPoint{rect.size.width / 2, 0}; + case WindowPositioner::Anchor::top_right: + return rect.top_left + WindowPoint{rect.size.width, 0}; + case WindowPositioner::Anchor::left: + return rect.top_left + WindowPoint{0, rect.size.height / 2}; + case WindowPositioner::Anchor::center: + return rect.top_left + + WindowPoint{rect.size.width / 2, rect.size.height / 2}; + case WindowPositioner::Anchor::right: + return rect.top_left + WindowPoint{rect.size.width, rect.size.height / 2}; + case WindowPositioner::Anchor::bottom_left: + return rect.top_left + WindowPoint{0, rect.size.height}; + case WindowPositioner::Anchor::bottom: + return rect.top_left + WindowPoint{rect.size.width / 2, rect.size.height}; + case WindowPositioner::Anchor::bottom_right: + return rect.top_left + WindowPoint{rect.size.width, rect.size.height}; + default: + std::cerr << "Unknown anchor value: " << static_cast(anchor) << '\n'; + std::abort(); + } +} + +WindowPoint constrain_to(WindowRectangle const& r, WindowPoint const& p) { + return {std::clamp(p.x, r.top_left.x, r.top_left.x + r.size.width), + std::clamp(p.y, r.top_left.y, r.top_left.y + r.size.height)}; +} + +WindowPositioner::Anchor flip_anchor_x(WindowPositioner::Anchor anchor) { + switch (anchor) { + case WindowPositioner::Anchor::top_left: + return WindowPositioner::Anchor::top_right; + case WindowPositioner::Anchor::top_right: + return WindowPositioner::Anchor::top_left; + case WindowPositioner::Anchor::left: + return WindowPositioner::Anchor::right; + case WindowPositioner::Anchor::right: + return WindowPositioner::Anchor::left; + case WindowPositioner::Anchor::bottom_left: + return WindowPositioner::Anchor::bottom_right; + case WindowPositioner::Anchor::bottom_right: + return WindowPositioner::Anchor::bottom_left; + default: + return anchor; + } +} + +WindowPositioner::Anchor flip_anchor_y(WindowPositioner::Anchor anchor) { + switch (anchor) { + case WindowPositioner::Anchor::top_left: + return WindowPositioner::Anchor::bottom_left; + case WindowPositioner::Anchor::top: + return WindowPositioner::Anchor::bottom; + case WindowPositioner::Anchor::top_right: + return WindowPositioner::Anchor::bottom_right; + case WindowPositioner::Anchor::bottom_left: + return WindowPositioner::Anchor::top_left; + case WindowPositioner::Anchor::bottom: + return WindowPositioner::Anchor::top; + case WindowPositioner::Anchor::bottom_right: + return WindowPositioner::Anchor::top_right; + default: + return anchor; + } +} + +WindowPoint flip_offset_x(WindowPoint const& p) { + return {-1 * p.x, p.y}; +} + +WindowPoint flip_offset_y(WindowPoint const& p) { + return {p.x, -1 * p.y}; +} + +} // namespace + +WindowRectangle PlaceWindow(WindowPositioner const& positioner, + WindowSize child_size, + WindowRectangle const& anchor_rect, + WindowRectangle const& parent_rect, + WindowRectangle const& output_rect) { + WindowRectangle default_result; + + { + WindowPoint const result = + constrain_to(parent_rect, anchor_position_for( + anchor_rect, positioner.parent_anchor) + + positioner.offset) + + offset_for(child_size, positioner.child_anchor); + + if (output_rect.contains({result, child_size})) { + return WindowRectangle{result, child_size}; + } + + default_result = WindowRectangle{result, child_size}; + } + + if (static_cast(positioner.constraint_adjustment) & + static_cast(WindowPositioner::ConstraintAdjustment::flip_x)) { + WindowPoint const result = + constrain_to(parent_rect, + anchor_position_for( + anchor_rect, flip_anchor_x(positioner.parent_anchor)) + + flip_offset_x(positioner.offset)) + + offset_for(child_size, flip_anchor_x(positioner.child_anchor)); + + if (output_rect.contains({result, child_size})) { + return WindowRectangle{result, child_size}; + } + } + + if (static_cast(positioner.constraint_adjustment) & + static_cast(WindowPositioner::ConstraintAdjustment::flip_y)) { + WindowPoint const result = + constrain_to(parent_rect, + anchor_position_for( + anchor_rect, flip_anchor_y(positioner.parent_anchor)) + + flip_offset_y(positioner.offset)) + + offset_for(child_size, flip_anchor_y(positioner.child_anchor)); + + if (output_rect.contains({result, child_size})) { + return WindowRectangle{result, child_size}; + } + } + + if (static_cast(positioner.constraint_adjustment) & + static_cast(WindowPositioner::ConstraintAdjustment::flip_x) && + static_cast(positioner.constraint_adjustment) & + static_cast(WindowPositioner::ConstraintAdjustment::flip_y)) { + WindowPoint const result = + constrain_to( + parent_rect, + anchor_position_for(anchor_rect, flip_anchor_x(flip_anchor_y( + positioner.parent_anchor))) + + flip_offset_x(flip_offset_y(positioner.offset))) + + offset_for(child_size, + flip_anchor_x(flip_anchor_y(positioner.child_anchor))); + + if (output_rect.contains({result, child_size})) { + return WindowRectangle{result, child_size}; + } + } + + { + WindowPoint result = + constrain_to(parent_rect, anchor_position_for( + anchor_rect, positioner.parent_anchor) + + positioner.offset) + + offset_for(child_size, positioner.child_anchor); + + if (static_cast(positioner.constraint_adjustment) & + static_cast(WindowPositioner::ConstraintAdjustment::slide_x)) { + int const left_overhang = result.x - output_rect.top_left.x; + int const right_overhang = + (result.x + child_size.width) - + (output_rect.top_left.x + output_rect.size.width); + + if (left_overhang < 0) { + result.x -= left_overhang; + } else if (right_overhang > 0) { + result.x -= right_overhang; + } + } + + if (static_cast(positioner.constraint_adjustment) & + static_cast(WindowPositioner::ConstraintAdjustment::slide_y)) { + int const top_overhang = result.y - output_rect.top_left.y; + int const bot_overhang = + (result.y + child_size.height) - + (output_rect.top_left.y + output_rect.size.height); + + if (top_overhang < 0) { + result.y -= top_overhang; + } else if (bot_overhang > 0) { + result.y -= bot_overhang; + } + } + + if (output_rect.contains({result, child_size})) { + return WindowRectangle{result, child_size}; + } + } + + { + WindowPoint result = + constrain_to(parent_rect, anchor_position_for( + anchor_rect, positioner.parent_anchor) + + positioner.offset) + + offset_for(child_size, positioner.child_anchor); + + if (static_cast(positioner.constraint_adjustment) & + static_cast(WindowPositioner::ConstraintAdjustment::resize_x)) { + int const left_overhang = result.x - output_rect.top_left.x; + int const right_overhang = + (result.x + child_size.width) - + (output_rect.top_left.x + output_rect.size.width); + + if (left_overhang < 0) { + result.x -= left_overhang; + child_size.width += left_overhang; + } + + if (right_overhang > 0) { + child_size.width -= right_overhang; + } + } + + if (static_cast(positioner.constraint_adjustment) & + static_cast(WindowPositioner::ConstraintAdjustment::resize_y)) { + int const top_overhang = result.y - output_rect.top_left.y; + int const bot_overhang = + (result.y + child_size.height) - + (output_rect.top_left.y + output_rect.size.height); + + if (top_overhang < 0) { + result.y -= top_overhang; + child_size.height += top_overhang; + } + + if (bot_overhang > 0) { + child_size.height -= bot_overhang; + } + } + + if (output_rect.contains({result, child_size})) { + return WindowRectangle{result, child_size}; + } + } + + return default_result; +} + +} // namespace flutter diff --git a/shell/platform/common/client_wrapper/include/flutter/windowing.h b/shell/platform/common/windowing.h similarity index 76% rename from shell/platform/common/client_wrapper/include/flutter/windowing.h rename to shell/platform/common/windowing.h index c25d05c1cda49..571272fb55c11 100644 --- a/shell/platform/common/client_wrapper/include/flutter/windowing.h +++ b/shell/platform/common/windowing.h @@ -2,28 +2,26 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CLIENT_WRAPPER_INCLUDE_FLUTTER_WINDOWING_H_ -#define FLUTTER_SHELL_PLATFORM_COMMON_CLIENT_WRAPPER_INCLUDE_FLUTTER_WINDOWING_H_ +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_WINDOWING_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_WINDOWING_H_ #include namespace flutter { -// The unique identifier for a view. +// A unique identifier for a view. using FlutterViewId = int64_t; -// A point (x, y) in 2D space for window positioning. +// A point in 2D space for window positioning using integer coordinates. struct WindowPoint { - int x{0}; - int y{0}; + int x = 0; + int y = 0; - friend auto operator+(WindowPoint const& lhs, - WindowPoint const& rhs) -> WindowPoint { + friend WindowPoint operator+(WindowPoint const& lhs, WindowPoint const& rhs) { return {lhs.x + rhs.x, lhs.y + rhs.y}; } - friend auto operator-(WindowPoint const& lhs, - WindowPoint const& rhs) -> WindowPoint { + friend WindowPoint operator-(WindowPoint const& lhs, WindowPoint const& rhs) { return {lhs.x - rhs.x, lhs.y - rhs.y}; } @@ -32,10 +30,10 @@ struct WindowPoint { } }; -// A size (width, height) in 2D space. +// A 2D size using integer dimensions. struct WindowSize { - int width{0}; - int height{0}; + int width = 0; + int height = 0; explicit operator WindowPoint() const { return {width, height}; } @@ -52,7 +50,7 @@ struct WindowRectangle { // Checks if this rectangle fully contains |rect|. // Note: An empty rectangle can still contain other empty rectangles, // which are treated as points or lines of thickness zero - auto contains(WindowRectangle const& rect) const -> bool { + bool contains(WindowRectangle const& rect) const { return rect.top_left.x >= top_left.x && rect.top_left.x + rect.size.width <= top_left.x + size.width && rect.top_left.y >= top_left.y && @@ -103,9 +101,9 @@ struct WindowPositioner { // rectangle. std::optional anchor_rect; // Specifies which anchor of the parent window to align to. - Anchor parent_anchor{Anchor::center}; + Anchor parent_anchor = Anchor::center; // Specifies which anchor of the child window to align with the parent. - Anchor child_anchor{Anchor::center}; + Anchor child_anchor = Anchor::center; // Offset relative to the position of the anchor on the anchor rectangle and // the anchor on the child. WindowPoint offset; @@ -118,24 +116,16 @@ struct WindowPositioner { enum class WindowArchetype { // Regular top-level window. regular, - // A window that is on a layer above regular windows and is not dockable. - floating_regular, - // Dialog window. - dialog, - // Satellite window attached to a regular, floating_regular or dialog window. - satellite, // Popup. popup, - // Tooltip. - tip, }; // Window metadata returned as the result of creating a Flutter window. struct WindowMetadata { // The ID of the view used for this window, which is unique to each window. - FlutterViewId view_id{0}; - // The type of the window (e.g., regular, dialog, popup, etc). - WindowArchetype archetype{WindowArchetype::regular}; + FlutterViewId view_id = 0; + // The type of the window. + WindowArchetype archetype = WindowArchetype::regular; // Size of the created window, in logical coordinates. WindowSize size; // The ID of the view used by the parent window. If not set, the window is @@ -143,8 +133,6 @@ struct WindowMetadata { std::optional parent_id; }; -namespace internal { - // Computes the screen-space rectangle for a child window placed according to // the given |positioner|. |child_size| is the frame size of the child window. // |anchor_rect| is the rectangle relative to which the child window is placed. @@ -153,14 +141,12 @@ namespace internal { // are in physical coordinates. Note: WindowPositioner::anchor_rect is not used // in this function; use |anchor_rect| to set the anchor rectangle for the // child. -auto PlaceWindow(WindowPositioner const& positioner, - WindowSize child_size, - WindowRectangle const& anchor_rect, - WindowRectangle const& parent_rect, - WindowRectangle const& output_rect) -> WindowRectangle; - -} // namespace internal +WindowRectangle PlaceWindow(WindowPositioner const& positioner, + WindowSize child_size, + WindowRectangle const& anchor_rect, + WindowRectangle const& parent_rect, + WindowRectangle const& output_rect); } // namespace flutter -#endif // FLUTTER_SHELL_PLATFORM_COMMON_CLIENT_WRAPPER_INCLUDE_FLUTTER_WINDOWING_H_ +#endif // FLUTTER_SHELL_PLATFORM_COMMON_WINDOWING_H_ diff --git a/shell/platform/common/windowing_unittests.cc b/shell/platform/common/windowing_unittests.cc new file mode 100644 index 0000000000000..cc21cd8cf8f94 --- /dev/null +++ b/shell/platform/common/windowing_unittests.cc @@ -0,0 +1,528 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/common/windowing.h" + +#include + +#include "flutter/fml/logging.h" +#include "gtest/gtest.h" + +namespace flutter { + +namespace { + +using Positioner = WindowPositioner; +using Anchor = Positioner::Anchor; +using Constraint = Positioner::ConstraintAdjustment; +using Rectangle = WindowRectangle; +using Point = WindowPoint; +using Size = WindowSize; + +struct WindowPlacementTest + : ::testing::TestWithParam> { + struct ClientAnchorsToParentConfig { + Rectangle const display_area = {{0, 0}, {800, 600}}; + Size const parent_size = {400, 300}; + Size const child_size = {100, 50}; + Point const parent_position = { + (display_area.size.width - parent_size.width) / 2, + (display_area.size.height - parent_size.height) / 2}; + } client_anchors_to_parent_config; + + Rectangle const display_area = {{0, 0}, {640, 480}}; + Size const parent_size = {600, 400}; + Size const child_size = {300, 300}; + Rectangle const rectangle_away_from_rhs = {{20, 20}, {20, 20}}; + Rectangle const rectangle_near_rhs = {{590, 20}, {10, 20}}; + Rectangle const rectangle_away_from_bottom = {{20, 20}, {20, 20}}; + Rectangle const rectangle_near_bottom = {{20, 380}, {20, 20}}; + Rectangle const rectangle_near_both_sides = {{0, 20}, {600, 20}}; + Rectangle const rectangle_near_both_sides_and_bottom = {{0, 380}, {600, 20}}; + Rectangle const rectangle_near_all_sides = {{0, 20}, {600, 380}}; + Rectangle const rectangle_near_both_bottom_right = {{400, 380}, {200, 20}}; + Point const parent_position = { + (display_area.size.width - parent_size.width) / 2, + (display_area.size.height - parent_size.height) / 2}; + + Positioner positioner; + + Rectangle anchor_rect() { + Rectangle rectangle{positioner.anchor_rect.value()}; + return {rectangle.top_left + parent_position, rectangle.size}; + } + + Rectangle parent_rect() { return {parent_position, parent_size}; } + + Point on_top_edge() { + return anchor_rect().top_left - Point{0, child_size.height}; + } + + Point on_left_edge() { + return anchor_rect().top_left - Point{child_size.width, 0}; + } +}; + +std::vector> all_anchor_combinations() { + std::array const all_anchors = { + Anchor::top_left, Anchor::top, Anchor::top_right, + Anchor::left, Anchor::center, Anchor::right, + Anchor::bottom_left, Anchor::bottom, Anchor::bottom_right, + }; + std::vector> combinations; + combinations.reserve(all_anchors.size() * all_anchors.size()); + + for (Anchor const parent_anchor : all_anchors) { + for (Anchor const child_anchor : all_anchors) { + combinations.push_back(std::make_tuple(parent_anchor, child_anchor)); + } + } + return combinations; +} + +} // namespace + +TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenRectAnchorRightOfParent) { + Rectangle const& display_area = client_anchors_to_parent_config.display_area; + Size const& parent_size = client_anchors_to_parent_config.parent_size; + Size const& child_size = client_anchors_to_parent_config.child_size; + Point const& parent_position = + client_anchors_to_parent_config.parent_position; + + int const rect_size = 10; + Rectangle const overlapping_right = { + parent_position + + Point{parent_size.width - rect_size / 2, parent_size.height / 2}, + {rect_size, rect_size}}; + + Positioner const positioner = { + .anchor_rect = overlapping_right, + .parent_anchor = Anchor::top_right, + .child_anchor = Anchor::top_left, + .constraint_adjustment = + static_cast(static_cast(Constraint::slide_y) | + static_cast(Constraint::resize_x))}; + + WindowRectangle const child_rect = + PlaceWindow(positioner, child_size, positioner.anchor_rect.value(), + {parent_position, parent_size}, display_area); + + Point const expected_position = + parent_position + Point{parent_size.width, parent_size.height / 2}; + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, child_size); +} + +TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenRectAnchorAboveParent) { + Rectangle const& display_area = client_anchors_to_parent_config.display_area; + Size const& parent_size = client_anchors_to_parent_config.parent_size; + Size const& child_size = client_anchors_to_parent_config.child_size; + Point const& parent_position = + client_anchors_to_parent_config.parent_position; + + int const rect_size = 10; + Rectangle const overlapping_above = { + parent_position + Point{parent_size.width / 2, -rect_size / 2}, + {rect_size, rect_size}}; + + Positioner const positioner = {.anchor_rect = overlapping_above, + .parent_anchor = Anchor::top_right, + .child_anchor = Anchor::bottom_right, + .constraint_adjustment = Constraint::slide_x}; + + WindowRectangle const child_rect = + PlaceWindow(positioner, child_size, positioner.anchor_rect.value(), + {parent_position, parent_size}, display_area); + + Point const expected_position = parent_position + + Point{parent_size.width / 2 + rect_size, 0} - + static_cast(child_size); + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, child_size); +} + +TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenOffsetRightOfParent) { + Rectangle const& display_area = client_anchors_to_parent_config.display_area; + Size const& parent_size = client_anchors_to_parent_config.parent_size; + Size const& child_size = client_anchors_to_parent_config.child_size; + Point const& parent_position = + client_anchors_to_parent_config.parent_position; + + int const rect_size = 10; + Rectangle const mid_right = { + parent_position + + Point{parent_size.width - rect_size, parent_size.height / 2}, + {rect_size, rect_size}}; + + Positioner const positioner = { + .anchor_rect = mid_right, + .parent_anchor = Anchor::top_right, + .child_anchor = Anchor::top_left, + .offset = Point{rect_size, 0}, + .constraint_adjustment = + static_cast(static_cast(Constraint::slide_y) | + static_cast(Constraint::resize_x))}; + + WindowRectangle const child_rect = + PlaceWindow(positioner, child_size, positioner.anchor_rect.value(), + {parent_position, parent_size}, display_area); + + Point const expected_position = + parent_position + Point{parent_size.width, parent_size.height / 2}; + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, child_size); +} + +TEST_F(WindowPlacementTest, ClientAnchorsToParentGivenOffsetAboveParent) { + Rectangle const& display_area = client_anchors_to_parent_config.display_area; + Size const& parent_size = client_anchors_to_parent_config.parent_size; + Size const& child_size = client_anchors_to_parent_config.child_size; + Point const& parent_position = + client_anchors_to_parent_config.parent_position; + + int const rect_size = 10; + Rectangle const mid_top = {parent_position + Point{parent_size.width / 2, 0}, + {rect_size, rect_size}}; + + Positioner const positioner = {.anchor_rect = mid_top, + .parent_anchor = Anchor::top_right, + .child_anchor = Anchor::bottom_right, + .offset = Point{0, -rect_size}, + .constraint_adjustment = Constraint::slide_x}; + + WindowRectangle const child_rect = + PlaceWindow(positioner, child_size, positioner.anchor_rect.value(), + {parent_position, parent_size}, display_area); + + Point const expected_position = parent_position + + Point{parent_size.width / 2 + rect_size, 0} - + static_cast(child_size); + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, child_size); +} + +TEST_F(WindowPlacementTest, + ClientAnchorsToParentGivenRectAndOffsetBelowLeftParent) { + Rectangle const& display_area = client_anchors_to_parent_config.display_area; + Size const& parent_size = client_anchors_to_parent_config.parent_size; + Size const& child_size = client_anchors_to_parent_config.child_size; + Point const& parent_position = + client_anchors_to_parent_config.parent_position; + + int const rect_size = 10; + Rectangle const below_left = { + parent_position + Point{-rect_size, parent_size.height}, + {rect_size, rect_size}}; + + Positioner const positioner = { + .anchor_rect = below_left, + .parent_anchor = Anchor::bottom_left, + .child_anchor = Anchor::top_right, + .offset = Point{-rect_size, rect_size}, + .constraint_adjustment = Constraint::resize_any}; + + WindowRectangle const child_rect = + PlaceWindow(positioner, child_size, positioner.anchor_rect.value(), + {parent_position, parent_size}, display_area); + + Point const expected_position = parent_position + + Point{0, parent_size.height} - + Point{child_size.width, 0}; + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, child_size); +} + +TEST_P(WindowPlacementTest, CanAttachByEveryAnchorGivenNoConstraintAdjustment) { + positioner.anchor_rect = Rectangle{{100, 50}, {20, 20}}; + positioner.constraint_adjustment = Constraint{}; + std::tie(positioner.parent_anchor, positioner.child_anchor) = GetParam(); + + auto const position_of = [](Anchor anchor, Rectangle rectangle) -> Point { + switch (anchor) { + case Anchor::top_left: + return rectangle.top_left; + case Anchor::top: + return rectangle.top_left + Point{rectangle.size.width / 2, 0}; + case Anchor::top_right: + return rectangle.top_left + Point{rectangle.size.width, 0}; + case Anchor::left: + return rectangle.top_left + Point{0, rectangle.size.height / 2}; + case Anchor::center: + return rectangle.top_left + + Point{rectangle.size.width / 2, rectangle.size.height / 2}; + case Anchor::right: + return rectangle.top_left + + Point{rectangle.size.width, rectangle.size.height / 2}; + case Anchor::bottom_left: + return rectangle.top_left + Point{0, rectangle.size.height}; + case Anchor::bottom: + return rectangle.top_left + + Point{rectangle.size.width / 2, rectangle.size.height}; + case Anchor::bottom_right: + return rectangle.top_left + static_cast(rectangle.size); + default: + FML_UNREACHABLE(); + } + }; + + Point const anchor_position = + position_of(positioner.parent_anchor, anchor_rect()); + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(position_of(positioner.child_anchor, child_rect), anchor_position); +} + +INSTANTIATE_TEST_SUITE_P(AnchorCombinations, + WindowPlacementTest, + ::testing::ValuesIn(all_anchor_combinations())); + +TEST_F(WindowPlacementTest, + PlacementIsFlippedGivenAnchorRectNearRightSideAndOffset) { + int const x_offset = 42; + int const y_offset = 13; + + positioner.anchor_rect = rectangle_near_rhs; + positioner.constraint_adjustment = Constraint::flip_x; + positioner.offset = Point{x_offset, y_offset}; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::top_right; + + Point const expected_position = + on_left_edge() + Point{-1 * x_offset, y_offset}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, + PlacementIsFlippedGivenAnchorRectNearBottomAndOffset) { + int const x_offset = 42; + int const y_offset = 13; + + positioner.anchor_rect = rectangle_near_bottom; + positioner.constraint_adjustment = Constraint::flip_y; + positioner.offset = Point{x_offset, y_offset}; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_left; + + Point const expected_position = + on_top_edge() + Point{x_offset, -1 * y_offset}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, + PlacementIsFlippedBothWaysGivenAnchorRectNearBottomRightAndOffset) { + int const x_offset = 42; + int const y_offset = 13; + + positioner.anchor_rect = rectangle_near_both_bottom_right; + positioner.constraint_adjustment = Constraint::flip_any; + positioner.offset = Point{x_offset, y_offset}; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_right; + + Point const expected_position = anchor_rect().top_left - + static_cast(child_size) - + Point{x_offset, y_offset}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, PlacementCanSlideInXGivenAnchorRectNearRightSide) { + positioner.anchor_rect = rectangle_near_rhs; + positioner.constraint_adjustment = Constraint::slide_x; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::top_right; + + Point const expected_position = { + (display_area.top_left.x + display_area.size.width) - child_size.width, + anchor_rect().top_left.y}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, PlacementCanSlideInXGivenAnchorRectNearLeftSide) { + Rectangle const rectangle_near_left_side = {{0, 20}, {20, 20}}; + + positioner.anchor_rect = rectangle_near_left_side; + positioner.constraint_adjustment = Constraint::slide_x; + positioner.child_anchor = Anchor::top_right; + positioner.parent_anchor = Anchor::top_left; + + Point const expected_position = {display_area.top_left.x, + anchor_rect().top_left.y}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, PlacementCanSlideInYGivenAnchorRectNearBottom) { + positioner.anchor_rect = rectangle_near_bottom; + positioner.constraint_adjustment = Constraint::slide_y; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_left; + + Point const expected_position = { + anchor_rect().top_left.x, + (display_area.top_left.y + display_area.size.height) - child_size.height}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, PlacementCanSlideInYGivenAnchorRectNearTop) { + positioner.anchor_rect = rectangle_near_all_sides; + positioner.constraint_adjustment = Constraint::slide_y; + positioner.child_anchor = Anchor::bottom_left; + positioner.parent_anchor = Anchor::top_left; + + Point const expected_position = {anchor_rect().top_left.x, + display_area.top_left.y}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, + PlacementCanSlideInXAndYGivenAnchorRectNearBottomRightAndOffset) { + positioner.anchor_rect = rectangle_near_both_bottom_right; + positioner.constraint_adjustment = Constraint::slide_any; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_left; + + Point const expected_position = { + (display_area.top_left + static_cast(display_area.size)) - + static_cast(child_size)}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); +} + +TEST_F(WindowPlacementTest, PlacementCanResizeInXGivenAnchorRectNearRightSide) { + positioner.anchor_rect = rectangle_near_rhs; + positioner.constraint_adjustment = Constraint::resize_x; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::top_right; + + Point const expected_position = + anchor_rect().top_left + Point{anchor_rect().size.width, 0}; + Size const expected_size = { + (display_area.top_left.x + display_area.size.width) - + (anchor_rect().top_left.x + anchor_rect().size.width), + child_size.height}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, expected_size); +} + +TEST_F(WindowPlacementTest, PlacementCanResizeInXGivenAnchorRectNearLeftSide) { + Rectangle const rectangle_near_left_side = {{0, 20}, {20, 20}}; + + positioner.anchor_rect = rectangle_near_left_side; + positioner.constraint_adjustment = Constraint::resize_x; + positioner.child_anchor = Anchor::top_right; + positioner.parent_anchor = Anchor::top_left; + + Point const expected_position = {display_area.top_left.x, + anchor_rect().top_left.y}; + Size const expected_size = { + anchor_rect().top_left.x - display_area.top_left.x, child_size.height}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, expected_size); +} + +TEST_F(WindowPlacementTest, PlacementCanResizeInYGivenAnchorRectNearBottom) { + positioner.anchor_rect = rectangle_near_bottom; + positioner.constraint_adjustment = Constraint::resize_y; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_left; + + Point const expected_position = + anchor_rect().top_left + Point{0, anchor_rect().size.height}; + Size const expected_size = { + child_size.width, + (display_area.top_left.y + display_area.size.height) - + (anchor_rect().top_left.y + anchor_rect().size.height)}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, expected_size); +} + +TEST_F(WindowPlacementTest, PlacementCanResizeInYGivenAnchorRectNearTop) { + positioner.anchor_rect = rectangle_near_all_sides; + positioner.constraint_adjustment = Constraint::resize_y; + positioner.child_anchor = Anchor::bottom_left; + positioner.parent_anchor = Anchor::top_left; + + Point const expected_position = {anchor_rect().top_left.x, + display_area.top_left.y}; + Size const expected_size = { + child_size.width, anchor_rect().top_left.y - display_area.top_left.y}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, expected_size); +} + +TEST_F(WindowPlacementTest, + PlacementCanResizeInXAndYGivenAnchorRectNearBottomRightAndOffset) { + positioner.anchor_rect = rectangle_near_both_bottom_right; + positioner.constraint_adjustment = Constraint::resize_any; + positioner.child_anchor = Anchor::top_left; + positioner.parent_anchor = Anchor::bottom_right; + + Point const expected_position = + anchor_rect().top_left + static_cast(anchor_rect().size); + Size const expected_size = { + (display_area.top_left.x + display_area.size.width) - expected_position.x, + (display_area.top_left.y + display_area.size.height) - + expected_position.y}; + + WindowRectangle const child_rect = PlaceWindow( + positioner, child_size, anchor_rect(), parent_rect(), display_area); + + EXPECT_EQ(child_rect.top_left, expected_position); + EXPECT_EQ(child_rect.size, expected_size); +} + +} // namespace flutter diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn index e80662e592a72..a412f4eae04c9 100644 --- a/shell/platform/windows/BUILD.gn +++ b/shell/platform/windows/BUILD.gn @@ -73,6 +73,10 @@ source_set("flutter_windows_source") { "external_texture_d3d.h", "external_texture_pixelbuffer.cc", "external_texture_pixelbuffer.h", + "flutter_host_window.cc", + "flutter_host_window.h", + "flutter_host_window_controller.cc", + "flutter_host_window_controller.h", "flutter_key_map.g.cc", "flutter_platform_node_delegate_windows.cc", "flutter_platform_node_delegate_windows.h", @@ -125,6 +129,8 @@ source_set("flutter_windows_source") { "window_proc_delegate_manager.cc", "window_proc_delegate_manager.h", "window_state.h", + "windowing_handler.cc", + "windowing_handler.h", "windows_lifecycle_manager.cc", "windows_lifecycle_manager.h", "windows_proc_table.cc", @@ -202,6 +208,7 @@ executable("flutter_windows_unittests") { "cursor_handler_unittests.cc", "direct_manipulation_unittests.cc", "dpi_utils_unittests.cc", + "flutter_host_window_controller_unittests.cc", "flutter_project_bundle_unittests.cc", "flutter_window_unittests.cc", "flutter_windows_engine_unittests.cc", @@ -249,6 +256,7 @@ executable("flutter_windows_unittests") { "text_input_plugin_unittest.cc", "window_proc_delegate_manager_unittests.cc", "window_unittests.cc", + "windowing_handler_unittests.cc", "windows_lifecycle_manager_unittests.cc", ] diff --git a/shell/platform/windows/client_wrapper/BUILD.gn b/shell/platform/windows/client_wrapper/BUILD.gn index aaf2abf2ef97d..1d40c3c1314fb 100644 --- a/shell/platform/windows/client_wrapper/BUILD.gn +++ b/shell/platform/windows/client_wrapper/BUILD.gn @@ -11,19 +11,12 @@ _wrapper_includes = [ "include/flutter/flutter_engine.h", "include/flutter/flutter_view_controller.h", "include/flutter/flutter_view.h", - "include/flutter/flutter_win32_window.h", - "include/flutter/flutter_window_controller.h", "include/flutter/plugin_registrar_windows.h", - "include/flutter/win32_window.h", - "include/flutter/win32_wrapper.h", ] _wrapper_sources = [ "flutter_engine.cc", "flutter_view_controller.cc", - "flutter_win32_window.cc", - "flutter_window_controller.cc", - "win32_window.cc", ] # This code will be merged into .../common/client_wrapper for client use, @@ -47,8 +40,6 @@ source_set("client_wrapper_windows") { "//flutter/shell/platform/windows:flutter_windows_headers", ] - libs = [ "dwmapi.lib" ] - configs += [ "//flutter/shell/platform/common:desktop_library_implementation" ] @@ -88,7 +79,6 @@ executable("client_wrapper_windows_unittests") { "flutter_engine_unittests.cc", "flutter_view_controller_unittests.cc", "flutter_view_unittests.cc", - "flutter_window_controller_unittests.cc", "plugin_registrar_windows_unittests.cc", ] @@ -99,7 +89,6 @@ executable("client_wrapper_windows_unittests") { ":client_wrapper_library_stubs_windows", ":client_wrapper_windows", ":client_wrapper_windows_fixtures", - "//flutter/shell/platform/common/client_wrapper", "//flutter/shell/platform/common/client_wrapper:client_wrapper_library_stubs", "//flutter/testing", @@ -121,9 +110,6 @@ client_wrapper_file_archive_list = [ win_client_wrapper_file_archive_list = [ "flutter_engine.cc", "flutter_view_controller.cc", - "flutter_win32_window.cc", - "flutter_window_controller.cc", - "win32_window.cc", ] zip_bundle("client_wrapper_archive") { diff --git a/shell/platform/windows/client_wrapper/flutter_engine.cc b/shell/platform/windows/client_wrapper/flutter_engine.cc index 3cbe59622fc4f..7860947aa068b 100644 --- a/shell/platform/windows/client_wrapper/flutter_engine.cc +++ b/shell/platform/windows/client_wrapper/flutter_engine.cc @@ -63,7 +63,7 @@ bool FlutterEngine::Run(const char* entry_point) { } void FlutterEngine::ShutDown() { - if (engine_) { + if (engine_ && owns_engine_) { FlutterDesktopEngineDestroy(engine_); } engine_ = nullptr; @@ -113,7 +113,8 @@ std::optional FlutterEngine::ProcessExternalWindowMessage( return std::nullopt; } -FlutterDesktopEngineRef FlutterEngine::engine() const { +FlutterDesktopEngineRef FlutterEngine::RelinquishEngine() { + owns_engine_ = false; return engine_; } diff --git a/shell/platform/windows/client_wrapper/flutter_view_controller.cc b/shell/platform/windows/client_wrapper/flutter_view_controller.cc index 1dbc5a0dfa96f..98c65e10c27bc 100644 --- a/shell/platform/windows/client_wrapper/flutter_view_controller.cc +++ b/shell/platform/windows/client_wrapper/flutter_view_controller.cc @@ -11,22 +11,10 @@ namespace flutter { FlutterViewController::FlutterViewController(int width, int height, - const DartProject& project) - : FlutterViewController(width, - height, - std::make_shared(project)) {} - -FlutterViewController::FlutterViewController( - int width, - int height, - std::shared_ptr engine) { - FlutterDesktopViewControllerProperties properties = {}; - properties.width = width; - properties.height = height; - - engine_ = std::move(engine); - controller_ = - FlutterDesktopEngineCreateViewController(engine_->engine(), &properties); + const DartProject& project) { + engine_ = std::make_shared(project); + controller_ = FlutterDesktopViewControllerCreate(width, height, + engine_->RelinquishEngine()); if (!controller_) { std::cerr << "Failed to create view controller." << std::endl; return; diff --git a/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc b/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc index f79842f280978..837c2e13e583d 100644 --- a/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc +++ b/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc @@ -17,8 +17,10 @@ namespace { class TestWindowsApi : public testing::StubFlutterWindowsApi { public: // |flutter::testing::StubFlutterWindowsApi| - FlutterDesktopViewControllerRef EngineCreateViewController( - const FlutterDesktopViewControllerProperties* properties) override { + FlutterDesktopViewControllerRef ViewControllerCreate( + int width, + int height, + FlutterDesktopEngineRef engine) override { return reinterpret_cast(2); } @@ -61,13 +63,11 @@ TEST(FlutterViewControllerTest, CreateDestroy) { testing::ScopedStubFlutterWindowsApi scoped_api_stub( std::make_unique()); auto test_api = static_cast(scoped_api_stub.stub()); - - // Create and destroy a view controller. - // This should also create and destroy an engine. { FlutterViewController controller(100, 100, project); } - EXPECT_TRUE(test_api->view_controller_destroyed()); - EXPECT_TRUE(test_api->engine_destroyed()); + // Per the C API, once a view controller has taken ownership of an engine + // the engine destruction method should not be called. + EXPECT_FALSE(test_api->engine_destroyed()); } TEST(FlutterViewControllerTest, GetViewId) { diff --git a/shell/platform/windows/client_wrapper/flutter_win32_window.cc b/shell/platform/windows/client_wrapper/flutter_win32_window.cc deleted file mode 100644 index 227508b8c571a..0000000000000 --- a/shell/platform/windows/client_wrapper/flutter_win32_window.cc +++ /dev/null @@ -1,77 +0,0 @@ -#include "include/flutter/flutter_win32_window.h" - -#include - -namespace flutter { - -FlutterWin32Window::FlutterWin32Window(std::shared_ptr engine) - : engine_{std::move(engine)} {} - -FlutterWin32Window::FlutterWin32Window(std::shared_ptr engine, - std::shared_ptr wrapper) - : engine_{std::move(engine)}, Win32Window{std::move(wrapper)} {} - -auto FlutterWin32Window::GetFlutterViewId() const -> FlutterViewId { - return view_controller_->view_id(); -}; - -auto FlutterWin32Window::OnCreate() -> bool { - if (!Win32Window::OnCreate()) { - return false; - } - - auto const client_rect{GetClientArea()}; - - // The size here must match the window dimensions to avoid unnecessary surface - // creation / destruction in the startup path. - view_controller_ = std::make_unique( - client_rect.right - client_rect.left, - client_rect.bottom - client_rect.top, engine_); - // Ensure that basic setup of the controller was successful. - if (!view_controller_->view()) { - return false; - } - - SetChildContent(view_controller_->view()->GetNativeWindow()); - - // TODO(loicsharma): Hide the window until the first frame is rendered. - // Single window apps use the engine's next frame callback to show the window. - // This doesn't work for multi window apps as the engine cannot have multiple - // next frame callbacks. If multiple windows are created, only the last one - // will be shown. - return true; -} - -void FlutterWin32Window::OnDestroy() { - if (view_controller_) { - view_controller_ = nullptr; - } - - Win32Window::OnDestroy(); -} - -auto FlutterWin32Window::MessageHandler(HWND hwnd, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) -> LRESULT { - // Give Flutter, including plugins, an opportunity to handle window messages. - if (view_controller_) { - auto const result{view_controller_->HandleTopLevelWindowProc( - hwnd, message, wparam, lparam)}; - if (result) { - return *result; - } - } - - switch (message) { - case WM_FONTCHANGE: - engine_->ReloadSystemFonts(); - break; - default: - break; - } - - return Win32Window::MessageHandler(hwnd, message, wparam, lparam); -} - -} // namespace flutter diff --git a/shell/platform/windows/client_wrapper/flutter_window_controller.cc b/shell/platform/windows/client_wrapper/flutter_window_controller.cc deleted file mode 100644 index 18978444f12fc..0000000000000 --- a/shell/platform/windows/client_wrapper/flutter_window_controller.cc +++ /dev/null @@ -1,542 +0,0 @@ -#include "include/flutter/flutter_window_controller.h" - -#include "include/flutter/encodable_value.h" -#include "include/flutter/flutter_win32_window.h" -#include "include/flutter/standard_method_codec.h" - -#include -#include - -#include - -namespace { - -auto const* const kChannel{"flutter/windowing"}; -auto const* const kErrorCodeInvalidValue{"INVALID_VALUE"}; -auto const* const kErrorCodeUnavailable{"UNAVAILABLE"}; - -// Retrieves the value associated with |key| from |map|, ensuring it matches -// the expected type |T|. Returns the value if found and correctly typed, -// otherwise logs an error in |result| and returns std::nullopt. -template -auto GetSingleValueForKeyOrSendError(std::string const& key, - flutter::EncodableMap const* map, - flutter::MethodResult<>& result) - -> std::optional { - if (auto const it{map->find(flutter::EncodableValue(key))}; - it != map->end()) { - if (auto const* const value{std::get_if(&it->second)}) { - return *value; - } else { - result.Error(kErrorCodeInvalidValue, "Value for '" + key + - "' key must be of type '" + - typeid(T).name() + "'."); - } - } else { - result.Error(kErrorCodeInvalidValue, - "Map does not contain required '" + key + "' key."); - } - return std::nullopt; -} - -// Retrieves a list of values associated with |key| from |map|, ensuring the -// list has |Size| elements, all of type |T|. Returns the list if found and -// valid, otherwise logs an error in |result| and returns std::nullopt. -template -auto GetListValuesForKeyOrSendError(std::string const& key, - flutter::EncodableMap const* map, - flutter::MethodResult<>& result) - -> std::optional> { - if (auto const it{map->find(flutter::EncodableValue(key))}; - it != map->end()) { - if (auto const* const array{ - std::get_if>(&it->second)}) { - if (array->size() != Size) { - result.Error(kErrorCodeInvalidValue, - "Array for '" + key + "' key must have " + - std::to_string(Size) + " values."); - return std::nullopt; - } - std::vector decoded_values; - for (auto const& value : *array) { - if (std::holds_alternative(value)) { - decoded_values.push_back(std::get(value)); - } else { - result.Error(kErrorCodeInvalidValue, - "Array for '" + key + - "' key must only have values of type '" + - typeid(T).name() + "'."); - return std::nullopt; - } - } - return decoded_values; - } else { - result.Error(kErrorCodeInvalidValue, - "Value for '" + key + "' key must be an array."); - } - } else { - result.Error(kErrorCodeInvalidValue, - "Map does not contain required '" + key + "' key."); - } - return std::nullopt; -} - -// Converts a |flutter::WindowArchetype| to its corresponding wide string -// representation. -auto ArchetypeToWideString(flutter::WindowArchetype archetype) -> std::wstring { - switch (archetype) { - case flutter::WindowArchetype::regular: - return L"regular"; - case flutter::WindowArchetype::floating_regular: - return L"floating_regular"; - case flutter::WindowArchetype::dialog: - return L"dialog"; - case flutter::WindowArchetype::satellite: - return L"satellite"; - case flutter::WindowArchetype::popup: - return L"popup"; - case flutter::WindowArchetype::tip: - return L"tip"; - } - std::cerr - << "Unhandled window archetype encountered in archetypeToWideString: " - << static_cast(archetype) << "\n"; - std::abort(); -} - -} // namespace - -namespace flutter { - -FlutterWindowController::~FlutterWindowController() { - { - std::lock_guard lock(mutex_); - if (channel_) { - channel_->SetMethodCallHandler(nullptr); - } - } - DestroyWindows(); -} - -void FlutterWindowController::DestroyWindows() { - std::unique_lock lock(mutex_); - std::vector view_ids; - view_ids.reserve(windows_.size()); - for (auto const& [view_id, _] : windows_) { - view_ids.push_back(view_id); - } - lock.unlock(); - for (auto const& view_id : view_ids) { - DestroyFlutterWindow(view_id); - } -} - -void FlutterWindowController::SetEngine(std::shared_ptr engine) { - DestroyWindows(); - std::lock_guard const lock(mutex_); - engine_ = std::move(engine); - channel_ = std::make_unique>( - engine_->messenger(), kChannel, &StandardMethodCodec::GetInstance()); - channel_->SetMethodCallHandler( - [this](MethodCall<> const& call, std::unique_ptr> result) { - MethodCallHandler(call, *result); - }); -} - -auto FlutterWindowController::CreateFlutterWindow( - std::wstring const& title, - WindowSize const& size, - WindowArchetype archetype, - std::optional positioner, - std::optional parent_view_id) - -> std::optional { - std::unique_lock lock(mutex_); - if (!engine_) { - std::cerr << "Cannot create window without an engine.\n"; - return std::nullopt; - } - - auto window{std::make_unique(engine_, win32_)}; - - std::optional const parent_hwnd{ - parent_view_id.has_value() && - windows_.find(parent_view_id.value()) != windows_.end() - ? std::optional{windows_[parent_view_id.value()]->GetHandle()} - : std::nullopt}; - - lock.unlock(); - - if (!window->Create(title, size, archetype, parent_hwnd, positioner)) { - return std::nullopt; - } - - lock.lock(); - - // Assume first window is the main window - if (windows_.empty()) { - window->SetQuitOnClose(true); - } - - auto const view_id{window->GetFlutterViewId()}; - windows_[view_id] = std::move(window); - - SendOnWindowCreated(view_id, parent_view_id); - - WindowMetadata result{.view_id = view_id, - .archetype = archetype, - .size = GetWindowSize(view_id), - .parent_id = parent_view_id}; - - return result; -} - -auto FlutterWindowController::DestroyFlutterWindow(FlutterViewId view_id) - -> bool { - std::unique_lock lock(mutex_); - auto it{windows_.find(view_id)}; - if (it != windows_.end()) { - auto* const window{it->second.get()}; - - lock.unlock(); - - // |window| will be removed from |windows_| when WM_NCDESTROY is handled - win32_->DestroyWindow(window->GetHandle()); - - return true; - } - return false; -} - -FlutterWindowController::FlutterWindowController() - : win32_{std::make_shared()} {} - -FlutterWindowController::FlutterWindowController( - std::shared_ptr wrapper) - : win32_{std::move(wrapper)} {} - -void FlutterWindowController::MethodCallHandler(MethodCall<> const& call, - MethodResult<>& result) { - if (call.method_name() == "createWindow") { - HandleCreateWindow(WindowArchetype::regular, call, result); - } else if (call.method_name() == "createPopup") { - HandleCreateWindow(WindowArchetype::popup, call, result); - } else if (call.method_name() == "destroyWindow") { - HandleDestroyWindow(call, result); - } else { - result.NotImplemented(); - } -} - -auto FlutterWindowController::MessageHandler(HWND hwnd, - UINT message, - WPARAM wparam, - LPARAM lparam) -> LRESULT { - switch (message) { - case WM_NCDESTROY: { - std::unique_lock lock{mutex_}; - auto const it{std::find_if(windows_.begin(), windows_.end(), - [hwnd](auto const& window) { - return window.second->GetHandle() == hwnd; - })}; - if (it != windows_.end()) { - auto const view_id{it->first}; - auto const quit_on_close{it->second.get()->GetQuitOnClose()}; - - windows_.erase(it); - - if (quit_on_close) { - auto it2{windows_.begin()}; - while (it2 != windows_.end()) { - auto const& that{it2->second}; - lock.unlock(); - DestroyWindow(that->GetHandle()); - lock.lock(); - it2 = windows_.begin(); - } - } - - SendOnWindowDestroyed(view_id); - } - } - return 0; - case WM_ACTIVATE: - if (wparam != WA_INACTIVE) { - if (auto* const window{Win32Window::GetThisFromHandle(hwnd)}) { - if (window->GetArchetype() != WindowArchetype::popup) { - // If a non-popup window is activated, close popups for all windows - std::unique_lock lock(mutex_); - auto it{windows_.begin()}; - while (it != windows_.end()) { - lock.unlock(); - auto const num_popups_closed{it->second->CloseChildPopups()}; - lock.lock(); - if (num_popups_closed > 0) { - it = windows_.begin(); - } else { - ++it; - } - } - } else { - // If a popup window is activated, close its child popups - window->CloseChildPopups(); - } - } - } - break; - case WM_ACTIVATEAPP: - if (wparam == FALSE) { - if (auto* const window{Win32Window::GetThisFromHandle(hwnd)}) { - // Close child popups from all windows if a window - // belonging to a different application is being activated - window->CloseChildPopups(); - } - } - break; - case WM_SIZE: { - std::lock_guard lock{mutex_}; - auto const it{std::find_if(windows_.begin(), windows_.end(), - [hwnd](auto const& window) { - return window.second->GetHandle() == hwnd; - })}; - if (it != windows_.end()) { - auto const view_id{it->first}; - SendOnWindowChanged(view_id); - } - } break; - default: - break; - } - - if (auto* const window{Win32Window::GetThisFromHandle(hwnd)}) { - return window->MessageHandler(hwnd, message, wparam, lparam); - } - return DefWindowProc(hwnd, message, wparam, lparam); -} - -void FlutterWindowController::SendOnWindowCreated( - FlutterViewId view_id, - std::optional parent_view_id) const { - if (channel_) { - channel_->InvokeMethod( - "onWindowCreated", - std::make_unique(EncodableMap{ - {EncodableValue("viewId"), EncodableValue(view_id)}, - {EncodableValue("parentViewId"), - parent_view_id ? EncodableValue(parent_view_id.value()) - : EncodableValue()}})); - } -} - -void FlutterWindowController::SendOnWindowDestroyed( - FlutterViewId view_id) const { - if (channel_) { - channel_->InvokeMethod( - "onWindowDestroyed", - std::make_unique(EncodableMap{ - {EncodableValue("viewId"), EncodableValue(view_id)}, - })); - } -} - -void FlutterWindowController::SendOnWindowChanged(FlutterViewId view_id) const { - if (channel_) { - auto const size{GetWindowSize(view_id)}; - channel_->InvokeMethod( - "onWindowChanged", - std::make_unique(EncodableMap{ - {EncodableValue("viewId"), EncodableValue(view_id)}, - {EncodableValue("size"), - EncodableValue(EncodableList{EncodableValue(size.width), - EncodableValue(size.height)})}, - {EncodableValue("relativePosition"), EncodableValue()}, // TODO - {EncodableValue("isMoving"), EncodableValue()}})); // TODO - } -} - -void FlutterWindowController::HandleCreateWindow(WindowArchetype archetype, - MethodCall<> const& call, - MethodResult<>& result) { - auto const* const arguments{call.arguments()}; - auto const* const map{std::get_if(arguments)}; - if (!map) { - result.Error(kErrorCodeInvalidValue, "Method call argument is not a map."); - return; - } - - std::wstring const title{ArchetypeToWideString(archetype)}; - - auto const size_list{ - GetListValuesForKeyOrSendError("size", map, result)}; - if (!size_list) { - return; - } - if (size_list->at(0) < 0 || size_list->at(1) < 0) { - result.Error(kErrorCodeInvalidValue, - "Values for 'size' key (" + std::to_string(size_list->at(0)) + - ", " + std::to_string(size_list->at(1)) + - ") must be nonnegative."); - return; - } - - std::optional positioner; - std::optional anchor_rect; - - if (archetype == WindowArchetype::popup) { - if (auto const anchor_rect_it{map->find(EncodableValue("anchorRect"))}; - anchor_rect_it != map->end()) { - if (!anchor_rect_it->second.IsNull()) { - auto const anchor_rect_list{ - GetListValuesForKeyOrSendError("anchorRect", map, result)}; - if (!anchor_rect_list) { - return; - } - anchor_rect = - WindowRectangle{{anchor_rect_list->at(0), anchor_rect_list->at(1)}, - {anchor_rect_list->at(2), anchor_rect_list->at(3)}}; - } - } else { - result.Error(kErrorCodeInvalidValue, - "Map does not contain required 'anchorRect' key."); - return; - } - - auto const positioner_parent_anchor{GetSingleValueForKeyOrSendError( - "positionerParentAnchor", map, result)}; - if (!positioner_parent_anchor) { - return; - } - auto const positioner_child_anchor{GetSingleValueForKeyOrSendError( - "positionerChildAnchor", map, result)}; - if (!positioner_child_anchor) { - return; - } - auto const child_anchor{ - static_cast(positioner_child_anchor.value())}; - - auto const positioner_offset_list{GetListValuesForKeyOrSendError( - "positionerOffset", map, result)}; - if (!positioner_offset_list) { - return; - } - auto const positioner_constraint_adjustment{ - GetSingleValueForKeyOrSendError("positionerConstraintAdjustment", - map, result)}; - if (!positioner_constraint_adjustment) { - return; - } - positioner = WindowPositioner{ - .anchor_rect = anchor_rect, - .parent_anchor = static_cast( - positioner_parent_anchor.value()), - .child_anchor = child_anchor, - .offset = {positioner_offset_list->at(0), - positioner_offset_list->at(1)}, - .constraint_adjustment = - static_cast( - positioner_constraint_adjustment.value())}; - } - - std::optional parent_view_id; - if (archetype == WindowArchetype::popup) { - if (auto const parent_it{map->find(EncodableValue("parent"))}; - parent_it != map->end()) { - if (parent_it->second.IsNull()) { - result.Error(kErrorCodeInvalidValue, - "Value for 'parent' key must not be null."); - return; - } else { - if (auto const* const parent{std::get_if(&parent_it->second)}) { - parent_view_id = *parent >= 0 ? std::optional(*parent) - : std::nullopt; - if (!parent_view_id.has_value() && - archetype == WindowArchetype::popup) { - result.Error(kErrorCodeInvalidValue, - "Value for 'parent' key (" + - std::to_string(parent_view_id.value()) + - ") must be nonnegative."); - return; - } - } else { - result.Error(kErrorCodeInvalidValue, - "Value for 'parent' key must be of type int."); - return; - } - } - } else { - result.Error(kErrorCodeInvalidValue, - "Map does not contain required 'parent' key."); - return; - } - } - - if (auto const data_opt{CreateFlutterWindow( - title, {.width = size_list->at(0), .height = size_list->at(1)}, - archetype, positioner, parent_view_id)}) { - auto const& data{data_opt.value()}; - result.Success(EncodableValue(EncodableMap{ - {EncodableValue("viewId"), EncodableValue(data.view_id)}, - {EncodableValue("archetype"), - EncodableValue(static_cast(data.archetype))}, - {EncodableValue("size"), - EncodableValue(EncodableList{EncodableValue(data.size.width), - EncodableValue(data.size.height)})}, - {EncodableValue("parentViewId"), - data.parent_id ? EncodableValue(data.parent_id.value()) - : EncodableValue()}})); - } else { - result.Error(kErrorCodeUnavailable, "Can't create window."); - } -} - -void FlutterWindowController::HandleDestroyWindow(MethodCall<> const& call, - MethodResult<>& result) { - auto const* const arguments{call.arguments()}; - auto const* const map{std::get_if(arguments)}; - if (!map) { - result.Error(kErrorCodeInvalidValue, "Method call argument is not a map."); - return; - } - - auto const view_id{ - GetSingleValueForKeyOrSendError("viewId", map, result)}; - if (!view_id) { - return; - } - if (view_id.value() < 0) { - result.Error(kErrorCodeInvalidValue, "Value for 'viewId' (" + - std::to_string(view_id.value()) + - ") cannot be negative."); - return; - } - - if (!DestroyFlutterWindow(view_id.value())) { - result.Error(kErrorCodeInvalidValue, "Can't find window with 'viewId' (" + - std::to_string(view_id.value()) + - ")."); - return; - } - - result.Success(); -} - -WindowSize FlutterWindowController::GetWindowSize( - flutter::FlutterViewId view_id) const { - auto* const hwnd{windows_.at(view_id)->GetHandle()}; - RECT frame_rect; - DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rect, - sizeof(frame_rect)); - - // Convert to logical coordinates - auto const dpr{FlutterDesktopGetDpiForHWND(hwnd) / - static_cast(USER_DEFAULT_SCREEN_DPI)}; - frame_rect.left = static_cast(frame_rect.left / dpr); - frame_rect.top = static_cast(frame_rect.top / dpr); - frame_rect.right = static_cast(frame_rect.right / dpr); - frame_rect.bottom = static_cast(frame_rect.bottom / dpr); - - auto const width{frame_rect.right - frame_rect.left}; - auto const height{frame_rect.bottom - frame_rect.top}; - return {static_cast(width), static_cast(height)}; -} - -} // namespace flutter diff --git a/shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc b/shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc deleted file mode 100644 index 2e90d4898e905..0000000000000 --- a/shell/platform/windows/client_wrapper/flutter_window_controller_unittests.cc +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "flutter/shell/platform/common/client_wrapper/include/flutter/encodable_value.h" -#include "flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_win32_window.h" -#include "flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h" -#include "flutter/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h" - -#include "gmock/gmock.h" -#include "gtest/gtest.h" - -using ::testing::_; -using ::testing::AnyNumber; -using ::testing::Eq; -using ::testing::Gt; -using ::testing::IsNull; -using ::testing::Mock; -using ::testing::NiceMock; -using ::testing::Return; -using ::testing::StrEq; - -namespace flutter { - -namespace { - -HWND const k_hwnd{reinterpret_cast(-1)}; - -// Stub implementation to validate calls to the API. -class TestWindowsApi : public testing::StubFlutterWindowsApi { - public: - // |flutter::testing::StubFlutterWindowsApi| - FlutterDesktopViewControllerRef EngineCreateViewController( - const FlutterDesktopViewControllerProperties* properties) override { - return reinterpret_cast(2); - } -}; - -// Mocked classes -class MockWin32Wrapper : public Win32Wrapper { - public: - MOCK_METHOD(HWND, - CreateWindowEx, - (DWORD dwExStyle, - LPCWSTR lpClassName, - LPCWSTR lpWindowName, - DWORD dwStyle, - int X, - int Y, - int nWidth, - int nHeight, - HWND hWndParent, - HMENU hMenu, - HINSTANCE hInstance, - LPVOID lpParam), - (override)); - MOCK_METHOD(BOOL, DestroyWindow, (HWND hWnd), (override)); -}; - -class MockMethodResult : public MethodResult<> { - public: - MOCK_METHOD(void, - SuccessInternal, - (EncodableValue const* result), - (override)); - MOCK_METHOD(void, - ErrorInternal, - (std::string const& error_code, - std::string const& error_message, - EncodableValue const* error_details), - (override)); - MOCK_METHOD(void, NotImplementedInternal, (), (override)); -}; - -class MockFlutterWindowController : public FlutterWindowController { - public: - using FlutterWindowController::MessageHandler; - using FlutterWindowController::MethodCallHandler; - - MockFlutterWindowController(std::shared_ptr wrapper) - : FlutterWindowController(std::move(wrapper)) {} - - MOCK_METHOD(void, - SendOnWindowCreated, - (FlutterViewId view_id, - std::optional parent_view_id), - (override, const)); - MOCK_METHOD(void, - SendOnWindowDestroyed, - (FlutterViewId view_id), - (override, const)); - MOCK_METHOD(void, - SendOnWindowChanged, - (FlutterViewId view_id), - (override, const)); -}; - -// Test fixture -class FlutterWindowControllerTest : public ::testing::Test { - protected: - void SetUp() override { - DartProject project(L"test"); - engine_ = std::make_shared(project); - mock_win32_ = std::make_shared>(); - mock_controller_ = - std::make_unique>(mock_win32_); - mock_controller_->SetEngine(engine_); - - ON_CALL(*mock_win32_, CreateWindowEx).WillByDefault(Return(k_hwnd)); - ON_CALL(*mock_win32_, DestroyWindow).WillByDefault([&](HWND hwnd) { - mock_controller_->MessageHandler(hwnd, WM_NCDESTROY, 0, 0); - return TRUE; - }); - } - - std::shared_ptr engine_; - std::shared_ptr> mock_win32_; - std::unique_ptr> mock_controller_; -}; - -} // namespace - -TEST_F(FlutterWindowControllerTest, CreateRegularWindow) { - testing::ScopedStubFlutterWindowsApi scoped_api_stub( - std::make_unique()); - auto test_api{static_cast(scoped_api_stub.stub())}; - - auto const title{L"window"}; - WindowSize const size{800, 600}; - auto const archetype{WindowArchetype::regular}; - std::optional const positioner; - std::optional const parent_view_id; - - EXPECT_CALL(*mock_win32_, - CreateWindowEx(0, _, StrEq(title), WS_OVERLAPPEDWINDOW, - CW_USEDEFAULT, CW_USEDEFAULT, Gt(size.width), - Gt(size.height), IsNull(), _, _, _)) - .Times(1); - - auto const result{mock_controller_->CreateFlutterWindow( - title, size, archetype, positioner, parent_view_id)}; - - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(result->view_id, 1); - EXPECT_FALSE(result->parent_id.has_value()); - EXPECT_EQ(result->archetype, archetype); -} - -TEST_F(FlutterWindowControllerTest, DestroyWindow) { - testing::ScopedStubFlutterWindowsApi scoped_api_stub( - std::make_unique()); - auto test_api{static_cast(scoped_api_stub.stub())}; - - auto const title{L"window"}; - WindowSize const size{800, 600}; - auto const archetype{WindowArchetype::regular}; - - auto const create_result{mock_controller_->CreateFlutterWindow( - title, size, archetype, std::nullopt, std::nullopt)}; - - ASSERT_TRUE(create_result.has_value()); - - EXPECT_CALL(*mock_win32_, DestroyWindow(k_hwnd)).Times(1); - - EXPECT_TRUE(mock_controller_->DestroyFlutterWindow(1)); -} - -TEST_F(FlutterWindowControllerTest, DestroyWindowWithInvalidView) { - testing::ScopedStubFlutterWindowsApi scoped_api_stub( - std::make_unique()); - auto test_api{static_cast(scoped_api_stub.stub())}; - - auto const title{L"window"}; - WindowSize const size{800, 600}; - auto const archetype{WindowArchetype::regular}; - - auto const create_result{mock_controller_->CreateFlutterWindow( - title, size, archetype, std::nullopt, std::nullopt)}; - - ASSERT_TRUE(create_result.has_value()); - - EXPECT_CALL(*mock_win32_, DestroyWindow(k_hwnd)).Times(0); - - EXPECT_FALSE(mock_controller_->DestroyFlutterWindow(9999)); - - Mock::VerifyAndClearExpectations(mock_win32_.get()); -} - -TEST_F(FlutterWindowControllerTest, SendOnWindowCreated) { - testing::ScopedStubFlutterWindowsApi scoped_api_stub( - std::make_unique()); - auto test_api{static_cast(scoped_api_stub.stub())}; - - auto const title{L"window"}; - WindowSize const size{800, 600}; - auto const archetype{WindowArchetype::regular}; - - EXPECT_CALL(*mock_controller_, SendOnWindowCreated(1, Eq(std::nullopt))) - .Times(1); - - auto const create_result{mock_controller_->CreateFlutterWindow( - title, size, archetype, std::nullopt, std::nullopt)}; -} - -TEST_F(FlutterWindowControllerTest, SendOnWindowDestroyed) { - testing::ScopedStubFlutterWindowsApi scoped_api_stub( - std::make_unique()); - auto test_api{static_cast(scoped_api_stub.stub())}; - - auto const title{L"window"}; - WindowSize const size{800, 600}; - auto const archetype{WindowArchetype::regular}; - - auto const create_result{mock_controller_->CreateFlutterWindow( - title, size, archetype, std::nullopt, std::nullopt)}; - - ASSERT_TRUE(create_result.has_value()); - - EXPECT_CALL(*mock_controller_, SendOnWindowDestroyed).Times(1); - - mock_controller_->DestroyFlutterWindow(1); -} - -TEST_F(FlutterWindowControllerTest, SendOnWindowChangedWhenWindowIsResized) { - testing::ScopedStubFlutterWindowsApi scoped_api_stub( - std::make_unique()); - auto test_api{static_cast(scoped_api_stub.stub())}; - - auto const title{L"window"}; - WindowSize const size{800, 600}; - auto const archetype{WindowArchetype::regular}; - - auto const create_result{mock_controller_->CreateFlutterWindow( - title, size, archetype, std::nullopt, std::nullopt)}; - - EXPECT_CALL(*mock_controller_, SendOnWindowChanged(1)).Times(1); - - mock_controller_->MessageHandler(k_hwnd, WM_SIZE, 0, 0); -} - -TEST_F(FlutterWindowControllerTest, CreateRegularWindowUsingMethodCall) { - testing::ScopedStubFlutterWindowsApi scoped_api_stub( - std::make_unique()); - auto test_api{static_cast(scoped_api_stub.stub())}; - - WindowSize const size{800, 600}; - - EncodableMap const arguments{ - {EncodableValue("size"), - EncodableValue(EncodableList{EncodableValue(size.width), - EncodableValue(size.height)})}, - }; - MethodCall<> call("createWindow", - std::make_unique(arguments)); - - NiceMock mock_result; - - EXPECT_CALL(mock_result, SuccessInternal(_)).Times(1); - EXPECT_CALL(*mock_win32_, - CreateWindowEx(0, _, StrEq(L"regular"), WS_OVERLAPPEDWINDOW, - CW_USEDEFAULT, CW_USEDEFAULT, Gt(size.width), - Gt(size.height), IsNull(), _, _, _)) - .Times(1); - - mock_controller_->MethodCallHandler(call, mock_result); -} - -TEST_F(FlutterWindowControllerTest, DestroyWindowUsingMethodCall) { - testing::ScopedStubFlutterWindowsApi scoped_api_stub( - std::make_unique()); - auto test_api{static_cast(scoped_api_stub.stub())}; - - auto const title{L"window"}; - WindowSize size{800, 600}; - auto const archetype{WindowArchetype::regular}; - auto create_result{mock_controller_->CreateFlutterWindow( - title, size, archetype, std::nullopt, std::nullopt)}; - - ASSERT_TRUE(create_result.has_value()); - - EncodableMap const arguments{ - {EncodableValue("viewId"), - EncodableValue(static_cast(create_result->view_id))}, - }; - MethodCall<> call("destroyWindow", - std::make_unique(arguments)); - - NiceMock mock_result; - - EXPECT_CALL(mock_result, SuccessInternal(_)).Times(1); - EXPECT_CALL(*mock_win32_, DestroyWindow(k_hwnd)).Times(1); - - mock_controller_->MethodCallHandler(call, mock_result); -} - -} // namespace flutter diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h index 89ca2d188b46f..0369db35a14fc 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h +++ b/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h @@ -98,8 +98,11 @@ class FlutterEngine : public PluginRegistry { // For access to the engine handle. friend class FlutterViewController; - // Get the handle for interacting with the C API's engine reference. - FlutterDesktopEngineRef engine() const; + // Gives up ownership of |engine_|, but keeps a weak reference to it. + // + // This is intended to be used by FlutterViewController, since the underlying + // C API for view controllers takes over engine ownership. + FlutterDesktopEngineRef RelinquishEngine(); // Handle for interacting with the C API's engine reference. FlutterDesktopEngineRef engine_ = nullptr; @@ -107,6 +110,9 @@ class FlutterEngine : public PluginRegistry { // Messenger for communicating with the engine. std::unique_ptr messenger_; + // Whether or not this wrapper owns |engine_|. + bool owns_engine_ = true; + // Whether |Run| has been called successfully. // // This is used to improve error messages. This can be false while the engine diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h index b26e017a6760a..4007534a5d73e 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h +++ b/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h @@ -32,16 +32,6 @@ class FlutterViewController { // |dart_project| will be used to configure the engine backing this view. FlutterViewController(int width, int height, const DartProject& project); - // Creates a FlutterView that can be parented into a Windows View hierarchy - // either using HWNDs. - // - // This creates the view on an existing FlutterEngine. - // - // |dart_project| will be used to configure the engine backing this view. - FlutterViewController(int width, - int height, - std::shared_ptr engine); - virtual ~FlutterViewController(); // Prevent copying. @@ -54,9 +44,6 @@ class FlutterViewController { // Returns the engine running Flutter content in this view. FlutterEngine* engine() const { return engine_.get(); } - // Returns the engine running Flutter content in this view. - std::shared_ptr shared_engine() const { return engine_; } - // Returns the view managed by this controller. FlutterView* view() const { return view_.get(); } diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_win32_window.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_win32_window.h deleted file mode 100644 index b91d50fef7c75..0000000000000 --- a/shell/platform/windows/client_wrapper/include/flutter/flutter_win32_window.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WIN32_WINDOW_H_ -#define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WIN32_WINDOW_H_ - -#include "flutter_view_controller.h" - -#include "win32_window.h" - -namespace flutter { - -// A window that does nothing but host a Flutter view. -class FlutterWin32Window : public Win32Window { - public: - // Creates a new FlutterWin32Window hosting a Flutter view running |engine|. - explicit FlutterWin32Window(std::shared_ptr engine); - FlutterWin32Window(std::shared_ptr engine, - std::shared_ptr wrapper); - ~FlutterWin32Window() override = default; - - auto GetFlutterViewId() const -> FlutterViewId; - - protected: - // Win32Window: - auto OnCreate() -> bool override; - void OnDestroy() override; - auto MessageHandler(HWND hwnd, - UINT message, - WPARAM wparam, - LPARAM lparam) -> LRESULT override; - - private: - // The engine this window is attached to. - std::shared_ptr engine_; - - // The Flutter instance hosted by this window. - std::unique_ptr view_controller_; -}; - -} // namespace flutter - -#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WIN32_WINDOW_H_ diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h deleted file mode 100644 index ecaf0ce467ffb..0000000000000 --- a/shell/platform/windows/client_wrapper/include/flutter/flutter_window_controller.h +++ /dev/null @@ -1,71 +0,0 @@ -#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WINDOW_CONTROLLER_H_ -#define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WINDOW_CONTROLLER_H_ - -#include - -#include "flutter_engine.h" -#include "method_channel.h" -#include "win32_wrapper.h" -#include "windowing.h" - -namespace flutter { - -// A singleton controller for Flutter windows. -class FlutterWindowController { - public: - virtual ~FlutterWindowController(); - - // Prevent copying. - FlutterWindowController(FlutterWindowController const&) = delete; - FlutterWindowController& operator=(FlutterWindowController const&) = delete; - - void SetEngine(std::shared_ptr engine); - auto CreateFlutterWindow(std::wstring const& title, - WindowSize const& size, - WindowArchetype archetype, - std::optional positioner, - std::optional parent_view_id) - -> std::optional; - auto DestroyFlutterWindow(FlutterViewId view_id) -> bool; - - static FlutterWindowController& GetInstance() { - static FlutterWindowController instance; - return instance; - } - - protected: - FlutterWindowController(); - FlutterWindowController(std::shared_ptr wrapper); - - void MethodCallHandler(MethodCall<> const& call, MethodResult<>& result); - auto MessageHandler(HWND hwnd, - UINT message, - WPARAM wparam, - LPARAM lparam) -> LRESULT; - - virtual void SendOnWindowCreated( - FlutterViewId view_id, - std::optional parent_view_id) const; - virtual void SendOnWindowDestroyed(FlutterViewId view_id) const; - virtual void SendOnWindowChanged(FlutterViewId view_id) const; - - private: - friend class Win32Window; - - void DestroyWindows(); - auto GetWindowSize(FlutterViewId view_id) const -> WindowSize; - void HandleCreateWindow(WindowArchetype archetype, - MethodCall<> const& call, - MethodResult<>& result); - void HandleDestroyWindow(MethodCall<> const& call, MethodResult<>& result); - - mutable std::mutex mutex_; - std::shared_ptr win32_; - std::unique_ptr> channel_; - std::shared_ptr engine_; - std::unordered_map> windows_; -}; - -} // namespace flutter - -#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_FLUTTER_WINDOW_CONTROLLER_H_ diff --git a/shell/platform/windows/client_wrapper/include/flutter/win32_window.h b/shell/platform/windows/client_wrapper/include/flutter/win32_window.h deleted file mode 100644 index 75d8053f81067..0000000000000 --- a/shell/platform/windows/client_wrapper/include/flutter/win32_window.h +++ /dev/null @@ -1,125 +0,0 @@ -#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WINDOW_H_ -#define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WINDOW_H_ - -#include "win32_wrapper.h" -#include "windowing.h" - -#include - -#include -#include -#include -#include - -namespace flutter { - -// A class abstraction for a high DPI-aware Win32 Window. Intended to be -// inherited from by classes that wish to specialize with custom -// rendering and input handling. -class Win32Window { - public: - Win32Window(); - explicit Win32Window(std::shared_ptr wrapper); - virtual ~Win32Window(); - - // Retrieves a class instance pointer for |hwnd|. - static auto GetThisFromHandle(HWND hwnd) -> Win32Window*; - - // Returns the backing window handle to enable clients to set icon and other - // window properties. Returns nullptr if the window has been destroyed. - auto GetHandle() const -> HWND; - - // If |quit_on_close| is true, closing this window will quit the application. - void SetQuitOnClose(bool quit_on_close); - - // Returns true if closing this window will cause the application to quit. - auto GetQuitOnClose() const -> bool; - - // Returns the bounds of the current client area. - auto GetClientArea() const -> RECT; - - // Returns the current window archetype. - auto GetArchetype() const -> WindowArchetype; - - protected: - // Creates a native Win32 window. |title| is the window title string. - // |client_size| specifies the requested size of the client rectangle (i.e., - // the size of the view). The window style is determined by |archetype|. For - // |FlutterWindowArchetype::popup|, both |parent| and |positioner| must be - // provided; |positioner| is used only for this archetype. After successful - // creation, |OnCreate| is called, and its result is returned. Otherwise, the - // return value is false. - auto Create(std::wstring const& title, - WindowSize const& client_size, - WindowArchetype archetype, - std::optional parent, - std::optional positioner) -> bool; - - // Release OS resources associated with window. - void Destroy(); - - // Inserts |content| into the window tree. - void SetChildContent(HWND content); - - // Processes and route salient window messages for mouse handling, - // size change and DPI. Delegates handling of these to member overloads that - // inheriting classes can handle. - virtual auto MessageHandler(HWND hwnd, - UINT message, - WPARAM wparam, - LPARAM lparam) -> LRESULT; - - // Called when Create is called, allowing subclass window-related setup. - // Subclasses should return false if setup fails. - virtual auto OnCreate() -> bool; - - // Called when Destroy is called. - virtual void OnDestroy(); - - private: - friend class FlutterWindowController; - - // OS callback called by message pump. Handles the WM_NCCREATE message which - // is passed when the non-client area is being created and enables automatic - // non-client DPI scaling so that the non-client area automatically - // responds to changes in DPI. All other messages are handled by the - // controller's MessageHandler. - static auto CALLBACK WndProc(HWND hwnd, - UINT message, - WPARAM wparam, - LPARAM lparam) -> LRESULT; - - // Wrapper for Win32 API calls. - std::shared_ptr win32_; - - // The window's archetype (e.g., regular, dialog, popup). - WindowArchetype archetype_{WindowArchetype::regular}; - - // Windows that have this window as their parent or owner. - std::set children_; - - // The number of popups in |children_|, used to quickly check whether this - // window has any popups. - size_t num_child_popups_{0}; - - // Indicates whether closing this window will quit the application. - bool quit_on_close_{false}; - - // Handle for the top-level window. - HWND window_handle_{nullptr}; - - // Handle for hosted child content window. - HWND child_content_{nullptr}; - - // Controls whether the non-client area can be redrawn as inactive. - // Enabled by default, but temporarily disabled during child popup destruction - // to prevent flickering. - bool enable_redraw_non_client_as_inactive_{true}; - - // Closes the popups of this window and returns the number of popups closed. - auto CloseChildPopups() -> std::size_t; -}; - -} // namespace flutter - -#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WINDOW_H_ diff --git a/shell/platform/windows/client_wrapper/include/flutter/win32_wrapper.h b/shell/platform/windows/client_wrapper/include/flutter/win32_wrapper.h deleted file mode 100644 index 5e81e2130f64f..0000000000000 --- a/shell/platform/windows/client_wrapper/include/flutter/win32_wrapper.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WRAPPER_H_ -#define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WRAPPER_H_ - -#include - -namespace flutter { - -// Wraps Win32 API calls to enable mock-based testing. -class Win32Wrapper { - public: - virtual ~Win32Wrapper() = default; - - virtual HWND CreateWindowEx(DWORD dwExStyle, - LPCWSTR lpClassName, - LPCWSTR lpWindowName, - DWORD dwStyle, - int X, - int Y, - int nWidth, - int nHeight, - HWND hWndParent, - HMENU hMenu, - HINSTANCE hInstance, - LPVOID lpParam) { - return ::CreateWindowEx(dwExStyle, lpClassName, lpWindowName, dwStyle, X, Y, - nWidth, nHeight, hWndParent, hMenu, hInstance, - lpParam); - } - virtual BOOL DestroyWindow(HWND hWnd) { return ::DestroyWindow(hWnd); } -}; - -} // namespace flutter - -#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_WIN32_WRAPPER_H_ diff --git a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc index b5428bc1d26b1..f51d7f14ad879 100644 --- a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc +++ b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc @@ -114,15 +114,6 @@ bool FlutterDesktopEngineRun(FlutterDesktopEngineRef engine, return true; } -FlutterDesktopViewControllerRef FlutterDesktopEngineCreateViewController( - FlutterDesktopEngineRef engine, - const FlutterDesktopViewControllerProperties* properties) { - if (s_stub_implementation) { - return s_stub_implementation->EngineCreateViewController(properties); - } - return nullptr; -} - uint64_t FlutterDesktopEngineProcessMessages(FlutterDesktopEngineRef engine) { if (s_stub_implementation) { return s_stub_implementation->EngineProcessMessages(); @@ -237,17 +228,3 @@ void FlutterDesktopPluginRegistrarUnregisterTopLevelWindowProcDelegate( ->PluginRegistrarUnregisterTopLevelWindowProcDelegate(delegate); } } - -UINT FlutterDesktopGetDpiForMonitor(HMONITOR monitor) { - if (s_stub_implementation) { - return s_stub_implementation->GetDpiForMonitor(monitor); - } - return 96; -} - -UINT FlutterDesktopGetDpiForHWND(HWND hwnd) { - if (s_stub_implementation) { - return s_stub_implementation->GetDpiForHWND(hwnd); - } - return 96; -} diff --git a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h index a9780a8c3a53c..8f3eb0905ac7a 100644 --- a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h +++ b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h @@ -63,12 +63,6 @@ class StubFlutterWindowsApi { // Called for FlutterDesktopEngineRun. virtual bool EngineRun(const char* entry_point) { return true; } - // Called for FlutterDesktopEngineCreateViewController. - virtual FlutterDesktopViewControllerRef EngineCreateViewController( - const FlutterDesktopViewControllerProperties* properties) { - return nullptr; - } - // Called for FlutterDesktopEngineProcessMessages. virtual uint64_t EngineProcessMessages() { return 0; } @@ -121,12 +115,6 @@ class StubFlutterWindowsApi { LRESULT* result) { return false; } - - // Called for FlutterDesktopGetDpiForMonitor. - virtual UINT GetDpiForMonitor(HMONITOR monitor) { return 96; } - - // Called for FlutterDesktopGetDpiForHWND. - virtual UINT GetDpiForHWND(HWND hwnd) { return 96; } }; // A test helper that owns a stub implementation, making it the test stub for diff --git a/shell/platform/windows/client_wrapper/win32_window.cc b/shell/platform/windows/client_wrapper/win32_window.cc deleted file mode 100644 index 0e95cc9817791..0000000000000 --- a/shell/platform/windows/client_wrapper/win32_window.cc +++ /dev/null @@ -1,662 +0,0 @@ -#include "include/flutter/win32_window.h" -#include "include/flutter/flutter_window_controller.h" - -#include "flutter_windows.h" - -#include -#include -#include -#include -#include - -#include - -namespace { - -auto const* const kWindowClassName{L"FLUTTER_WIN32_WINDOW"}; - -// The number of Win32Window objects that currently exist. -static int gActiveWindowCount{0}; -// A mutex for thread-safe use of the window count. -static std::mutex gActiveWindowMutex; - -// Retrieves the calling thread's last-error code message as a string, -// or a fallback message if the error message cannot be formatted. -auto GetLastErrorAsString() -> std::string { - LPWSTR message_buffer{nullptr}; - - if (auto const size{FormatMessage( - FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - nullptr, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - reinterpret_cast(&message_buffer), 0, nullptr)}) { - std::wstring const wide_message(message_buffer, size); - LocalFree(message_buffer); - message_buffer = nullptr; - - if (auto const buffer_size{ - WideCharToMultiByte(CP_UTF8, 0, wide_message.c_str(), -1, nullptr, - 0, nullptr, nullptr)}) { - std::string message(buffer_size, 0); - WideCharToMultiByte(CP_UTF8, 0, wide_message.c_str(), -1, &message[0], - buffer_size, nullptr, nullptr); - return message; - } - } - - if (message_buffer) { - LocalFree(message_buffer); - } - std::ostringstream oss; - oss << "Format message failed with 0x" << std::hex << std::setfill('0') - << std::setw(8) << GetLastError() << '\n'; - return oss.str(); -} - -// Estimates the size of the window frame, in physical coordinates, based on -// the given |window_size| (in physical coordinates) and the specified -// |window_style|, |extended_window_style|, and parent window |parent_hwnd|. -auto GetFrameSizeForWindowSize(flutter::WindowSize const& window_size, - DWORD window_style, - DWORD extended_window_style, - HWND parent_hwnd) -> flutter::WindowSize { - RECT frame_rect{0, 0, static_cast(window_size.width), - static_cast(window_size.height)}; - - WNDCLASS window_class{0}; - window_class.lpfnWndProc = DefWindowProc; - window_class.hInstance = GetModuleHandle(nullptr); - window_class.lpszClassName = L"FLUTTER_WIN32_WINDOW_TEMPORARY"; - RegisterClass(&window_class); - - window_style &= ~WS_VISIBLE; - if (auto const window{CreateWindowEx( - extended_window_style, window_class.lpszClassName, L"", window_style, - CW_USEDEFAULT, CW_USEDEFAULT, window_size.width, window_size.height, - parent_hwnd, nullptr, GetModuleHandle(nullptr), nullptr)}) { - DwmGetWindowAttribute(window, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rect, - sizeof(frame_rect)); - DestroyWindow(window); - } - - UnregisterClass(window_class.lpszClassName, nullptr); - - return {static_cast(frame_rect.right - frame_rect.left), - static_cast(frame_rect.bottom - frame_rect.top)}; -} - -// Calculates the required window size, in physical coordinates, to -// accommodate the given |client_size| (in logical coordinates) for a window -// with the specified |window_style| and |extended_window_style|. The result -// accounts for window borders, non-client areas, and drop-shadow effects. -auto GetWindowSizeForClientSize(flutter::WindowSize const& client_size, - DWORD window_style, - DWORD extended_window_style, - HWND parent_hwnd) -> flutter::WindowSize { - auto const dpi{FlutterDesktopGetDpiForHWND(parent_hwnd)}; - auto const scale_factor{static_cast(dpi) / USER_DEFAULT_SCREEN_DPI}; - RECT rect{.left = 0, - .top = 0, - .right = static_cast(client_size.width * scale_factor), - .bottom = static_cast(client_size.height * scale_factor)}; - - HMODULE const user32_module{LoadLibraryA("User32.dll")}; - if (user32_module) { - using AdjustWindowRectExForDpi = BOOL __stdcall( - LPRECT lpRect, DWORD dwStyle, BOOL bMenu, DWORD dwExStyle, UINT dpi); - - auto* const adjust_window_rect_ext_for_dpi{ - reinterpret_cast( - GetProcAddress(user32_module, "AdjustWindowRectExForDpi"))}; - if (adjust_window_rect_ext_for_dpi) { - if (adjust_window_rect_ext_for_dpi(&rect, window_style, FALSE, - extended_window_style, dpi)) { - FreeLibrary(user32_module); - return {static_cast(rect.right - rect.left), - static_cast(rect.bottom - rect.top)}; - } else { - std::cerr << "Failed to run AdjustWindowRectExForDpi: " - << GetLastErrorAsString() << '\n'; - } - } else { - std::cerr << "Failed to retrieve AdjustWindowRectExForDpi address from " - "User32.dll.\n"; - } - FreeLibrary(user32_module); - } else { - std::cerr << "Failed to load User32.dll.\n"; - } - - if (!AdjustWindowRectEx(&rect, window_style, FALSE, extended_window_style)) { - std::cerr << "Failed to run AdjustWindowRectEx: " << GetLastErrorAsString() - << '\n'; - } - return {static_cast(rect.right - rect.left), - static_cast(rect.bottom - rect.top)}; -} - -// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module -// so that the non-client area automatically responds to changes in DPI. -// This API is only needed for PerMonitor V1 awareness mode. -void EnableFullDpiSupportIfAvailable(HWND hwnd) { - HMODULE user32_module = LoadLibraryA("User32.dll"); - if (!user32_module) { - return; - } - - using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); - - auto enable_non_client_dpi_scaling = - reinterpret_cast( - GetProcAddress(user32_module, "EnableNonClientDpiScaling")); - if (enable_non_client_dpi_scaling != nullptr) { - enable_non_client_dpi_scaling(hwnd); - } - - FreeLibrary(user32_module); -} - -// Dynamically loads |SetWindowCompositionAttribute| from the User32 module to -// make the window's background transparent. -void EnableTransparentWindowBackground(HWND hwnd) { - HMODULE const user32_module{LoadLibraryA("User32.dll")}; - if (!user32_module) { - return; - } - - enum WINDOWCOMPOSITIONATTRIB { WCA_ACCENT_POLICY = 19 }; - - struct WINDOWCOMPOSITIONATTRIBDATA { - WINDOWCOMPOSITIONATTRIB Attrib; - PVOID pvData; - SIZE_T cbData; - }; - - using SetWindowCompositionAttribute = - BOOL(__stdcall*)(HWND, WINDOWCOMPOSITIONATTRIBDATA*); - - auto set_window_composition_attribute{ - reinterpret_cast( - GetProcAddress(user32_module, "SetWindowCompositionAttribute"))}; - if (set_window_composition_attribute != nullptr) { - enum ACCENT_STATE { ACCENT_DISABLED = 0 }; - - struct ACCENT_POLICY { - ACCENT_STATE AccentState; - DWORD AccentFlags; - DWORD GradientColor; - DWORD AnimationId; - }; - - // Set the accent policy to disable window composition - ACCENT_POLICY accent{ACCENT_DISABLED, 2, static_cast(0), 0}; - WINDOWCOMPOSITIONATTRIBDATA data{.Attrib = WCA_ACCENT_POLICY, - .pvData = &accent, - .cbData = sizeof(accent)}; - set_window_composition_attribute(hwnd, &data); - - // Extend the frame into the client area and set the window's system - // backdrop type for visual effects - MARGINS const margins{-1}; - ::DwmExtendFrameIntoClientArea(hwnd, &margins); - INT effect_value{1}; - ::DwmSetWindowAttribute(hwnd, DWMWA_SYSTEMBACKDROP_TYPE, &effect_value, - sizeof(BOOL)); - } - - FreeLibrary(user32_module); -} - -/// Window attribute that enables dark mode window decorations. -/// -/// Redefined in case the developer's machine has a Windows SDK older than -/// version 10.0.22000.0. -/// See: -/// https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute -#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE -#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 -#endif - -// Update the window frame's theme to match the system theme. -void UpdateTheme(HWND window) { - // Registry key for app theme preference. - const wchar_t kGetPreferredBrightnessRegKey[] = - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; - const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; - - // A value of 0 indicates apps should use dark mode. A non-zero or missing - // value indicates apps should use light mode. - DWORD light_mode; - DWORD light_mode_size = sizeof(light_mode); - LSTATUS const result = - RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, - kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD, nullptr, - &light_mode, &light_mode_size); - - if (result == ERROR_SUCCESS) { - BOOL enable_dark_mode = light_mode == 0; - DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, - &enable_dark_mode, sizeof(enable_dark_mode)); - } -} - -auto IsClassRegistered(LPCWSTR class_name) -> bool { - WNDCLASSEX window_class{}; - return GetClassInfoEx(GetModuleHandle(nullptr), class_name, &window_class) != - 0; -} - -} // namespace - -namespace flutter { - -Win32Window::Win32Window() : win32_{std::make_shared()} {} - -Win32Window::Win32Window(std::shared_ptr wrapper) - : win32_{std::move(wrapper)} {} - -Win32Window::~Win32Window() { - std::lock_guard lock(gActiveWindowMutex); - if (--gActiveWindowCount == 0) { - UnregisterClass(kWindowClassName, GetModuleHandle(nullptr)); - } -} - -auto Win32Window::GetThisFromHandle(HWND hwnd) -> Win32Window* { - return reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); -} - -auto Win32Window::GetHandle() const -> HWND { - return window_handle_; -} - -void Win32Window::SetQuitOnClose(bool quit_on_close) { - quit_on_close_ = quit_on_close; -} - -auto Win32Window::GetQuitOnClose() const -> bool { - return quit_on_close_; -} - -auto Win32Window::GetClientArea() const -> RECT { - RECT client_rect; - GetClientRect(window_handle_, &client_rect); - return client_rect; -} - -auto Win32Window::GetArchetype() const -> WindowArchetype { - return archetype_; -} - -auto Win32Window::Create(std::wstring const& title, - WindowSize const& client_size, - WindowArchetype archetype, - std::optional parent, - std::optional positioner) -> bool { - std::lock_guard lock(gActiveWindowMutex); - - archetype_ = archetype; - - DWORD window_style{}; - DWORD extended_window_style{}; - - switch (archetype) { - case WindowArchetype::regular: - window_style |= WS_OVERLAPPEDWINDOW; - break; - case WindowArchetype::floating_regular: - // TODO - break; - case WindowArchetype::dialog: - // TODO - break; - case WindowArchetype::satellite: - // TODO - break; - case WindowArchetype::popup: - window_style |= WS_POPUP; - if (auto* const parent_window{ - GetThisFromHandle(parent.value_or(nullptr))}) { - if (parent_window->child_content_ != nullptr) { - SetFocus(parent_window->child_content_); - } - parent_window->children_.insert(this); - ++parent_window->num_child_popups_; - } - break; - case WindowArchetype::tip: - // TODO - break; - default: - std::cerr << "Unhandled window archetype: " << static_cast(archetype) - << "\n"; - std::abort(); - } - - // Window rectangle in physical coordinates. - // Default positioning values (CW_USEDEFAULT) are used - // if the window has no parent or positioner. - auto const window_rect{[&]() -> WindowRectangle { - auto const window_size{GetWindowSizeForClientSize( - client_size, window_style, extended_window_style, - parent.value_or(nullptr))}; - if (parent && positioner) { - auto const frame_size{GetFrameSizeForWindowSize( - window_size, window_style, extended_window_style, parent.value())}; - - // The rectangle of the parent's client area, in physical coordinates - auto const parent_rect{[](HWND parent_window) -> WindowRectangle { - RECT client_rect; - GetClientRect(parent_window, &client_rect); - POINT top_left{client_rect.left, client_rect.top}; - ClientToScreen(parent_window, &top_left); - POINT bottom_right{client_rect.right, client_rect.bottom}; - ClientToScreen(parent_window, &bottom_right); - return {{top_left.x, top_left.y}, - {bottom_right.x - top_left.x, bottom_right.y - top_left.y}}; - }(parent.value())}; - - // The anchor rectangle, in physical coordinates - auto const anchor_rect{[](WindowPositioner const& positioner, - HWND parent_window, - WindowRectangle const& parent_rect) - -> WindowRectangle { - if (positioner.anchor_rect) { - auto const dpr{FlutterDesktopGetDpiForHWND(parent_window) / - static_cast(USER_DEFAULT_SCREEN_DPI)}; - return { - {parent_rect.top_left.x + - static_cast(positioner.anchor_rect->top_left.x * dpr), - parent_rect.top_left.y + - static_cast(positioner.anchor_rect->top_left.y * dpr)}, - {static_cast(positioner.anchor_rect->size.width * dpr), - static_cast(positioner.anchor_rect->size.height * dpr)}}; - } else { - // If the anchor rect specified in the positioner is std::nullopt, - // return an anchor rect that is equal to the window frame area - RECT frame_rect; - DwmGetWindowAttribute(parent_window, DWMWA_EXTENDED_FRAME_BOUNDS, - &frame_rect, sizeof(frame_rect)); - return {{frame_rect.left, frame_rect.top}, - {frame_rect.right - frame_rect.left, - frame_rect.bottom - frame_rect.top}}; - } - }(positioner.value(), parent.value(), parent_rect)}; - - // Rectangle of the monitor that has the largest area of intersection - // with the anchor rectangle, in physical coordinates - auto const output_rect{ - [](RECT anchor_rect) - -> WindowRectangle { - auto* monitor{ - MonitorFromRect(&anchor_rect, MONITOR_DEFAULTTONEAREST)}; - MONITORINFO mi; - mi.cbSize = sizeof(MONITORINFO); - auto const bounds{GetMonitorInfo(monitor, &mi) ? mi.rcWork - : RECT{0, 0, 0, 0}}; - return {{bounds.left, bounds.top}, - {bounds.right - bounds.left, bounds.bottom - bounds.top}}; - }({.left = static_cast(anchor_rect.top_left.x), - .top = static_cast(anchor_rect.top_left.y), - .right = static_cast(anchor_rect.top_left.x + - anchor_rect.size.width), - .bottom = static_cast(anchor_rect.top_left.y + - anchor_rect.size.height)})}; - - auto const rect{internal::PlaceWindow( - positioner.value(), frame_size, anchor_rect, - positioner->anchor_rect ? parent_rect : anchor_rect, output_rect)}; - - return {rect.top_left, - {rect.size.width + window_size.width - frame_size.width, - rect.size.height + window_size.height - frame_size.height}}; - } - return {{CW_USEDEFAULT, CW_USEDEFAULT}, window_size}; - }()}; - - if (!IsClassRegistered(kWindowClassName)) { - auto const idi_app_icon{101}; - WNDCLASSEX window_class{}; - window_class.cbSize = sizeof(WNDCLASSEX); - window_class.style = CS_HREDRAW | CS_VREDRAW; - window_class.lpfnWndProc = Win32Window::WndProc; - window_class.cbClsExtra = 0; - window_class.cbWndExtra = 0; - window_class.hInstance = GetModuleHandle(nullptr); - window_class.hIcon = - LoadIcon(window_class.hInstance, MAKEINTRESOURCE(idi_app_icon)); - window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); - window_class.hbrBackground = 0; - window_class.lpszMenuName = nullptr; - window_class.lpszClassName = kWindowClassName; - window_class.hIconSm = nullptr; - - RegisterClassEx(&window_class); - } - - window_handle_ = win32_->CreateWindowEx( - extended_window_style, kWindowClassName, title.c_str(), window_style, - window_rect.top_left.x, window_rect.top_left.y, window_rect.size.width, - window_rect.size.height, parent.value_or(nullptr), nullptr, - GetModuleHandle(nullptr), this); - - if (!window_handle_) { - auto const error_message{GetLastErrorAsString()}; - std::cerr << "Cannot create window due to a CreateWindowEx error: " - << error_message.c_str() << '\n'; - return false; - } - - // Adjust the window position so its origin aligns with the top-left corner - // of the window frame, not the window rectangle (which includes the - // drop-shadow). This adjustment must be done post-creation since the frame - // rectangle is only available after the window has been created. - RECT frame_rc; - DwmGetWindowAttribute(window_handle_, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rc, - sizeof(frame_rc)); - RECT window_rc; - GetWindowRect(window_handle_, &window_rc); - auto const left_dropshadow_width{frame_rc.left - window_rc.left}; - auto const top_dropshadow_height{window_rc.top - frame_rc.top}; - SetWindowPos(window_handle_, nullptr, window_rc.left - left_dropshadow_width, - window_rc.top - top_dropshadow_height, 0, 0, - SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); - - UpdateTheme(window_handle_); - - gActiveWindowCount++; - - ShowWindow(window_handle_, SW_SHOW); - - return OnCreate(); -} - -void Win32Window::Destroy() { - OnDestroy(); -} - -void Win32Window::SetChildContent(HWND content) { - child_content_ = content; - SetParent(content, window_handle_); - auto const client_rect{GetClientArea()}; - - MoveWindow(content, client_rect.left, client_rect.top, - client_rect.right - client_rect.left, - client_rect.bottom - client_rect.top, true); - - SetFocus(child_content_); -} - -auto Win32Window::MessageHandler(HWND hwnd, - UINT message, - WPARAM wparam, - LPARAM lparam) -> LRESULT { - switch (message) { - case WM_DESTROY: - Destroy(); - if (quit_on_close_) { - PostQuitMessage(0); - } - return 0; - - case WM_DPICHANGED: { - auto* const new_scaled_window_rect{reinterpret_cast(lparam)}; - auto const width{new_scaled_window_rect->right - - new_scaled_window_rect->left}; - auto const height{new_scaled_window_rect->bottom - - new_scaled_window_rect->top}; - SetWindowPos(hwnd, nullptr, new_scaled_window_rect->left, - new_scaled_window_rect->top, width, height, - SWP_NOZORDER | SWP_NOACTIVATE); - return 0; - } - case WM_SIZE: { - if (child_content_ != nullptr) { - // Resize and reposition the child content window - auto const client_rect{GetClientArea()}; - MoveWindow(child_content_, client_rect.left, client_rect.top, - client_rect.right - client_rect.left, - client_rect.bottom - client_rect.top, TRUE); - } - return 0; - } - - case WM_ACTIVATE: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return 0; - - case WM_NCACTIVATE: - if (wparam == FALSE && archetype_ != WindowArchetype::popup) { - if (!enable_redraw_non_client_as_inactive_ || num_child_popups_ > 0) { - // If an inactive title bar is to be drawn, and this is a top-level - // window with popups, force the title bar to be drawn in its active - // colors - return TRUE; - } - } - break; - - case WM_MOUSEACTIVATE: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return MA_ACTIVATE; - - case WM_DWMCOLORIZATIONCOLORCHANGED: - UpdateTheme(hwnd); - return 0; - - default: - break; - } - - return DefWindowProc(window_handle_, message, wparam, lparam); -} - -auto Win32Window::OnCreate() -> bool { - // No-op; provided for subclasses. - return true; -} - -void Win32Window::OnDestroy() { - switch (archetype_) { - case WindowArchetype::regular: - break; - case WindowArchetype::floating_regular: - break; - case WindowArchetype::dialog: - break; - case WindowArchetype::satellite: - break; - case WindowArchetype::popup: - if (auto* const parent_window_handle{GetParent(window_handle_)}) { - if (auto* const parent_window{ - GetThisFromHandle(parent_window_handle)}) { - parent_window->children_.erase(this); - assert(parent_window->num_child_popups_ > 0); - --parent_window->num_child_popups_; - } - } - break; - case WindowArchetype::tip: - break; - default: - std::cerr << "Unhandled window archetype encountered in " - "Win32Window::OnDestroy: " - << static_cast(archetype_) << "\n"; - std::abort(); - } -} - -// static -auto CALLBACK Win32Window::WndProc(HWND hwnd, - UINT message, - WPARAM wparam, - LPARAM lparam) -> LRESULT { - if (message == WM_NCCREATE) { - auto* const create_struct{reinterpret_cast(lparam)}; - SetWindowLongPtr(hwnd, GWLP_USERDATA, - reinterpret_cast(create_struct->lpCreateParams)); - auto* const window{ - static_cast(create_struct->lpCreateParams)}; - window->window_handle_ = hwnd; - - EnableFullDpiSupportIfAvailable(hwnd); - EnableTransparentWindowBackground(hwnd); - } else if (auto* const window{GetThisFromHandle(hwnd)}) { - return FlutterWindowController::GetInstance().MessageHandler( - hwnd, message, wparam, lparam); - } - - return DefWindowProc(hwnd, message, wparam, lparam); -} - -auto Win32Window::CloseChildPopups() -> std::size_t { - if (num_child_popups_ == 0) { - return 0; - } - - std::set popups; - for (auto* const child : children_) { - if (child->archetype_ == WindowArchetype::popup) { - popups.insert(child); - } - } - - for (auto it{children_.begin()}; it != children_.end();) { - if ((*it)->archetype_ == WindowArchetype::popup) { - it = children_.erase(it); - } else { - ++it; - } - } - - auto const previous_num_child_popups{num_child_popups_}; - - for (auto* popup : popups) { - auto const parent_handle{GetParent(popup->window_handle_)}; - if (auto* const parent{GetThisFromHandle(parent_handle)}) { - // Popups' parents are drawn with active colors even though they are - // actually inactive. When a popup is destroyed, the parent might be - // redrawn as inactive (reflecting its true state) before being redrawn as - // active. To prevent flickering during this transition, disable - // redrawing the non-client area as inactive. - parent->enable_redraw_non_client_as_inactive_ = false; - DestroyWindow(popup->GetHandle()); - parent->enable_redraw_non_client_as_inactive_ = true; - - // Repaint parent window to make sure its title bar is painted with the - // color based on its actual activation state - if (parent->num_child_popups_ == 0) { - SetWindowPos(parent_handle, nullptr, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); - } - } - } - - return previous_num_child_popups - num_child_popups_; -} - -} // namespace flutter diff --git a/shell/platform/windows/flutter_host_window.cc b/shell/platform/windows/flutter_host_window.cc new file mode 100644 index 0000000000000..82a8a0d03ba74 --- /dev/null +++ b/shell/platform/windows/flutter_host_window.cc @@ -0,0 +1,743 @@ +// 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 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::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. + 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}}; + } + } + 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; + } + + // Adjust the window position so its origin aligns with the top-left corner + // of the window frame, not the window rectangle (which includes the + // drop-shadow). This adjustment must be done post-creation since the frame + // rectangle is only available after the window has been created. + RECT frame_rc; + DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rc, + sizeof(frame_rc)); + RECT window_rc; + GetWindowRect(hwnd, &window_rc); + LONG const left_dropshadow_width = frame_rc.left - window_rc.left; + LONG const top_dropshadow_height = window_rc.top - frame_rc.top; + SetWindowPos(hwnd, nullptr, window_rc.left - left_dropshadow_width, + window_rc.top - top_dropshadow_height, 0, 0, + SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + + // Set up the view. + RECT client_rect; + GetClientRect(hwnd, &client_rect); + int const width = client_rect.right - client_rect.left; + int const height = client_rect.bottom - client_rect.top; + + FlutterWindowsEngine* const engine = window_controller_->engine(); + auto view_window = std::make_unique( + width, height, engine->windows_proc_table()); + + std::unique_ptr view = + engine->CreateView(std::move(view_window)); + if (!view) { + FML_LOG(ERROR) << "Failed to create view"; + return; + } + + view_controller_ = + std::make_unique(nullptr, std::move(view)); + + // Launch the engine if it is not running already. + if (!engine->running() && !engine->Run()) { + FML_LOG(ERROR) << "Failed to launch engine"; + return; + } + // Must happen after engine is running. + view_controller_->view()->SendInitialBounds(); + // The Windows embedder listens to accessibility updates using the + // view's HWND. The embedder's accessibility features may be stale if + // the app was in headless mode. + view_controller_->engine()->UpdateAccessibilityFeatures(); + + // Ensure that basic setup of the view controller was successful. + if (!view_controller_->view()) { + FML_LOG(ERROR) << "Failed to set up the view controller"; + return; + } + + // 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); + + 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_; +} + +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::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 (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_MOUSEACTIVATE: + FocusViewOf(this); + return MA_ACTIVATE; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + + default: + break; + } + + return DefWindowProc(hwnd, message, wparam, lparam); +} + +void FlutterHostWindow::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT client_rect; + GetClientRect(window_handle_, &client_rect); + MoveWindow(content, client_rect.left, client_rect.top, + client_rect.right - client_rect.left, + client_rect.bottom - client_rect.top, true); +} + +} // namespace flutter diff --git a/shell/platform/windows/flutter_host_window.h b/shell/platform/windows/flutter_host_window.h new file mode 100644 index 0000000000000..37df57fde1978 --- /dev/null +++ b/shell/platform/windows/flutter_host_window.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_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::popup|, both |owner| and |positioner| must be provided, + // with |positioner| used only for this archetype. 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(); + + // 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); + + // 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; + + // 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..f0e902d28755a --- /dev/null +++ b/shell/platform/windows/flutter_host_window_controller.cc @@ -0,0 +1,257 @@ +// 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(); + + // |window| will be removed from |windows_| when WM_NCDESTROY is handled. + PostMessage(window->GetWindowHandle(), WM_CLOSE, 0, 0); + + return true; + } + return false; +} + +FlutterHostWindow* FlutterHostWindowController::GetHostWindow( + FlutterViewId view_id) const { + if (auto it = windows_.find(view_id); it != windows_.end()) { + return it->second.get(); + } + return nullptr; +} + +LRESULT FlutterHostWindowController::HandleMessage(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) { + switch (message) { + case WM_NCDESTROY: { + auto const it = std::find_if( + windows_.begin(), windows_.end(), [hwnd](auto const& window) { + return window.second->GetWindowHandle() == hwnd; + }); + if (it != windows_.end()) { + FlutterViewId const view_id = it->first; + bool const quit_on_close = it->second->GetQuitOnClose(); + + windows_.erase(it); + + SendOnWindowDestroyed(view_id); + + if (quit_on_close) { + DestroyAllWindows(); + } + } + } + return 0; + case WM_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(); + } + } + } + break; + case WM_ACTIVATEAPP: + if (wparam == FALSE) { + if (FlutterHostWindow* const window = + FlutterHostWindow::GetThisFromHandle(hwnd)) { + // Close owned popups if a window belonging to a different application + // is being activated. + window->CloseOwnedPopups(); + } + } + 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::SendOnWindowChanged( + FlutterViewId view_id) const { + if (channel_) { + WindowSize const size = GetWindowSize(view_id); + channel_->InvokeMethod( + kOnWindowChangedMethod, + std::make_unique(EncodableMap{ + {EncodableValue(kViewIdKey), EncodableValue(view_id)}, + {EncodableValue(kSizeKey), + EncodableValue(EncodableList{EncodableValue(size.width), + EncodableValue(size.height)})}, + {EncodableValue(kRelativePositionKey), EncodableValue()}, + {EncodableValue(kIsMovingKey), EncodableValue()}})); + } +} + +void FlutterHostWindowController::SendOnWindowCreated( + FlutterViewId view_id, + std::optional parent_view_id) const { + if (channel_) { + channel_->InvokeMethod( + kOnWindowCreatedMethod, + std::make_unique(EncodableMap{ + {EncodableValue(kViewIdKey), EncodableValue(view_id)}, + {EncodableValue(kParentViewIdKey), + parent_view_id ? EncodableValue(parent_view_id.value()) + : EncodableValue()}})); + } +} + +void FlutterHostWindowController::SendOnWindowDestroyed( + FlutterViewId view_id) const { + if (channel_) { + channel_->InvokeMethod( + kOnWindowDestroyedMethod, + std::make_unique(EncodableMap{ + {EncodableValue(kViewIdKey), EncodableValue(view_id)}, + })); + } +} + +} // namespace flutter diff --git a/shell/platform/windows/flutter_host_window_controller.h b/shell/platform/windows/flutter_host_window_controller.h new file mode 100644 index 0000000000000..e6e076661d2ce --- /dev/null +++ b/shell/platform/windows/flutter_host_window_controller.h @@ -0,0 +1,109 @@ +// 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::popup|, both + // |parent_view_id| and |positioner| must be provided; |positioner| is used + // only for this archetype. 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; + + // Sends the "onWindowChanged" message to the Flutter engine. + void SendOnWindowChanged(FlutterViewId view_id) const; + + // Sends the "onWindowCreated" message to the Flutter engine. + void SendOnWindowCreated(FlutterViewId view_id, + std::optional parent_view_id) const; + + // Sends the "onWindowDestroyed" message to the Flutter engine. + void SendOnWindowDestroyed(FlutterViewId view_id) const; + + // The Flutter engine that owns this controller. + FlutterWindowsEngine* const engine_; + + // The windowing channel through which the controller sends messages. + std::shared_ptr> channel_; + + // The host windows managed by this controller. + std::map> windows_; + + FML_DISALLOW_COPY_AND_ASSIGN(FlutterHostWindowController); +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_HOST_WINDOW_CONTROLLER_H_ diff --git a/shell/platform/windows/flutter_host_window_controller_unittests.cc b/shell/platform/windows/flutter_host_window_controller_unittests.cc new file mode 100644 index 0000000000000..278545cc8e657 --- /dev/null +++ b/shell/platform/windows/flutter_host_window_controller_unittests.cc @@ -0,0 +1,200 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include "flutter/shell/platform/windows/windowing_handler.h" + +#include "flutter/fml/macros.h" +#include "flutter/shell/platform/common/client_wrapper/include/flutter/standard_method_codec.h" +#include "flutter/shell/platform/windows/flutter_host_window_controller.h" +#include "flutter/shell/platform/windows/testing/flutter_windows_engine_builder.h" +#include "flutter/shell/platform/windows/testing/test_binary_messenger.h" +#include "flutter/shell/platform/windows/testing/windows_test.h" +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { + +namespace { + +static constexpr char kChannelName[] = "flutter/windowing"; +static constexpr char kOnWindowCreatedMethod[] = "onWindowCreated"; +static constexpr char kOnWindowDestroyedMethod[] = "onWindowDestroyed"; +static constexpr char kViewIdKey[] = "viewId"; +static constexpr char kParentViewIdKey[] = "parentViewId"; + +// Process the next Win32 message if there is one. This can be used to +// pump the Windows platform thread task runner. +void PumpMessage() { + ::MSG msg; + if (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } +} + +class FlutterHostWindowControllerTest : public WindowsTest { + public: + FlutterHostWindowControllerTest() = default; + virtual ~FlutterHostWindowControllerTest() = default; + + protected: + void SetUp() override { + InitializeCOM(); + FlutterWindowsEngineBuilder builder(GetContext()); + engine_ = builder.Build(); + controller_ = std::make_unique(engine_.get()); + } + + FlutterWindowsEngine* engine() { return engine_.get(); } + FlutterHostWindowController* host_window_controller() { + return controller_.get(); + } + + private: + void InitializeCOM() const { + FML_CHECK(SUCCEEDED(::CoInitializeEx(nullptr, COINIT_MULTITHREADED))); + } + + std::unique_ptr engine_; + std::unique_ptr controller_; + + FML_DISALLOW_COPY_AND_ASSIGN(FlutterHostWindowControllerTest); +}; + +} // namespace + +TEST_F(FlutterHostWindowControllerTest, CreateRegularWindow) { + bool called_onWindowCreated = false; + + // Test messenger with a handler for onWindowCreated. + TestBinaryMessenger messenger([&](const std::string& channel, + const uint8_t* message, size_t size, + BinaryReply reply) { + // Ensure the message is sent on the windowing channel. + ASSERT_EQ(channel, kChannelName); + + // Ensure the decoded method call is valid. + auto const method = StandardMethodCodec::GetInstance().DecodeMethodCall( + std::vector(message, message + size)); + ASSERT_NE(method, nullptr); + + // Handle the onWindowCreated method. + if (method->method_name() == kOnWindowCreatedMethod) { + called_onWindowCreated = true; + + // Validate the method arguments. + auto const& args = *method->arguments(); + ASSERT_TRUE(std::holds_alternative(args)); + auto const& args_map = std::get(args); + + // Ensure the viewId is present and valid. + auto const& it_viewId = args_map.find(EncodableValue(kViewIdKey)); + ASSERT_NE(it_viewId, args_map.end()); + auto const* value_viewId = std::get_if(&it_viewId->second); + ASSERT_NE(value_viewId, nullptr); + EXPECT_GE(*value_viewId, 0); + EXPECT_NE(engine()->view(*value_viewId), nullptr); + + // Ensure the parentViewId is a std::monostate (indicating no parent). + auto const& it_parentViewId = + args_map.find(EncodableValue(kParentViewIdKey)); + ASSERT_NE(it_parentViewId, args_map.end()); + auto const* value_parentViewId = + std::get_if(&it_parentViewId->second); + EXPECT_NE(value_parentViewId, nullptr); + } + }); + + // Create the windowing handler with the test messenger. + WindowingHandler windowing_handler(&messenger, host_window_controller()); + + // Define parameters for the window to be created. + WindowSize const size = {800, 600}; + wchar_t const* const title = L"window"; + WindowArchetype const archetype = WindowArchetype::regular; + + // Create the window. + std::optional const result = + host_window_controller()->CreateHostWindow(title, size, archetype, + std::nullopt, std::nullopt); + + // Verify the onWindowCreated callback was invoked. + EXPECT_TRUE(called_onWindowCreated); + + // Validate the returned metadata. + ASSERT_TRUE(result.has_value()); + EXPECT_NE(engine()->view(result->view_id), nullptr); + EXPECT_EQ(result->archetype, archetype); + EXPECT_GE(result->size.width, size.width); + EXPECT_GE(result->size.height, size.height); + EXPECT_FALSE(result->parent_id.has_value()); + + // Verify the window exists and the view has the expected dimensions. + FlutterHostWindow* const window = + host_window_controller()->GetHostWindow(result->view_id); + ASSERT_NE(window, nullptr); + RECT client_rect; + GetClientRect(window->GetWindowHandle(), &client_rect); + EXPECT_EQ(client_rect.right - client_rect.left, size.width); + EXPECT_EQ(client_rect.bottom - client_rect.top, size.height); +} + +TEST_F(FlutterHostWindowControllerTest, DestroyWindow) { + bool done = false; + + // Test messenger with a handler for onWindowDestroyed. + TestBinaryMessenger messenger([&](const std::string& channel, + const uint8_t* message, size_t size, + BinaryReply reply) { + // Ensure the message is sent on the windowing channel. + ASSERT_EQ(channel, kChannelName); + + // Ensure the decoded method call is valid. + auto const method = StandardMethodCodec::GetInstance().DecodeMethodCall( + std::vector(message, message + size)); + ASSERT_NE(method, nullptr); + + // Handle the onWindowDestroyed method. + if (method->method_name() == kOnWindowDestroyedMethod) { + // Validate the method arguments. + auto const& args = *method->arguments(); + ASSERT_TRUE(std::holds_alternative(args)); + auto const& args_map = std::get(args); + + // Ensure the viewId is present but not valid anymore. + auto const& it_viewId = args_map.find(EncodableValue(kViewIdKey)); + ASSERT_NE(it_viewId, args_map.end()); + auto const* value_viewId = std::get_if(&it_viewId->second); + ASSERT_NE(value_viewId, nullptr); + EXPECT_GE(*value_viewId, 0); + EXPECT_EQ(engine()->view(*value_viewId), nullptr); + + done = true; + } + }); + + // Create the windowing handler with the test messenger. + WindowingHandler windowing_handler(&messenger, host_window_controller()); + + // Define parameters for the window to be created. + WindowSize const size = {800, 600}; + wchar_t const* const title = L"window"; + WindowArchetype const archetype = WindowArchetype::regular; + + // Create the window. + std::optional const result = + host_window_controller()->CreateHostWindow(title, size, archetype, + std::nullopt, std::nullopt); + ASSERT_TRUE(result.has_value()); + + // Destroy the window and ensure onWindowDestroyed was invoked. + EXPECT_TRUE(host_window_controller()->DestroyHostWindow(result->view_id)); + + // Pump messages for the Windows platform task runner. + while (!done) { + PumpMessage(); + } +} + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc index d08591a10b520..b8add6beee8f6 100644 --- a/shell/platform/windows/flutter_windows_engine.cc +++ b/shell/platform/windows/flutter_windows_engine.cc @@ -194,6 +194,10 @@ FlutterWindowsEngine::FlutterWindowsEngine( enable_impeller_ = std::find(switches.begin(), switches.end(), "--enable-impeller=true") != switches.end(); + enable_multi_window_ = + std::find(switches.begin(), switches.end(), + "--enable-multi-window=true") != switches.end(); + egl_manager_ = egl::Manager::Create(); window_proc_delegate_manager_ = std::make_unique(); window_proc_delegate_manager_->RegisterTopLevelWindowProcDelegate( @@ -222,6 +226,12 @@ FlutterWindowsEngine::FlutterWindowsEngine( std::make_unique(messenger_wrapper_.get(), this); platform_handler_ = std::make_unique(messenger_wrapper_.get(), this); + if (enable_multi_window_) { + host_window_controller_ = + std::make_unique(this); + windowing_handler_ = std::make_unique( + messenger_wrapper_.get(), host_window_controller_.get()); + } settings_plugin_ = std::make_unique(messenger_wrapper_.get(), task_runner_.get()); } diff --git a/shell/platform/windows/flutter_windows_engine.h b/shell/platform/windows/flutter_windows_engine.h index 2d7b730580099..011020b25dd3f 100644 --- a/shell/platform/windows/flutter_windows_engine.h +++ b/shell/platform/windows/flutter_windows_engine.h @@ -30,6 +30,7 @@ #include "flutter/shell/platform/windows/egl/manager.h" #include "flutter/shell/platform/windows/egl/proc_table.h" #include "flutter/shell/platform/windows/flutter_desktop_messenger.h" +#include "flutter/shell/platform/windows/flutter_host_window.h" #include "flutter/shell/platform/windows/flutter_project_bundle.h" #include "flutter/shell/platform/windows/flutter_windows_texture_registrar.h" #include "flutter/shell/platform/windows/keyboard_handler_base.h" @@ -42,6 +43,7 @@ #include "flutter/shell/platform/windows/text_input_plugin.h" #include "flutter/shell/platform/windows/window_proc_delegate_manager.h" #include "flutter/shell/platform/windows/window_state.h" +#include "flutter/shell/platform/windows/windowing_handler.h" #include "flutter/shell/platform/windows/windows_lifecycle_manager.h" #include "flutter/shell/platform/windows/windows_proc_table.h" #include "third_party/rapidjson/include/rapidjson/document.h" @@ -425,9 +427,16 @@ class FlutterWindowsEngine { // Handler for the flutter/platform channel. std::unique_ptr platform_handler_; + // Handler for the flutter/windowing channel. + std::unique_ptr windowing_handler_; + // Handlers for keyboard events from Windows. std::unique_ptr keyboard_key_handler_; + // The controller that manages the lifecycle of |FlutterHostWindow|s, native + // Win32 windows hosting a Flutter view in their client area. + std::unique_ptr host_window_controller_; + // Handlers for text events from Windows. std::unique_ptr text_input_plugin_; @@ -456,6 +465,8 @@ class FlutterWindowsEngine { bool enable_impeller_ = false; + bool enable_multi_window_ = false; + // The manager for WindowProc delegate registration and callbacks. std::unique_ptr window_proc_delegate_manager_; diff --git a/shell/platform/windows/flutter_windows_internal.h b/shell/platform/windows/flutter_windows_internal.h index 47b98e983a48a..bb1e6f767905f 100644 --- a/shell/platform/windows/flutter_windows_internal.h +++ b/shell/platform/windows/flutter_windows_internal.h @@ -14,6 +14,31 @@ extern "C" { // Declare functions that are currently in-progress and shall be exposed to the // public facing API upon completion. +// Properties for configuring a Flutter view controller. +typedef struct { + // The view's initial width. + int width; + + // The view's initial height. + int height; +} FlutterDesktopViewControllerProperties; + +// Creates a view for the given engine. +// +// The |engine| will be started if it is not already running. +// +// The caller owns the returned reference, and is responsible for calling +// |FlutterDesktopViewControllerDestroy|. Returns a null pointer in the event of +// an error. +// +// Unlike |FlutterDesktopViewControllerCreate|, this does *not* take ownership +// of |engine| and |FlutterDesktopEngineDestroy| must be called to destroy +// the engine. +FLUTTER_EXPORT FlutterDesktopViewControllerRef +FlutterDesktopEngineCreateViewController( + FlutterDesktopEngineRef engine, + const FlutterDesktopViewControllerProperties* properties); + typedef int64_t PlatformViewId; typedef struct { diff --git a/shell/platform/windows/public/flutter_windows.h b/shell/platform/windows/public/flutter_windows.h index d7b2a30520b04..80d78766f9383 100644 --- a/shell/platform/windows/public/flutter_windows.h +++ b/shell/platform/windows/public/flutter_windows.h @@ -70,15 +70,6 @@ typedef struct { } FlutterDesktopEngineProperties; -// Properties for configuring a Flutter view controller. -typedef struct { - // The view's initial width. - int width; - - // The view's initial height. - int height; -} FlutterDesktopViewControllerProperties; - // ========== View Controller ========== // Creates a view that hosts and displays the given engine instance. @@ -174,22 +165,6 @@ FLUTTER_EXPORT bool FlutterDesktopEngineDestroy(FlutterDesktopEngineRef engine); FLUTTER_EXPORT bool FlutterDesktopEngineRun(FlutterDesktopEngineRef engine, const char* entry_point); -// Creates a view for the given engine. -// -// The |engine| will be started if it is not already running. -// -// The caller owns the returned reference, and is responsible for calling -// |FlutterDesktopViewControllerDestroy|. Returns a null pointer in the event of -// an error. -// -// Unlike |FlutterDesktopViewControllerCreate|, this does *not* take ownership -// of |engine| and |FlutterDesktopEngineDestroy| must be called to destroy -// the engine. -FLUTTER_EXPORT FlutterDesktopViewControllerRef -FlutterDesktopEngineCreateViewController( - FlutterDesktopEngineRef engine, - const FlutterDesktopViewControllerProperties* properties); - // DEPRECATED: This is no longer necessary to call, Flutter will take care of // processing engine messages transparently through DispatchMessage. // diff --git a/shell/platform/windows/windowing_handler.cc b/shell/platform/windows/windowing_handler.cc new file mode 100644 index 0000000000000..e3ef23c34665a --- /dev/null +++ b/shell/platform/windows/windowing_handler.cc @@ -0,0 +1,323 @@ +// 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 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::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 == 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::popup) { + if (auto const anchor_rect_it = map->find(EncodableValue(kAnchorRectKey)); + anchor_rect_it != map->end()) { + if (!anchor_rect_it->second.IsNull()) { + auto const anchor_rect_list = + GetListValuesForKeyOrSendError(kAnchorRectKey, map, result); + if (!anchor_rect_list) { + return; + } + anchor_rect = + WindowRectangle{{anchor_rect_list->at(0), anchor_rect_list->at(1)}, + {anchor_rect_list->at(2), anchor_rect_list->at(3)}}; + } + } else { + result.Error(kInvalidValueError, "Map does not contain required '" + + std::string(kAnchorRectKey) + + "' key."); + return; + } + + auto const positioner_parent_anchor = GetSingleValueForKeyOrSendError( + kPositionerParentAnchorKey, map, result); + if (!positioner_parent_anchor) { + return; + } + auto const positioner_child_anchor = GetSingleValueForKeyOrSendError( + kPositionerChildAnchorKey, map, result); + if (!positioner_child_anchor) { + return; + } + auto const child_anchor = + static_cast(positioner_child_anchor.value()); + + auto const positioner_offset_list = GetListValuesForKeyOrSendError( + kPositionerOffsetKey, map, result); + if (!positioner_offset_list) { + return; + } + auto const positioner_constraint_adjustment = + GetSingleValueForKeyOrSendError(kPositionerConstraintAdjustmentKey, + map, result); + if (!positioner_constraint_adjustment) { + return; + } + positioner = WindowPositioner{ + .anchor_rect = anchor_rect, + .parent_anchor = static_cast( + positioner_parent_anchor.value()), + .child_anchor = child_anchor, + .offset = {positioner_offset_list->at(0), + positioner_offset_list->at(1)}, + .constraint_adjustment = + static_cast( + positioner_constraint_adjustment.value())}; + } + + std::optional parent_view_id; + if (archetype == WindowArchetype::popup) { + if (auto const parent_it = map->find(EncodableValue(kParentKey)); + parent_it != map->end()) { + if (parent_it->second.IsNull()) { + 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::popup)) { + result.Error(kInvalidValueError, + "Value for '" + std::string(kParentKey) + "' (" + + std::to_string(parent_view_id.value()) + + ") must be nonnegative."); + return; + } + } else { + result.Error(kInvalidValueError, "Value for '" + + std::string(kParentKey) + + "' must be of type int."); + return; + } + } + } else { + result.Error(kInvalidValueError, "Map does not contain required '" + + std::string(kParentKey) + "' key."); + return; + } + } + + if (std::optional const data_opt = + controller_->CreateHostWindow( + title, {.width = size_list->at(0), .height = size_list->at(1)}, + archetype, positioner, parent_view_id)) { + WindowMetadata const& data = data_opt.value(); + result.Success(EncodableValue(EncodableMap{ + {EncodableValue(kViewIdKey), EncodableValue(data.view_id)}, + {EncodableValue(kArchetypeKey), + EncodableValue(static_cast(data.archetype))}, + {EncodableValue(kSizeKey), + EncodableValue(EncodableList{EncodableValue(data.size.width), + EncodableValue(data.size.height)})}, + {EncodableValue(kParentViewIdKey), + data.parent_id ? EncodableValue(data.parent_id.value()) + : EncodableValue()}})); + } else { + result.Error(kUnavailableError, "Can't create window."); + } +} + +void WindowingHandler::HandleDestroyWindow(MethodCall<> const& call, + MethodResult<>& result) { + auto const* const arguments = call.arguments(); + auto const* const map = std::get_if(arguments); + if (!map) { + result.Error(kInvalidValueError, "Method call argument is not a map."); + return; + } + + auto const view_id = + GetSingleValueForKeyOrSendError(kViewIdKey, map, result); + if (!view_id) { + return; + } + if (view_id.value() < 0) { + result.Error(kInvalidValueError, + "Value for '" + std::string(kViewIdKey) + "' (" + + std::to_string(view_id.value()) + ") cannot be negative."); + return; + } + + if (!controller_->DestroyHostWindow(view_id.value())) { + result.Error(kInvalidValueError, + "Can't find window with '" + std::string(kViewIdKey) + "' (" + + std::to_string(view_id.value()) + ")."); + return; + } + + result.Success(); +} + +} // namespace flutter diff --git a/shell/platform/windows/windowing_handler.h b/shell/platform/windows/windowing_handler.h new file mode 100644 index 0000000000000..c527826eda50b --- /dev/null +++ b/shell/platform/windows/windowing_handler.h @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOWING_HANDLER_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOWING_HANDLER_H_ + +#include "flutter/fml/macros.h" +#include "flutter/shell/platform/common/client_wrapper/include/flutter/method_channel.h" +#include "flutter/shell/platform/windows/flutter_host_window_controller.h" + +namespace flutter { + +// Handler for the windowing channel. +class WindowingHandler { + public: + explicit WindowingHandler(flutter::BinaryMessenger* messenger, + flutter::FlutterHostWindowController* controller); + + private: + // Handler for method calls received on |channel_|. Messages are + // redirected to either HandleCreateWindow or HandleDestroyWindow. + void HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result); + + // Handles the creation of windows. + void HandleCreateWindow(flutter::WindowArchetype archetype, + flutter::MethodCall<> const& call, + flutter::MethodResult<>& result); + // Handles the destruction of windows. + void HandleDestroyWindow(flutter::MethodCall<> const& call, + flutter::MethodResult<>& result); + + // The MethodChannel used for communication with the Flutter engine. + std::shared_ptr> channel_; + + // The controller of the host windows. + flutter::FlutterHostWindowController* controller_; + + FML_DISALLOW_COPY_AND_ASSIGN(WindowingHandler); +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOWING_HANDLER_H_ diff --git a/shell/platform/windows/windowing_handler_unittests.cc b/shell/platform/windows/windowing_handler_unittests.cc new file mode 100644 index 0000000000000..be2db4ceed3da --- /dev/null +++ b/shell/platform/windows/windowing_handler_unittests.cc @@ -0,0 +1,151 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include "flutter/shell/platform/windows/windowing_handler.h" + +#include "flutter/fml/macros.h" +#include "flutter/shell/platform/common/client_wrapper/include/flutter/method_result_functions.h" +#include "flutter/shell/platform/common/client_wrapper/include/flutter/standard_method_codec.h" +#include "flutter/shell/platform/windows/flutter_host_window_controller.h" +#include "flutter/shell/platform/windows/testing/flutter_windows_engine_builder.h" +#include "flutter/shell/platform/windows/testing/test_binary_messenger.h" +#include "flutter/shell/platform/windows/testing/windows_test.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { + +namespace { +using ::testing::_; +using ::testing::Eq; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::StrEq; + +static constexpr char kChannelName[] = "flutter/windowing"; + +static constexpr char kCreateWindowMethod[] = "createWindow"; +static constexpr char kDestroyWindowMethod[] = "destroyWindow"; + +void SimulateWindowingMessage(TestBinaryMessenger* messenger, + const std::string& method_name, + std::unique_ptr arguments, + MethodResult* result_handler) { + MethodCall<> call(method_name, std::move(arguments)); + + auto message = StandardMethodCodec::GetInstance().EncodeMethodCall(call); + + EXPECT_TRUE(messenger->SimulateEngineMessage( + kChannelName, message->data(), message->size(), + [&result_handler](const uint8_t* reply, size_t reply_size) { + StandardMethodCodec::GetInstance().DecodeAndProcessResponseEnvelope( + reply, reply_size, result_handler); + })); +} + +class MockFlutterHostWindowController : public FlutterHostWindowController { + public: + MockFlutterHostWindowController(FlutterWindowsEngine* engine) + : FlutterHostWindowController(engine) {} + ~MockFlutterHostWindowController() = default; + + MOCK_METHOD(std::optional, + CreateHostWindow, + (std::wstring const& title, + WindowSize const& size, + WindowArchetype archetype, + std::optional positioner, + std::optional parent_view_id), + (override)); + MOCK_METHOD(bool, DestroyHostWindow, (FlutterViewId view_id), (override)); + + private: + FML_DISALLOW_COPY_AND_ASSIGN(MockFlutterHostWindowController); +}; + +} // namespace + +class WindowingHandlerTest : public WindowsTest { + public: + WindowingHandlerTest() = default; + virtual ~WindowingHandlerTest() = default; + + protected: + void SetUp() override { + FlutterWindowsEngineBuilder builder(GetContext()); + engine_ = builder.Build(); + + mock_controller_ = + std::make_unique>( + engine_.get()); + + ON_CALL(*mock_controller_, CreateHostWindow) + .WillByDefault(Return(WindowMetadata{})); + ON_CALL(*mock_controller_, DestroyHostWindow).WillByDefault(Return(true)); + } + + MockFlutterHostWindowController* controller() { + return mock_controller_.get(); + } + + private: + std::unique_ptr engine_; + std::unique_ptr> mock_controller_; + + FML_DISALLOW_COPY_AND_ASSIGN(WindowingHandlerTest); +}; + +TEST_F(WindowingHandlerTest, HandleCreateRegularWindow) { + TestBinaryMessenger messenger; + WindowingHandler windowing_handler(&messenger, controller()); + + WindowSize const size = {800, 600}; + EncodableMap const arguments = { + {EncodableValue("size"), + EncodableValue(EncodableList{EncodableValue(size.width), + EncodableValue(size.height)})}, + }; + + bool success = false; + MethodResultFunctions<> result_handler( + [&success](const EncodableValue* result) { success = true; }, nullptr, + nullptr); + + EXPECT_CALL( + *controller(), + CreateHostWindow(StrEq(L"regular"), size, WindowArchetype::regular, + Eq(std::nullopt), Eq(std::nullopt))) + .Times(1); + + SimulateWindowingMessage(&messenger, kCreateWindowMethod, + std::make_unique(arguments), + &result_handler); + + EXPECT_TRUE(success); +} + +TEST_F(WindowingHandlerTest, HandleDestroyWindow) { + TestBinaryMessenger messenger; + WindowingHandler windowing_handler(&messenger, controller()); + + EncodableMap const arguments = { + {EncodableValue("viewId"), EncodableValue(1)}, + }; + + bool success = false; + MethodResultFunctions<> result_handler( + [&success](const EncodableValue* result) { success = true; }, nullptr, + nullptr); + + EXPECT_CALL(*controller(), DestroyHostWindow(1)).Times(1); + + SimulateWindowingMessage(&messenger, kDestroyWindowMethod, + std::make_unique(arguments), + &result_handler); + + EXPECT_TRUE(success); +} + +} // namespace testing +} // namespace flutter