diff --git a/dev/bots/allowlist.dart b/dev/bots/allowlist.dart index 91f5da871f202..24fc2cb6d62c2 100644 --- a/dev/bots/allowlist.dart +++ b/dev/bots/allowlist.dart @@ -23,6 +23,7 @@ const Set kCorePackageAllowList = { 'clock', 'collection', 'fake_async', + 'ffi', 'file', 'flutter', 'flutter_driver', diff --git a/engine/src/flutter/ci/licenses_golden/excluded_files b/engine/src/flutter/ci/licenses_golden/excluded_files index d6c6f639e005d..a15699bb42d62 100644 --- a/engine/src/flutter/ci/licenses_golden/excluded_files +++ b/engine/src/flutter/ci/licenses_golden/excluded_files @@ -416,6 +416,7 @@ ../../../flutter/shell/platform/windows/direct_manipulation_unittests.cc ../../../flutter/shell/platform/windows/dpi_utils_unittests.cc ../../../flutter/shell/platform/windows/fixtures +../../../flutter/shell/platform/windows/flutter_host_window_controller_unittests.cc ../../../flutter/shell/platform/windows/flutter_project_bundle_unittests.cc ../../../flutter/shell/platform/windows/flutter_window_unittests.cc ../../../flutter/shell/platform/windows/flutter_windows_engine_unittests.cc diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index e6fc889d9e26b..c7877ffd2944d 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -52793,6 +52793,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 @@ -52813,6 +52815,7 @@ ORIGIN: ../../../flutter/shell/platform/common/text_input_model.cc + ../../../fl 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/darwin/common/InternalFlutterSwiftCommon-Bridging-Header.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/common/windowing.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/availability_version_check.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/availability_version_check.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/buffer_conversions.h + ../../../flutter/LICENSE @@ -53091,6 +53094,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 @@ -53549,6 +53555,10 @@ ORIGIN: ../../../flutter/shell/platform/windows/external_texture_d3d.h + ../../. ORIGIN: ../../../flutter/shell/platform/windows/external_texture_pixelbuffer.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/external_texture_pixelbuffer.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/flutter_desktop_messenger.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/windows/flutter_host_window.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/windows/flutter_host_window.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/windows/flutter_host_window_controller.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/windows/flutter_host_window_controller.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/flutter_key_map.g.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/flutter_platform_node_delegate_windows.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/windows/flutter_platform_node_delegate_windows.h + ../../../flutter/LICENSE @@ -55832,6 +55842,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 @@ -55854,6 +55866,7 @@ FILE: ../../../flutter/shell/platform/common/text_range.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 +FILE: ../../../flutter/shell/platform/common/windowing.h FILE: ../../../flutter/shell/platform/darwin/common/availability_version_check.cc FILE: ../../../flutter/shell/platform/darwin/common/availability_version_check.h FILE: ../../../flutter/shell/platform/darwin/common/buffer_conversions.h @@ -56142,6 +56155,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 @@ -56606,6 +56622,10 @@ FILE: ../../../flutter/shell/platform/windows/external_texture_d3d.h FILE: ../../../flutter/shell/platform/windows/external_texture_pixelbuffer.cc FILE: ../../../flutter/shell/platform/windows/external_texture_pixelbuffer.h FILE: ../../../flutter/shell/platform/windows/flutter_desktop_messenger.h +FILE: ../../../flutter/shell/platform/windows/flutter_host_window.cc +FILE: ../../../flutter/shell/platform/windows/flutter_host_window.h +FILE: ../../../flutter/shell/platform/windows/flutter_host_window_controller.cc +FILE: ../../../flutter/shell/platform/windows/flutter_host_window_controller.h FILE: ../../../flutter/shell/platform/windows/flutter_key_map.g.cc FILE: ../../../flutter/shell/platform/windows/flutter_platform_node_delegate_windows.cc FILE: ../../../flutter/shell/platform/windows/flutter_platform_node_delegate_windows.h diff --git a/engine/src/flutter/shell/platform/common/BUILD.gn b/engine/src/flutter/shell/platform/common/BUILD.gn index 5ebfb2236d2c5..b27be6bfa0388 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 4d6e8daded041..19d61dd947987 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 0000000000000..9d1f537876f1e --- /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 0000000000000..91b50b50598c0 --- /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_; + IsolateScope() = delete; + IsolateScope(IsolateScope const&) = delete; +}; + +} // 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 0000000000000..5c37df323c59f --- /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 dbfec091477ba..4f445e75b0aef 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 1deb212fb3f3c..a9ebb3ad41477 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 @@ @implementation FlutterEngine { // factories. Lifecycle is tied to the engine. FlutterPlatformViewController* _platformViewController; + // Used to manage Flutter windows. + FlutterWindowController* _windowController; + // A message channel for sending user settings to the flutter engine. FlutterBasicMessageChannel* _settingsChannel; @@ -483,8 +486,19 @@ @implementation FlutterEngine { // 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 @@ - (instancetype)initWithName:(NSString*)labelPrefix [_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 @@ - (instancetype)initWithName:(NSString*)labelPrefix [[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 @@ - (FlutterTaskRunnerDescription)createPlatformThreadTaskDescription { 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; @@ -732,6 +762,12 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { [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); @@ -792,10 +828,12 @@ - (void)registerViewController:(FlutterViewController*)controller 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)]; @@ -813,6 +851,32 @@ - (void)registerViewController:(FlutterViewController*)controller 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 { @@ -830,14 +894,36 @@ - (void)viewControllerViewDidLoad:(FlutterViewController*)viewController { 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. @@ -849,9 +935,13 @@ - (void)deregisterViewControllerForIdentifier:(FlutterViewIdentifier)viewIdentif @"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 { @@ -938,11 +1028,46 @@ - (FlutterCompositor*)createFlutterCompositor { #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 { @@ -1166,8 +1291,16 @@ - (void)onVSync:(uintptr_t)baton { 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(); @@ -1249,6 +1382,7 @@ - (void)addInternalPlugins { [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 ec34b267a9246..7e562522f6121 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 a784a69b94d0b..db89d04f5faa7 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 712a7bcfefc3f..0b0cfc4d847ed 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 @@ -20,6 +20,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 0af31382a4a87..d95c4af9f61e6 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 @@ -115,7 +115,7 @@ - (void)waitForVSync:(uintptr_t)baton { 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 @@ -151,16 +151,22 @@ - (void)waitForVSync:(uintptr_t)baton { } } -- (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 4fdd063deee89..fe68abaf2e783 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 @@ - (void)invalidate { [displayLink tickWithTimestamp:CACurrentMediaTime() targetTimestamp:CACurrentMediaTime() + 1.0 / 60.0]; EXPECT_TRUE(displayLink.paused); + [waiter invalidate]; } static void BusyWait(CFTimeInterval duration) { @@ -111,6 +112,8 @@ static void BusyWait(CFTimeInterval duration) { 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 @@ static void BusyWait(CFTimeInterval duration) { 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 5346c452dae70..4111bc52937ab 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 @@ - (void)viewWillDisappear { _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 301d584fdf61e..d2d8434ddf355 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 0000000000000..d32c226a5da9d --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.h @@ -0,0 +1,103 @@ +// 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 +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 0000000000000..95aa3548ea598 --- /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 0000000000000..36068f950f8c4 --- /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 208d466cbf487..a58f2eecb94e6 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 e80662e592a72..7b31e8570d1f6 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 b811fab5b03b8..4ae7fd3d59b1c 100644 --- a/engine/src/flutter/shell/platform/windows/fixtures/main.dart +++ b/engine/src/flutter/shell/platform/windows/fixtures/main.dart @@ -405,6 +405,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 3b6d76db1852a..7a80bbd889ec7 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 2fa8784054606..40a8f87387340 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 0000000000000..2723bb99857f9 --- /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 0000000000000..8a6875e83f45a --- /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 0000000000000..11ff976bc3ba6 --- /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 0000000000000..675b470c0681f --- /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 0000000000000..02c943b8e227f --- /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 2cb8e26fc1c66..2be3c056c03db 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 dd1190a7f245d..9290e1302d1eb 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); }; diff --git a/examples/multi_window_ref_app/.gitignore b/examples/multi_window_ref_app/.gitignore new file mode 100644 index 0000000000000..79c113f9b5017 --- /dev/null +++ b/examples/multi_window_ref_app/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/examples/multi_window_ref_app/.metadata b/examples/multi_window_ref_app/.metadata new file mode 100644 index 0000000000000..c9660e951c922 --- /dev/null +++ b/examples/multi_window_ref_app/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "f07dbe9f9b40ecc5557632d6feb70a198dab5668" + channel: "[user-branch]" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f07dbe9f9b40ecc5557632d6feb70a198dab5668 + base_revision: f07dbe9f9b40ecc5557632d6feb70a198dab5668 + - platform: macos + create_revision: f07dbe9f9b40ecc5557632d6feb70a198dab5668 + base_revision: f07dbe9f9b40ecc5557632d6feb70a198dab5668 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/examples/multi_window_ref_app/.vscode/launch.json b/examples/multi_window_ref_app/.vscode/launch.json new file mode 100644 index 0000000000000..c846434f923e5 --- /dev/null +++ b/examples/multi_window_ref_app/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "multi_window_ref_app", + "request": "launch", + "type": "dart", + "program": "lib/main.dart", + "toolArgs": [ + "--local-engine=host_debug_unopt_arm64", + "--local-engine-host=host_debug_unopt_arm64", + ] + }, + { + "name": "multi_window_ref_app (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "multi_window_ref_app (release mode)", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "(Windows) Attach", + "type": "cppvsdbg", + "request": "attach", + } + ] +} \ No newline at end of file diff --git a/examples/multi_window_ref_app/README.md b/examples/multi_window_ref_app/README.md new file mode 100644 index 0000000000000..39b39942c528b --- /dev/null +++ b/examples/multi_window_ref_app/README.md @@ -0,0 +1,5 @@ +# multi_window_ref_app + +A reference application demonstrating multi-window support for Flutter using a +rich semantics windowing API. At the moment, only the Windows platform is +supported. \ No newline at end of file diff --git a/examples/multi_window_ref_app/analysis_options.yaml b/examples/multi_window_ref_app/analysis_options.yaml new file mode 100644 index 0000000000000..0d2902135caec --- /dev/null +++ b/examples/multi_window_ref_app/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/examples/multi_window_ref_app/lib/app/main_window.dart b/examples/multi_window_ref_app/lib/app/main_window.dart new file mode 100644 index 0000000000000..0073ab0566c11 --- /dev/null +++ b/examples/multi_window_ref_app/lib/app/main_window.dart @@ -0,0 +1,267 @@ +// Copyright 2014 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 'package:flutter/material.dart'; +import 'package:multi_window_ref_app/app/window_controller_render.dart'; + +import 'regular_window_content.dart'; +import 'window_settings.dart'; +import 'window_settings_dialog.dart'; +import 'window_manager_model.dart'; +import 'regular_window_edit_dialog.dart'; + +class MainWindow extends StatefulWidget { + MainWindow({super.key, required WindowController mainController}) { + _windowManagerModel.add(KeyedWindowController( + isMainWindow: true, key: UniqueKey(), controller: mainController)); + } + + final WindowManagerModel _windowManagerModel = WindowManagerModel(); + final WindowSettings _settings = WindowSettings(); + + @override + State createState() => _MainWindowState(); +} + +class _MainWindowState extends State { + @override + Widget build(BuildContext context) { + final child = Scaffold( + appBar: AppBar( + title: const Text('Multi Window Reference App'), + ), + body: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 60, + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: _ActiveWindowsTable( + windowManagerModel: widget._windowManagerModel), + ), + ), + Expanded( + flex: 40, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ListenableBuilder( + listenable: widget._windowManagerModel, + builder: (BuildContext context, Widget? child) { + return _WindowCreatorCard( + selectedWindow: widget._windowManagerModel.selected, + windowManagerModel: widget._windowManagerModel, + windowSettings: widget._settings); + }) + ], + ), + ), + ], + ), + ); + + return ViewAnchor( + view: ListenableBuilder( + listenable: widget._windowManagerModel, + builder: (BuildContext context, Widget? _) { + final List childViews = []; + for (final KeyedWindowController controller + in widget._windowManagerModel.windows) { + if (controller.parent == null && !controller.isMainWindow) { + childViews.add(WindowControllerRender( + controller: controller.controller, + key: controller.key, + windowSettings: widget._settings, + windowManagerModel: widget._windowManagerModel, + onDestroyed: () => + widget._windowManagerModel.remove(controller.key), + onError: () => + widget._windowManagerModel.remove(controller.key), + )); + } + } + + return ViewCollection(views: childViews); + }), + child: child); + } +} + +class _ActiveWindowsTable extends StatelessWidget { + const _ActiveWindowsTable({required this.windowManagerModel}); + + final WindowManagerModel windowManagerModel; + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: windowManagerModel, + builder: (BuildContext context, Widget? widget) { + return DataTable( + showBottomBorder: true, + onSelectAll: (selected) { + windowManagerModel.select(null); + }, + columns: const [ + DataColumn( + label: SizedBox( + width: 20, + child: Text( + 'ID', + style: TextStyle( + fontSize: 16, + ), + ), + ), + ), + DataColumn( + label: SizedBox( + width: 120, + child: Text( + 'Type', + style: TextStyle( + fontSize: 16, + ), + ), + ), + ), + DataColumn( + label: SizedBox( + width: 20, + child: Text(''), + ), + numeric: true), + ], + rows: (windowManagerModel.windows) + .map((KeyedWindowController controller) { + return DataRow( + key: controller.key, + color: WidgetStateColor.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return Theme.of(context).colorScheme.primary.withAlpha(20); + } + return Colors.transparent; + }), + selected: controller.controller == windowManagerModel.selected, + onSelectChanged: (selected) { + if (selected != null) { + windowManagerModel.select(selected + ? controller.controller.rootView.viewId + : null); + } + }, + cells: [ + DataCell(Text('${controller.controller.rootView.viewId}')), + DataCell( + ListenableBuilder( + listenable: controller.controller, + builder: (BuildContext context, Widget? _) => Text( + controller.controller.type + .toString() + .replaceFirst('WindowArchetype.', ''))), + ), + DataCell( + ListenableBuilder( + listenable: controller.controller, + builder: (BuildContext context, Widget? _) => + Row(children: [ + IconButton( + icon: const Icon(Icons.edit_outlined), + onPressed: () => _showWindowEditDialog( + controller, context)), + IconButton( + icon: const Icon(Icons.delete_outlined), + onPressed: () async { + controller.controller.destroy(); + }, + ) + ])), + ), + ], + ); + }).toList(), + ); + }); + } + + void _showWindowEditDialog( + KeyedWindowController controller, BuildContext context) { + if (controller.controller.type != WindowArchetype.regular) { + return; + } + + showRegularWindowEditDialog( + context: context, + controller: controller.controller as RegularWindowController); + } +} + +class _WindowCreatorCard extends StatelessWidget { + const _WindowCreatorCard( + {required this.selectedWindow, + required this.windowManagerModel, + required this.windowSettings}); + + final WindowController? selectedWindow; + final WindowManagerModel windowManagerModel; + final WindowSettings windowSettings; + + @override + Widget build(BuildContext context) { + return Card.outlined( + margin: const EdgeInsets.symmetric(horizontal: 25), + child: Padding( + padding: const EdgeInsets.fromLTRB(25, 0, 25, 5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.only(top: 10, bottom: 10), + child: Text( + 'New Window', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16.0, + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + OutlinedButton( + onPressed: () async { + final UniqueKey key = UniqueKey(); + windowManagerModel.add(KeyedWindowController( + key: key, + controller: RegularWindowController( + delegate: WindowControllerDelegate( + onDestroyed: () => windowManagerModel.remove(key), + ), + title: "Regular", + contentSize: WindowSizing( + preferredSize: windowSettings.regularSize), + ))); + }, + child: const Text('Regular'), + ), + const SizedBox(height: 8), + Container( + alignment: Alignment.bottomRight, + child: TextButton( + child: const Text('SETTINGS'), + onPressed: () { + windowSettingsDialog(context, windowSettings); + }, + ), + ), + const SizedBox(width: 8), + ], + ), + ], + ), + ), + ); + } +} diff --git a/examples/multi_window_ref_app/lib/app/regular_window_content.dart b/examples/multi_window_ref_app/lib/app/regular_window_content.dart new file mode 100644 index 0000000000000..981598f82c940 --- /dev/null +++ b/examples/multi_window_ref_app/lib/app/regular_window_content.dart @@ -0,0 +1,214 @@ +// Copyright 2014 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 'package:flutter/material.dart'; +import 'package:multi_window_ref_app/app/window_controller_render.dart'; +import 'package:multi_window_ref_app/app/window_manager_model.dart'; +import 'package:multi_window_ref_app/app/window_settings.dart'; +import 'dart:math'; +import 'package:vector_math/vector_math_64.dart'; + +class RegularWindowContent extends StatefulWidget { + const RegularWindowContent( + {super.key, + required this.window, + required this.windowSettings, + required this.windowManagerModel}); + + final RegularWindowController window; + final WindowSettings windowSettings; + final WindowManagerModel windowManagerModel; + + @override + State createState() => _RegularWindowContentState(); +} + +class WindowControllerDelegate extends RegularWindowControllerDelegate { + WindowControllerDelegate({required this.onDestroyed}); + + @override + void onWindowDestroyed() { + onDestroyed(); + super.onWindowDestroyed(); + } + + final VoidCallback onDestroyed; +} + +class _RegularWindowContentState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _animation; + late final Color cubeColor; + + @override + void initState() { + super.initState(); + _animation = AnimationController( + vsync: this, + lowerBound: 0, + upperBound: 2 * pi, + duration: const Duration(seconds: 15), + )..repeat(); + cubeColor = _generateRandomDarkColor(); + } + + @override + void dispose() { + _animation.dispose(); + super.dispose(); + } + + Color _generateRandomDarkColor() { + final random = Random(); + const int lowerBound = 32; + const int span = 160; + int red = lowerBound + random.nextInt(span); + int green = lowerBound + random.nextInt(span); + int blue = lowerBound + random.nextInt(span); + return Color.fromARGB(255, red, green, blue); + } + + @override + Widget build(BuildContext context) { + final dpr = MediaQuery.of(context).devicePixelRatio; + + final child = Scaffold( + appBar: AppBar(title: Text('${widget.window.type}')), + body: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return CustomPaint( + size: const Size(200, 200), + painter: _RotatedWireCube( + angle: _animation.value, color: cubeColor), + ); + }, + ), + ], + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + final UniqueKey key = UniqueKey(); + widget.windowManagerModel.add(KeyedWindowController( + key: key, + controller: RegularWindowController( + contentSize: WindowSizing( + preferredSize: widget.windowSettings.regularSize), + delegate: WindowControllerDelegate( + onDestroyed: () => + widget.windowManagerModel.remove(key), + ), + title: "Regular", + ))); + }, + child: const Text('Create Regular Window'), + ), + const SizedBox(height: 20), + ListenableBuilder( + listenable: widget.window, + builder: (BuildContext context, Widget? _) { + return Text( + 'View #${widget.window.rootView.viewId}\n' + 'Size: ${(widget.window.contentSize.width).toStringAsFixed(1)}\u00D7${(widget.window.contentSize.height).toStringAsFixed(1)}\n' + 'Device Pixel Ratio: $dpr', + textAlign: TextAlign.center, + ); + }) + ], + ), + ], + )), + ); + + return ViewAnchor( + view: ListenableBuilder( + listenable: widget.windowManagerModel, + builder: (BuildContext context, Widget? _) { + final List childViews = []; + for (final KeyedWindowController controller + in widget.windowManagerModel.windows) { + if (controller.parent == widget.window) { + childViews.add(WindowControllerRender( + controller: controller.controller, + key: controller.key, + windowSettings: widget.windowSettings, + windowManagerModel: widget.windowManagerModel, + onDestroyed: () => + widget.windowManagerModel.remove(controller.key), + onError: () => + widget.windowManagerModel.remove(controller.key), + )); + } + } + + return ViewCollection(views: childViews); + }), + child: child); + } +} + +class _RotatedWireCube extends CustomPainter { + static List vertices = [ + Vector3(-0.5, -0.5, -0.5), + Vector3(0.5, -0.5, -0.5), + Vector3(0.5, 0.5, -0.5), + Vector3(-0.5, 0.5, -0.5), + Vector3(-0.5, -0.5, 0.5), + Vector3(0.5, -0.5, 0.5), + Vector3(0.5, 0.5, 0.5), + Vector3(-0.5, 0.5, 0.5), + ]; + + static const List> edges = [ + [0, 1], [1, 2], [2, 3], [3, 0], // Front face + [4, 5], [5, 6], [6, 7], [7, 4], // Back face + [0, 4], [1, 5], [2, 6], [3, 7], // Connecting front and back + ]; + + final double angle; + final Color color; + + _RotatedWireCube({required this.angle, required this.color}); + + Offset scaleAndCenter(Vector3 point, double size, Offset center) { + final scale = size / 2; + return Offset(center.dx + point.x * scale, center.dy - point.y * scale); + } + + @override + void paint(Canvas canvas, Size size) { + final rotatedVertices = vertices + .map((vertex) => Matrix4.rotationX(angle).transformed3(vertex)) + .map((vertex) => Matrix4.rotationY(angle).transformed3(vertex)) + .map((vertex) => Matrix4.rotationZ(angle).transformed3(vertex)) + .toList(); + + final center = Offset(size.width / 2, size.height / 2); + + final paint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + + for (var edge in edges) { + final p1 = scaleAndCenter(rotatedVertices[edge[0]], size.width, center); + final p2 = scaleAndCenter(rotatedVertices[edge[1]], size.width, center); + canvas.drawLine(p1, p2, paint); + } + } + + @override + bool shouldRepaint(_RotatedWireCube oldDelegate) => true; +} diff --git a/examples/multi_window_ref_app/lib/app/regular_window_edit_dialog.dart b/examples/multi_window_ref_app/lib/app/regular_window_edit_dialog.dart new file mode 100644 index 0000000000000..64b2bca18a688 --- /dev/null +++ b/examples/multi_window_ref_app/lib/app/regular_window_edit_dialog.dart @@ -0,0 +1,184 @@ +// Copyright 2014 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 'package:flutter/material.dart'; + +void showRegularWindowEditDialog( + {required BuildContext context, + required RegularWindowController controller}) { + showDialog( + context: context, + builder: (context) => _RegularWindowEditDialog( + controller: controller, onClose: () => Navigator.pop(context))); +} + +class _RegularWindowEditDialog extends StatefulWidget { + const _RegularWindowEditDialog( + {required this.controller, required this.onClose}); + + final RegularWindowController controller; + final VoidCallback onClose; + + @override + State createState() => _RegularWindowEditDialogState(); +} + +class _RegularWindowEditDialogState extends State<_RegularWindowEditDialog> { + late Size initialSize; + late String initialTitle; + late bool initialFullscreen; + late bool initialMaximized; + late bool initialMinimized; + + late final TextEditingController widthController; + late final TextEditingController heightController; + late final TextEditingController titleController; + + bool? nextIsFullscreen; + bool? nextIsMaximized; + bool? nextIsMinized; + + @override + void initState() { + super.initState(); + initialSize = widget.controller.contentSize; + initialTitle = ""; // TODO: Get the title + initialFullscreen = widget.controller.isFullscreen(); + initialMaximized = widget.controller.isMaximized(); + initialMinimized = widget.controller.isMinimized(); + + widthController = TextEditingController(text: initialSize.width.toString()); + heightController = + TextEditingController(text: initialSize.height.toString()); + titleController = TextEditingController(text: initialTitle); + + widget.controller.addListener(_onNotification); + } + + void _onNotification() { + // We listen on the state of the controller. If a value that the user + // can edit changes from what it was initially set to, we invalidate + // their current change and store the new "initial" value. + if (widget.controller.contentSize != initialSize) { + initialSize = widget.controller.contentSize; + widthController.text = widget.controller.contentSize.width.toString(); + heightController.text = widget.controller.contentSize.height.toString(); + } + if (widget.controller.isFullscreen() != initialFullscreen) { + setState(() { + initialFullscreen = widget.controller.isFullscreen(); + nextIsFullscreen = null; + }); + } + if (widget.controller.isMaximized() != initialMaximized) { + setState(() { + initialMaximized = widget.controller.isMaximized(); + nextIsMaximized = null; + }); + } + if (widget.controller.isMinimized() != initialMinimized) { + setState(() { + initialMinimized = widget.controller.isMinimized(); + nextIsMinized = null; + }); + } + } + + @override + void dispose() { + super.dispose(); + widget.controller.removeListener(_onNotification); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text("Edit Window Properties"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: widthController, + keyboardType: TextInputType.number, + decoration: InputDecoration(labelText: "Width"), + ), + TextField( + controller: heightController, + keyboardType: TextInputType.number, + decoration: InputDecoration(labelText: "Height"), + ), + TextField( + controller: titleController, + decoration: InputDecoration(labelText: "Title"), + ), + CheckboxListTile( + title: const Text('Fullscreen'), + value: nextIsFullscreen ?? initialFullscreen, + onChanged: (bool? value) { + if (value != null) { + setState(() => nextIsFullscreen = value); + } + }), + CheckboxListTile( + title: const Text('Maximized'), + value: nextIsMaximized ?? initialMaximized, + onChanged: (bool? value) { + if (value != null) { + setState(() => nextIsMaximized = value); + } + }), + CheckboxListTile( + title: const Text('Minimized'), + value: nextIsMinized ?? initialMinimized, + onChanged: (bool? value) { + if (value != null) { + setState(() => nextIsMinized = value); + } + }) + ], + ), + actions: [ + TextButton( + onPressed: () => widget.onClose(), + child: Text("Cancel"), + ), + TextButton( + onPressed: () => _onSave(), + child: Text("Save"), + ), + ], + ); + } + + void _onSave() { + double? width = double.tryParse(widthController.text); + double? height = double.tryParse(heightController.text); + String? title = titleController.text.isEmpty ? null : titleController.text; + if (width != null && height != null) { + widget.controller.updateContentSize( + WindowSizing(preferredSize: Size(width, height)), + ); + } + if (title != null) { + widget.controller.setTitle(title); + } + if (nextIsFullscreen != null) { + if (widget.controller.isFullscreen() != nextIsFullscreen) { + widget.controller.setFullscreen(nextIsFullscreen!); + } + } + if (nextIsMaximized != null) { + if (widget.controller.isMaximized() != nextIsMaximized) { + widget.controller.setMaximized(nextIsMaximized!); + } + } + if (nextIsMinized != null) { + if (widget.controller.isMinimized() != nextIsMinized) { + widget.controller.setMinimized(nextIsMinized!); + } + } + + widget.onClose(); + } +} diff --git a/examples/multi_window_ref_app/lib/app/window_controller_render.dart b/examples/multi_window_ref_app/lib/app/window_controller_render.dart new file mode 100644 index 0000000000000..1e2ab569f8cb6 --- /dev/null +++ b/examples/multi_window_ref_app/lib/app/window_controller_render.dart @@ -0,0 +1,40 @@ +// Copyright 2014 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 'package:flutter/material.dart'; +import 'regular_window_content.dart'; +import 'window_manager_model.dart'; +import 'window_settings.dart'; + +class WindowControllerRender extends StatelessWidget { + const WindowControllerRender({ + required this.controller, + required this.onDestroyed, + required this.onError, + required this.windowSettings, + required this.windowManagerModel, + required super.key, + }); + + final WindowController controller; + final VoidCallback onDestroyed; + final VoidCallback onError; + final WindowSettings windowSettings; + final WindowManagerModel windowManagerModel; + + @override + Widget build(BuildContext context) { + switch (controller.type) { + case WindowArchetype.regular: + return RegularWindow( + key: key, + controller: controller as RegularWindowController, + child: RegularWindowContent( + window: controller as RegularWindowController, + windowSettings: windowSettings, + windowManagerModel: windowManagerModel), + ); + } + } +} diff --git a/examples/multi_window_ref_app/lib/app/window_manager_model.dart b/examples/multi_window_ref_app/lib/app/window_manager_model.dart new file mode 100644 index 0000000000000..741f3a74ad94a --- /dev/null +++ b/examples/multi_window_ref_app/lib/app/window_manager_model.dart @@ -0,0 +1,55 @@ +// Copyright 2014 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 'package:flutter/widgets.dart'; + +class KeyedWindowController { + KeyedWindowController( + {this.parent, + this.isMainWindow = false, + required this.key, + required this.controller}); + + final WindowController? parent; + final bool isMainWindow; + final UniqueKey key; + final WindowController controller; +} + +/// Manages a flat list of all of the [WindowController]s that have been +/// created by the application as well as which controller is currently +/// selected by the UI. +class WindowManagerModel extends ChangeNotifier { + final List _windows = []; + List get windows => _windows; + int? _selectedViewId; + WindowController? get selected { + if (_selectedViewId == null) { + return null; + } + + for (final KeyedWindowController controller in _windows) { + if (controller.controller.rootView.viewId == _selectedViewId) { + return controller.controller; + } + } + + return null; + } + + void add(KeyedWindowController window) { + _windows.add(window); + notifyListeners(); + } + + void remove(UniqueKey key) { + _windows.removeWhere((KeyedWindowController window) => window.key == key); + notifyListeners(); + } + + void select(int? viewId) { + _selectedViewId = viewId; + notifyListeners(); + } +} diff --git a/examples/multi_window_ref_app/lib/app/window_settings.dart b/examples/multi_window_ref_app/lib/app/window_settings.dart new file mode 100644 index 0000000000000..70aa13db5fbaa --- /dev/null +++ b/examples/multi_window_ref_app/lib/app/window_settings.dart @@ -0,0 +1,17 @@ +// Copyright 2014 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 'package:flutter/material.dart'; + +class WindowSettings extends ChangeNotifier { + WindowSettings({Size regularSize = const Size(400, 300)}) + : _regularSize = regularSize; + + Size _regularSize; + Size get regularSize => _regularSize; + set regularSize(Size value) { + _regularSize = value; + notifyListeners(); + } +} diff --git a/examples/multi_window_ref_app/lib/app/window_settings_dialog.dart b/examples/multi_window_ref_app/lib/app/window_settings_dialog.dart new file mode 100644 index 0000000000000..481f1a8c068ea --- /dev/null +++ b/examples/multi_window_ref_app/lib/app/window_settings_dialog.dart @@ -0,0 +1,94 @@ +// Copyright 2014 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 'package:flutter/material.dart'; +import 'package:multi_window_ref_app/app/window_settings.dart'; + +Future windowSettingsDialog( + BuildContext context, WindowSettings settings) async { + return await showDialog( + barrierDismissible: true, + context: context, + builder: (BuildContext ctx) { + return SimpleDialog( + contentPadding: const EdgeInsets.all(4), + titlePadding: const EdgeInsets.fromLTRB(24, 10, 24, 0), + title: const Center( + child: Text('Window Settings'), + ), + children: [ + SizedBox( + width: 600, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: ListTile( + title: const Text('Regular'), + subtitle: ListenableBuilder( + listenable: settings, + builder: (BuildContext ctx, Widget? _) { + return Row( + children: [ + Expanded( + child: TextFormField( + initialValue: settings.regularSize.width + .toString(), + decoration: const InputDecoration( + labelText: 'Initial width', + ), + onChanged: (String value) => + settings.regularSize = Size( + double.tryParse(value) ?? 0, + settings.regularSize.height), + ), + ), + const SizedBox( + width: 20, + ), + Expanded( + child: TextFormField( + initialValue: settings + .regularSize.height + .toString(), + decoration: const InputDecoration( + labelText: 'Initial height', + ), + onChanged: (String value) => + settings.regularSize = Size( + settings.regularSize.width, + double.tryParse(value) ?? 0), + ), + ), + ], + ); + }), + ), + ), + const SizedBox( + width: 10, + ), + ], + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextButton( + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }, + child: const Text('Apply'), + ), + ), + const SizedBox( + height: 2, + ), + ], + ); + }); +} diff --git a/examples/multi_window_ref_app/lib/main.dart b/examples/multi_window_ref_app/lib/main.dart new file mode 100644 index 0000000000000..21056d60e2226 --- /dev/null +++ b/examples/multi_window_ref_app/lib/main.dart @@ -0,0 +1,22 @@ +// Copyright 2014 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 'package:flutter/material.dart'; +import 'app/main_window.dart'; + +void main() { + final RegularWindowController controller = RegularWindowController( + contentSize: WindowSizing( + preferredSize: const Size(800, 600), + constraints: const BoxConstraints(minWidth: 640, minHeight: 480), + ), + title: "Multi-Window Reference Application", + ); + runWidget( + RegularWindow( + controller: controller, + child: MaterialApp(home: MainWindow(mainController: controller)), + ), + ); +} diff --git a/examples/multi_window_ref_app/macos/.gitignore b/examples/multi_window_ref_app/macos/.gitignore new file mode 100644 index 0000000000000..746adbb6b9e14 --- /dev/null +++ b/examples/multi_window_ref_app/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/examples/multi_window_ref_app/macos/Flutter/Flutter-Debug.xcconfig b/examples/multi_window_ref_app/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000000000..c2efd0b608ba8 --- /dev/null +++ b/examples/multi_window_ref_app/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/examples/multi_window_ref_app/macos/Flutter/Flutter-Release.xcconfig b/examples/multi_window_ref_app/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000000000..c2efd0b608ba8 --- /dev/null +++ b/examples/multi_window_ref_app/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/examples/multi_window_ref_app/macos/Runner.xcodeproj/project.pbxproj b/examples/multi_window_ref_app/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000000000..c1e14c02ab186 --- /dev/null +++ b/examples/multi_window_ref_app/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* multi_window_ref_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "multi_window_ref_app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* multi_window_ref_app.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* multi_window_ref_app.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.multiWindowRefApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/multi_window_ref_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/multi_window_ref_app"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.multiWindowRefApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/multi_window_ref_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/multi_window_ref_app"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.multiWindowRefApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/multi_window_ref_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/multi_window_ref_app"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/examples/multi_window_ref_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/multi_window_ref_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000000..18d981003d68d --- /dev/null +++ b/examples/multi_window_ref_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/multi_window_ref_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/examples/multi_window_ref_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000000000..e7813cac2f1bf --- /dev/null +++ b/examples/multi_window_ref_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/multi_window_ref_app/macos/Runner.xcworkspace/contents.xcworkspacedata b/examples/multi_window_ref_app/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000000..1d526a16ed0f1 --- /dev/null +++ b/examples/multi_window_ref_app/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/multi_window_ref_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/multi_window_ref_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000000..18d981003d68d --- /dev/null +++ b/examples/multi_window_ref_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/multi_window_ref_app/macos/Runner/AppDelegate.swift b/examples/multi_window_ref_app/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000000000..84cd1f9d64f7b --- /dev/null +++ b/examples/multi_window_ref_app/macos/Runner/AppDelegate.swift @@ -0,0 +1,25 @@ +// Copyright 2014 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 Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } + + var engine : FlutterEngine?; + + + override func applicationDidFinishLaunching(_ notification: Notification) { + engine = FlutterEngine(name: "project", project: nil); + engine?.run(withEntrypoint:nil); + } +} diff --git a/examples/multi_window_ref_app/macos/Runner/Assets.xcassets/Contents.json b/examples/multi_window_ref_app/macos/Runner/Assets.xcassets/Contents.json new file mode 100644 index 0000000000000..73c00596a7fca --- /dev/null +++ b/examples/multi_window_ref_app/macos/Runner/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/multi_window_ref_app/macos/Runner/Base.lproj/MainMenu.xib b/examples/multi_window_ref_app/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000000000..a62d87856b51b --- /dev/null +++ b/examples/multi_window_ref_app/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,332 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/multi_window_ref_app/macos/Runner/Configs/AppInfo.xcconfig b/examples/multi_window_ref_app/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000000000..35840c478829e --- /dev/null +++ b/examples/multi_window_ref_app/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = multi_window_ref_app + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.multiWindowRefApp + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/examples/multi_window_ref_app/macos/Runner/Configs/Debug.xcconfig b/examples/multi_window_ref_app/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000000000..36b0fd9464f45 --- /dev/null +++ b/examples/multi_window_ref_app/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/examples/multi_window_ref_app/macos/Runner/Configs/Release.xcconfig b/examples/multi_window_ref_app/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000000000..dff4f49561c81 --- /dev/null +++ b/examples/multi_window_ref_app/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/examples/multi_window_ref_app/macos/Runner/Configs/Warnings.xcconfig b/examples/multi_window_ref_app/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000000000..42bcbf4780b18 --- /dev/null +++ b/examples/multi_window_ref_app/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/examples/multi_window_ref_app/macos/Runner/DebugProfile.entitlements b/examples/multi_window_ref_app/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000000000..dddb8a30c851e --- /dev/null +++ b/examples/multi_window_ref_app/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/examples/multi_window_ref_app/macos/Runner/Info.plist b/examples/multi_window_ref_app/macos/Runner/Info.plist new file mode 100644 index 0000000000000..4789daa6a443e --- /dev/null +++ b/examples/multi_window_ref_app/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/examples/multi_window_ref_app/macos/Runner/MainFlutterWindow.swift b/examples/multi_window_ref_app/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000000000..a6a6b9af83072 --- /dev/null +++ b/examples/multi_window_ref_app/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,19 @@ +// Copyright 2014 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 Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/examples/multi_window_ref_app/macos/Runner/Release.entitlements b/examples/multi_window_ref_app/macos/Runner/Release.entitlements new file mode 100644 index 0000000000000..852fa1a4728ae --- /dev/null +++ b/examples/multi_window_ref_app/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/examples/multi_window_ref_app/macos/RunnerTests/RunnerTests.swift b/examples/multi_window_ref_app/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000000000..eeb5f29483661 --- /dev/null +++ b/examples/multi_window_ref_app/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,16 @@ +// Copyright 2014 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 Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/examples/multi_window_ref_app/pubspec.yaml b/examples/multi_window_ref_app/pubspec.yaml new file mode 100644 index 0000000000000..fd7fc9122bdce --- /dev/null +++ b/examples/multi_window_ref_app/pubspec.yaml @@ -0,0 +1,48 @@ +name: multi_window_ref_app +description: "Reference app for the multi-view windowing API." +version: 1.0.0+1 + +environment: + sdk: '>=3.5.0-180.0.dev <4.0.0' + +dependencies: + flutter: + sdk: flutter + stack_trace: 1.12.1 + vector_math: 2.2.0 + + characters: 1.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.19.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + ffi: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + material_color_utilities: 0.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: 5.0.0 + + async: 2.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + boolean_selector: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + clock: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + crypto: 3.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + file: 7.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + intl: 0.20.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + matcher: 0.12.17 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + process: 5.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + source_span: 1.10.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + string_scanner: 1.4.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + term_glyph: 1.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.7.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + typed_data: 1.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 15.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webdriver: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + +flutter: + uses-material-design: true + +# PUBSPEC CHECKSUM: 78a6 diff --git a/examples/multi_window_ref_app/test/widget_test.dart b/examples/multi_window_ref_app/test/widget_test.dart new file mode 100644 index 0000000000000..31030bc0daaa8 --- /dev/null +++ b/examples/multi_window_ref_app/test/widget_test.dart @@ -0,0 +1,9 @@ +// Copyright 2014 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 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async {}); +} diff --git a/examples/multi_window_ref_app/windows/.gitignore b/examples/multi_window_ref_app/windows/.gitignore new file mode 100644 index 0000000000000..d492d0d98c8fd --- /dev/null +++ b/examples/multi_window_ref_app/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/examples/multi_window_ref_app/windows/CMakeLists.txt b/examples/multi_window_ref_app/windows/CMakeLists.txt new file mode 100644 index 0000000000000..4450980427578 --- /dev/null +++ b/examples/multi_window_ref_app/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(multi_window_ref_app LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "multi_window_ref_app") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/examples/multi_window_ref_app/windows/flutter/CMakeLists.txt b/examples/multi_window_ref_app/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000000000..a71c6e2c5e4f3 --- /dev/null +++ b/examples/multi_window_ref_app/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") +set(CMAKE_CXX_STANDARD 20) + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/examples/multi_window_ref_app/windows/runner/CMakeLists.txt b/examples/multi_window_ref_app/windows/runner/CMakeLists.txt new file mode 100644 index 0000000000000..697f43451ac08 --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/CMakeLists.txt @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "main.cpp" + "utils.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/examples/multi_window_ref_app/windows/runner/Runner.rc b/examples/multi_window_ref_app/windows/runner/Runner.rc new file mode 100644 index 0000000000000..909820ff45c09 --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/Runner.rc @@ -0,0 +1,111 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "The Flutter Authors" "\0" + VALUE "FileDescription", "A reference application demonstrating Flutter's multi-window API." "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "Flutter Multi-Window Reference App" "\0" + VALUE "LegalCopyright", "Copyright 2014 The Flutter Authors. All rights reserved." "\0" + VALUE "OriginalFilename", "multi_window_ref_app.exe" "\0" + VALUE "ProductName", "Flutter Multi-Window Reference App" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/examples/multi_window_ref_app/windows/runner/main.cpp b/examples/multi_window_ref_app/windows/runner/main.cpp new file mode 100644 index 0000000000000..11a76c8ceffa2 --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/main.cpp @@ -0,0 +1,41 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include + +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t* command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + auto command_line_arguments{GetCommandLineArguments()}; + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + auto const engine{std::make_shared(project)}; + RegisterPlugins(engine.get()); + engine->Run(); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/examples/multi_window_ref_app/windows/runner/resource.h b/examples/multi_window_ref_app/windows/runner/resource.h new file mode 100644 index 0000000000000..69cacf3cead96 --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/resource.h @@ -0,0 +1,19 @@ +// Copyright 2014 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. + +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/examples/multi_window_ref_app/windows/runner/runner.exe.manifest b/examples/multi_window_ref_app/windows/runner/runner.exe.manifest new file mode 100644 index 0000000000000..153653e8d67f8 --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/examples/multi_window_ref_app/windows/runner/utils.cpp b/examples/multi_window_ref_app/windows/runner/utils.cpp new file mode 100644 index 0000000000000..6abcd65042070 --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/utils.cpp @@ -0,0 +1,68 @@ +// Copyright 2014 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 "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/examples/multi_window_ref_app/windows/runner/utils.h b/examples/multi_window_ref_app/windows/runner/utils.h new file mode 100644 index 0000000000000..54414c989ba71 --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/utils.h @@ -0,0 +1,23 @@ +// Copyright 2014 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 RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/flutter/lib/src/widgets/_window_ffi.dart b/packages/flutter/lib/src/widgets/_window_ffi.dart new file mode 100644 index 0000000000000..897ba1d321876 --- /dev/null +++ b/packages/flutter/lib/src/widgets/_window_ffi.dart @@ -0,0 +1,21 @@ +// Copyright 2014 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 'dart:io'; + +import 'window.dart'; +import 'window_macos.dart'; +import 'window_win32.dart'; + +/// Creates a default [WindowingOwner] for current platform. +/// Only supported on desktop platforms. +WindowingOwner? createDefaultOwner() { + if (Platform.isMacOS) { + return WindowingOwnerMacOS(); + } else if (Platform.isWindows) { + return WindowingOwnerWin32(); + } else { + return null; + } +} diff --git a/packages/flutter/lib/src/widgets/_window_web.dart b/packages/flutter/lib/src/widgets/_window_web.dart new file mode 100644 index 0000000000000..160eaeb192f27 --- /dev/null +++ b/packages/flutter/lib/src/widgets/_window_web.dart @@ -0,0 +1,11 @@ +// Copyright 2014 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 'window.dart'; + +/// Creates a default [WindowingOwner] for web. Returns `null` as web does not +/// support multiple windows. +WindowingOwner? createDefaultOwner() { + return null; +} diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index 8ed41cbbd8e74..9b3df9b790e00 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -44,6 +44,7 @@ import 'router.dart'; import 'service_extensions.dart'; import 'view.dart'; import 'widget_inspector.dart'; +import 'window.dart'; export 'dart:ui' show AppLifecycleState, Locale; @@ -459,6 +460,7 @@ mixin WidgetsBinding return true; }()); platformMenuDelegate = DefaultPlatformMenuDelegate(); + _windowingOwner = createWindowingOwner(); } /// The current [WidgetsBinding], if one has been created. @@ -1438,6 +1440,21 @@ mixin WidgetsBinding Locale? computePlatformResolvedLocale(List supportedLocales) { return platformDispatcher.computePlatformResolvedLocale(supportedLocales); } + + /// The [WindowingOwner] is responsible for creating and managing [WindowController]s. + /// Default [WindowingOwner] supports standard Flutter desktop embedders. + /// + /// Custom [WindowingOwner] can be provided by overriding [createWindowingOwner]. + WindowingOwner get windowingOwner => _windowingOwner; + late WindowingOwner _windowingOwner; + + /// Creates the [WindowingOwner] instance available via [windowingOwner]. + /// Can be overriden in subclasses to create embedder-specific [WindowingOwner] + /// implementation. + @protected + WindowingOwner createWindowingOwner() { + return WindowingOwner.createDefaultOwner(); + } } /// Inflate the given widget and attach it to the view. diff --git a/packages/flutter/lib/src/widgets/window.dart b/packages/flutter/lib/src/widgets/window.dart index a9e827862b0e0..3e290f9b6430d 100644 --- a/packages/flutter/lib/src/widgets/window.dart +++ b/packages/flutter/lib/src/widgets/window.dart @@ -2,5 +2,317 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -/// Placeholder to be used in a future version of Flutter. -abstract final class Window {} +import 'dart:ui' show AppExitType, FlutterView, Display; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +import '_window_ffi.dart' if (dart.library.js_util) '_window_web.dart' as window_impl; +import 'binding.dart'; +import 'framework.dart'; +import 'view.dart'; + +/// Defines the possible archetypes for a window. +enum WindowArchetype { + /// Defines a traditional window + regular, +} + +/// Defines sizing request for a window. +class WindowSizing { + /// Creates a new [WindowSizing] object. + WindowSizing({this.preferredSize, this.constraints}); + + /// Preferred size of the window. This may not be honored by the platform. + final Size? preferredSize; + + /// Constraints for the window. This may not be honored by the platform. + final BoxConstraints? constraints; +} + +/// Base class for window controllers. +/// +/// A window controller must provide a [future] that resolves to a +/// a [WindowCreationResult] object. This object contains the view +/// associated with the window, the archetype of the window, the size +/// of the window, and the state of the window. +/// +/// The caller may also provide a callback to be called when the window +/// is destroyed, and a callback to be called when an error is encountered +/// during the creation of the window. +/// +/// Each [WindowController] is associated with exactly one root [FlutterView]. +/// +/// When the window is destroyed for any reason (either by the caller or by the +/// platform), the content of the controller will thereafter be invalid. Callers +/// may check if this content is invalid via the [isReady] property. +/// +/// This class implements the [Listenable] interface, so callers can listen +/// for changes to the window's properties. +abstract class WindowController with ChangeNotifier { + @protected + /// Sets the view associated with this window. + // ignore: use_setters_to_change_properties + void setView(FlutterView view) { + _view = view; + } + + /// The archetype of the window. + WindowArchetype get type; + + /// The current size of the window. This may differ from the requested size. + Size get contentSize; + + /// Destroys this window. It is permissible to call this method multiple times. + void destroy(); + + /// The root view associated to this window, which is unique to each window. + FlutterView get rootView => _view; + late final FlutterView _view; +} + +/// Delegate class for regular window controller. +mixin class RegularWindowControllerDelegate { + /// Invoked when user attempts to close the window. Default implementation + /// destroys the window. Subclass can override the behavior to delay + /// or prevent the window from closing. + void onWindowCloseRequested(RegularWindowController controller) { + controller.destroy(); + } + + /// Invoked when the window is closed. Default implementation exits the + /// application if this was the last top-level window. + void onWindowDestroyed() { + final WindowingOwner owner = WidgetsBinding.instance.windowingOwner; + if (!owner.hasTopLevelWindows()) { + // No more top-level windows, exit the application. + ServicesBinding.instance.exitApplication(AppExitType.cancelable); + } + } +} + +/// A controller for a regular window. +/// +/// A regular window is a traditional window that can be resized, minimized, +/// maximized, and closed. Upon construction, the window is created for the +/// platform with the provided properties. +/// +/// This class does not interact with the widget tree. Instead, it is typically +/// provided to the [RegularWindow] widget, who does the work of rendering the +/// content inside of this window. +/// +/// An example usage might look like: +/// ```dart +/// final RegularWindowController controller = RegularWindowController( +/// contentSize: const WindowSizing( +/// size: Size(800, 600), +/// constraints: BoxConstraints(minWidth: 640, minHeight: 480), +/// ), +/// title: "Example Window", +/// ); +/// runWidget(RegularWindow( +/// controller: controller, +/// child: MaterialApp(home: Container()))); +/// ``` +/// +/// When provided to a [RegularWindow] widget, widgets inside of the [child] +/// parameter will have access to the [RegularWindowController] via the +/// [WindowControllerContext] widget. +abstract class RegularWindowController extends WindowController { + /// Creates a [RegularWindowController] with the provided properties. + /// Upon construction, the window is created for the platform. + /// + /// [contentSize] sizing requests for the window. This may not be honored by the platform + /// [title] the title of the window + /// [state] the initial state of the window + /// [delegate] optional delegate for the controller controller. + factory RegularWindowController({ + required WindowSizing contentSize, + String? title, + RegularWindowControllerDelegate? delegate, + }) { + WidgetsFlutterBinding.ensureInitialized(); + final WindowingOwner owner = WidgetsBinding.instance.windowingOwner; + final RegularWindowController controller = owner.createRegularWindowController( + contentSize: contentSize, + delegate: delegate ?? RegularWindowControllerDelegate(), + ); + if (title != null) { + controller.setTitle(title); + } + return controller; + } + + @protected + /// Creates an empty [RegularWindowController]. + RegularWindowController.empty(); + + @override + WindowArchetype get type => WindowArchetype.regular; + + /// Request change for the window content size. + /// + /// [contentSize] describes the new requested window size. The properties + /// of this object are applied independently of each other. For example, + /// setting [WindowSizing.preferredSize] does not affect the [WindowSizing.constraints] + /// set previously. + /// + /// System compositor is free to ignore the request. + void updateContentSize(WindowSizing sizing); + + /// Request change for the window title. + /// [title] new title of the window. + void setTitle(String title); + + /// Requests that the window be displayed in its current size and position. + /// If the window is minimized or maximized, the window returns to the size + /// and position that it had before that state was applied. + void activate(); + + /// Requests the window to be maximized. This has no effect + /// if the window is currently full screen or minimized, but may + /// affect the window size upon restoring it from minimized or + /// full screen state. + void setMaximized(bool maximized); + + /// Returns whether window is currently maximized. + bool isMaximized(); + + /// Requests window to be minimized. + void setMinimized(bool minimized); + + /// Returns whether window is currently minimized. + bool isMinimized(); + + /// Request change for the window to enter or exit fullscreen state. + /// [fullscreen] whether to enter or exit fullscreen state. + /// [displayId] optional [Display] identifier to use for fullscreen mode. + /// Specifying the [displayId] might not be supported on all platforms. + void setFullscreen(bool fullscreen, {int? displayId}); + + /// Returns whether window is currently in fullscreen mode. + bool isFullscreen(); +} + +/// [WindowingOwner] is responsible for creating and managing window controllers. +/// +/// Custom subclass can be provided by subclassing [WidgetsBinding] and +/// overriding the [createWindowingOwner] method. +abstract class WindowingOwner { + /// Creates a [RegularWindowController] with the provided properties. + RegularWindowController createRegularWindowController({ + required WindowSizing contentSize, + required RegularWindowControllerDelegate delegate, + }); + + /// Returns whether application has any top level windows created by this + /// windowing owner. + bool hasTopLevelWindows(); + + /// Creates default windowing owner for standard desktop embedders. + static WindowingOwner createDefaultOwner() { + return window_impl.createDefaultOwner() ?? _FallbackWindowingOwner(); + } +} + +/// Windowing delegate used on platforms that do not support windowing. +class _FallbackWindowingOwner extends WindowingOwner { + @override + RegularWindowController createRegularWindowController({ + required WindowSizing contentSize, + required RegularWindowControllerDelegate delegate, + }) { + throw UnsupportedError( + 'Current platform does not support windowing.\n' + 'Implement a WindowingDelegate for this platform.', + ); + } + + @override + bool hasTopLevelWindows() { + return false; + } +} + +/// The [RegularWindow] widget provides a way to render a regular window in the +/// widget tree. The provided [controller] creates the native window that backs +/// the widget. The [child] widget is rendered into this newly created window. +/// +/// While the window is being created, the [RegularWindow] widget will render +/// an empty [ViewCollection] widget. Once the window is created, the [child] +/// widget will be rendered into the window inside of a [View]. +/// +/// An example usage might look like: +/// ```dart +/// final RegularWindowController controller = RegularWindowController( +/// contentSize: const WindowSizing( +/// size: Size(800, 600), +/// constraints: BoxConstraints(minWidth: 640, minHeight: 480), +/// ), +/// title: "Example Window", +/// ); +/// runApp(RegularWindow( +/// controller: controller, +/// child: MaterialApp(home: Container()))); +/// ``` +/// +/// When a [RegularWindow] widget is removed from the tree, the window that was created +/// by the [controller] is automatically destroyed if it has not yet been destroyed. +/// +/// Widgets in the same tree as the [child] widget will have access to the +/// [RegularWindowController] via the [WindowControllerContext] widget. +class RegularWindow extends StatefulWidget { + /// Creates a regular window widget. + /// [controller] the controller for this window + /// [child] the content to render into this window + /// [key] the key for this widget + const RegularWindow({super.key, required this.controller, required this.child}); + + /// Controller for this widget. + final RegularWindowController controller; + + /// The content rendered into this window. + final Widget child; + + @override + State createState() => _RegularWindowState(); +} + +class _RegularWindowState extends State { + @override + void dispose() { + super.dispose(); + widget.controller.destroy(); + } + + @override + Widget build(BuildContext context) { + return View( + view: widget.controller.rootView, + child: WindowControllerContext(controller: widget.controller, child: widget.child), + ); + } +} + +/// Provides descendants with access to the [WindowController] associated with +/// the window that is being rendered. +class WindowControllerContext extends InheritedWidget { + /// Creates a new [WindowControllerContext] + /// [controller] the controller associated with this window + /// [child] the child widget + const WindowControllerContext({super.key, required this.controller, required super.child}); + + /// The controller associated with this window. + final WindowController controller; + + /// Returns the [WindowContext] if any + static WindowController? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType()?.controller; + } + + @override + bool updateShouldNotify(WindowControllerContext oldWidget) { + return controller != oldWidget.controller; + } +} diff --git a/packages/flutter/lib/src/widgets/window_macos.dart b/packages/flutter/lib/src/widgets/window_macos.dart new file mode 100644 index 0000000000000..7637b5ca48af4 --- /dev/null +++ b/packages/flutter/lib/src/widgets/window_macos.dart @@ -0,0 +1,299 @@ +// Copyright 2014 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 'dart:ffi' hide Size; +import 'dart:ui' show FlutterView; + +import 'package:ffi/ffi.dart' as ffi; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +import 'binding.dart'; +import 'window.dart'; + +/// The macOS implementation of the windowing API. +class WindowingOwnerMacOS extends WindowingOwner { + @override + RegularWindowController createRegularWindowController({ + required WindowSizing contentSize, + required RegularWindowControllerDelegate delegate, + }) { + final RegularWindowControllerMacOS res = RegularWindowControllerMacOS( + owner: this, + delegate: delegate, + contentSize: contentSize, + ); + _activeControllers.add(res); + return res; + } + + @override + bool hasTopLevelWindows() { + return _activeControllers.isNotEmpty; + } + + final List _activeControllers = []; + + /// Returns the window handle for the given [view], or null is the window + /// handle is not available. + /// The window handle is a pointer to NSWindow instance. + static Pointer getWindowHandle(FlutterView view) { + return _getWindowHandle(PlatformDispatcher.instance.engineId!, view.viewId); + } + + @Native Function(Int64, Int64)>(symbol: 'InternalFlutter_Window_GetHandle') + external static Pointer _getWindowHandle(int engineId, int viewId); +} + +/// The macOS implementation of the regular window controller. +class RegularWindowControllerMacOS extends RegularWindowController { + /// Creates a new regular window controller for macOS. When this constructor + /// completes the FlutterView is created and framework is aware of it. + RegularWindowControllerMacOS({ + required WindowingOwnerMacOS owner, + required RegularWindowControllerDelegate delegate, + required WindowSizing contentSize, + String? title, + }) : _owner = owner, + _delegate = delegate, + super.empty() { + _onClose = NativeCallable.isolateLocal(_handleOnClose); + _onResize = NativeCallable.isolateLocal(_handleOnResize); + final Pointer<_WindowCreationRequest> request = + ffi.calloc<_WindowCreationRequest>() + ..ref.contentSize.set(contentSize) + ..ref.onClose = _onClose.nativeFunction + ..ref.onSizeChange = _onResize.nativeFunction; + + final int viewId = _createWindow(PlatformDispatcher.instance.engineId!, request); + ffi.calloc.free(request); + final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere( + (FlutterView view) => view.viewId == viewId, + ); + setView(flutterView); + if (title != null) { + setTitle(title); + } + } + + /// Returns window handle for the current window. + /// The handle is a pointer to NSWindow instance. + Pointer getWindowHandle() { + _ensureNotDestroyed(); + return WindowingOwnerMacOS.getWindowHandle(rootView); + } + + bool _destroyed = false; + + @override + void destroy() { + if (_destroyed) { + return; + } + final Pointer handle = getWindowHandle(); + _destroyed = true; + _owner._activeControllers.remove(this); + _destroyWindow(PlatformDispatcher.instance.engineId!, handle); + _delegate.onWindowDestroyed(); + _onClose.close(); + _onResize.close(); + } + + void _handleOnClose() { + _delegate.onWindowCloseRequested(this); + } + + void _handleOnResize() { + notifyListeners(); + } + + @override + void updateContentSize(WindowSizing sizing) { + _ensureNotDestroyed(); + final Pointer<_Sizing> ffiSizing = ffi.calloc<_Sizing>(); + ffiSizing.ref.set(sizing); + _setWindowContentSize(getWindowHandle(), ffiSizing); + ffi.calloc.free(ffiSizing); + } + + @override + void setTitle(String title) { + _ensureNotDestroyed(); + final Pointer titlePointer = title.toNativeUtf8(); + _setWindowTitle(getWindowHandle(), titlePointer); + ffi.calloc.free(titlePointer); + } + + final WindowingOwnerMacOS _owner; + final RegularWindowControllerDelegate _delegate; + late final NativeCallable _onClose; + late final NativeCallable _onResize; + + @override + Size get contentSize { + _ensureNotDestroyed(); + final _Size size = _getWindowContentSize(getWindowHandle()); + return Size(size.width, size.height); + } + + @override + void activate() { + _ensureNotDestroyed(); + _activate(getWindowHandle()); + } + + @override + void setMaximized(bool maximized) { + _ensureNotDestroyed(); + _setMaximized(getWindowHandle(), maximized); + } + + @override + bool isMaximized() { + _ensureNotDestroyed(); + return _isMaximized(getWindowHandle()); + } + + @override + void setMinimized(bool minimized) { + _ensureNotDestroyed(); + if (minimized) { + _minimize(getWindowHandle()); + } else { + _unminimize(getWindowHandle()); + } + } + + @override + bool isMinimized() { + _ensureNotDestroyed(); + return _isMinimized(getWindowHandle()); + } + + @override + void setFullscreen(bool fullscreen, {int? displayId}) { + _ensureNotDestroyed(); + _setFullscreen(getWindowHandle(), fullscreen); + } + + @override + bool isFullscreen() { + _ensureNotDestroyed(); + return _isFullscreen(getWindowHandle()); + } + + void _ensureNotDestroyed() { + if (_destroyed) { + throw StateError('Window has been destroyed.'); + } + } + + @Native)>( + symbol: 'InternalFlutter_WindowController_CreateRegularWindow', + ) + external static int _createWindow(int engineId, Pointer<_WindowCreationRequest> request); + + @Native)>(symbol: 'InternalFlutter_Window_Destroy') + external static void _destroyWindow(int engineId, Pointer handle); + + @Native<_Size Function(Pointer)>(symbol: 'InternalFlutter_Window_GetContentSize') + external static _Size _getWindowContentSize(Pointer windowHandle); + + @Native, Pointer<_Sizing>)>( + symbol: 'InternalFlutter_Window_SetContentSize', + ) + external static void _setWindowContentSize(Pointer windowHandle, Pointer<_Sizing> size); + + @Native, Pointer)>( + symbol: 'InternalFlutter_Window_SetTitle', + ) + external static void _setWindowTitle(Pointer windowHandle, Pointer title); + + @Native, Bool)>(symbol: 'InternalFlutter_Window_SetMaximized') + external static void _setMaximized(Pointer windowHandle, bool maximized); + + @Native)>(symbol: 'InternalFlutter_Window_IsMaximized') + external static bool _isMaximized(Pointer windowHandle); + + @Native)>(symbol: 'InternalFlutter_Window_Minimize') + external static void _minimize(Pointer windowHandle); + + @Native)>(symbol: 'InternalFlutter_Window_Unminimize') + external static void _unminimize(Pointer windowHandle); + + @Native)>(symbol: 'InternalFlutter_Window_IsMinimized') + external static bool _isMinimized(Pointer windowHandle); + + @Native, Bool)>(symbol: 'InternalFlutter_Window_SetFullScreen') + external static void _setFullscreen(Pointer windowHandle, bool fullscreen); + + @Native)>(symbol: 'InternalFlutter_Window_IsFullScreen') + external static bool _isFullscreen(Pointer windowHandle); + + @Native)>(symbol: 'InternalFlutter_Window_Activate') + external static void _activate(Pointer windowHandle); +} + +final class _Sizing extends Struct { + @Bool() + external bool hasSize; + + @Double() + external double width; + + @Double() + external double height; + + @Bool() + external bool hasConstraints; + + @Double() + external double minWidth; + + @Double() + external double minHeight; + + @Double() + external double maxWidth; + + @Double() + external double maxHeight; + + void set(WindowSizing sizing) { + final Size? size = sizing.preferredSize; + if (size != null) { + hasSize = true; + width = size.width; + height = size.height; + } else { + hasSize = false; + } + + final BoxConstraints? constraints = sizing.constraints; + if (constraints != null) { + hasConstraints = true; + minWidth = constraints.minWidth; + minHeight = constraints.minHeight; + maxWidth = constraints.maxWidth; + maxHeight = constraints.maxHeight; + } else { + hasConstraints = false; + } + } +} + +final class _WindowCreationRequest extends Struct { + external _Sizing contentSize; + + external Pointer> onClose; + external Pointer> onSizeChange; +} + +final class _Size extends Struct { + @Double() + external double width; + + @Double() + external double height; +} diff --git a/packages/flutter/lib/src/widgets/window_win32.dart b/packages/flutter/lib/src/widgets/window_win32.dart new file mode 100644 index 0000000000000..ce9572cba4c9c --- /dev/null +++ b/packages/flutter/lib/src/widgets/window_win32.dart @@ -0,0 +1,365 @@ +// Copyright 2014 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 'dart:ffi' hide Size; +import 'dart:ui' show FlutterView; +import 'package:ffi/ffi.dart' as ffi; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +import 'binding.dart'; +import 'window.dart'; + +/// Handler for Win32 messages. +abstract class WindowsMessageHandler { + /// Handles a window message. Returned value, if not null will be + /// returned to the system as LRESULT and will stop all other + /// handlers from being called. + int? handleWindowsMessage( + FlutterView view, + Pointer windowHandle, + int message, + int wParam, + int lParam, + ); +} + +/// Windowing owner implementation for Windows. +class WindowingOwnerWin32 extends WindowingOwner { + /// Creates a new [WindowingOwnerWin32] instance. + WindowingOwnerWin32() { + final Pointer<_WindowingInitRequest> request = + ffi.calloc<_WindowingInitRequest>() + ..ref.onMessage = + NativeCallable)>.isolateLocal( + _onMessage, + ).nativeFunction; + _initializeWindowing(PlatformDispatcher.instance.engineId!, request); + ffi.calloc.free(request); + } + + @override + RegularWindowController createRegularWindowController({ + required WindowSizing contentSize, + required RegularWindowControllerDelegate delegate, + }) { + return RegularWindowControllerWin32(owner: this, delegate: delegate, contentSize: contentSize); + } + + /// Register new message handler. The handler will be called for unhandled + /// messages for all top level windows. + void addMessageHandler(WindowsMessageHandler handler) { + _messageHandlers.add(handler); + } + + /// Unregister message handler. + void removeMessageHandler(WindowsMessageHandler handler) { + _messageHandlers.remove(handler); + } + + final List _messageHandlers = []; + + void _onMessage(Pointer<_WindowsMessage> message) { + final List handlers = List.from(_messageHandlers); + final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere( + (FlutterView view) => view.viewId == message.ref.viewId, + ); + for (final WindowsMessageHandler handler in handlers) { + final int? result = handler.handleWindowsMessage( + flutterView, + message.ref.windowHandle, + message.ref.message, + message.ref.wParam, + message.ref.lParam, + ); + if (result != null) { + message.ref.handled = true; + message.ref.lResult = result; + return; + } + } + } + + @override + bool hasTopLevelWindows() { + return _hasTopLevelWindows(PlatformDispatcher.instance.engineId!); + } + + @Native(symbol: 'InternalFlutterWindows_WindowManager_HasTopLevelWindows') + external static bool _hasTopLevelWindows(int engineId); + + @Native)>( + symbol: 'InternalFlutterWindows_WindowManager_Initialize', + ) + external static void _initializeWindowing(int engineId, Pointer<_WindowingInitRequest> request); +} + +/// The Win32 implementation of the regular window controller. +class RegularWindowControllerWin32 extends RegularWindowController + implements WindowsMessageHandler { + /// Creates a new regular window controller for Win32. When this constructor + /// completes the FlutterView is created and framework is aware of it. + RegularWindowControllerWin32({ + required WindowingOwnerWin32 owner, + required RegularWindowControllerDelegate delegate, + required WindowSizing contentSize, + }) : _owner = owner, + _delegate = delegate, + super.empty() { + owner.addMessageHandler(this); + final Pointer<_WindowCreationRequest> request = + ffi.calloc<_WindowCreationRequest>()..ref.contentSize.set(contentSize); + final int viewId = _createWindow(PlatformDispatcher.instance.engineId!, request); + ffi.calloc.free(request); + final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere( + (FlutterView view) => view.viewId == viewId, + ); + setView(flutterView); + } + + @override + Size get contentSize { + _ensureNotDestroyed(); + final _Size size = _getWindowContentSize(getWindowHandle()); + final Size result = Size(size.width, size.height); + return result; + } + + @override + void setTitle(String title) { + _ensureNotDestroyed(); + final Pointer titlePointer = title.toNativeUtf16(); + _setWindowTitle(getWindowHandle(), titlePointer); + ffi.calloc.free(titlePointer); + } + + @override + void updateContentSize(WindowSizing sizing) { + _ensureNotDestroyed(); + final Pointer<_Sizing> ffiSizing = ffi.calloc<_Sizing>(); + ffiSizing.ref.set(sizing); + _setWindowContentSize(getWindowHandle(), ffiSizing); + ffi.calloc.free(ffiSizing); + } + + @override + void activate() { + _ensureNotDestroyed(); + _showWindow(getWindowHandle(), SW_RESTORE); + } + + @override + bool isFullscreen() { + return false; + } + + @override + void setFullscreen(bool fullscreen, {int? displayId}) {} + + @override + bool isMaximized() { + _ensureNotDestroyed(); + return _isZoomed(getWindowHandle()) != 0; + } + + @override + bool isMinimized() { + _ensureNotDestroyed(); + return _isIconic(getWindowHandle()) != 0; + } + + @override + void setMinimized(bool minimized) { + _ensureNotDestroyed(); + if (minimized) { + _showWindow(getWindowHandle(), SW_MINIMIZE); + } else { + _showWindow(getWindowHandle(), SW_RESTORE); + } + } + + @override + void setMaximized(bool maximized) { + _ensureNotDestroyed(); + if (maximized) { + _showWindow(getWindowHandle(), SW_MAXIMIZE); + } else { + _showWindow(getWindowHandle(), SW_RESTORE); + } + } + + /// Returns HWND pointer to the top level window. + Pointer getWindowHandle() { + _ensureNotDestroyed(); + return _getWindowHandle(PlatformDispatcher.instance.engineId!, rootView.viewId); + } + + void _ensureNotDestroyed() { + if (_destroyed) { + throw StateError('Window has been destroyed.'); + } + } + + final RegularWindowControllerDelegate _delegate; + bool _destroyed = false; + + @override + void destroy() { + if (_destroyed) { + return; + } + _destroyWindow(getWindowHandle()); + _destroyed = true; + _delegate.onWindowDestroyed(); + _owner.removeMessageHandler(this); + } + + static const int _WM_SIZE = 0x0005; + static const int _WM_CLOSE = 0x0010; + + static const int SW_RESTORE = 9; + static const int SW_MAXIMIZE = 3; + static const int SW_MINIMIZE = 6; + + @override + int? handleWindowsMessage( + FlutterView view, + Pointer windowHandle, + int message, + int wParam, + int lParam, + ) { + if (view.viewId != rootView.viewId) { + return null; + } + + if (message == _WM_CLOSE) { + _delegate.onWindowCloseRequested(this); + return 0; + } else if (message == _WM_SIZE) { + notifyListeners(); + } + return null; + } + + final WindowingOwnerWin32 _owner; + + @Native)>( + symbol: 'InternalFlutterWindows_WindowManager_CreateRegularWindow', + ) + external static int _createWindow(int engineId, Pointer<_WindowCreationRequest> request); + + @Native Function(Int64, Int64)>(symbol: 'InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle') + external static Pointer _getWindowHandle(int engineId, int viewId); + + @Native)>(symbol: 'DestroyWindow') + external static void _destroyWindow(Pointer windowHandle); + + @Native<_Size Function(Pointer)>(symbol: 'InternalFlutterWindows_WindowManager_GetWindowContentSize') + external static _Size _getWindowContentSize(Pointer windowHandle); + + @Native, Pointer)>(symbol: 'SetWindowTextW') + external static void _setWindowTitle(Pointer windowHandle, Pointer title); + + @Native, Pointer<_Sizing>)>(symbol: 'InternalFlutterWindows_WindowManager_SetWindowContentSize') + external static void _setWindowContentSize(Pointer windowHandle, Pointer<_Sizing> size); + + @Native, Int32)>(symbol: 'ShowWindow') + external static void _showWindow(Pointer windowHandle, int command); + + @Native)>(symbol: 'IsIconic') + external static int _isIconic(Pointer windowHandle); + + @Native)>(symbol: 'IsZoomed') + external static int _isZoomed(Pointer windowHandle); +} + +/// Request to initialize windowing system. +final class _WindowingInitRequest extends Struct { + external Pointer)>> onMessage; +} + +final class _Sizing extends Struct { + @Bool() + external bool hasSize; + + @Double() + external double width; + + @Double() + external double height; + + @Bool() + external bool hasConstraints; + + @Double() + external double minWidth; + + @Double() + external double minHeight; + + @Double() + external double maxWidth; + + @Double() + external double maxHeight; + + void set(WindowSizing sizing) { + final Size? size = sizing.preferredSize; + if (size != null) { + hasSize = true; + width = size.width; + height = size.height; + } else { + hasSize = false; + } + + final BoxConstraints? constraints = sizing.constraints; + if (constraints != null) { + hasConstraints = true; + minWidth = constraints.minWidth; + minHeight = constraints.minHeight; + maxWidth = constraints.maxWidth; + maxHeight = constraints.maxHeight; + } else { + hasConstraints = false; + } + } +} + +final class _WindowCreationRequest extends Struct { + external _Sizing contentSize; +} + +/// Windows message received for all top level windows (regardless whether +/// they are created using a windowing controller). +final class _WindowsMessage extends Struct { + @Int64() + external int viewId; + + external Pointer windowHandle; + + @Int32() + external int message; + + @Int64() + external int wParam; + + @Int64() + external int lParam; + + @Int64() + external int lResult; + + @Bool() + external bool handled; +} + +final class _Size extends Struct { + @Double() + external double width; + + @Double() + external double height; +} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index efe2858c67443..5104162087656 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -180,3 +180,4 @@ export 'src/widgets/widget_inspector.dart'; export 'src/widgets/widget_span.dart'; export 'src/widgets/widget_state.dart'; export 'src/widgets/will_pop_scope.dart'; +export 'src/widgets/window.dart'; diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml index 3654490d76367..a8e79a218a818 100644 --- a/packages/flutter/pubspec.yaml +++ b/packages/flutter/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: vector_math: 2.2.0 sky_engine: sdk: flutter + ffi: ^2.1.4 dev_dependencies: flutter_driver: diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index 5808af6386397..07903161d26ec 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -35,6 +35,7 @@ import 'test_default_binary_messenger.dart'; import 'test_exception_reporter.dart'; import 'test_text_input.dart'; import 'window.dart'; +import 'windowing.dart'; /// Phases that can be reached by [WidgetTester.pumpWidget] and /// [TestWidgetsFlutterBinding.pump]. @@ -204,6 +205,11 @@ abstract class TestWidgetsFlutterBinding extends BindingBase debugDisableShadows = disableShadows; } + @override + WindowingOwner createWindowingOwner() { + return TestWindowingOwner(); + } + /// Deprecated. Will be removed in a future version of Flutter. /// /// This property has been deprecated to prepare for Flutter's upcoming diff --git a/packages/flutter_test/lib/src/windowing.dart b/packages/flutter_test/lib/src/windowing.dart new file mode 100644 index 0000000000000..b1b82cf112c8e --- /dev/null +++ b/packages/flutter_test/lib/src/windowing.dart @@ -0,0 +1,21 @@ +// Copyright 2014 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 'package:flutter/widgets.dart'; + +/// WindowingOwner used in Flutter Tester. +class TestWindowingOwner extends WindowingOwner { + @override + RegularWindowController createRegularWindowController({ + required WindowSizing contentSize, + required RegularWindowControllerDelegate delegate, + }) { + throw UnsupportedError('Current platform does not support windowing.\n'); + } + + @override + bool hasTopLevelWindows() { + return false; + } +}