diff --git a/engine/src/flutter/ci/licenses_golden/excluded_files b/engine/src/flutter/ci/licenses_golden/excluded_files index 0c2d283c60a..8d09aab2c1c 100644 --- a/engine/src/flutter/ci/licenses_golden/excluded_files +++ b/engine/src/flutter/ci/licenses_golden/excluded_files @@ -435,6 +435,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/window_manager_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/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 3e80b6e2018..47bcd92f5c5 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -53288,6 +53288,8 @@ ORIGIN: ../../../flutter/shell/platform/common/flutter_platform_node_delegate.h ORIGIN: ../../../flutter/shell/platform/common/geometry.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/incoming_message_dispatcher.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/incoming_message_dispatcher.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/common/isolate_scope.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/common/isolate_scope.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/json_message_codec.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/json_message_codec.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/json_method_codec.cc + ../../../flutter/LICENSE @@ -53307,6 +53309,7 @@ ORIGIN: ../../../flutter/shell/platform/common/text_editing_delta.h + ../../../f ORIGIN: ../../../flutter/shell/platform/common/text_input_model.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/text_input_model.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/text_range.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/common/windowing.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon-Bridging-Header.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/SwiftTestingMain.swift + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/SwiftTestingRunner.swift + ../../../flutter/LICENSE @@ -53594,6 +53597,9 @@ ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterVie ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProviderTest.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.mm + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowControllerTest.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMap.g.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMapTest.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMap_Internal.h + ../../../flutter/LICENSE @@ -54071,6 +54077,8 @@ ORIGIN: ../../../flutter/shell/platform/windows/flutter_windows_view.cc + ../../ ORIGIN: ../../../flutter/shell/platform/windows/flutter_windows_view.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/flutter_windows_view_controller.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/flutter_windows_view_controller.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/windows/host_window.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/windows/host_window.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/keyboard_handler_base.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/keyboard_key_channel_handler.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/keyboard_key_channel_handler.h + ../../../flutter/LICENSE @@ -54105,6 +54113,8 @@ ORIGIN: ../../../flutter/shell/platform/windows/text_input_plugin.cc + ../../../ ORIGIN: ../../../flutter/shell/platform/windows/text_input_plugin.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/window_binding_handler.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/window_binding_handler_delegate.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/windows/window_manager.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/windows/window_manager.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/window_proc_delegate_manager.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/window_proc_delegate_manager.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/window_state.h + ../../../flutter/LICENSE @@ -56349,6 +56359,8 @@ FILE: ../../../flutter/shell/platform/common/flutter_platform_node_delegate.h FILE: ../../../flutter/shell/platform/common/geometry.h FILE: ../../../flutter/shell/platform/common/incoming_message_dispatcher.cc FILE: ../../../flutter/shell/platform/common/incoming_message_dispatcher.h +FILE: ../../../flutter/shell/platform/common/isolate_scope.cc +FILE: ../../../flutter/shell/platform/common/isolate_scope.h FILE: ../../../flutter/shell/platform/common/json_message_codec.cc FILE: ../../../flutter/shell/platform/common/json_message_codec.h FILE: ../../../flutter/shell/platform/common/json_method_codec.cc @@ -56368,6 +56380,7 @@ FILE: ../../../flutter/shell/platform/common/text_editing_delta.h FILE: ../../../flutter/shell/platform/common/text_input_model.cc FILE: ../../../flutter/shell/platform/common/text_input_model.h FILE: ../../../flutter/shell/platform/common/text_range.h +FILE: ../../../flutter/shell/platform/common/windowing.h FILE: ../../../flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon-Bridging-Header.h FILE: ../../../flutter/shell/platform/darwin/common/SwiftTestingMain.swift FILE: ../../../flutter/shell/platform/darwin/common/SwiftTestingRunner.swift @@ -56660,6 +56673,9 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewE FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProviderTest.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.h +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.mm +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowControllerTest.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMap.g.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMapTest.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMap_Internal.h @@ -57141,6 +57157,8 @@ FILE: ../../../flutter/shell/platform/windows/flutter_windows_view.cc FILE: ../../../flutter/shell/platform/windows/flutter_windows_view.h FILE: ../../../flutter/shell/platform/windows/flutter_windows_view_controller.cc FILE: ../../../flutter/shell/platform/windows/flutter_windows_view_controller.h +FILE: ../../../flutter/shell/platform/windows/host_window.cc +FILE: ../../../flutter/shell/platform/windows/host_window.h FILE: ../../../flutter/shell/platform/windows/keyboard_handler_base.h FILE: ../../../flutter/shell/platform/windows/keyboard_key_channel_handler.cc FILE: ../../../flutter/shell/platform/windows/keyboard_key_channel_handler.h @@ -57175,6 +57193,8 @@ FILE: ../../../flutter/shell/platform/windows/text_input_plugin.cc FILE: ../../../flutter/shell/platform/windows/text_input_plugin.h FILE: ../../../flutter/shell/platform/windows/window_binding_handler.h FILE: ../../../flutter/shell/platform/windows/window_binding_handler_delegate.h +FILE: ../../../flutter/shell/platform/windows/window_manager.cc +FILE: ../../../flutter/shell/platform/windows/window_manager.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 diff --git a/engine/src/flutter/shell/platform/common/BUILD.gn b/engine/src/flutter/shell/platform/common/BUILD.gn index 5ebfb2236d2..b27be6bfa03 100644 --- a/engine/src/flutter/shell/platform/common/BUILD.gn +++ b/engine/src/flutter/shell/platform/common/BUILD.gn @@ -57,6 +57,16 @@ source_set("common_cpp_input") { deps = [ "//flutter/fml:fml" ] } +source_set("common_cpp_isolate_scope") { + public = [ "isolate_scope.h" ] + sources = [ "isolate_scope.cc" ] + + deps = [ + "$dart_src/runtime:dart_api", + "//flutter/fml:fml", + ] +} + source_set("common_cpp_enums") { public = [ "app_lifecycle_state.h", @@ -142,11 +152,14 @@ source_set("common_cpp_core") { public = [ "geometry.h", "path_utils.h", + "windowing.h", ] sources = [ "path_utils.cc" ] public_configs = [ "//flutter:config" ] + + deps = [ "//flutter/fml:fml" ] } if (enable_unittests) { diff --git a/engine/src/flutter/shell/platform/common/geometry.h b/engine/src/flutter/shell/platform/common/geometry.h index 4d6e8daded0..19d61dd9479 100644 --- a/engine/src/flutter/shell/platform/common/geometry.h +++ b/engine/src/flutter/shell/platform/common/geometry.h @@ -6,6 +6,8 @@ #define FLUTTER_SHELL_PLATFORM_COMMON_GEOMETRY_H_ #include +#include +#include namespace flutter { @@ -45,6 +47,7 @@ class Size { bool operator==(const Size& other) const { return width_ == other.width_ && height_ == other.height_; } + bool operator!=(const Size& other) const { return !(*this == other); } private: double width_ = 0.0; @@ -78,6 +81,27 @@ class Rect { Size size_; }; +// Encapsulates a min and max size that represents the constraints that some +// arbitrary box is able to take up. +class BoxConstraints { + public: + BoxConstraints() = default; + BoxConstraints(const std::optional& smallest, + const std::optional& biggest) + : smallest_(smallest.value_or(Size(0, 0))), + biggest_( + biggest.value_or(Size(std::numeric_limits::infinity(), + std::numeric_limits::infinity()))) {} + BoxConstraints(const BoxConstraints& other) = default; + Size biggest() const { return biggest_; } + Size smallest() const { return smallest_; } + + private: + Size smallest_ = Size(0, 0); + Size biggest_ = Size(std::numeric_limits::infinity(), + std::numeric_limits::infinity()); +}; + } // namespace flutter #endif // FLUTTER_SHELL_PLATFORM_COMMON_GEOMETRY_H_ diff --git a/engine/src/flutter/shell/platform/common/isolate_scope.cc b/engine/src/flutter/shell/platform/common/isolate_scope.cc new file mode 100644 index 00000000000..9d1f537876f --- /dev/null +++ b/engine/src/flutter/shell/platform/common/isolate_scope.cc @@ -0,0 +1,40 @@ +// 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/isolate_scope.h" + +namespace flutter { + +Isolate Isolate::Current() { + Dart_Isolate isolate = Dart_CurrentIsolate(); + return Isolate(isolate); +} + +IsolateScope::IsolateScope(const Isolate& isolate) { + isolate_ = isolate.isolate_; + previous_ = Dart_CurrentIsolate(); + if (previous_ == isolate_) { + return; + } + if (previous_) { + Dart_ExitIsolate(); + } + Dart_EnterIsolate(isolate_); +}; + +IsolateScope::~IsolateScope() { + Dart_Isolate current = Dart_CurrentIsolate(); + FML_DCHECK(!current || current == isolate_); + if (previous_ == isolate_) { + return; + } + if (current) { + Dart_ExitIsolate(); + } + if (previous_) { + Dart_EnterIsolate(previous_); + } +} + +} // namespace flutter diff --git a/engine/src/flutter/shell/platform/common/isolate_scope.h b/engine/src/flutter/shell/platform/common/isolate_scope.h new file mode 100644 index 00000000000..6d7c7cc0d08 --- /dev/null +++ b/engine/src/flutter/shell/platform/common/isolate_scope.h @@ -0,0 +1,47 @@ +// 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/fml/logging.h" +#include "third_party/dart/runtime/include/dart_api.h" + +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_ISOLATE_SCOPE_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_ISOLATE_SCOPE_H_ + +namespace flutter { + +/// This class is a thin wrapper around dart isolate. It can be used +/// as argument to IsolateScope constructor to enter and exit the isolate. +class Isolate { + public: + /// Retrieve the current Dart Isolate. If no isolate is current, this + /// results in a crash. + static Isolate Current(); + + ~Isolate() = default; + + private: + explicit Isolate(Dart_Isolate isolate) : isolate_(isolate) { + FML_DCHECK(isolate_ != nullptr); + } + + friend class IsolateScope; + Dart_Isolate isolate_; +}; + +// Enters provided isolate for as long as the scope is alive. +class IsolateScope { + public: + explicit IsolateScope(const Isolate& isolate); + ~IsolateScope(); + + private: + Dart_Isolate isolate_; + Dart_Isolate previous_; + + FML_DISALLOW_COPY_AND_ASSIGN(IsolateScope); +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_COMMON_ISOLATE_SCOPE_H_ diff --git a/engine/src/flutter/shell/platform/common/windowing.h b/engine/src/flutter/shell/platform/common/windowing.h new file mode 100644 index 00000000000..5c37df323c5 --- /dev/null +++ b/engine/src/flutter/shell/platform/common/windowing.h @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_WINDOWING_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_WINDOWING_H_ + +namespace flutter { + +// Types of windows. +// The value must match value from WindowType in the Dart code +// in packages/flutter/lib/src/widgets/window.dart +enum class WindowArchetype { + // Regular top-level window. + kRegular, +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_COMMON_WINDOWING_H_ diff --git a/engine/src/flutter/shell/platform/darwin/macos/BUILD.gn b/engine/src/flutter/shell/platform/darwin/macos/BUILD.gn index dbfec091477..4f445e75b0a 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/macos/BUILD.gn @@ -133,6 +133,8 @@ source_set("flutter_framework_source") { "framework/Source/FlutterViewEngineProvider.h", "framework/Source/FlutterViewEngineProvider.mm", "framework/Source/FlutterViewProvider.h", + "framework/Source/FlutterWindowController.h", + "framework/Source/FlutterWindowController.mm", "framework/Source/KeyCodeMap.g.mm", ] @@ -143,8 +145,10 @@ source_set("flutter_framework_source") { "//flutter/flow:flow", "//flutter/fml", "//flutter/shell/platform/common:common_cpp_accessibility", + "//flutter/shell/platform/common:common_cpp_core", "//flutter/shell/platform/common:common_cpp_enums", "//flutter/shell/platform/common:common_cpp_input", + "//flutter/shell/platform/common:common_cpp_isolate_scope", "//flutter/shell/platform/common:common_cpp_switches", "//flutter/shell/platform/darwin/common:availability_version_check", "//flutter/shell/platform/darwin/common:framework_common", @@ -238,6 +242,7 @@ executable("flutter_desktop_darwin_unittests") { "framework/Source/KeyCodeMapTest.mm", "framework/Source/TestFlutterPlatformView.h", "framework/Source/TestFlutterPlatformView.mm", + "framework/source/FlutterWindowControllerTest.mm", ] deps = [ @@ -245,7 +250,9 @@ executable("flutter_desktop_darwin_unittests") { ":flutter_framework_source", "//flutter/fml", "//flutter/shell/platform/common:common_cpp_accessibility", + "//flutter/shell/platform/common:common_cpp_core", "//flutter/shell/platform/common:common_cpp_enums", + "//flutter/shell/platform/common:common_cpp_isolate_scope", "//flutter/shell/platform/darwin/common:framework_common", "//flutter/shell/platform/darwin/common:test_utils_swift", "//flutter/shell/platform/darwin/graphics", diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index 28801a2e4a6..fd922445d98 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -452,6 +452,9 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, void* user_ // factories. Lifecycle is tied to the engine. FlutterPlatformViewController* _platformViewController; + // Used to manage Flutter windows created by the Dart application + FlutterWindowController* _windowController; + // A message channel for sending user settings to the flutter engine. FlutterBasicMessageChannel* _settingsChannel; @@ -483,8 +486,19 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, void* user_ // The text input plugin that handles text editing state for text fields. FlutterTextInputPlugin* _textInputPlugin; + + // Whether the engine is running in multi-window mode. This affects behavior + // when adding view controller (it will fail when calling multiple times without + // _multiviewEnabled). + BOOL _multiViewEnabled; + + // View identifier for the next view to be created. + // Only used when multiview is enabled. + FlutterViewIdentifier _nextViewIdentifier; } +@synthesize windowController = _windowController; + - (instancetype)initWithName:(NSString*)labelPrefix project:(FlutterDartProject*)project { return [self initWithName:labelPrefix project:project allowHeadlessExecution:YES]; } @@ -528,6 +542,8 @@ static void SetThreadPriority(FlutterThreadPriority priority) { [_isResponseValid addObject:@YES]; _keyboardManager = [[FlutterKeyboardManager alloc] initWithDelegate:self]; _textInputPlugin = [[FlutterTextInputPlugin alloc] initWithDelegate:self]; + _multiViewEnabled = NO; + _nextViewIdentifier = 1; _embedderAPI.struct_size = sizeof(FlutterEngineProcTable); FlutterEngineGetProcAddresses(&_embedderAPI); @@ -549,6 +565,10 @@ static void SetThreadPriority(FlutterThreadPriority priority) { [[FlutterTimeConverter alloc] initWithEngine:self], _platformViewController); [self setUpPlatformViewChannel]; + + _windowController = [[FlutterWindowController alloc] init]; + _windowController.engine = self; + [self setUpAccessibilityChannel]; [self setUpNotificationCenterListeners]; id appDelegate = [[NSApplication sharedApplication] delegate]; @@ -623,6 +643,16 @@ static void SetThreadPriority(FlutterThreadPriority priority) { return cocoa_task_runner_description; } +- (void)onFocusChangeRequest:(const FlutterViewFocusChangeRequest*)request { + FlutterViewController* controller = [self viewControllerForIdentifier:request->view_id]; + if (controller == nil) { + return; + } + if (request->state == kFocused) { + [controller.flutterView.window makeFirstResponder:controller.flutterView]; + } +} + - (BOOL)runWithEntrypoint:(NSString*)entrypoint { if (self.running) { return NO; @@ -737,6 +767,12 @@ static void SetThreadPriority(FlutterThreadPriority priority) { [engine onVSync:baton]; }; + flutterArguments.view_focus_change_request_callback = + [](const FlutterViewFocusChangeRequest* request, void* user_data) { + FlutterEngine* engine = (__bridge FlutterEngine*)user_data; + [engine onFocusChangeRequest:request]; + }; + FlutterRendererConfig rendererConfig = [_renderer createRendererConfig]; FlutterEngineResult result = _embedderAPI.Initialize( FLUTTER_ENGINE_VERSION, &rendererConfig, &flutterArguments, (__bridge void*)(self), &_engine); @@ -797,10 +833,12 @@ static void SetThreadPriority(FlutterThreadPriority priority) { forIdentifier:(FlutterViewIdentifier)viewIdentifier { _macOSCompositor->AddView(viewIdentifier); NSAssert(controller != nil, @"The controller must not be nil."); - NSAssert(controller.engine == nil, - @"The FlutterViewController is unexpectedly attached to " - @"engine %@ before initialization.", - controller.engine); + if (!_multiViewEnabled) { + NSAssert(controller.engine == nil, + @"The FlutterViewController is unexpectedly attached to " + @"engine %@ before initialization.", + controller.engine); + } NSAssert([_viewControllers objectForKey:@(viewIdentifier)] == nil, @"The requested view ID is occupied."); [_viewControllers setObject:controller forKey:@(viewIdentifier)]; @@ -818,6 +856,32 @@ static void SetThreadPriority(FlutterThreadPriority priority) { if (controller.viewLoaded) { [self viewControllerViewDidLoad:controller]; } + + if (viewIdentifier != kFlutterImplicitViewId) { + // These will be overriden immediately after the FlutterView is created + // by actual values. + FlutterWindowMetricsEvent metrics{ + .struct_size = sizeof(FlutterWindowMetricsEvent), + .width = 0, + .height = 0, + .pixel_ratio = 1.0, + }; + bool added = false; + FlutterAddViewInfo info{.struct_size = sizeof(FlutterAddViewInfo), + .view_id = viewIdentifier, + .view_metrics = &metrics, + .user_data = &added, + .add_view_callback = [](const FlutterAddViewResult* r) { + auto added = reinterpret_cast(r->user_data); + *added = true; + }}; + // The callback should be called synchronously from platform thread. + _embedderAPI.AddView(_engine, &info); + FML_DCHECK(added); + if (!added) { + NSLog(@"Failed to add view with ID %llu", viewIdentifier); + } + } } - (void)viewControllerViewDidLoad:(FlutterViewController*)viewController { @@ -835,14 +899,36 @@ static void SetThreadPriority(FlutterThreadPriority priority) { engine->_embedderAPI.OnVsync(_engine, baton, timeNanos, targetTimeNanos); } }]; - FML_DCHECK([_vsyncWaiters objectForKey:@(viewController.viewIdentifier)] == nil); @synchronized(_vsyncWaiters) { + FML_DCHECK([_vsyncWaiters objectForKey:@(viewController.viewIdentifier)] == nil); [_vsyncWaiters setObject:waiter forKey:@(viewController.viewIdentifier)]; } } - (void)deregisterViewControllerForIdentifier:(FlutterViewIdentifier)viewIdentifier { + if (viewIdentifier != kFlutterImplicitViewId) { + bool removed = false; + FlutterRemoveViewInfo info; + info.struct_size = sizeof(FlutterRemoveViewInfo); + info.view_id = viewIdentifier; + info.user_data = &removed; + // RemoveViewCallback is not finished synchronously, the remove_view_callback + // is called from raster thread when the engine knows for sure that the resources + // associated with the view are no longer needed. + info.remove_view_callback = [](const FlutterRemoveViewResult* r) { + auto removed = reinterpret_cast(r->user_data); + [FlutterRunLoop.mainRunLoop performBlock:^{ + *removed = true; + }]; + }; + _embedderAPI.RemoveView(_engine, &info); + while (!removed) { + [[FlutterRunLoop mainRunLoop] pollFlutterMessagesOnce]; + } + } + _macOSCompositor->RemoveView(viewIdentifier); + FlutterViewController* controller = [self viewControllerForIdentifier:viewIdentifier]; // The controller can be nil. The engine stores only a weak ref, and this // method could have been called from the controller's dealloc. @@ -854,9 +940,13 @@ static void SetThreadPriority(FlutterThreadPriority priority) { @"the FlutterEngine is mocked. Please subclass these classes instead."); } [_viewControllers removeObjectForKey:@(viewIdentifier)]; + + FlutterVSyncWaiter* waiter = nil; @synchronized(_vsyncWaiters) { + waiter = [_vsyncWaiters objectForKey:@(viewIdentifier)]; [_vsyncWaiters removeObjectForKey:@(viewIdentifier)]; } + [waiter invalidate]; } - (void)shutDownIfNeeded { @@ -943,11 +1033,46 @@ static void SetThreadPriority(FlutterThreadPriority priority) { #pragma mark - Framework-internal methods - (void)addViewController:(FlutterViewController*)controller { - // FlutterEngine can only handle the implicit view for now. Adding more views - // throws an assertion. - NSAssert(self.viewController == nil, - @"The engine already has a view controller for the implicit view."); - self.viewController = controller; + if (!_multiViewEnabled) { + // When multiview is disabled, the engine will only assign views to the implicit view ID. + // The implicit view ID can be reused if and only if the implicit view is unassigned. + NSAssert(self.viewController == nil, + @"The engine already has a view controller for the implicit view."); + self.viewController = controller; + } else { + // When multiview is enabled, the engine will assign views to a self-incrementing ID. + // The implicit view ID can not be reused. + FlutterViewIdentifier viewIdentifier = _nextViewIdentifier++; + [self registerViewController:controller forIdentifier:viewIdentifier]; + } +} + +- (void)enableMultiView { + if (!_multiViewEnabled) { + NSAssert(self.viewController == nil, + @"Multiview can only be enabled before adding any view controllers."); + _multiViewEnabled = YES; + } +} + +- (void)windowDidBecomeKey:(FlutterViewIdentifier)viewIdentifier { + FlutterViewFocusEvent event{ + .struct_size = sizeof(FlutterViewFocusEvent), + .view_id = viewIdentifier, + .state = kFocused, + .direction = kUndefined, + }; + _embedderAPI.SendViewFocusEvent(_engine, &event); +} + +- (void)windowDidResignKey:(FlutterViewIdentifier)viewIdentifier { + FlutterViewFocusEvent event{ + .struct_size = sizeof(FlutterViewFocusEvent), + .view_id = viewIdentifier, + .state = kUnfocused, + .direction = kUndefined, + }; + _embedderAPI.SendViewFocusEvent(_engine, &event); } - (void)removeViewController:(nonnull FlutterViewController*)viewController { @@ -1171,8 +1296,16 @@ static void SetThreadPriority(FlutterThreadPriority priority) { auto block = ^{ // TODO(knopp): Use vsync waiter for correct view. // https://github.com/flutter/flutter/issues/142845 - FlutterVSyncWaiter* waiter = [_vsyncWaiters objectForKey:@(kFlutterImplicitViewId)]; - [waiter waitForVSync:baton]; + FlutterVSyncWaiter* waiter = + [_vsyncWaiters objectForKey:[_vsyncWaiters.keyEnumerator nextObject]]; + if (waiter != nil) { + [waiter waitForVSync:baton]; + } else { + // Sometimes there is a vsync request right after the last view is removed. + // It still need to be handled, otherwise the engine will stop producing frames + // even if a new view is added later. + self.embedderAPI.OnVsync(_engine, baton, 0, 0); + } }; if ([NSThread isMainThread]) { block(); @@ -1254,6 +1387,7 @@ static void SetThreadPriority(FlutterThreadPriority priority) { [FlutterMouseCursorPlugin registerWithRegistrar:[self registrarForPlugin:@"mousecursor"] delegate:self]; [FlutterMenuPlugin registerWithRegistrar:[self registrarForPlugin:@"menu"]]; + _settingsChannel = [FlutterBasicMessageChannel messageChannelWithName:kFlutterSettingsChannel binaryMessenger:self.binaryMessenger diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h index ec34b267a92..7e562522f61 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h @@ -18,6 +18,7 @@ #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.h" NS_ASSUME_NONNULL_BEGIN @@ -223,6 +224,34 @@ typedef NS_ENUM(NSInteger, FlutterAppExitResponse) { */ @property(nonatomic, readonly) FlutterTextInputPlugin* textInputPlugin; +@property(nonatomic, readonly) FlutterWindowController* windowController; + +/** + * Enables multi-view support. + * + * Called by [FlutterWindowController] before the first view is added. This + * affects the behavior when adding view controllers: + * + * - When multiview is disabled, the engine will only assign views to the + * implicit view ID. The implicit view ID can be reused if and only if the + * implicit view ID is unassigned. + * - When multiview is enabled, the engine will assign views to a + * self-incrementing ID. + * + * Calling enableMultiView when multiview is already enabled is a noop. + */ +- (void)enableMultiView; + +/** + * Notifies the engine that window with the given identifier has been made key. + */ +- (void)windowDidBecomeKey:(FlutterViewIdentifier)viewIdentifier; + +/** + * Notifies the engine that window with the given identifier has resigned being key. + */ +- (void)windowDidResignKey:(FlutterViewIdentifier)viewIdentifier; + /** * Returns an array of screen objects representing all of the screens available on the system. */ @@ -238,6 +267,7 @@ typedef NS_ENUM(NSInteger, FlutterAppExitResponse) { * This function must be called on the main thread. */ + (nullable FlutterEngine*)engineForIdentifier:(int64_t)identifier; + @end NS_ASSUME_NONNULL_END diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.h b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.h index a784a69b94d..db89d04f5fa 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.h +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.h @@ -14,7 +14,7 @@ #include #include -@interface FlutterPlatformViewController : NSViewController +@interface FlutterPlatformViewController : NSObject @end @interface FlutterPlatformViewController () diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.h b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.h index 6a99a5ae025..66a42dc1110 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.h +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.h @@ -24,6 +24,10 @@ /// This function must be called on the main thread. - (void)waitForVSync:(uintptr_t)baton; +/// Invalidates the waiter. This must be called on the main thread +/// before the instance is deallocated. +- (void)invalidate; + @end #endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERVSYNCWAITER_H_ diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.mm index c596638b10f..d5133cb5651 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.mm @@ -118,7 +118,7 @@ static const CFTimeInterval kTimerLatencyCompensation = 0.001; TRACE_VSYNC("VSyncRequest", _pendingBaton.value_or(0)); CFTimeInterval tick_interval = _displayLink.nominalOutputRefreshPeriod; - if (_displayLink.paused || tick_interval == 0) { + if (_displayLink.paused || tick_interval == 0 || _lastTargetTimestamp == 0) { // When starting display link the first notification will come in the middle // of next frame, which would incur a whole frame period of latency. // To avoid that, first vsync notification will be fired using a timer @@ -154,16 +154,22 @@ static const CFTimeInterval kTimerLatencyCompensation = 0.001; } } -- (void)dealloc { +- (void)invalidate { + // It is possible that there is pending vsync request while the view for which + // this waiter belongs is being destroyed. In that case trigger the vsync + // immediately to avoid deadlock. if (_pendingBaton.has_value()) { - [FlutterLogger logWarning:@"Deallocating FlutterVSyncWaiter with a pending vsync"]; + CFTimeInterval now = CACurrentMediaTime(); + _block(now, now, _pendingBaton.value()); + _pendingBaton = std::nullopt; } - // It is possible that block running on UI thread held the last reference to - // the waiter, in which case reschedule to main thread. - FlutterDisplayLink* link = _displayLink; - dispatch_async(dispatch_get_main_queue(), ^{ - [link invalidate]; - }); + + [_displayLink invalidate]; + _displayLink = nil; +} + +- (void)dealloc { + FML_DCHECK(_displayLink == nil); } @end diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiterTest.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiterTest.mm index 4fdd063deee..fe68abaf2e7 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiterTest.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiterTest.mm @@ -55,6 +55,7 @@ TEST_F(FlutterVSyncWaiterTest, RequestsInitialVSync) { [displayLink tickWithTimestamp:CACurrentMediaTime() targetTimestamp:CACurrentMediaTime() + 1.0 / 60.0]; EXPECT_TRUE(displayLink.paused); + [waiter invalidate]; } static void BusyWait(CFTimeInterval duration) { @@ -111,6 +112,8 @@ TEST_F(FlutterVSyncWaiterTest, FirstVSyncIsSynthesized) { EXPECT_DOUBLE_EQ(timestamp, expectedTimestamp); EXPECT_DOUBLE_EQ(targetTimestamp, expectedTimestamp + displayLink.nominalOutputRefreshPeriod); EXPECT_EQ(baton, size_t(1)); + + [waiter invalidate]; }; // First argument if the wait duration after reference vsync. @@ -204,4 +207,6 @@ TEST_F(FlutterVSyncWaiterTest, VSyncWorks) { EXPECT_DOUBLE_EQ(entries[3].timestamp, now + 3 * displayLink.nominalOutputRefreshPeriod); EXPECT_DOUBLE_EQ(entries[3].targetTimestamp, now + 4 * displayLink.nominalOutputRefreshPeriod); EXPECT_EQ(entries[3].baton, size_t(3)); + + [waiter invalidate]; } diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index 5346c452dae..4111bc52937 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -327,6 +327,7 @@ static void CommonInit(FlutterViewController* controller, FlutterEngine* engine) @"engine %@ before initialization.", controller.engine); [engine addViewController:controller]; + NSCAssert(controller.engine != nil, @"The FlutterViewController unexpectedly stays unattached after initialization. " @"In unit tests, this is likely because either the FlutterViewController or " @@ -418,13 +419,17 @@ static void CommonInit(FlutterViewController* controller, FlutterEngine* engine) _keyUpMonitor = nil; } -- (void)dealloc { +- (void)dispose { if ([self attached]) { [_engine removeViewController:self]; } [self.flutterView shutDown]; } +- (void)dealloc { + [self dispose]; +} + #pragma mark - Public methods - (void)setMouseTrackingMode:(FlutterMouseTrackingMode)mode { diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h index 301d584fdf6..d2d8434ddf3 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h @@ -53,6 +53,14 @@ */ - (void)updateSemantics:(nonnull const FlutterSemanticsUpdate2*)update; +/** + * Removes this controller from the engine. The controller is removed from the engine + * on dealloc, however in multi-window scenario the controller needs to be unregistered + * from the engine eagerly - because the FlutterView needs to be removed from the + * Flutter isolate before destroying the controller and window. + */ +- (void)dispose; + @end // Private methods made visible for testing diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.h b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.h new file mode 100644 index 00000000000..39681175cec --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.h @@ -0,0 +1,100 @@ +// 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_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERWINDOWCONTROLLER_H_ +#define FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERWINDOWCONTROLLER_H_ + +#import + +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" +#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h" + +@class FlutterEngine; + +@interface FlutterWindowController : NSObject + +@property(nonatomic, weak) FlutterEngine* engine; + +@end + +@interface FlutterWindowController (Testing) + +- (void)closeAllWindows; + +@end + +struct FlutterWindowSizing { + bool has_size; + double width; + double height; + bool has_constraints; + double min_width; + double min_height; + double max_width; + double max_height; +}; + +struct FlutterWindowCreationRequest { + FlutterWindowSizing contentSize; + void (*on_close)(); + void (*on_size_change)(); +}; + +struct FlutterWindowSize { + double width; + double height; +}; + +extern "C" { + +// NOLINTBEGIN(google-objc-function-naming) + +FLUTTER_DARWIN_EXPORT +int64_t InternalFlutter_WindowController_CreateRegularWindow( + int64_t engine_id, + const FlutterWindowCreationRequest* request); + +FLUTTER_DARWIN_EXPORT +void InternalFlutter_Window_Destroy(int64_t engine_id, void* window); + +FLUTTER_DARWIN_EXPORT +void* InternalFlutter_Window_GetHandle(int64_t engine_id, FlutterViewIdentifier view_id); + +FLUTTER_DARWIN_EXPORT +FlutterWindowSize InternalFlutter_Window_GetContentSize(void* window); + +FLUTTER_DARWIN_EXPORT +void InternalFlutter_Window_SetContentSize(void* window, const FlutterWindowSizing* size); + +FLUTTER_DARWIN_EXPORT +void InternalFlutter_Window_SetTitle(void* window, const char* title); + +FLUTTER_DARWIN_EXPORT +void InternalFlutter_Window_SetMaximized(void* window, bool maximized); + +FLUTTER_DARWIN_EXPORT +bool InternalFlutter_Window_IsMaximized(void* window); + +FLUTTER_DARWIN_EXPORT +void InternalFlutter_Window_Minimize(void* window); + +FLUTTER_DARWIN_EXPORT +void InternalFlutter_Window_Unminimize(void* window); + +FLUTTER_DARWIN_EXPORT +bool InternalFlutter_Window_IsMinimized(void* window); + +FLUTTER_DARWIN_EXPORT +void InternalFlutter_Window_SetFullScreen(void* window, bool fullScreen); + +FLUTTER_DARWIN_EXPORT +bool InternalFlutter_Window_IsFullScreen(void* window); + +FLUTTER_DARWIN_EXPORT +void InternalFlutter_Window_Activate(void* window); + +// NOLINTEND(google-objc-function-naming) +} + +#endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERWINDOWCONTROLLER_H_ diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.mm new file mode 100644 index 00000000000..95aa3548ea5 --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.mm @@ -0,0 +1,269 @@ +// 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. + +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.h" +#include + +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h" + +#include "flutter/shell/platform/common/isolate_scope.h" + +// A delegate for a Flutter managed window. +@interface FlutterWindowOwner : NSObject { + // Strong reference to the window. This is the only strong reference to the + // window. + NSWindow* _window; + FlutterViewController* _flutterViewController; + std::optional _isolate; + FlutterWindowCreationRequest _creationRequest; +} + +@property(readonly, nonatomic) NSWindow* window; +@property(readonly, nonatomic) FlutterViewController* flutterViewController; + +- (instancetype)initWithWindow:(NSWindow*)window + flutterViewController:(FlutterViewController*)viewController + creationRequest:(const FlutterWindowCreationRequest&)creationRequest; + +@end + +@interface NSWindow (FlutterWindowSizing) + +- (void)flutterSetContentSize:(FlutterWindowSizing)contentSize; + +@end + +@implementation NSWindow (FlutterWindowSizing) +- (void)flutterSetContentSize:(FlutterWindowSizing)contentSize { + if (contentSize.has_size) { + [self setContentSize:NSMakeSize(contentSize.width, contentSize.height)]; + } + if (contentSize.has_constraints) { + [self setContentMinSize:NSMakeSize(contentSize.min_width, contentSize.min_height)]; + if (contentSize.max_width > 0 && contentSize.max_height > 0) { + [self setContentMaxSize:NSMakeSize(contentSize.max_width, contentSize.max_height)]; + } else { + [self setContentMaxSize:NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX)]; + } + } +} + +@end + +@implementation FlutterWindowOwner + +@synthesize window = _window; +@synthesize flutterViewController = _flutterViewController; + +- (instancetype)initWithWindow:(NSWindow*)window + flutterViewController:(FlutterViewController*)viewController + creationRequest:(const FlutterWindowCreationRequest&)creationRequest { + if (self = [super init]) { + _window = window; + _flutterViewController = viewController; + _creationRequest = creationRequest; + _isolate = flutter::Isolate::Current(); + } + return self; +} + +- (void)windowDidBecomeKey:(NSNotification*)notification { + [_flutterViewController.engine windowDidBecomeKey:_flutterViewController.viewIdentifier]; +} + +- (void)windowDidResignKey:(NSNotification*)notification { + [_flutterViewController.engine windowDidResignKey:_flutterViewController.viewIdentifier]; +} + +- (BOOL)windowShouldClose:(NSWindow*)sender { + flutter::IsolateScope isolate_scope(*_isolate); + _creationRequest.on_close(); + return NO; +} + +- (void)windowDidResize:(NSNotification*)notification { + flutter::IsolateScope isolate_scope(*_isolate); + _creationRequest.on_size_change(); +} + +// Miniaturize does not trigger resize event, but for now there +// is no other way to get notification about the state change. +- (void)windowDidMiniaturize:(NSNotification*)notification { + flutter::IsolateScope isolate_scope(*_isolate); + _creationRequest.on_size_change(); +} + +// Deminiaturize does not trigger resize event, but for now there +// is no other way to get notification about the state change. +- (void)windowDidDeminiaturize:(NSNotification*)notification { + flutter::IsolateScope isolate_scope(*_isolate); + _creationRequest.on_size_change(); +} + +@end + +@interface FlutterWindowController () { + NSMutableArray* _windows; +} + +@end + +@implementation FlutterWindowController + +- (instancetype)init { + self = [super init]; + if (self != nil) { + _windows = [NSMutableArray array]; + } + return self; +} + +- (FlutterViewIdentifier)createRegularWindow:(const FlutterWindowCreationRequest*)request { + FlutterViewController* c = [[FlutterViewController alloc] initWithEngine:_engine + nibName:nil + bundle:nil]; + + NSWindow* window = [[NSWindow alloc] init]; + // If this is not set there will be double free on window close when + // using ARC. + [window setReleasedWhenClosed:NO]; + + window.contentViewController = c; + window.styleMask = NSWindowStyleMaskResizable | NSWindowStyleMaskTitled | + NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; + [window flutterSetContentSize:request->contentSize]; + [window setIsVisible:YES]; + [window makeKeyAndOrderFront:nil]; + + FlutterWindowOwner* w = [[FlutterWindowOwner alloc] initWithWindow:window + flutterViewController:c + creationRequest:*request]; + window.delegate = w; + [_windows addObject:w]; + + return c.viewIdentifier; +} + +- (void)destroyWindow:(NSWindow*)window { + FlutterWindowOwner* owner = nil; + for (FlutterWindowOwner* o in _windows) { + if (o.window == window) { + owner = o; + break; + } + } + if (owner != nil) { + [_windows removeObject:owner]; + // Make sure to unregister the controller from the engine and remove the FlutterView + // before destroying the window and Flutter NSView. + [owner.flutterViewController dispose]; + owner.window.delegate = nil; + [owner.window close]; + } +} + +- (void)closeAllWindows { + for (FlutterWindowOwner* owner in _windows) { + [owner.flutterViewController dispose]; + [owner.window close]; + } + [_windows removeAllObjects]; +} + +@end + +// NOLINTBEGIN(google-objc-function-naming) + +int64_t InternalFlutter_WindowController_CreateRegularWindow( + int64_t engine_id, + const FlutterWindowCreationRequest* request) { + FlutterEngine* engine = [FlutterEngine engineForIdentifier:engine_id]; + [engine enableMultiView]; + return [engine.windowController createRegularWindow:request]; +} + +void InternalFlutter_Window_Destroy(int64_t engine_id, void* window) { + NSWindow* w = (__bridge NSWindow*)window; + FlutterEngine* engine = [FlutterEngine engineForIdentifier:engine_id]; + [engine.windowController destroyWindow:w]; +} + +void* InternalFlutter_Window_GetHandle(int64_t engine_id, FlutterViewIdentifier view_id) { + FlutterEngine* engine = [FlutterEngine engineForIdentifier:engine_id]; + FlutterViewController* controller = [engine viewControllerForIdentifier:view_id]; + return (__bridge void*)controller.view.window; +} + +FlutterWindowSize InternalFlutter_Window_GetContentSize(void* window) { + NSWindow* w = (__bridge NSWindow*)window; + return { + .width = w.frame.size.width, + .height = w.frame.size.height, + }; +} + +void InternalFlutter_Window_SetContentSize(void* window, const FlutterWindowSizing* size) { + NSWindow* w = (__bridge NSWindow*)window; + [w flutterSetContentSize:*size]; +} + +void InternalFlutter_Window_SetTitle(void* window, const char* title) { + NSWindow* w = (__bridge NSWindow*)window; + w.title = [NSString stringWithUTF8String:title]; +} + +void InternalFlutter_Window_SetMaximized(void* window, bool maximized) { + NSWindow* w = (__bridge NSWindow*)window; + if (maximized & !w.isZoomed) { + [w zoom:nil]; + } else if (!maximized && w.isZoomed) { + [w zoom:nil]; + } +} + +bool InternalFlutter_Window_IsMaximized(void* window) { + NSWindow* w = (__bridge NSWindow*)window; + return w.isZoomed; +} + +void InternalFlutter_Window_Minimize(void* window) { + NSWindow* w = (__bridge NSWindow*)window; + [w miniaturize:nil]; +} + +void InternalFlutter_Window_Unminimize(void* window) { + NSWindow* w = (__bridge NSWindow*)window; + [w deminiaturize:nil]; +} + +bool InternalFlutter_Window_IsMinimized(void* window) { + NSWindow* w = (__bridge NSWindow*)window; + return w.isMiniaturized; +} + +void InternalFlutter_Window_SetFullScreen(void* window, bool fullScreen) { + NSWindow* w = (__bridge NSWindow*)window; + bool isFullScreen = (w.styleMask & NSWindowStyleMaskFullScreen) != 0; + if (fullScreen && !isFullScreen) { + [w toggleFullScreen:nil]; + } else if (!fullScreen && isFullScreen) { + [w toggleFullScreen:nil]; + } +} + +bool InternalFlutter_Window_IsFullScreen(void* window) { + NSWindow* w = (__bridge NSWindow*)window; + return (w.styleMask & NSWindowStyleMaskFullScreen) != 0; +} + +void InternalFlutter_Window_Activate(void* window) { + NSWindow* w = (__bridge NSWindow*)window; + [NSApplication.sharedApplication activateIgnoringOtherApps:YES]; + [w makeKeyAndOrderFront:nil]; +} + +// NOLINTEND(google-objc-function-naming) diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowControllerTest.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowControllerTest.mm new file mode 100644 index 00000000000..36068f950f8 --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowControllerTest.mm @@ -0,0 +1,194 @@ +// 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. + +#import "flutter/shell/platform/common/isolate_scope.h" +#import "flutter/shell/platform/common/windowing.h" + +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.h" +#import "flutter/testing/testing.h" +#import "third_party/googletest/googletest/include/gtest/gtest.h" + +namespace flutter::testing { + +class FlutterWindowControllerTest : public FlutterEngineTest { + public: + FlutterWindowControllerTest() = default; + + void SetUp() { + FlutterEngineTest::SetUp(); + + [GetFlutterEngine() runWithEntrypoint:@"testWindowController"]; + + bool signalled = false; + + AddNativeCallback("SignalNativeTest", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + isolate_ = Isolate::Current(); + signalled = true; + })); + + while (!signalled) { + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false); + } + } + + void TearDown() { + [GetFlutterEngine().windowController closeAllWindows]; + FlutterEngineTest::TearDown(); + } + + protected: + flutter::Isolate& isolate() { + if (isolate_) { + return *isolate_; + } else { + FML_LOG(ERROR) << "Isolate is not set."; + FML_UNREACHABLE(); + } + } + + std::optional isolate_; +}; + +class FlutterWindowControllerRetainTest : public ::testing::Test {}; + +TEST_F(FlutterWindowControllerTest, CreateRegularWindow) { + FlutterWindowCreationRequest request{ + .contentSize = {.has_size = true, .width = 800, .height = 600}, + .on_close = [] {}, + .on_size_change = [] {}, + }; + + FlutterEngine* engine = GetFlutterEngine(); + int64_t engineId = reinterpret_cast(engine); + + { + IsolateScope isolate_scope(isolate()); + int64_t handle = InternalFlutter_WindowController_CreateRegularWindow(engineId, &request); + EXPECT_EQ(handle, 1); + + FlutterViewController* viewController = [engine viewControllerForIdentifier:handle]; + EXPECT_NE(viewController, nil); + CGSize size = viewController.view.frame.size; + EXPECT_EQ(size.width, 800); + EXPECT_EQ(size.height, 600); + } +} + +TEST_F(FlutterWindowControllerRetainTest, WindowControllerDoesNotRetainEngine) { + FlutterWindowCreationRequest request{ + .contentSize = {.has_size = true, .width = 800, .height = 600}, + .on_close = [] {}, + .on_size_change = [] {}, + }; + + __weak FlutterEngine* weakEngine = nil; + @autoreleasepool { + NSString* fixtures = @(flutter::testing::GetFixturesPath()); + NSLog(@"Fixtures path: %@", fixtures); + FlutterDartProject* project = [[FlutterDartProject alloc] + initWithAssetsPath:fixtures + ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; + + static std::optional isolate; + isolate = std::nullopt; + + project.rootIsolateCreateCallback = [](void*) { isolate = flutter::Isolate::Current(); }; + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" + project:project + allowHeadlessExecution:YES]; + weakEngine = engine; + [engine runWithEntrypoint:@"testWindowControllerRetainCycle"]; + + int64_t engineId = reinterpret_cast(engine); + + { + FML_DCHECK(isolate.has_value()); + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + IsolateScope isolateScope(*isolate); + int64_t handle = InternalFlutter_WindowController_CreateRegularWindow(engineId, &request); + EXPECT_EQ(handle, 1); + } + + [engine.windowController closeAllWindows]; + [engine shutDownEngine]; + } + EXPECT_EQ(weakEngine, nil); +} + +TEST_F(FlutterWindowControllerTest, DestroyRegularWindow) { + FlutterWindowCreationRequest request{ + .contentSize = {.has_size = true, .width = 800, .height = 600}, + .on_close = [] {}, + .on_size_change = [] {}, + }; + + FlutterEngine* engine = GetFlutterEngine(); + int64_t engine_id = reinterpret_cast(engine); + + IsolateScope isolate_scope(isolate()); + int64_t handle = InternalFlutter_WindowController_CreateRegularWindow(engine_id, &request); + FlutterViewController* viewController = [engine viewControllerForIdentifier:handle]; + + InternalFlutter_Window_Destroy(engine_id, (__bridge void*)viewController.view.window); + viewController = [engine viewControllerForIdentifier:handle]; + EXPECT_EQ(viewController, nil); +} + +TEST_F(FlutterWindowControllerTest, InternalFlutter_Window_GetHandle) { + FlutterWindowCreationRequest request{ + .contentSize = {.has_size = true, .width = 800, .height = 600}, + .on_close = [] {}, + .on_size_change = [] {}, + }; + + FlutterEngine* engine = GetFlutterEngine(); + int64_t engine_id = reinterpret_cast(engine); + + IsolateScope isolate_scope(isolate()); + int64_t handle = InternalFlutter_WindowController_CreateRegularWindow(engine_id, &request); + FlutterViewController* viewController = [engine viewControllerForIdentifier:handle]; + + void* window_handle = InternalFlutter_Window_GetHandle(engine_id, handle); + EXPECT_EQ(window_handle, (__bridge void*)viewController.view.window); +} + +TEST_F(FlutterWindowControllerTest, WindowStates) { + FlutterWindowCreationRequest request{ + .contentSize = {.has_size = true, .width = 800, .height = 600}, + .on_close = [] {}, + .on_size_change = [] {}, + }; + + FlutterEngine* engine = GetFlutterEngine(); + int64_t engine_id = reinterpret_cast(engine); + + IsolateScope isolate_scope(isolate()); + int64_t handle = InternalFlutter_WindowController_CreateRegularWindow(engine_id, &request); + + FlutterViewController* viewController = [engine viewControllerForIdentifier:handle]; + NSWindow* window = viewController.view.window; + void* windowHandle = (__bridge void*)window; + + EXPECT_EQ(window.zoomed, NO); + EXPECT_EQ(window.miniaturized, NO); + EXPECT_EQ(window.styleMask & NSWindowStyleMaskFullScreen, 0u); + + InternalFlutter_Window_SetMaximized(windowHandle, true); + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, false); + EXPECT_EQ(window.zoomed, YES); + + InternalFlutter_Window_SetMaximized(windowHandle, false); + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, false); + EXPECT_EQ(window.zoomed, NO); + + // FullScreen toggle does not seem to work when the application is not run from a bundle. + + InternalFlutter_Window_Minimize(windowHandle); + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, false); + EXPECT_EQ(window.miniaturized, YES); +} +} // namespace flutter::testing diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/fixtures/flutter_desktop_test.dart b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/fixtures/flutter_desktop_test.dart index 208d466cbf4..a58f2eecb94 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/fixtures/flutter_desktop_test.dart +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/fixtures/flutter_desktop_test.dart @@ -93,3 +93,11 @@ external void notifyEngineId(int? engineId); void testEngineId() { notifyEngineId(PlatformDispatcher.instance.engineId); } + +@pragma('vm:entry-point') +void testWindowController() { + signalNativeTest(); +} + +@pragma('vm:entry-point') +void testWindowControllerRetainCycle() {} diff --git a/engine/src/flutter/shell/platform/windows/BUILD.gn b/engine/src/flutter/shell/platform/windows/BUILD.gn index e80662e592a..7b31e8570d1 100644 --- a/engine/src/flutter/shell/platform/windows/BUILD.gn +++ b/engine/src/flutter/shell/platform/windows/BUILD.gn @@ -89,6 +89,8 @@ source_set("flutter_windows_source") { "flutter_windows_view.h", "flutter_windows_view_controller.cc", "flutter_windows_view_controller.h", + "host_window.cc", + "host_window.h", "keyboard_handler_base.h", "keyboard_key_channel_handler.cc", "keyboard_key_channel_handler.h", @@ -122,6 +124,8 @@ source_set("flutter_windows_source") { "text_input_plugin.h", "window_binding_handler.h", "window_binding_handler_delegate.h", + "window_manager.cc", + "window_manager.h", "window_proc_delegate_manager.cc", "window_proc_delegate_manager.h", "window_state.h", @@ -158,6 +162,7 @@ source_set("flutter_windows_source") { "//flutter/impeller/renderer/backend/gles", "//flutter/shell/platform/common:common_cpp", "//flutter/shell/platform/common:common_cpp_input", + "//flutter/shell/platform/common:common_cpp_isolate_scope", "//flutter/shell/platform/common:common_cpp_switches", "//flutter/shell/platform/common/client_wrapper:client_wrapper", "//flutter/shell/platform/embedder:embedder_as_internal_library", @@ -247,6 +252,7 @@ executable("flutter_windows_unittests") { "testing/wm_builders.cc", "testing/wm_builders.h", "text_input_plugin_unittest.cc", + "window_manager_unittests.cc", "window_proc_delegate_manager_unittests.cc", "window_unittests.cc", "windows_lifecycle_manager_unittests.cc", diff --git a/engine/src/flutter/shell/platform/windows/fixtures/main.dart b/engine/src/flutter/shell/platform/windows/fixtures/main.dart index 3f874134698..8a7a5db8d94 100644 --- a/engine/src/flutter/shell/platform/windows/fixtures/main.dart +++ b/engine/src/flutter/shell/platform/windows/fixtures/main.dart @@ -404,6 +404,11 @@ void testEngineId() { notifyEngineId(ui.PlatformDispatcher.instance.engineId); } +@pragma('vm:entry-point') +void testWindowController() { + signal(); +} + @pragma('vm:entry-point') Future sendSemanticsTreeInfo() async { // Wait until semantics are enabled. diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc index 3b6d76db185..7a80bbd889e 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc @@ -25,6 +25,7 @@ #include "flutter/shell/platform/windows/keyboard_key_channel_handler.h" #include "flutter/shell/platform/windows/system_utils.h" #include "flutter/shell/platform/windows/task_runner.h" +#include "flutter/shell/platform/windows/window_manager.h" #include "flutter/third_party/accessibility/ax/ax_node.h" #include "shell/platform/windows/flutter_project_bundle.h" @@ -205,8 +206,18 @@ FlutterWindowsEngine::FlutterWindowsEngine( FlutterWindowsEngine* that = static_cast(user_data); BASE_DCHECK(that->lifecycle_manager_); - return that->lifecycle_manager_->WindowProc(hwnd, msg, wpar, lpar, - result); + bool handled = + that->lifecycle_manager_->WindowProc(hwnd, msg, wpar, lpar, result); + if (handled) { + return true; + } + auto message_result = + that->window_manager_->HandleMessage(hwnd, msg, wpar, lpar); + if (message_result) { + *result = *message_result; + return true; + } + return false; }, static_cast(this)); @@ -224,6 +235,7 @@ FlutterWindowsEngine::FlutterWindowsEngine( std::make_unique(messenger_wrapper_.get(), this); platform_handler_ = std::make_unique(messenger_wrapper_.get(), this); + window_manager_ = std::make_unique(this); settings_plugin_ = std::make_unique(messenger_wrapper_.get(), task_runner_.get()); } @@ -499,6 +511,7 @@ bool FlutterWindowsEngine::Run(std::string_view entrypoint) { bool FlutterWindowsEngine::Stop() { if (engine_) { + window_manager_->OnEngineShutdown(); for (const auto& [callback, registrar] : plugin_registrar_destruction_callbacks_) { callback(registrar); @@ -839,6 +852,20 @@ HCURSOR FlutterWindowsEngine::GetCursorByName( return windows_proc_table_->LoadCursor(nullptr, idc_name); } +FlutterWindowsView* FlutterWindowsEngine::GetViewFromTopLevelWindow( + HWND hwnd) const { + std::shared_lock read_lock(views_mutex_); + auto const iterator = + std::find_if(views_.begin(), views_.end(), [hwnd](auto const& pair) { + FlutterWindowsView* const view = pair.second; + return GetAncestor(view->GetWindowHandle(), GA_ROOT) == hwnd; + }); + if (iterator != views_.end()) { + return iterator->second; + } + return nullptr; +} + void FlutterWindowsEngine::SendSystemLocales() { std::vector languages = GetPreferredLanguageInfo(*windows_proc_table_); diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h index 2fa87840546..40a8f873873 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h @@ -32,6 +32,7 @@ #include "flutter/shell/platform/windows/flutter_desktop_messenger.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/host_window.h" #include "flutter/shell/platform/windows/keyboard_handler_base.h" #include "flutter/shell/platform/windows/keyboard_key_embedder_handler.h" #include "flutter/shell/platform/windows/platform_handler.h" @@ -315,6 +316,12 @@ class FlutterWindowsEngine { // Sets the cursor directly from a cursor handle. void SetFlutterCursor(HCURSOR cursor) const; + WindowManager* window_manager() { return window_manager_.get(); } + + // Returns the root view associated with the top-level window with |hwnd| as + // the window handle or nullptr if no such view could be found. + FlutterWindowsView* GetViewFromTopLevelWindow(HWND hwnd) const; + protected: // Creates the keyboard key handler. // @@ -453,6 +460,10 @@ class FlutterWindowsEngine { // Handlers for keyboard events from Windows. std::unique_ptr keyboard_key_handler_; + // The manager that manages the lifecycle of |HostWindow|s, native + // Win32 windows hosting a Flutter view in their client area. + std::unique_ptr window_manager_; + // Handlers for text events from Windows. std::unique_ptr text_input_plugin_; diff --git a/engine/src/flutter/shell/platform/windows/host_window.cc b/engine/src/flutter/shell/platform/windows/host_window.cc new file mode 100644 index 00000000000..2723bb99857 --- /dev/null +++ b/engine/src/flutter/shell/platform/windows/host_window.cc @@ -0,0 +1,495 @@ +// 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/host_window.h" + +#include + +#include "flutter/shell/platform/windows/dpi_utils.h" +#include "flutter/shell/platform/windows/flutter_window.h" +#include "flutter/shell/platform/windows/flutter_windows_view_controller.h" +#include "flutter/shell/platform/windows/window_manager.h" + +namespace { + +constexpr wchar_t kWindowClassName[] = L"FLUTTER_HOST_WINDOW"; + +// Clamps |size| to the size of the virtual screen. Both the parameter and +// return size are in physical coordinates. +flutter::Size ClampToVirtualScreen(flutter::Size size) { + double const virtual_screen_width = GetSystemMetrics(SM_CXVIRTUALSCREEN); + double const virtual_screen_height = GetSystemMetrics(SM_CYVIRTUALSCREEN); + + return flutter::Size(std::clamp(size.width(), 0.0, virtual_screen_width), + std::clamp(size.height(), 0.0, virtual_screen_height)); +} + +void EnableTransparentWindowBackground(HWND hwnd, + flutter::WindowsProcTable const& win32) { + 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}; + flutter::WindowsProcTable::WINDOWCOMPOSITIONATTRIBDATA data = { + .Attrib = + flutter::WindowsProcTable::WINDOWCOMPOSITIONATTRIB::WCA_ACCENT_POLICY, + .pvData = &accent, + .cbData = sizeof(accent)}; + win32.SetWindowCompositionAttribute(hwnd, &data); + + // Extend the frame into the client area and set the window's system + // backdrop type for visual effects. + MARGINS const margins = {-1}; + win32.DwmExtendFrameIntoClientArea(hwnd, &margins); + INT effect_value = 1; + win32.DwmSetWindowAttribute(hwnd, DWMWA_SYSTEMBACKDROP_TYPE, &effect_value, + sizeof(BOOL)); +} + +// Retrieves the calling thread's last-error code message as a string, +// or a fallback message if the error message cannot be formatted. +std::string GetLastErrorAsString() { + LPWSTR message_buffer = nullptr; + + if (DWORD const size = FormatMessage( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&message_buffer), 0, nullptr)) { + std::wstring const wide_message(message_buffer, size); + LocalFree(message_buffer); + message_buffer = nullptr; + + if (int const buffer_size = + WideCharToMultiByte(CP_UTF8, 0, wide_message.c_str(), -1, nullptr, + 0, nullptr, nullptr)) { + std::string message(buffer_size, 0); + WideCharToMultiByte(CP_UTF8, 0, wide_message.c_str(), -1, &message[0], + buffer_size, nullptr, nullptr); + return message; + } + } + + if (message_buffer) { + LocalFree(message_buffer); + } + std::ostringstream oss; + oss << "Format message failed with 0x" << std::hex << std::setfill('0') + << std::setw(8) << GetLastError(); + return oss.str(); +} + +// Calculates the required window size, in physical coordinates, to +// accommodate the given |client_size|, in logical coordinates, constrained by +// optional |smallest| and |biggest|, for a window with the specified +// |window_style| and |extended_window_style|. If |owner_hwnd| is not null, the +// DPI of the display with the largest area of intersection with |owner_hwnd| is +// used for the calculation; otherwise, the primary display's DPI is used. The +// resulting size includes window borders, non-client areas, and drop shadows. +// On error, returns std::nullopt and logs an error message. +std::optional GetWindowSizeForClientSize( + flutter::WindowsProcTable const& win32, + flutter::Size const& client_size, + std::optional smallest, + std::optional biggest, + 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 = { + .right = static_cast(client_size.width() * scale_factor), + .bottom = static_cast(client_size.height() * scale_factor)}; + + if (!win32.AdjustWindowRectExForDpi(&rect, window_style, FALSE, + extended_window_style, dpi)) { + FML_LOG(ERROR) << "Failed to run AdjustWindowRectExForDpi: " + << GetLastErrorAsString(); + return std::nullopt; + } + + double width = static_cast(rect.right - rect.left); + double height = static_cast(rect.bottom - rect.top); + + // Apply size constraints + double const non_client_width = width - (client_size.width() * scale_factor); + double const non_client_height = + height - (client_size.height() * scale_factor); + if (smallest) { + flutter::Size min_physical_size = ClampToVirtualScreen( + flutter::Size(smallest->width() * scale_factor + non_client_width, + smallest->height() * scale_factor + non_client_height)); + width = std::max(width, min_physical_size.width()); + height = std::max(height, min_physical_size.height()); + } + if (biggest) { + flutter::Size max_physical_size = ClampToVirtualScreen( + flutter::Size(biggest->width() * scale_factor + non_client_width, + biggest->height() * scale_factor + non_client_height)); + width = std::min(width, max_physical_size.width()); + height = std::min(height, max_physical_size.height()); + } + + return flutter::Size{width, height}; +} + +// 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 + +// Updates 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)); + } +} + +// Inserts |content| into the window tree. +void SetChildContent(HWND content, HWND window) { + SetParent(content, window); + RECT client_rect; + GetClientRect(window, &client_rect); + MoveWindow(content, client_rect.left, client_rect.top, + client_rect.right - client_rect.left, + client_rect.bottom - client_rect.top, true); +} + +} // namespace + +namespace flutter { + +std::unique_ptr HostWindow::CreateRegularWindow( + WindowManager* window_manager, + FlutterWindowsEngine* engine, + const WindowSizing& content_size) { + DWORD window_style = WS_OVERLAPPEDWINDOW; + DWORD extended_window_style = 0; + std::optional smallest = std::nullopt; + std::optional biggest = std::nullopt; + + if (content_size.has_view_constraints) { + smallest = Size(content_size.view_min_width, content_size.view_min_height); + if (content_size.view_max_width > 0 && content_size.view_max_height > 0) { + biggest = Size(content_size.view_max_width, content_size.view_max_height); + } + } + + // TODO(knopp): What about windows sized to content? + FML_CHECK(content_size.has_preferred_view_size); + + // Calculate the screen space window rectangle for the new window. + // Default positioning values (CW_USEDEFAULT) are used + // if the window has no owner. + Rect const initial_window_rect = [&]() -> Rect { + std::optional const window_size = GetWindowSizeForClientSize( + *engine->windows_proc_table(), + Size(content_size.preferred_view_width, + content_size.preferred_view_height), + smallest, biggest, window_style, extended_window_style, nullptr); + return {{CW_USEDEFAULT, CW_USEDEFAULT}, + window_size ? *window_size : Size{CW_USEDEFAULT, CW_USEDEFAULT}}; + }(); + + // Set up the view. + auto view_window = std::make_unique( + initial_window_rect.width(), initial_window_rect.height(), + engine->windows_proc_table()); + + std::unique_ptr view = + engine->CreateView(std::move(view_window)); + if (view == nullptr) { + FML_LOG(ERROR) << "Failed to create view"; + return nullptr; + } + + std::unique_ptr view_controller = + std::make_unique(nullptr, std::move(view)); + FML_CHECK(engine->running()); + // 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. + engine->UpdateAccessibilityFeatures(); + + // 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 = HostWindow::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(); + return nullptr; + } + } + + // Create the native window. + HWND hwnd = CreateWindowEx( + extended_window_style, kWindowClassName, L"", window_style, + initial_window_rect.left(), initial_window_rect.top(), + initial_window_rect.width(), initial_window_rect.height(), nullptr, + nullptr, GetModuleHandle(nullptr), engine->windows_proc_table().get()); + if (!hwnd) { + FML_LOG(ERROR) << "Cannot create window: " << GetLastErrorAsString(); + return nullptr; + } + + // 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_rect; + DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rect, + sizeof(frame_rect)); + RECT window_rect; + GetWindowRect(hwnd, &window_rect); + LONG const left_dropshadow_width = frame_rect.left - window_rect.left; + LONG const top_dropshadow_height = window_rect.top - frame_rect.top; + SetWindowPos(hwnd, nullptr, window_rect.left - left_dropshadow_width, + window_rect.top - top_dropshadow_height, 0, 0, + SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + + UpdateTheme(hwnd); + + SetChildContent(view_controller->view()->GetWindowHandle(), hwnd); + + // 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_SHOWNORMAL); + return std::unique_ptr(new HostWindow( + window_manager, engine, WindowArchetype::kRegular, + std::move(view_controller), BoxConstraints(smallest, biggest), hwnd)); +} + +HostWindow::HostWindow( + WindowManager* window_manager, + FlutterWindowsEngine* engine, + WindowArchetype archetype, + std::unique_ptr view_controller, + const BoxConstraints& box_constraints, + HWND hwnd) + : window_manager_(window_manager), + engine_(engine), + archetype_(archetype), + view_controller_(std::move(view_controller)), + window_handle_(hwnd), + box_constraints_(box_constraints) { + SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast(this)); +} + +HostWindow::~HostWindow() { + if (view_controller_) { + // Unregister the window class. Fail silently if other windows are still + // using the class, as only the last window can successfully unregister it. + if (!UnregisterClass(kWindowClassName, GetModuleHandle(nullptr))) { + // Clear the error state after the failed unregistration. + SetLastError(ERROR_SUCCESS); + } + } +} + +HostWindow* HostWindow::GetThisFromHandle(HWND hwnd) { + return reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); +} + +HWND HostWindow::GetWindowHandle() const { + return window_handle_; +} + +void HostWindow::FocusViewOf(HostWindow* window) { + auto child_content = window->view_controller_->view()->GetWindowHandle(); + if (window != nullptr && child_content != nullptr) { + SetFocus(child_content); + } +}; + +LRESULT HostWindow::WndProc(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) { + if (message == WM_NCCREATE) { + auto* const create_struct = reinterpret_cast(lparam); + auto* const windows_proc_table = + static_cast(create_struct->lpCreateParams); + windows_proc_table->EnableNonClientDpiScaling(hwnd); + EnableTransparentWindowBackground(hwnd, *windows_proc_table); + } else if (HostWindow* const window = GetThisFromHandle(hwnd)) { + return window->HandleMessage(hwnd, message, wparam, lparam); + } + + return DefWindowProc(hwnd, message, wparam, lparam); +} + +LRESULT HostWindow::HandleMessage(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) { + auto result = engine_->window_proc_delegate_manager()->OnTopLevelWindowProc( + window_handle_, message, wparam, lparam); + if (result) { + return *result; + } + + switch (message) { + 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_GETMINMAXINFO: { + RECT window_rect; + GetWindowRect(hwnd, &window_rect); + RECT client_rect; + GetClientRect(hwnd, &client_rect); + LONG const non_client_width = (window_rect.right - window_rect.left) - + (client_rect.right - client_rect.left); + LONG const non_client_height = (window_rect.bottom - window_rect.top) - + (client_rect.bottom - client_rect.top); + + UINT const dpi = flutter::GetDpiForHWND(hwnd); + double const scale_factor = + static_cast(dpi) / USER_DEFAULT_SCREEN_DPI; + + MINMAXINFO* info = reinterpret_cast(lparam); + Size const min_physical_size = ClampToVirtualScreen(Size( + box_constraints_.smallest().width() * scale_factor + non_client_width, + box_constraints_.smallest().height() * scale_factor + + non_client_height)); + + info->ptMinTrackSize.x = min_physical_size.width(); + info->ptMinTrackSize.y = min_physical_size.height(); + Size const max_physical_size = ClampToVirtualScreen(Size( + box_constraints_.biggest().width() * scale_factor + non_client_width, + box_constraints_.biggest().height() * scale_factor + + non_client_height)); + + info->ptMaxTrackSize.x = max_physical_size.width(); + info->ptMaxTrackSize.y = max_physical_size.height(); + return 0; + } + + case WM_SIZE: { + auto child_content = view_controller_->view()->GetWindowHandle(); + if (child_content != nullptr) { + // Resize and reposition the child content window. + RECT client_rect; + GetClientRect(hwnd, &client_rect); + MoveWindow(child_content, client_rect.left, client_rect.top, + client_rect.right - client_rect.left, + client_rect.bottom - client_rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + FocusViewOf(this); + return 0; + + case WM_MOUSEACTIVATE: + FocusViewOf(this); + return MA_ACTIVATE; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + + default: + break; + } + + if (!view_controller_) { + return 0; + } + + return DefWindowProc(hwnd, message, wparam, lparam); +} + +void HostWindow::SetContentSize(const WindowSizing& size) { + WINDOWINFO window_info = {.cbSize = sizeof(WINDOWINFO)}; + GetWindowInfo(window_handle_, &window_info); + + std::optional smallest, biggest; + if (size.has_view_constraints) { + smallest = Size(size.view_min_width, size.view_min_height); + if (size.view_max_width > 0 && size.view_max_height > 0) { + biggest = Size(size.view_max_width, size.view_max_height); + } + } + + box_constraints_ = BoxConstraints(smallest, biggest); + + if (size.has_preferred_view_size) { + std::optional const window_size = GetWindowSizeForClientSize( + *engine_->windows_proc_table(), + Size(size.preferred_view_width, size.preferred_view_height), + box_constraints_.smallest(), box_constraints_.biggest(), + window_info.dwStyle, window_info.dwExStyle, nullptr); + + if (window_size) { + SetWindowPos(window_handle_, NULL, 0, 0, window_size->width(), + window_size->height(), + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); + } + } +} + +} // namespace flutter diff --git a/engine/src/flutter/shell/platform/windows/host_window.h b/engine/src/flutter/shell/platform/windows/host_window.h new file mode 100644 index 00000000000..8a6875e83f4 --- /dev/null +++ b/engine/src/flutter/shell/platform/windows/host_window.h @@ -0,0 +1,99 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_HOST_WINDOW_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_HOST_WINDOW_H_ + +#include +#include +#include + +#include "flutter/fml/macros.h" +#include "flutter/shell/platform/common/geometry.h" +#include "flutter/shell/platform/common/windowing.h" +#include "flutter/shell/platform/windows/window_manager.h" + +namespace flutter { + +class WindowManager; +class FlutterWindowsView; +class FlutterWindowsViewController; + +// A Win32 window that hosts a |FlutterWindow| in its client area. +class HostWindow { + public: + virtual ~HostWindow(); + + // Creates a native Win32 window with a child view confined to its client + // area. |controller| is a pointer to the controller that manages the + // |HostWindow|. |engine| is a pointer to the engine that manages + // the controller. On success, a valid window handle can be retrieved + // via |HostWindow::GetWindowHandle|. |nullptr| will be returned + // on failure. + static std::unique_ptr CreateRegularWindow( + WindowManager* controller, + FlutterWindowsEngine* engine, + const WindowSizing& content_size); + + // Returns the instance pointer for |hwnd| or nullptr if invalid. + static HostWindow* GetThisFromHandle(HWND hwnd); + + // Returns the backing window handle, or nullptr if the native window is not + // created or has already been destroyed. + HWND GetWindowHandle() const; + + // Resizes the window to accommodate a client area of the given + // |size|. + void SetContentSize(const WindowSizing& size); + + private: + friend WindowManager; + + HostWindow(WindowManager* controller, + FlutterWindowsEngine* engine, + WindowArchetype archetype, + std::unique_ptr view_controller, + const BoxConstraints& constraints, + HWND hwnd); + + // Sets the focus to the child view window of |window|. + static void FocusViewOf(HostWindow* window); + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. Delegates other messages to the controller. + static LRESULT WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); + + // Processes and routes salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + LRESULT HandleMessage(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); + + // Controller for this window. + WindowManager* const window_manager_ = nullptr; + + // The Flutter engine that owns this window. + FlutterWindowsEngine* engine_; + + // Controller for the view hosted in this window. Value-initialized if the + // window is created from an existing top-level native window created by the + // runner. + std::unique_ptr view_controller_; + + // The window archetype. + WindowArchetype archetype_ = WindowArchetype::kRegular; + + // Backing handle for this window. + HWND window_handle_ = nullptr; + + // The constraints on the window's client area. + BoxConstraints box_constraints_; + + FML_DISALLOW_COPY_AND_ASSIGN(HostWindow); +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_HOST_WINDOW_H_ diff --git a/engine/src/flutter/shell/platform/windows/window_manager.cc b/engine/src/flutter/shell/platform/windows/window_manager.cc new file mode 100644 index 00000000000..11ff976bc3b --- /dev/null +++ b/engine/src/flutter/shell/platform/windows/window_manager.cc @@ -0,0 +1,158 @@ +// 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/window_manager.h" + +#include +#include +#include + +#include "embedder.h" +#include "flutter/shell/platform/common/windowing.h" +#include "flutter/shell/platform/windows/flutter_windows_engine.h" +#include "flutter/shell/platform/windows/flutter_windows_view_controller.h" +#include "flutter/shell/platform/windows/host_window.h" +#include "fml/logging.h" +#include "shell/platform/windows/client_wrapper/include/flutter/flutter_view.h" +#include "shell/platform/windows/flutter_windows_view.h" +#include "shell/platform/windows/host_window.h" + +namespace flutter { + +WindowManager::WindowManager(FlutterWindowsEngine* engine) : engine_(engine) {} + +void WindowManager::Initialize(const WindowingInitRequest* request) { + on_message_ = request->on_message; + isolate_ = Isolate::Current(); +} + +bool WindowManager::HasTopLevelWindows() const { + return !active_windows_.empty(); +} + +FlutterViewId WindowManager::CreateRegularWindow( + const WindowCreationRequest* request) { + auto window = + HostWindow::CreateRegularWindow(this, engine_, request->content_size); + if (!window || !window->GetWindowHandle()) { + FML_LOG(ERROR) << "Failed to create host window"; + return -1; + } + FlutterViewId const view_id = window->view_controller_->view()->view_id(); + active_windows_[window->GetWindowHandle()] = std::move(window); + return view_id; +} + +void WindowManager::OnEngineShutdown() { + // Don't send any more messages to isolate. + on_message_ = nullptr; + std::vector active_handles; + active_handles.reserve(active_windows_.size()); + for (auto& [hwnd, window] : active_windows_) { + active_handles.push_back(hwnd); + } + for (auto hwnd : active_handles) { + // This will destroy the window, which will in turn remove the + // HostWindow from map when handling WM_NCDESTROY inside + // HandleMessage. + DestroyWindow(hwnd); + } +} + +std::optional WindowManager::HandleMessage(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) { + if (message == WM_NCDESTROY) { + active_windows_.erase(hwnd); + } + + FlutterWindowsView* view = engine_->GetViewFromTopLevelWindow(hwnd); + if (!view) { + FML_LOG(WARNING) << "Received message for unknown view"; + return std::nullopt; + } + + WindowsMessage message_struct = {.view_id = view->view_id(), + .hwnd = hwnd, + .message = message, + .wParam = wparam, + .lParam = lparam, + .result = 0, + .handled = false}; + + // Not initialized yet. + if (!isolate_) { + return std::nullopt; + } + + IsolateScope scope(*isolate_); + on_message_(&message_struct); + if (message_struct.handled) { + return message_struct.result; + } else { + return std::nullopt; + } +} + +} // namespace flutter + +void InternalFlutterWindows_WindowManager_Initialize( + int64_t engine_id, + const flutter::WindowingInitRequest* request) { + flutter::FlutterWindowsEngine* engine = + flutter::FlutterWindowsEngine::GetEngineForId(engine_id); + engine->window_manager()->Initialize(request); +} + +bool InternalFlutterWindows_WindowManager_HasTopLevelWindows( + int64_t engine_id) { + flutter::FlutterWindowsEngine* engine = + flutter::FlutterWindowsEngine::GetEngineForId(engine_id); + return engine->window_manager()->HasTopLevelWindows(); +} + +FlutterViewId InternalFlutterWindows_WindowManager_CreateRegularWindow( + int64_t engine_id, + const flutter::WindowCreationRequest* request) { + flutter::FlutterWindowsEngine* engine = + flutter::FlutterWindowsEngine::GetEngineForId(engine_id); + return engine->window_manager()->CreateRegularWindow(request); +} + +HWND InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle( + int64_t engine_id, + FlutterViewId view_id) { + flutter::FlutterWindowsEngine* engine = + flutter::FlutterWindowsEngine::GetEngineForId(engine_id); + flutter::FlutterWindowsView* view = engine->view(view_id); + if (view == nullptr) { + return nullptr; + } else { + return GetAncestor(view->GetWindowHandle(), GA_ROOT); + } +} + +FlutterWindowSize InternalFlutterWindows_WindowManager_GetWindowContentSize( + HWND hwnd) { + RECT rect; + GetClientRect(hwnd, &rect); + double const dpr = FlutterDesktopGetDpiForHWND(hwnd) / + static_cast(USER_DEFAULT_SCREEN_DPI); + double const width = rect.right / dpr; + double const height = rect.bottom / dpr; + return { + .width = rect.right / dpr, + .height = rect.bottom / dpr, + }; +} + +void InternalFlutterWindows_WindowManager_SetWindowContentSize( + HWND hwnd, + const flutter::WindowSizing* size) { + flutter::HostWindow* window = flutter::HostWindow::GetThisFromHandle(hwnd); + if (window) { + window->SetContentSize(*size); + } +} diff --git a/engine/src/flutter/shell/platform/windows/window_manager.h b/engine/src/flutter/shell/platform/windows/window_manager.h new file mode 100644 index 00000000000..675b470c068 --- /dev/null +++ b/engine/src/flutter/shell/platform/windows/window_manager.h @@ -0,0 +1,135 @@ +// 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_WINDOW_MANAGER_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOW_MANAGER_H_ + +#include +#include +#include +#include +#include + +#include "flutter/shell/platform/common/public/flutter_export.h" + +#include "flutter/fml/macros.h" +#include "flutter/shell/platform/common/isolate_scope.h" +#include "flutter/shell/platform/embedder/embedder.h" + +namespace flutter { + +class FlutterWindowsEngine; +class HostWindow; +struct WindowingInitRequest; + +struct WindowsMessage { + FlutterViewId view_id; + HWND hwnd; + UINT message; + WPARAM wParam; + LPARAM lParam; + LRESULT result; + bool handled; +}; + +struct WindowSizing { + bool has_preferred_view_size; + double preferred_view_width; + double preferred_view_height; + bool has_view_constraints; + double view_min_width; + double view_min_height; + double view_max_width; + double view_max_height; +}; + +struct WindowingInitRequest { + void (*on_message)(WindowsMessage*); +}; + +struct WindowCreationRequest { + WindowSizing content_size; +}; + +// A manager class for managing |HostWindow| instances. +// A unique instance of this class is owned by |FlutterWindowsEngine|. +class WindowManager { + public: + explicit WindowManager(FlutterWindowsEngine* engine); + virtual ~WindowManager() = default; + + void Initialize(const WindowingInitRequest* request); + + bool HasTopLevelWindows() const; + + FlutterViewId CreateRegularWindow(const WindowCreationRequest* request); + + // Message handler called by |HostWindow::WndProc| to process window + // messages before delegating them to the host window. This allows the + // manager to process messages that affect the state of other host windows. + std::optional HandleMessage(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam); + + void OnEngineShutdown(); + + private: + // The Flutter engine that owns this manager. + FlutterWindowsEngine* const engine_; + + // Callback that relays windows messages to the isolate. Set + // during Initialize(). + std::function on_message_; + + // Isolate that runs the Dart code. Set during Initialize(). + std::optional isolate_; + + // A map of active windows. Used to destroy remaining windows on engine + // shutdown. + std::unordered_map> active_windows_; + + FML_DISALLOW_COPY_AND_ASSIGN(WindowManager); +}; + +} // namespace flutter + +extern "C" { + +FLUTTER_EXPORT +void InternalFlutterWindows_WindowManager_Initialize( + int64_t engine_id, + const flutter::WindowingInitRequest* request); + +FLUTTER_EXPORT +bool InternalFlutterWindows_WindowManager_HasTopLevelWindows(int64_t engine_id); + +FLUTTER_EXPORT +FlutterViewId InternalFlutterWindows_WindowManager_CreateRegularWindow( + int64_t engine_id, + const flutter::WindowCreationRequest* request); + +// Retrives the HWND associated with this |engine_id| and |view_id|. Returns +// NULL if the HWND cannot be found +FLUTTER_EXPORT +HWND InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle( + int64_t engine_id, + FlutterViewId view_id); + +struct FlutterWindowSize { + double width; + double height; +}; + +FLUTTER_EXPORT +FlutterWindowSize InternalFlutterWindows_WindowManager_GetWindowContentSize( + HWND hwnd); + +FLUTTER_EXPORT +void InternalFlutterWindows_WindowManager_SetWindowContentSize( + HWND hwnd, + const flutter::WindowSizing* size); +} + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOW_MANAGER_H_ diff --git a/engine/src/flutter/shell/platform/windows/window_manager_unittests.cc b/engine/src/flutter/shell/platform/windows/window_manager_unittests.cc new file mode 100644 index 00000000000..02c943b8e22 --- /dev/null +++ b/engine/src/flutter/shell/platform/windows/window_manager_unittests.cc @@ -0,0 +1,160 @@ +// 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/testing/flutter_windows_engine_builder.h" +#include "flutter/shell/platform/windows/testing/windows_test.h" +#include "flutter/shell/platform/windows/window_manager.h" +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { + +namespace { + +class WindowManagerTest : public WindowsTest { + public: + WindowManagerTest() = default; + virtual ~WindowManagerTest() = default; + + protected: + void SetUp() override { + auto& context = GetContext(); + FlutterWindowsEngineBuilder builder(context); + + engine_ = builder.Build(); + ASSERT_TRUE(engine_); + + engine_->SetRootIsolateCreateCallback(context.GetRootIsolateCallback()); + ASSERT_TRUE(engine_->Run("testWindowController")); + + bool signalled = false; + context.AddNativeFunction( + "Signal", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + isolate_ = flutter::Isolate::Current(); + signalled = true; + })); + while (!signalled) { + engine_->task_runner()->ProcessTasks(); + } + } + + void TearDown() override { engine_->Stop(); } + + int64_t engine_id() { return reinterpret_cast(engine_.get()); } + flutter::Isolate& isolate() { return *isolate_; } + WindowCreationRequest* creation_request() { return &creation_request_; } + + private: + std::unique_ptr engine_; + std::optional isolate_; + WindowCreationRequest creation_request_{ + .content_size = + { + .has_preferred_view_size = true, + .preferred_view_width = 800, + .preferred_view_height = 600, + }, + }; + + FML_DISALLOW_COPY_AND_ASSIGN(WindowManagerTest); +}; + +} // namespace + +TEST_F(WindowManagerTest, WindowingInitialize) { + IsolateScope isolate_scope(isolate()); + + static bool received_message = false; + WindowingInitRequest init_request{ + .on_message = [](WindowsMessage* message) { received_message = true; }}; + + InternalFlutterWindows_WindowManager_Initialize(engine_id(), &init_request); + const int64_t view_id = + InternalFlutterWindows_WindowManager_CreateRegularWindow( + engine_id(), creation_request()); + DestroyWindow(InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle( + engine_id(), view_id)); + + EXPECT_TRUE(received_message); +} + +TEST_F(WindowManagerTest, HasTopLevelWindows) { + IsolateScope isolate_scope(isolate()); + + bool has_top_level_windows = + InternalFlutterWindows_WindowManager_HasTopLevelWindows(engine_id()); + EXPECT_FALSE(has_top_level_windows); + + InternalFlutterWindows_WindowManager_CreateRegularWindow(engine_id(), + creation_request()); + has_top_level_windows = + InternalFlutterWindows_WindowManager_HasTopLevelWindows(engine_id()); + EXPECT_TRUE(has_top_level_windows); +} + +TEST_F(WindowManagerTest, CreateRegularWindow) { + IsolateScope isolate_scope(isolate()); + + const int64_t view_id = + InternalFlutterWindows_WindowManager_CreateRegularWindow( + engine_id(), creation_request()); + EXPECT_EQ(view_id, 0); +} + +TEST_F(WindowManagerTest, GetWindowHandle) { + IsolateScope isolate_scope(isolate()); + + const int64_t view_id = + InternalFlutterWindows_WindowManager_CreateRegularWindow( + engine_id(), creation_request()); + const HWND window_handle = + InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(), + view_id); + EXPECT_NE(window_handle, nullptr); +} + +TEST_F(WindowManagerTest, GetWindowSize) { + IsolateScope isolate_scope(isolate()); + + const int64_t view_id = + InternalFlutterWindows_WindowManager_CreateRegularWindow( + engine_id(), creation_request()); + const HWND window_handle = + InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(), + view_id); + + FlutterWindowSize size = + InternalFlutterWindows_WindowManager_GetWindowContentSize(window_handle); + + EXPECT_EQ(size.width, creation_request()->content_size.preferred_view_width); + EXPECT_EQ(size.height, + creation_request()->content_size.preferred_view_height); +} + +TEST_F(WindowManagerTest, SetWindowSize) { + IsolateScope isolate_scope(isolate()); + + const int64_t view_id = + InternalFlutterWindows_WindowManager_CreateRegularWindow( + engine_id(), creation_request()); + const HWND window_handle = + InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(), + view_id); + + WindowSizing requestedSize{ + .has_preferred_view_size = true, + .preferred_view_width = 640, + .preferred_view_height = 480, + }; + InternalFlutterWindows_WindowManager_SetWindowContentSize(window_handle, + &requestedSize); + + FlutterWindowSize actual_size = + InternalFlutterWindows_WindowManager_GetWindowContentSize(window_handle); + EXPECT_EQ(actual_size.width, 640); + EXPECT_EQ(actual_size.height, 480); +} + +} // namespace testing +} // namespace flutter diff --git a/engine/src/flutter/shell/platform/windows/windows_proc_table.cc b/engine/src/flutter/shell/platform/windows/windows_proc_table.cc index 2cb8e26fc1c..2be3c056c03 100644 --- a/engine/src/flutter/shell/platform/windows/windows_proc_table.cc +++ b/engine/src/flutter/shell/platform/windows/windows_proc_table.cc @@ -13,6 +13,15 @@ WindowsProcTable::WindowsProcTable() { user32_ = fml::NativeLibrary::Create("user32.dll"); get_pointer_type_ = user32_->ResolveFunction("GetPointerType"); + enable_non_client_dpi_scaling_ = + user32_->ResolveFunction( + "EnableNonClientDpiScaling"); + set_window_composition_attribute_ = + user32_->ResolveFunction( + "SetWindowCompositionAttribute"); + adjust_window_rect_ext_for_dpi_ = + user32_->ResolveFunction( + "AdjustWindowRectExForDpi"); } WindowsProcTable::~WindowsProcTable() { @@ -67,4 +76,48 @@ HCURSOR WindowsProcTable::SetCursor(HCURSOR cursor) const { return ::SetCursor(cursor); } +BOOL WindowsProcTable::EnableNonClientDpiScaling(HWND hwnd) const { + if (!enable_non_client_dpi_scaling_.has_value()) { + return FALSE; + } + + return enable_non_client_dpi_scaling_.value()(hwnd); +} + +BOOL WindowsProcTable::SetWindowCompositionAttribute( + HWND hwnd, + WINDOWCOMPOSITIONATTRIBDATA* data) const { + if (!set_window_composition_attribute_.has_value()) { + return FALSE; + } + + return set_window_composition_attribute_.value()(hwnd, data); +} + +HRESULT WindowsProcTable::DwmExtendFrameIntoClientArea( + HWND hwnd, + const MARGINS* pMarInset) const { + return ::DwmExtendFrameIntoClientArea(hwnd, pMarInset); +} + +HRESULT WindowsProcTable::DwmSetWindowAttribute(HWND hwnd, + DWORD dwAttribute, + LPCVOID pvAttribute, + DWORD cbAttribute) const { + return ::DwmSetWindowAttribute(hwnd, dwAttribute, pvAttribute, cbAttribute); +} + +BOOL WindowsProcTable::AdjustWindowRectExForDpi(LPRECT lpRect, + DWORD dwStyle, + BOOL bMenu, + DWORD dwExStyle, + UINT dpi) const { + if (!adjust_window_rect_ext_for_dpi_.has_value()) { + return FALSE; + } + + return adjust_window_rect_ext_for_dpi_.value()(lpRect, dwStyle, bMenu, + dwExStyle, dpi); +} + } // namespace flutter diff --git a/engine/src/flutter/shell/platform/windows/windows_proc_table.h b/engine/src/flutter/shell/platform/windows/windows_proc_table.h index dd1190a7f24..9290e1302d1 100644 --- a/engine/src/flutter/shell/platform/windows/windows_proc_table.h +++ b/engine/src/flutter/shell/platform/windows/windows_proc_table.h @@ -5,6 +5,7 @@ #ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOWS_PROC_TABLE_H_ #define FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOWS_PROC_TABLE_H_ +#include #include #include "flutter/fml/macros.h" @@ -16,6 +17,14 @@ namespace flutter { // Windows, or for mocking Windows API calls. class WindowsProcTable { public: + enum WINDOWCOMPOSITIONATTRIB { WCA_ACCENT_POLICY = 19 }; + + struct WINDOWCOMPOSITIONATTRIBDATA { + WINDOWCOMPOSITIONATTRIB Attrib; + PVOID pvData; + SIZE_T cbData; + }; + WindowsProcTable(); virtual ~WindowsProcTable(); @@ -70,14 +79,70 @@ class WindowsProcTable { // https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-setcursor virtual HCURSOR SetCursor(HCURSOR cursor) const; + // Enables automatic display scaling of the non-client area portions of the + // specified top-level window. + // + // See: + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enablenonclientdpiscaling + virtual BOOL EnableNonClientDpiScaling(HWND hwnd) const; + + // Sets the current value of a specified Desktop Window Manager (DWM) + // attribute applied to a window. + // + // See: + // https://learn.microsoft.com/en-us/windows/win32/dwm/setwindowcompositionattribute + virtual BOOL SetWindowCompositionAttribute( + HWND hwnd, + WINDOWCOMPOSITIONATTRIBDATA* data) const; + + // Extends the window frame into the client area. + // + // See: + // https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/nf-dwmapi-dwmextendframeintoclientarea + virtual HRESULT DwmExtendFrameIntoClientArea(HWND hwnd, + const MARGINS* pMarInset) const; + + // Sets the value of Desktop Window Manager (DWM) non-client rendering + // attributes for a window. + // + // See: + // https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/nf-dwmapi-dwmsetwindowattribute + virtual HRESULT DwmSetWindowAttribute(HWND hwnd, + DWORD dwAttribute, + LPCVOID pvAttribute, + DWORD cbAttribute) const; + + // Calculates the required size of the window rectangle, based on the desired + // size of the client rectangle and the provided DPI. + // + // See: + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-adjustwindowrectexfordpi + virtual BOOL AdjustWindowRectExForDpi(LPRECT lpRect, + DWORD dwStyle, + BOOL bMenu, + DWORD dwExStyle, + UINT dpi) const; + private: using GetPointerType_ = BOOL __stdcall(UINT32 pointerId, POINTER_INPUT_TYPE* pointerType); + using EnableNonClientDpiScaling_ = BOOL __stdcall(HWND hwnd); + using SetWindowCompositionAttribute_ = + BOOL __stdcall(HWND, WINDOWCOMPOSITIONATTRIBDATA*); + using AdjustWindowRectExForDpi_ = BOOL __stdcall(LPRECT lpRect, + DWORD dwStyle, + BOOL bMenu, + DWORD dwExStyle, + UINT dpi); // The User32.dll library, used to resolve functions at runtime. fml::RefPtr user32_; std::optional get_pointer_type_; + std::optional enable_non_client_dpi_scaling_; + std::optional + set_window_composition_attribute_; + std::optional adjust_window_rect_ext_for_dpi_; FML_DISALLOW_COPY_AND_ASSIGN(WindowsProcTable); };