Multi-window support (engine) (#168728)

<!--
Thanks for filing a pull request!
Reviewers are typically assigned within a week of filing a request.
To learn more about code review, see our documentation on Tree Hygiene:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
-->

*Replace this paragraph with a description of what this PR is changing
or adding, and why. Consider including before/after screenshots.*

*List which issues are fixed by this PR. You must list at least one
issue. An issue is not required if the PR fixes something trivial like a
typo.*

*If you had to change anything in the [flutter/tests] repo, include a
link to the migration guide as per the [breaking change policy].*

## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [X] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Matthew Kosarek <matt.kosarek@canonical.com>
Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com>
This commit is contained in:
Matej Knopp 2025-07-08 19:19:02 +02:00 committed by GitHub
parent ae3823c2e8
commit fc286d0530
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 2174 additions and 25 deletions

View File

@ -435,6 +435,7 @@
../../../flutter/shell/platform/windows/task_runner_unittests.cc
../../../flutter/shell/platform/windows/testing
../../../flutter/shell/platform/windows/text_input_plugin_unittest.cc
../../../flutter/shell/platform/windows/window_manager_unittests.cc
../../../flutter/shell/platform/windows/window_proc_delegate_manager_unittests.cc
../../../flutter/shell/platform/windows/window_unittests.cc
../../../flutter/shell/platform/windows/windows_lifecycle_manager_unittests.cc

View File

@ -53288,6 +53288,8 @@ ORIGIN: ../../../flutter/shell/platform/common/flutter_platform_node_delegate.h
ORIGIN: ../../../flutter/shell/platform/common/geometry.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/common/incoming_message_dispatcher.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/common/incoming_message_dispatcher.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/common/isolate_scope.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/common/isolate_scope.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/common/json_message_codec.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/common/json_message_codec.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/common/json_method_codec.cc + ../../../flutter/LICENSE
@ -53307,6 +53309,7 @@ ORIGIN: ../../../flutter/shell/platform/common/text_editing_delta.h + ../../../f
ORIGIN: ../../../flutter/shell/platform/common/text_input_model.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/common/text_input_model.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/common/text_range.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/common/windowing.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon-Bridging-Header.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/common/SwiftTestingMain.swift + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/common/SwiftTestingRunner.swift + ../../../flutter/LICENSE
@ -53594,6 +53597,9 @@ ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterVie
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProviderTest.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowControllerTest.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMap.g.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMapTest.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMap_Internal.h + ../../../flutter/LICENSE
@ -54071,6 +54077,8 @@ ORIGIN: ../../../flutter/shell/platform/windows/flutter_windows_view.cc + ../../
ORIGIN: ../../../flutter/shell/platform/windows/flutter_windows_view.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/flutter_windows_view_controller.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/flutter_windows_view_controller.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/host_window.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/host_window.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/keyboard_handler_base.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/keyboard_key_channel_handler.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/keyboard_key_channel_handler.h + ../../../flutter/LICENSE
@ -54105,6 +54113,8 @@ ORIGIN: ../../../flutter/shell/platform/windows/text_input_plugin.cc + ../../../
ORIGIN: ../../../flutter/shell/platform/windows/text_input_plugin.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/window_binding_handler.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/window_binding_handler_delegate.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/window_manager.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/window_manager.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/window_proc_delegate_manager.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/window_proc_delegate_manager.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/window_state.h + ../../../flutter/LICENSE
@ -56349,6 +56359,8 @@ FILE: ../../../flutter/shell/platform/common/flutter_platform_node_delegate.h
FILE: ../../../flutter/shell/platform/common/geometry.h
FILE: ../../../flutter/shell/platform/common/incoming_message_dispatcher.cc
FILE: ../../../flutter/shell/platform/common/incoming_message_dispatcher.h
FILE: ../../../flutter/shell/platform/common/isolate_scope.cc
FILE: ../../../flutter/shell/platform/common/isolate_scope.h
FILE: ../../../flutter/shell/platform/common/json_message_codec.cc
FILE: ../../../flutter/shell/platform/common/json_message_codec.h
FILE: ../../../flutter/shell/platform/common/json_method_codec.cc
@ -56368,6 +56380,7 @@ FILE: ../../../flutter/shell/platform/common/text_editing_delta.h
FILE: ../../../flutter/shell/platform/common/text_input_model.cc
FILE: ../../../flutter/shell/platform/common/text_input_model.h
FILE: ../../../flutter/shell/platform/common/text_range.h
FILE: ../../../flutter/shell/platform/common/windowing.h
FILE: ../../../flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon-Bridging-Header.h
FILE: ../../../flutter/shell/platform/darwin/common/SwiftTestingMain.swift
FILE: ../../../flutter/shell/platform/darwin/common/SwiftTestingRunner.swift
@ -56660,6 +56673,9 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewE
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProviderTest.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.h
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowController.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterWindowControllerTest.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMap.g.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMapTest.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMap_Internal.h
@ -57141,6 +57157,8 @@ FILE: ../../../flutter/shell/platform/windows/flutter_windows_view.cc
FILE: ../../../flutter/shell/platform/windows/flutter_windows_view.h
FILE: ../../../flutter/shell/platform/windows/flutter_windows_view_controller.cc
FILE: ../../../flutter/shell/platform/windows/flutter_windows_view_controller.h
FILE: ../../../flutter/shell/platform/windows/host_window.cc
FILE: ../../../flutter/shell/platform/windows/host_window.h
FILE: ../../../flutter/shell/platform/windows/keyboard_handler_base.h
FILE: ../../../flutter/shell/platform/windows/keyboard_key_channel_handler.cc
FILE: ../../../flutter/shell/platform/windows/keyboard_key_channel_handler.h
@ -57175,6 +57193,8 @@ FILE: ../../../flutter/shell/platform/windows/text_input_plugin.cc
FILE: ../../../flutter/shell/platform/windows/text_input_plugin.h
FILE: ../../../flutter/shell/platform/windows/window_binding_handler.h
FILE: ../../../flutter/shell/platform/windows/window_binding_handler_delegate.h
FILE: ../../../flutter/shell/platform/windows/window_manager.cc
FILE: ../../../flutter/shell/platform/windows/window_manager.h
FILE: ../../../flutter/shell/platform/windows/window_proc_delegate_manager.cc
FILE: ../../../flutter/shell/platform/windows/window_proc_delegate_manager.h
FILE: ../../../flutter/shell/platform/windows/window_state.h

View File

@ -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) {

View File

@ -6,6 +6,8 @@
#define FLUTTER_SHELL_PLATFORM_COMMON_GEOMETRY_H_
#include <cmath>
#include <limits>
#include <optional>
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<Size>& smallest,
const std::optional<Size>& biggest)
: smallest_(smallest.value_or(Size(0, 0))),
biggest_(
biggest.value_or(Size(std::numeric_limits<double>::infinity(),
std::numeric_limits<double>::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<double>::infinity(),
std::numeric_limits<double>::infinity());
};
} // namespace flutter
#endif // FLUTTER_SHELL_PLATFORM_COMMON_GEOMETRY_H_

View File

@ -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

View File

@ -0,0 +1,47 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "flutter/fml/logging.h"
#include "third_party/dart/runtime/include/dart_api.h"
#ifndef FLUTTER_SHELL_PLATFORM_COMMON_ISOLATE_SCOPE_H_
#define FLUTTER_SHELL_PLATFORM_COMMON_ISOLATE_SCOPE_H_
namespace flutter {
/// This class is a thin wrapper around dart isolate. It can be used
/// as argument to IsolateScope constructor to enter and exit the isolate.
class Isolate {
public:
/// Retrieve the current Dart Isolate. If no isolate is current, this
/// results in a crash.
static Isolate Current();
~Isolate() = default;
private:
explicit Isolate(Dart_Isolate isolate) : isolate_(isolate) {
FML_DCHECK(isolate_ != nullptr);
}
friend class IsolateScope;
Dart_Isolate isolate_;
};
// Enters provided isolate for as long as the scope is alive.
class IsolateScope {
public:
explicit IsolateScope(const Isolate& isolate);
~IsolateScope();
private:
Dart_Isolate isolate_;
Dart_Isolate previous_;
FML_DISALLOW_COPY_AND_ASSIGN(IsolateScope);
};
} // namespace flutter
#endif // FLUTTER_SHELL_PLATFORM_COMMON_ISOLATE_SCOPE_H_

View File

@ -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_

View File

@ -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",

View File

@ -452,6 +452,9 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, void* user_
// factories. Lifecycle is tied to the engine.
FlutterPlatformViewController* _platformViewController;
// Used to manage Flutter windows created by the Dart application
FlutterWindowController* _windowController;
// A message channel for sending user settings to the flutter engine.
FlutterBasicMessageChannel* _settingsChannel;
@ -483,8 +486,19 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, void* user_
// The text input plugin that handles text editing state for text fields.
FlutterTextInputPlugin* _textInputPlugin;
// Whether the engine is running in multi-window mode. This affects behavior
// when adding view controller (it will fail when calling multiple times without
// _multiviewEnabled).
BOOL _multiViewEnabled;
// View identifier for the next view to be created.
// Only used when multiview is enabled.
FlutterViewIdentifier _nextViewIdentifier;
}
@synthesize windowController = _windowController;
- (instancetype)initWithName:(NSString*)labelPrefix project:(FlutterDartProject*)project {
return [self initWithName:labelPrefix project:project allowHeadlessExecution:YES];
}
@ -528,6 +542,8 @@ static void SetThreadPriority(FlutterThreadPriority priority) {
[_isResponseValid addObject:@YES];
_keyboardManager = [[FlutterKeyboardManager alloc] initWithDelegate:self];
_textInputPlugin = [[FlutterTextInputPlugin alloc] initWithDelegate:self];
_multiViewEnabled = NO;
_nextViewIdentifier = 1;
_embedderAPI.struct_size = sizeof(FlutterEngineProcTable);
FlutterEngineGetProcAddresses(&_embedderAPI);
@ -549,6 +565,10 @@ static void SetThreadPriority(FlutterThreadPriority priority) {
[[FlutterTimeConverter alloc] initWithEngine:self], _platformViewController);
[self setUpPlatformViewChannel];
_windowController = [[FlutterWindowController alloc] init];
_windowController.engine = self;
[self setUpAccessibilityChannel];
[self setUpNotificationCenterListeners];
id<NSApplicationDelegate> appDelegate = [[NSApplication sharedApplication] delegate];
@ -623,6 +643,16 @@ static void SetThreadPriority(FlutterThreadPriority priority) {
return cocoa_task_runner_description;
}
- (void)onFocusChangeRequest:(const FlutterViewFocusChangeRequest*)request {
FlutterViewController* controller = [self viewControllerForIdentifier:request->view_id];
if (controller == nil) {
return;
}
if (request->state == kFocused) {
[controller.flutterView.window makeFirstResponder:controller.flutterView];
}
}
- (BOOL)runWithEntrypoint:(NSString*)entrypoint {
if (self.running) {
return NO;
@ -737,6 +767,12 @@ static void SetThreadPriority(FlutterThreadPriority priority) {
[engine onVSync:baton];
};
flutterArguments.view_focus_change_request_callback =
[](const FlutterViewFocusChangeRequest* request, void* user_data) {
FlutterEngine* engine = (__bridge FlutterEngine*)user_data;
[engine onFocusChangeRequest:request];
};
FlutterRendererConfig rendererConfig = [_renderer createRendererConfig];
FlutterEngineResult result = _embedderAPI.Initialize(
FLUTTER_ENGINE_VERSION, &rendererConfig, &flutterArguments, (__bridge void*)(self), &_engine);
@ -797,10 +833,12 @@ static void SetThreadPriority(FlutterThreadPriority priority) {
forIdentifier:(FlutterViewIdentifier)viewIdentifier {
_macOSCompositor->AddView(viewIdentifier);
NSAssert(controller != nil, @"The controller must not be nil.");
NSAssert(controller.engine == nil,
@"The FlutterViewController is unexpectedly attached to "
@"engine %@ before initialization.",
controller.engine);
if (!_multiViewEnabled) {
NSAssert(controller.engine == nil,
@"The FlutterViewController is unexpectedly attached to "
@"engine %@ before initialization.",
controller.engine);
}
NSAssert([_viewControllers objectForKey:@(viewIdentifier)] == nil,
@"The requested view ID is occupied.");
[_viewControllers setObject:controller forKey:@(viewIdentifier)];
@ -818,6 +856,32 @@ static void SetThreadPriority(FlutterThreadPriority priority) {
if (controller.viewLoaded) {
[self viewControllerViewDidLoad:controller];
}
if (viewIdentifier != kFlutterImplicitViewId) {
// These will be overriden immediately after the FlutterView is created
// by actual values.
FlutterWindowMetricsEvent metrics{
.struct_size = sizeof(FlutterWindowMetricsEvent),
.width = 0,
.height = 0,
.pixel_ratio = 1.0,
};
bool added = false;
FlutterAddViewInfo info{.struct_size = sizeof(FlutterAddViewInfo),
.view_id = viewIdentifier,
.view_metrics = &metrics,
.user_data = &added,
.add_view_callback = [](const FlutterAddViewResult* r) {
auto added = reinterpret_cast<bool*>(r->user_data);
*added = true;
}};
// The callback should be called synchronously from platform thread.
_embedderAPI.AddView(_engine, &info);
FML_DCHECK(added);
if (!added) {
NSLog(@"Failed to add view with ID %llu", viewIdentifier);
}
}
}
- (void)viewControllerViewDidLoad:(FlutterViewController*)viewController {
@ -835,14 +899,36 @@ static void SetThreadPriority(FlutterThreadPriority priority) {
engine->_embedderAPI.OnVsync(_engine, baton, timeNanos, targetTimeNanos);
}
}];
FML_DCHECK([_vsyncWaiters objectForKey:@(viewController.viewIdentifier)] == nil);
@synchronized(_vsyncWaiters) {
FML_DCHECK([_vsyncWaiters objectForKey:@(viewController.viewIdentifier)] == nil);
[_vsyncWaiters setObject:waiter forKey:@(viewController.viewIdentifier)];
}
}
- (void)deregisterViewControllerForIdentifier:(FlutterViewIdentifier)viewIdentifier {
if (viewIdentifier != kFlutterImplicitViewId) {
bool removed = false;
FlutterRemoveViewInfo info;
info.struct_size = sizeof(FlutterRemoveViewInfo);
info.view_id = viewIdentifier;
info.user_data = &removed;
// RemoveViewCallback is not finished synchronously, the remove_view_callback
// is called from raster thread when the engine knows for sure that the resources
// associated with the view are no longer needed.
info.remove_view_callback = [](const FlutterRemoveViewResult* r) {
auto removed = reinterpret_cast<bool*>(r->user_data);
[FlutterRunLoop.mainRunLoop performBlock:^{
*removed = true;
}];
};
_embedderAPI.RemoveView(_engine, &info);
while (!removed) {
[[FlutterRunLoop mainRunLoop] pollFlutterMessagesOnce];
}
}
_macOSCompositor->RemoveView(viewIdentifier);
FlutterViewController* controller = [self viewControllerForIdentifier:viewIdentifier];
// The controller can be nil. The engine stores only a weak ref, and this
// method could have been called from the controller's dealloc.
@ -854,9 +940,13 @@ static void SetThreadPriority(FlutterThreadPriority priority) {
@"the FlutterEngine is mocked. Please subclass these classes instead.");
}
[_viewControllers removeObjectForKey:@(viewIdentifier)];
FlutterVSyncWaiter* waiter = nil;
@synchronized(_vsyncWaiters) {
waiter = [_vsyncWaiters objectForKey:@(viewIdentifier)];
[_vsyncWaiters removeObjectForKey:@(viewIdentifier)];
}
[waiter invalidate];
}
- (void)shutDownIfNeeded {
@ -943,11 +1033,46 @@ static void SetThreadPriority(FlutterThreadPriority priority) {
#pragma mark - Framework-internal methods
- (void)addViewController:(FlutterViewController*)controller {
// FlutterEngine can only handle the implicit view for now. Adding more views
// throws an assertion.
NSAssert(self.viewController == nil,
@"The engine already has a view controller for the implicit view.");
self.viewController = controller;
if (!_multiViewEnabled) {
// When multiview is disabled, the engine will only assign views to the implicit view ID.
// The implicit view ID can be reused if and only if the implicit view is unassigned.
NSAssert(self.viewController == nil,
@"The engine already has a view controller for the implicit view.");
self.viewController = controller;
} else {
// When multiview is enabled, the engine will assign views to a self-incrementing ID.
// The implicit view ID can not be reused.
FlutterViewIdentifier viewIdentifier = _nextViewIdentifier++;
[self registerViewController:controller forIdentifier:viewIdentifier];
}
}
- (void)enableMultiView {
if (!_multiViewEnabled) {
NSAssert(self.viewController == nil,
@"Multiview can only be enabled before adding any view controllers.");
_multiViewEnabled = YES;
}
}
- (void)windowDidBecomeKey:(FlutterViewIdentifier)viewIdentifier {
FlutterViewFocusEvent event{
.struct_size = sizeof(FlutterViewFocusEvent),
.view_id = viewIdentifier,
.state = kFocused,
.direction = kUndefined,
};
_embedderAPI.SendViewFocusEvent(_engine, &event);
}
- (void)windowDidResignKey:(FlutterViewIdentifier)viewIdentifier {
FlutterViewFocusEvent event{
.struct_size = sizeof(FlutterViewFocusEvent),
.view_id = viewIdentifier,
.state = kUnfocused,
.direction = kUndefined,
};
_embedderAPI.SendViewFocusEvent(_engine, &event);
}
- (void)removeViewController:(nonnull FlutterViewController*)viewController {
@ -1171,8 +1296,16 @@ static void SetThreadPriority(FlutterThreadPriority priority) {
auto block = ^{
// TODO(knopp): Use vsync waiter for correct view.
// https://github.com/flutter/flutter/issues/142845
FlutterVSyncWaiter* waiter = [_vsyncWaiters objectForKey:@(kFlutterImplicitViewId)];
[waiter waitForVSync:baton];
FlutterVSyncWaiter* waiter =
[_vsyncWaiters objectForKey:[_vsyncWaiters.keyEnumerator nextObject]];
if (waiter != nil) {
[waiter waitForVSync:baton];
} else {
// Sometimes there is a vsync request right after the last view is removed.
// It still need to be handled, otherwise the engine will stop producing frames
// even if a new view is added later.
self.embedderAPI.OnVsync(_engine, baton, 0, 0);
}
};
if ([NSThread isMainThread]) {
block();
@ -1254,6 +1387,7 @@ static void SetThreadPriority(FlutterThreadPriority priority) {
[FlutterMouseCursorPlugin registerWithRegistrar:[self registrarForPlugin:@"mousecursor"]
delegate:self];
[FlutterMenuPlugin registerWithRegistrar:[self registrarForPlugin:@"menu"]];
_settingsChannel =
[FlutterBasicMessageChannel messageChannelWithName:kFlutterSettingsChannel
binaryMessenger:self.binaryMessenger

View File

@ -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

View File

@ -14,7 +14,7 @@
#include <map>
#include <unordered_set>
@interface FlutterPlatformViewController : NSViewController
@interface FlutterPlatformViewController : NSObject
@end
@interface FlutterPlatformViewController ()

View File

@ -24,6 +24,10 @@
/// This function must be called on the main thread.
- (void)waitForVSync:(uintptr_t)baton;
/// Invalidates the waiter. This must be called on the main thread
/// before the instance is deallocated.
- (void)invalidate;
@end
#endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERVSYNCWAITER_H_

View File

@ -118,7 +118,7 @@ static const CFTimeInterval kTimerLatencyCompensation = 0.001;
TRACE_VSYNC("VSyncRequest", _pendingBaton.value_or(0));
CFTimeInterval tick_interval = _displayLink.nominalOutputRefreshPeriod;
if (_displayLink.paused || tick_interval == 0) {
if (_displayLink.paused || tick_interval == 0 || _lastTargetTimestamp == 0) {
// When starting display link the first notification will come in the middle
// of next frame, which would incur a whole frame period of latency.
// To avoid that, first vsync notification will be fired using a timer
@ -154,16 +154,22 @@ static const CFTimeInterval kTimerLatencyCompensation = 0.001;
}
}
- (void)dealloc {
- (void)invalidate {
// It is possible that there is pending vsync request while the view for which
// this waiter belongs is being destroyed. In that case trigger the vsync
// immediately to avoid deadlock.
if (_pendingBaton.has_value()) {
[FlutterLogger logWarning:@"Deallocating FlutterVSyncWaiter with a pending vsync"];
CFTimeInterval now = CACurrentMediaTime();
_block(now, now, _pendingBaton.value());
_pendingBaton = std::nullopt;
}
// It is possible that block running on UI thread held the last reference to
// the waiter, in which case reschedule to main thread.
FlutterDisplayLink* link = _displayLink;
dispatch_async(dispatch_get_main_queue(), ^{
[link invalidate];
});
[_displayLink invalidate];
_displayLink = nil;
}
- (void)dealloc {
FML_DCHECK(_displayLink == nil);
}
@end

View File

@ -55,6 +55,7 @@ TEST_F(FlutterVSyncWaiterTest, RequestsInitialVSync) {
[displayLink tickWithTimestamp:CACurrentMediaTime()
targetTimestamp:CACurrentMediaTime() + 1.0 / 60.0];
EXPECT_TRUE(displayLink.paused);
[waiter invalidate];
}
static void BusyWait(CFTimeInterval duration) {
@ -111,6 +112,8 @@ TEST_F(FlutterVSyncWaiterTest, FirstVSyncIsSynthesized) {
EXPECT_DOUBLE_EQ(timestamp, expectedTimestamp);
EXPECT_DOUBLE_EQ(targetTimestamp, expectedTimestamp + displayLink.nominalOutputRefreshPeriod);
EXPECT_EQ(baton, size_t(1));
[waiter invalidate];
};
// First argument if the wait duration after reference vsync.
@ -204,4 +207,6 @@ TEST_F(FlutterVSyncWaiterTest, VSyncWorks) {
EXPECT_DOUBLE_EQ(entries[3].timestamp, now + 3 * displayLink.nominalOutputRefreshPeriod);
EXPECT_DOUBLE_EQ(entries[3].targetTimestamp, now + 4 * displayLink.nominalOutputRefreshPeriod);
EXPECT_EQ(entries[3].baton, size_t(3));
[waiter invalidate];
}

View File

@ -327,6 +327,7 @@ static void CommonInit(FlutterViewController* controller, FlutterEngine* engine)
@"engine %@ before initialization.",
controller.engine);
[engine addViewController:controller];
NSCAssert(controller.engine != nil,
@"The FlutterViewController unexpectedly stays unattached after initialization. "
@"In unit tests, this is likely because either the FlutterViewController or "
@ -418,13 +419,17 @@ static void CommonInit(FlutterViewController* controller, FlutterEngine* engine)
_keyUpMonitor = nil;
}
- (void)dealloc {
- (void)dispose {
if ([self attached]) {
[_engine removeViewController:self];
}
[self.flutterView shutDown];
}
- (void)dealloc {
[self dispose];
}
#pragma mark - Public methods
- (void)setMouseTrackingMode:(FlutterMouseTrackingMode)mode {

View File

@ -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

View File

@ -0,0 +1,100 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERWINDOWCONTROLLER_H_
#define FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERWINDOWCONTROLLER_H_
#import <Cocoa/Cocoa.h>
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h"
@class FlutterEngine;
@interface FlutterWindowController : NSObject
@property(nonatomic, weak) FlutterEngine* engine;
@end
@interface FlutterWindowController (Testing)
- (void)closeAllWindows;
@end
struct FlutterWindowSizing {
bool has_size;
double width;
double height;
bool has_constraints;
double min_width;
double min_height;
double max_width;
double max_height;
};
struct FlutterWindowCreationRequest {
FlutterWindowSizing contentSize;
void (*on_close)();
void (*on_size_change)();
};
struct FlutterWindowSize {
double width;
double height;
};
extern "C" {
// NOLINTBEGIN(google-objc-function-naming)
FLUTTER_DARWIN_EXPORT
int64_t InternalFlutter_WindowController_CreateRegularWindow(
int64_t engine_id,
const FlutterWindowCreationRequest* request);
FLUTTER_DARWIN_EXPORT
void InternalFlutter_Window_Destroy(int64_t engine_id, void* window);
FLUTTER_DARWIN_EXPORT
void* InternalFlutter_Window_GetHandle(int64_t engine_id, FlutterViewIdentifier view_id);
FLUTTER_DARWIN_EXPORT
FlutterWindowSize InternalFlutter_Window_GetContentSize(void* window);
FLUTTER_DARWIN_EXPORT
void InternalFlutter_Window_SetContentSize(void* window, const FlutterWindowSizing* size);
FLUTTER_DARWIN_EXPORT
void InternalFlutter_Window_SetTitle(void* window, const char* title);
FLUTTER_DARWIN_EXPORT
void InternalFlutter_Window_SetMaximized(void* window, bool maximized);
FLUTTER_DARWIN_EXPORT
bool InternalFlutter_Window_IsMaximized(void* window);
FLUTTER_DARWIN_EXPORT
void InternalFlutter_Window_Minimize(void* window);
FLUTTER_DARWIN_EXPORT
void InternalFlutter_Window_Unminimize(void* window);
FLUTTER_DARWIN_EXPORT
bool InternalFlutter_Window_IsMinimized(void* window);
FLUTTER_DARWIN_EXPORT
void InternalFlutter_Window_SetFullScreen(void* window, bool fullScreen);
FLUTTER_DARWIN_EXPORT
bool InternalFlutter_Window_IsFullScreen(void* window);
FLUTTER_DARWIN_EXPORT
void InternalFlutter_Window_Activate(void* window);
// NOLINTEND(google-objc-function-naming)
}
#endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERWINDOWCONTROLLER_H_

View File

@ -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 <Foundation/Foundation.h>
#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 <NSWindowDelegate> {
// Strong reference to the window. This is the only strong reference to the
// window.
NSWindow* _window;
FlutterViewController* _flutterViewController;
std::optional<flutter::Isolate> _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<FlutterWindowOwner*>* _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)

View File

@ -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<flutter::Isolate> 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<int64_t>(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<flutter::Isolate> 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<int64_t>(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<int64_t>(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<int64_t>(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<int64_t>(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

View File

@ -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() {}

View File

@ -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",

View File

@ -404,6 +404,11 @@ void testEngineId() {
notifyEngineId(ui.PlatformDispatcher.instance.engineId);
}
@pragma('vm:entry-point')
void testWindowController() {
signal();
}
@pragma('vm:entry-point')
Future<void> sendSemanticsTreeInfo() async {
// Wait until semantics are enabled.

View File

@ -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<FlutterWindowsEngine*>(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<void*>(this));
@ -224,6 +235,7 @@ FlutterWindowsEngine::FlutterWindowsEngine(
std::make_unique<CursorHandler>(messenger_wrapper_.get(), this);
platform_handler_ =
std::make_unique<PlatformHandler>(messenger_wrapper_.get(), this);
window_manager_ = std::make_unique<WindowManager>(this);
settings_plugin_ = std::make_unique<SettingsPlugin>(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<LanguageInfo> languages =
GetPreferredLanguageInfo(*windows_proc_table_);

View File

@ -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<KeyboardHandlerBase> 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<WindowManager> window_manager_;
// Handlers for text events from Windows.
std::unique_ptr<TextInputPlugin> text_input_plugin_;

View File

@ -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 <dwmapi.h>
#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<DWORD>(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<LPTSTR>(&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<flutter::Size> GetWindowSizeForClientSize(
flutter::WindowsProcTable const& win32,
flutter::Size const& client_size,
std::optional<flutter::Size> smallest,
std::optional<flutter::Size> biggest,
DWORD window_style,
DWORD extended_window_style,
HWND owner_hwnd) {
UINT const dpi = flutter::GetDpiForHWND(owner_hwnd);
double const scale_factor =
static_cast<double>(dpi) / USER_DEFAULT_SCREEN_DPI;
RECT rect = {
.right = static_cast<LONG>(client_size.width() * scale_factor),
.bottom = static_cast<LONG>(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<double>(rect.right - rect.left);
double height = static_cast<double>(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> HostWindow::CreateRegularWindow(
WindowManager* window_manager,
FlutterWindowsEngine* engine,
const WindowSizing& content_size) {
DWORD window_style = WS_OVERLAPPEDWINDOW;
DWORD extended_window_style = 0;
std::optional<Size> smallest = std::nullopt;
std::optional<Size> 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<Size> 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<FlutterWindow>(
initial_window_rect.width(), initial_window_rect.height(),
engine->windows_proc_table());
std::unique_ptr<FlutterWindowsView> view =
engine->CreateView(std::move(view_window));
if (view == nullptr) {
FML_LOG(ERROR) << "Failed to create view";
return nullptr;
}
std::unique_ptr<FlutterWindowsViewController> view_controller =
std::make_unique<FlutterWindowsViewController>(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<HostWindow>(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<FlutterWindowsViewController> 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<LONG_PTR>(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<HostWindow*>(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<CREATESTRUCT*>(lparam);
auto* const windows_proc_table =
static_cast<WindowsProcTable*>(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<RECT*>(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<double>(dpi) / USER_DEFAULT_SCREEN_DPI;
MINMAXINFO* info = reinterpret_cast<MINMAXINFO*>(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<Size> 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<Size> 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

View File

@ -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 <windows.h>
#include <memory>
#include <optional>
#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<HostWindow> 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<FlutterWindowsViewController> 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<FlutterWindowsViewController> 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_

View File

@ -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 <dwmapi.h>
#include <optional>
#include <vector>
#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<HWND> 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<LRESULT> 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<double>(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);
}
}

View File

@ -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 <windows.h>
#include <functional>
#include <optional>
#include <unordered_map>
#include <vector>
#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<LRESULT> 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<void(WindowsMessage*)> on_message_;
// Isolate that runs the Dart code. Set during Initialize().
std::optional<Isolate> isolate_;
// A map of active windows. Used to destroy remaining windows on engine
// shutdown.
std::unordered_map<HWND, std::unique_ptr<HostWindow>> 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_

View File

@ -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<int64_t>(engine_.get()); }
flutter::Isolate& isolate() { return *isolate_; }
WindowCreationRequest* creation_request() { return &creation_request_; }
private:
std::unique_ptr<FlutterWindowsEngine> engine_;
std::optional<flutter::Isolate> 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

View File

@ -13,6 +13,15 @@ WindowsProcTable::WindowsProcTable() {
user32_ = fml::NativeLibrary::Create("user32.dll");
get_pointer_type_ =
user32_->ResolveFunction<GetPointerType_*>("GetPointerType");
enable_non_client_dpi_scaling_ =
user32_->ResolveFunction<EnableNonClientDpiScaling_*>(
"EnableNonClientDpiScaling");
set_window_composition_attribute_ =
user32_->ResolveFunction<SetWindowCompositionAttribute_*>(
"SetWindowCompositionAttribute");
adjust_window_rect_ext_for_dpi_ =
user32_->ResolveFunction<AdjustWindowRectExForDpi_*>(
"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

View File

@ -5,6 +5,7 @@
#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOWS_PROC_TABLE_H_
#define FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOWS_PROC_TABLE_H_
#include <dwmapi.h>
#include <optional>
#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<fml::NativeLibrary> user32_;
std::optional<GetPointerType_*> get_pointer_type_;
std::optional<EnableNonClientDpiScaling_*> enable_non_client_dpi_scaling_;
std::optional<SetWindowCompositionAttribute_*>
set_window_composition_attribute_;
std::optional<AdjustWindowRectExForDpi_*> adjust_window_rect_ext_for_dpi_;
FML_DISALLOW_COPY_AND_ASSIGN(WindowsProcTable);
};