Implement Regular Windows for the win32 framework + add an example application for regular windows (#173715)
## What's new?
- Implement and test `_window_win32.dart`, the Win32 implementation of
`_window.dart` :tada:
- Wrote a bunch of tests in `window_win32_test.dart` which uses a mock
API barrier to mock the native Win32 layer
- Update `BaseWindowController` to extend `ChangeNotifier` and use it to
inform the `WindowScope` of when things change
- Made `RegularWindowController.empty()` a non-internal constructor, as
it is needed by implementations
- Added a _large_ example application for windowing where users can test
out different windowing concepts and see how they work!
To test, simply run the tests or try out the example app!
## 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.
diff --git a/.ci.yaml b/.ci.yaml
index b1ec9d5..7cb993f 100644
--- a/.ci.yaml
+++ b/.ci.yaml
@@ -6987,6 +6987,20 @@
["devicelab", "hostonly", "windows", "arm64"]
task_name: windows_startup_test
+ - name: Windows windowing_test
+ recipe: devicelab/devicelab_drone
+ presubmit: true
+ bringup: true
+ timeout: 60
+ properties:
+ dependencies: >-
+ [
+ {"dependency": "vs_build", "version": "version:vs2019"}
+ ]
+ tags: >
+ ["devicelab", "hostonly", "windows"]
+ task_name: windowing_test_windows
+
- name: Windows flutter_tool_startup__windows
recipe: devicelab/devicelab_drone
presubmit: false
diff --git a/TESTOWNERS b/TESTOWNERS
index c8dfc6a..77b58aa 100644
--- a/TESTOWNERS
+++ b/TESTOWNERS
@@ -304,6 +304,7 @@
/dev/devicelab/bin/tasks/windows_desktop_impeller.dart @jonahwilliams @flutter/engine
/dev/devicelab/bin/tasks/windows_home_scroll_perf__timeline_summary.dart @jonahwilliams @flutter/engine
/dev/devicelab/bin/tasks/windows_startup_test.dart @loic-sharma @flutter/desktop
+/dev/devicelab/bin/tasks/windowing_test_windows.dart @mattkae @flutter/desktop
/dev/devicelab/bin/tasks/mac_desktop_impeller.dart @jonahwilliams @flutter/engine
/dev/devicelab/bin/tasks/linux_desktop_impeller.dart @jonahwilliams @flutter/engine
/dev/devicelab/bin/tasks/linux_feature_flags_test.dart @loic-sharma @flutter/desktop
diff --git a/dev/devicelab/bin/tasks/windowing_test_windows.dart b/dev/devicelab/bin/tasks/windowing_test_windows.dart
new file mode 100644
index 0000000..d682f2f
--- /dev/null
+++ b/dev/devicelab/bin/tasks/windowing_test_windows.dart
@@ -0,0 +1,12 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_devicelab/framework/devices.dart';
+import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/tasks/integration_tests.dart';
+
+Future<void> main() async {
+ deviceOperatingSystem = DeviceOperatingSystem.windows;
+ await task(createWindowingDriverTest());
+}
diff --git a/dev/devicelab/lib/tasks/integration_tests.dart b/dev/devicelab/lib/tasks/integration_tests.dart
index bae536b..f95b931 100644
--- a/dev/devicelab/lib/tasks/integration_tests.dart
+++ b/dev/devicelab/lib/tasks/integration_tests.dart
@@ -221,6 +221,16 @@
).call;
}
+TaskFunction createWindowingDriverTest() {
+ return () async {
+ await flutter('config', options: const <String>['--enable-windowing']);
+ return DriverTest(
+ '${flutterDirectory.path}/dev/integration_tests/windowing_test',
+ 'lib/main.dart',
+ ).call();
+ };
+}
+
TaskFunction createWideGamutTest() {
return IntegrationTest(
'${flutterDirectory.path}/dev/integration_tests/wide_gamut_test',
diff --git a/dev/integration_tests/windowing_test/.gitignore b/dev/integration_tests/windowing_test/.gitignore
new file mode 100644
index 0000000..3820a95
--- /dev/null
+++ b/dev/integration_tests/windowing_test/.gitignore
@@ -0,0 +1,45 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+/coverage/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/dev/integration_tests/windowing_test/.metadata b/dev/integration_tests/windowing_test/.metadata
new file mode 100644
index 0000000..873e782
--- /dev/null
+++ b/dev/integration_tests/windowing_test/.metadata
@@ -0,0 +1,26 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "e27f0a6943e07b6d3420fb185432705b87101fa8"
+ channel: "[user-branch]"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: windows
+ create_revision: e27f0a6943e07b6d3420fb185432705b87101fa8
+ base_revision: e27f0a6943e07b6d3420fb185432705b87101fa8
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
diff --git a/dev/integration_tests/windowing_test/README.md b/dev/integration_tests/windowing_test/README.md
new file mode 100644
index 0000000..c2808e4
--- /dev/null
+++ b/dev/integration_tests/windowing_test/README.md
@@ -0,0 +1,10 @@
+# windowing_test
+
+Integration tests for windowing.
+
+## Run
+From this directory, run:
+
+```sh
+flutter drive -t .\lib\main.dart --driver .\test_driver\windowing_test.dart
+```
diff --git a/dev/integration_tests/windowing_test/analysis_options.yaml b/dev/integration_tests/windowing_test/analysis_options.yaml
new file mode 100644
index 0000000..f16eead
--- /dev/null
+++ b/dev/integration_tests/windowing_test/analysis_options.yaml
@@ -0,0 +1,4 @@
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ rules:
\ No newline at end of file
diff --git a/dev/integration_tests/windowing_test/lib/main.dart b/dev/integration_tests/windowing_test/lib/main.dart
new file mode 100644
index 0000000..5ef97c9
--- /dev/null
+++ b/dev/integration_tests/windowing_test/lib/main.dart
@@ -0,0 +1,162 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore_for_file: invalid_use_of_internal_member
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/src/widgets/_window.dart';
+import 'package:flutter_driver/driver_extension.dart';
+
+class _MainRegularWindowControllerDelegate
+ extends RegularWindowControllerDelegate {
+ @override
+ void onWindowDestroyed() {
+ super.onWindowDestroyed();
+
+ exit(0);
+ }
+}
+
+late final RegularWindowController controller;
+
+void main() {
+ final Completer<void> windowCreated = Completer();
+ enableFlutterDriverExtension(
+ handler: (String? message) async {
+ await windowCreated.future;
+ if (message == null) {
+ return '';
+ }
+
+ final jsonMap = jsonDecode(message);
+ if (!jsonMap.containsKey('type')) {
+ throw ArgumentError('Message must contain a "type" field.');
+ }
+
+ if (jsonMap['type'] == 'get_size') {
+ return jsonEncode({
+ 'width': controller.contentSize.width,
+ 'height': controller.contentSize.height,
+ });
+ } else if (jsonMap['type'] == 'set_size') {
+ final Size size = Size(
+ jsonMap['width'].toDouble(),
+ jsonMap['height'].toDouble(),
+ );
+ controller.setSize(size);
+ } else if (jsonMap['type'] == 'set_constraints') {
+ final BoxConstraints constraints = BoxConstraints(
+ minWidth: jsonMap['min_width'].toDouble(),
+ minHeight: jsonMap['min_height'].toDouble(),
+ maxWidth: jsonMap['max_width'].toDouble(),
+ maxHeight: jsonMap['max_height'].toDouble(),
+ );
+ controller.setConstraints(constraints);
+ } else if (jsonMap['type'] == 'set_fullscreen') {
+ controller.setFullscreen(true);
+ } else if (jsonMap['type'] == 'unset_fullscreen') {
+ controller.setFullscreen(false);
+ } else if (jsonMap['type'] == 'get_fullscreen') {
+ return jsonEncode({'isFullscreen': controller.isFullscreen});
+ } else if (jsonMap['type'] == 'set_maximized') {
+ controller.setMaximized(true);
+ } else if (jsonMap['type'] == 'unset_maximized') {
+ controller.setMaximized(false);
+ } else if (jsonMap['type'] == 'get_maximized') {
+ return jsonEncode({'isMaximized': controller.isMaximized});
+ } else if (jsonMap['type'] == 'set_minimized') {
+ controller.setMinimized(true);
+ } else if (jsonMap['type'] == 'unset_minimized') {
+ controller.setMinimized(false);
+ } else if (jsonMap['type'] == 'get_minimized') {
+ return jsonEncode({'isMinimized': controller.isMinimized});
+ } else if (jsonMap['type'] == 'set_title') {
+ controller.setTitle(jsonMap['title']);
+ } else if (jsonMap['type'] == 'get_title') {
+ return jsonEncode({'title': controller.title});
+ } else if (jsonMap['type'] == 'set_activated') {
+ controller.activate();
+ } else if (jsonMap['type'] == 'get_activated') {
+ return jsonEncode({'isActivated': controller.isActivated});
+ } else {
+ throw ArgumentError('Unknown message type: ${jsonMap['type']}');
+ }
+
+ return '';
+ },
+ );
+ controller = RegularWindowController(
+ preferredSize: Size(640, 480),
+ title: 'Integration Test',
+ delegate: _MainRegularWindowControllerDelegate(),
+ );
+ windowCreated.complete();
+
+ runWidget(RegularWindow(controller: controller, child: MyApp()));
+}
+
+class MyApp extends StatelessWidget {
+ const MyApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'Flutter Demo',
+ theme: ThemeData(
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
+ ),
+ home: const MyHomePage(title: 'Flutter Demo Home Page'),
+ );
+ }
+}
+
+class MyHomePage extends StatefulWidget {
+ const MyHomePage({super.key, required this.title});
+
+ final String title;
+
+ @override
+ State<MyHomePage> createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State<MyHomePage> {
+ int _counter = 0;
+
+ void _incrementCounter() {
+ setState(() {
+ _counter++;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ backgroundColor: Theme.of(context).colorScheme.inversePrimary,
+ title: Text(widget.title),
+ ),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ const Text('You have pushed the button this many times:'),
+ Text(
+ '$_counter',
+ style: Theme.of(context).textTheme.headlineMedium,
+ ),
+ ],
+ ),
+ ),
+ floatingActionButton: FloatingActionButton(
+ onPressed: _incrementCounter,
+ tooltip: 'Increment',
+ child: const Icon(Icons.add),
+ ),
+ );
+ }
+}
diff --git a/dev/integration_tests/windowing_test/pubspec.yaml b/dev/integration_tests/windowing_test/pubspec.yaml
new file mode 100644
index 0000000..b05793b
--- /dev/null
+++ b/dev/integration_tests/windowing_test/pubspec.yaml
@@ -0,0 +1,22 @@
+name: windowing_test
+description: Flutter windowing integration tests.
+
+environment:
+ sdk: ^3.8.0-0
+
+dependencies:
+ flutter:
+ sdk: flutter
+ flutter_driver:
+ sdk: flutter
+ integration_test:
+ sdk: flutter
+ test: any
+
+ path: any
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+# PUBSPEC CHECKSUM: 77jv2r
diff --git a/dev/integration_tests/windowing_test/test_driver/main_test.dart b/dev/integration_tests/windowing_test/test_driver/main_test.dart
new file mode 100644
index 0000000..872f360
--- /dev/null
+++ b/dev/integration_tests/windowing_test/test_driver/main_test.dart
@@ -0,0 +1,129 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:flutter_driver/flutter_driver.dart';
+import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
+
+void main() {
+ group('end-to-end test', () {
+ late final FlutterDriver driver;
+
+ setUpAll(() async {
+ driver = await FlutterDriver.connect();
+ });
+
+ tearDownAll(() async {
+ await driver.close();
+ });
+
+ test('Can set and get title', () async {
+ await driver.requestData(
+ jsonEncode({'type': 'set_title', 'title': 'Hello World'}),
+ );
+ final response = await driver.requestData(
+ jsonEncode({'type': 'get_title'}),
+ );
+ final data = jsonDecode(response);
+ expect(data['title'], 'Hello World');
+ }, timeout: Timeout.none);
+
+ test('Initial controller size is correct', () async {
+ final response = await driver.requestData(
+ jsonEncode({'type': 'get_size'}),
+ );
+ final data = jsonDecode(response);
+ expect(data["width"], 640);
+ expect(data["height"], 480);
+ }, timeout: Timeout.none);
+
+ test('Can set and get size', () async {
+ await driver.requestData(
+ jsonEncode({'type': 'set_size', 'width': 800, 'height': 600}),
+ );
+ final response = await driver.requestData(
+ jsonEncode({'type': 'get_size'}),
+ );
+ final data = jsonDecode(response);
+ expect(data["width"], 800);
+ expect(data["height"], 600);
+ }, timeout: Timeout.none);
+
+ test('Can set constraints and see the resize', () async {
+ await driver.requestData(
+ jsonEncode({
+ 'type': 'set_constraints',
+ 'min_width': 0,
+ 'min_height': 0,
+ 'max_width': 500,
+ 'max_height': 501,
+ }),
+ );
+ final response = await driver.requestData(
+ jsonEncode({'type': 'get_size'}),
+ );
+ final data = jsonDecode(response);
+ expect(data["width"], 500);
+ expect(data["height"], 501);
+ }, timeout: Timeout.none);
+
+ test('Can set and get fullscreen', () async {
+ await driver.requestData(jsonEncode({'type': 'set_fullscreen'}));
+ var response = await driver.requestData(
+ jsonEncode({'type': 'get_fullscreen'}),
+ );
+ var data = jsonDecode(response);
+ expect(data["isFullscreen"], true);
+
+ await driver.requestData(jsonEncode({'type': 'unset_fullscreen'}));
+ response = await driver.requestData(
+ jsonEncode({'type': 'get_fullscreen'}),
+ );
+ data = jsonDecode(response);
+ expect(data["isFullscreen"], false);
+ }, timeout: Timeout.none);
+
+ test('Can set and get maximized', () async {
+ await driver.requestData(jsonEncode({'type': 'set_maximized'}));
+ var response = await driver.requestData(
+ jsonEncode({'type': 'get_maximized'}),
+ );
+ var data = jsonDecode(response);
+ expect(data["isMaximized"], true);
+
+ await driver.requestData(jsonEncode({'type': 'unset_maximized'}));
+ response = await driver.requestData(
+ jsonEncode({'type': 'get_maximized'}),
+ );
+ data = jsonDecode(response);
+ expect(data["isMaximized"], false);
+ }, timeout: Timeout.none);
+
+ test('Can set and get minimized', () async {
+ await driver.requestData(jsonEncode({'type': 'set_minimized'}));
+ var response = await driver.requestData(
+ jsonEncode({'type': 'get_minimized'}),
+ );
+ var data = jsonDecode(response);
+ expect(data["isMinimized"], true);
+
+ await driver.requestData(jsonEncode({'type': 'unset_minimized'}));
+ response = await driver.requestData(
+ jsonEncode({'type': 'get_minimized'}),
+ );
+ data = jsonDecode(response);
+ expect(data["isMinimized"], false);
+ }, timeout: Timeout.none);
+
+ test('Can set and get activated', () async {
+ await driver.requestData(jsonEncode({'type': 'set_activated'}));
+ final response = await driver.requestData(
+ jsonEncode({'type': 'get_activated'}),
+ );
+ final data = jsonDecode(response);
+ expect(data["isActivated"], true);
+ }, timeout: Timeout.none);
+ });
+}
diff --git a/dev/integration_tests/windowing_test/windows/.gitignore b/dev/integration_tests/windowing_test/windows/.gitignore
new file mode 100644
index 0000000..d492d0d
--- /dev/null
+++ b/dev/integration_tests/windowing_test/windows/.gitignore
@@ -0,0 +1,17 @@
+flutter/ephemeral/
+
+# Visual Studio user-specific files.
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# Visual Studio build-related files.
+x64/
+x86/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
diff --git a/dev/integration_tests/windowing_test/windows/CMakeLists.txt b/dev/integration_tests/windowing_test/windows/CMakeLists.txt
new file mode 100644
index 0000000..4e85c1d
--- /dev/null
+++ b/dev/integration_tests/windowing_test/windows/CMakeLists.txt
@@ -0,0 +1,108 @@
+# Project-level configuration.
+cmake_minimum_required(VERSION 3.14)
+project(windowing_test LANGUAGES CXX)
+
+# The name of the executable created for the application. Change this to change
+# the on-disk name of your application.
+set(BINARY_NAME "windowing_test")
+
+# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
+# versions of CMake.
+cmake_policy(VERSION 3.14...3.25)
+
+# Define build configuration option.
+get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
+if(IS_MULTICONFIG)
+ set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
+ CACHE STRING "" FORCE)
+else()
+ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+ set(CMAKE_BUILD_TYPE "Debug" CACHE
+ STRING "Flutter build mode" FORCE)
+ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+ "Debug" "Profile" "Release")
+ endif()
+endif()
+# Define settings for the Profile build mode.
+set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
+set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
+set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
+set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
+
+# Use Unicode for all projects.
+add_definitions(-DUNICODE -D_UNICODE)
+
+# Compilation settings that should be applied to most targets.
+#
+# Be cautious about adding new options here, as plugins use this function by
+# default. In most cases, you should add new options to specific targets instead
+# of modifying this function.
+function(APPLY_STANDARD_SETTINGS TARGET)
+ target_compile_features(${TARGET} PUBLIC cxx_std_17)
+ target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
+ target_compile_options(${TARGET} PRIVATE /EHsc)
+ target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
+ target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
+endfunction()
+
+# Flutter library and tool build rules.
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# Application build; see runner/CMakeLists.txt.
+add_subdirectory("runner")
+
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# Support files are copied into place next to the executable, so that it can
+# run in place. This is done instead of making a separate bundle (as on Linux)
+# so that building and running from within Visual Studio will work.
+set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
+# Make the "install" step default, as it's required to run.
+set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+ set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+if(PLUGIN_BUNDLED_LIBRARIES)
+ install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endif()
+
+# Copy the native assets provided by the build.dart from all packages.
+set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
+install(DIRECTORY "${NATIVE_ASSETS_DIR}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+ file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+ " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+ DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ CONFIGURATIONS Profile;Release
+ COMPONENT Runtime)
diff --git a/dev/integration_tests/windowing_test/windows/flutter/CMakeLists.txt b/dev/integration_tests/windowing_test/windows/flutter/CMakeLists.txt
new file mode 100644
index 0000000..903f489
--- /dev/null
+++ b/dev/integration_tests/windowing_test/windows/flutter/CMakeLists.txt
@@ -0,0 +1,109 @@
+# This file controls Flutter-level build steps. It should not be edited.
+cmake_minimum_required(VERSION 3.14)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
+
+# Set fallback configurations for older versions of the flutter tool.
+if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
+ set(FLUTTER_TARGET_PLATFORM "windows-x64")
+endif()
+
+# === Flutter Library ===
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+ "flutter_export.h"
+ "flutter_windows.h"
+ "flutter_messenger.h"
+ "flutter_plugin_registrar.h"
+ "flutter_texture_registrar.h"
+)
+list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+ "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
+add_dependencies(flutter flutter_assemble)
+
+# === Wrapper ===
+list(APPEND CPP_WRAPPER_SOURCES_CORE
+ "core_implementations.cc"
+ "standard_codec.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
+ "plugin_registrar.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_APP
+ "flutter_engine.cc"
+ "flutter_view_controller.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
+
+# Wrapper sources needed for a plugin.
+add_library(flutter_wrapper_plugin STATIC
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_PLUGIN}
+)
+apply_standard_settings(flutter_wrapper_plugin)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+ POSITION_INDEPENDENT_CODE ON)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+ CXX_VISIBILITY_PRESET hidden)
+target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
+target_include_directories(flutter_wrapper_plugin PUBLIC
+ "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_plugin flutter_assemble)
+
+# Wrapper sources needed for the runner.
+add_library(flutter_wrapper_app STATIC
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_APP}
+)
+apply_standard_settings(flutter_wrapper_app)
+target_link_libraries(flutter_wrapper_app PUBLIC flutter)
+target_include_directories(flutter_wrapper_app PUBLIC
+ "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_app flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
+set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
+add_custom_command(
+ OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+ ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
+ ${CPP_WRAPPER_SOURCES_APP}
+ ${PHONY_OUTPUT}
+ COMMAND ${CMAKE_COMMAND} -E env
+ ${FLUTTER_TOOL_ENVIRONMENT}
+ "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
+ ${FLUTTER_TARGET_PLATFORM} $<CONFIG>
+ VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+ "${FLUTTER_LIBRARY}"
+ ${FLUTTER_LIBRARY_HEADERS}
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_PLUGIN}
+ ${CPP_WRAPPER_SOURCES_APP}
+)
diff --git a/dev/integration_tests/windowing_test/windows/runner/CMakeLists.txt b/dev/integration_tests/windowing_test/windows/runner/CMakeLists.txt
new file mode 100644
index 0000000..394917c
--- /dev/null
+++ b/dev/integration_tests/windowing_test/windows/runner/CMakeLists.txt
@@ -0,0 +1,40 @@
+cmake_minimum_required(VERSION 3.14)
+project(runner LANGUAGES CXX)
+
+# Define the application target. To change its name, change BINARY_NAME in the
+# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
+# work.
+#
+# Any new source files that you add to the application should be added here.
+add_executable(${BINARY_NAME} WIN32
+ "flutter_window.cpp"
+ "main.cpp"
+ "utils.cpp"
+ "win32_window.cpp"
+ "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+ "Runner.rc"
+ "runner.exe.manifest"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Add preprocessor definitions for the build version.
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
+
+# Disable Windows macros that collide with C++ standard library functions.
+target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
+
+# Add dependency libraries and include directories. Add any application-specific
+# dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
+target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
+target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)
diff --git a/dev/integration_tests/windowing_test/windows/runner/Runner.rc b/dev/integration_tests/windowing_test/windows/runner/Runner.rc
new file mode 100644
index 0000000..88669f8
--- /dev/null
+++ b/dev/integration_tests/windowing_test/windows/runner/Runner.rc
@@ -0,0 +1,121 @@
+// Microsoft Visual C++ generated resource script.
+//
+#pragma code_page(65001)
+#include "resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winres.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Icon
+//
+
+// Icon with lowest ID value placed first to ensure application icon
+// remains consistent on all systems.
+IDI_APP_ICON ICON "resources\\app_icon.ico"
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
+#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
+#else
+#define VERSION_AS_NUMBER 1,0,0,0
+#endif
+
+#if defined(FLUTTER_VERSION)
+#define VERSION_AS_STRING FLUTTER_VERSION
+#else
+#define VERSION_AS_STRING "1.0.0"
+#endif
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION VERSION_AS_NUMBER
+ PRODUCTVERSION VERSION_AS_NUMBER
+ FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+ FILEFLAGS VS_FF_DEBUG
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_APP
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904e4"
+ BEGIN
+ VALUE "CompanyName", "com.example" "\0"
+ VALUE "FileDescription", "windowing_test" "\0"
+ VALUE "FileVersion", VERSION_AS_STRING "\0"
+ VALUE "InternalName", "windowing_test" "\0"
+ VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0"
+ VALUE "OriginalFilename", "windowing_test.exe" "\0"
+ VALUE "ProductName", "windowing_test" "\0"
+ VALUE "ProductVersion", VERSION_AS_STRING "\0"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1252
+ END
+END
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
diff --git a/dev/integration_tests/windowing_test/windows/runner/flutter_window.cpp b/dev/integration_tests/windowing_test/windows/runner/flutter_window.cpp
new file mode 100644
index 0000000..252aa26
--- /dev/null
+++ b/dev/integration_tests/windowing_test/windows/runner/flutter_window.cpp
@@ -0,0 +1,75 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "flutter_window.h"
+
+#include <optional>
+
+#include "flutter/generated_plugin_registrant.h"
+
+FlutterWindow::FlutterWindow(const flutter::DartProject& project)
+ : project_(project) {}
+
+FlutterWindow::~FlutterWindow() {}
+
+bool FlutterWindow::OnCreate() {
+ if (!Win32Window::OnCreate()) {
+ return false;
+ }
+
+ RECT frame = GetClientArea();
+
+ // The size here must match the window dimensions to avoid unnecessary surface
+ // creation / destruction in the startup path.
+ flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
+ frame.right - frame.left, frame.bottom - frame.top, project_);
+ // Ensure that basic setup of the controller was successful.
+ if (!flutter_controller_->engine() || !flutter_controller_->view()) {
+ return false;
+ }
+ RegisterPlugins(flutter_controller_->engine());
+ SetChildContent(flutter_controller_->view()->GetNativeWindow());
+
+ flutter_controller_->engine()->SetNextFrameCallback([&]() {
+ this->Show();
+ });
+
+ // Flutter can complete the first frame before the "show window" callback is
+ // registered. The following call ensures a frame is pending to ensure the
+ // window is shown. It is a no-op if the first frame hasn't completed yet.
+ flutter_controller_->ForceRedraw();
+
+ return true;
+}
+
+void FlutterWindow::OnDestroy() {
+ if (flutter_controller_) {
+ flutter_controller_ = nullptr;
+ }
+
+ Win32Window::OnDestroy();
+}
+
+LRESULT
+FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ // Give Flutter, including plugins, an opportunity to handle window messages.
+ if (flutter_controller_) {
+ std::optional<LRESULT> result =
+ flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
+ lparam);
+ if (result) {
+ return *result;
+ }
+ }
+
+ switch (message) {
+ case WM_FONTCHANGE:
+ flutter_controller_->engine()->ReloadSystemFonts();
+ break;
+ }
+
+ return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
+}
diff --git a/dev/integration_tests/windowing_test/windows/runner/flutter_window.h b/dev/integration_tests/windowing_test/windows/runner/flutter_window.h
new file mode 100644
index 0000000..bbc5836
--- /dev/null
+++ b/dev/integration_tests/windowing_test/windows/runner/flutter_window.h
@@ -0,0 +1,37 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef RUNNER_FLUTTER_WINDOW_H_
+#define RUNNER_FLUTTER_WINDOW_H_
+
+#include <flutter/dart_project.h>
+#include <flutter/flutter_view_controller.h>
+
+#include <memory>
+
+#include "win32_window.h"
+
+// A window that does nothing but host a Flutter view.
+class FlutterWindow : public Win32Window {
+ public:
+ // Creates a new FlutterWindow hosting a Flutter view running |project|.
+ explicit FlutterWindow(const flutter::DartProject& project);
+ virtual ~FlutterWindow();
+
+ protected:
+ // Win32Window:
+ bool OnCreate() override;
+ void OnDestroy() override;
+ LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
+ LPARAM const lparam) noexcept override;
+
+ private:
+ // The project to run.
+ flutter::DartProject project_;
+
+ // The Flutter instance hosted by this window.
+ std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
+};
+
+#endif // RUNNER_FLUTTER_WINDOW_H_
diff --git a/dev/integration_tests/windowing_test/windows/runner/main.cpp b/dev/integration_tests/windowing_test/windows/runner/main.cpp
new file mode 100644
index 0000000..5621ff1
--- /dev/null
+++ b/dev/integration_tests/windowing_test/windows/runner/main.cpp
@@ -0,0 +1,47 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <flutter/dart_project.h>
+#include <flutter/flutter_view_controller.h>
+#include <windows.h>
+
+#include "flutter_window.h"
+#include "utils.h"
+
+int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
+ _In_ wchar_t *command_line, _In_ int show_command) {
+ // Attach to console when present (e.g., 'flutter run') or create a
+ // new console when running with a debugger.
+ if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
+ CreateAndAttachConsole();
+ }
+
+ // Initialize COM, so that it is available for use in the library and/or
+ // plugins.
+ ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
+
+ flutter::DartProject project(L"data");
+
+ std::vector<std::string> command_line_arguments =
+ GetCommandLineArguments();
+
+ project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
+
+ FlutterWindow window(project);
+ Win32Window::Point origin(10, 10);
+ Win32Window::Size size(1280, 720);
+ if (!window.Create(L"windowing_test", origin, size)) {
+ return EXIT_FAILURE;
+ }
+ window.SetQuitOnClose(true);
+
+ ::MSG msg;
+ while (::GetMessage(&msg, nullptr, 0, 0)) {
+ ::TranslateMessage(&msg);
+ ::DispatchMessage(&msg);
+ }
+
+ ::CoUninitialize();
+ return EXIT_SUCCESS;
+}
diff --git a/dev/integration_tests/windowing_test/windows/runner/resource.h b/dev/integration_tests/windowing_test/windows/runner/resource.h
new file mode 100644
index 0000000..c245ff1
--- /dev/null
+++ b/dev/integration_tests/windowing_test/windows/runner/resource.h
@@ -0,0 +1,20 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by Runner.rc
+//
+#define IDI_APP_ICON 101
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 102
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1001
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/dev/integration_tests/windowing_test/windows/runner/runner.exe.manifest b/dev/integration_tests/windowing_test/windows/runner/runner.exe.manifest
new file mode 100644
index 0000000..153653e
--- /dev/null
+++ b/dev/integration_tests/windowing_test/windows/runner/runner.exe.manifest
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+ <application xmlns="urn:schemas-microsoft-com:asm.v3">
+ <windowsSettings>
+ <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
+ </windowsSettings>
+ </application>
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <!-- Windows 10 and Windows 11 -->
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
+ </application>
+ </compatibility>
+</assembly>
diff --git a/dev/integration_tests/windowing_test/windows/runner/utils.cpp b/dev/integration_tests/windowing_test/windows/runner/utils.cpp
new file mode 100644
index 0000000..02f4362
--- /dev/null
+++ b/dev/integration_tests/windowing_test/windows/runner/utils.cpp
@@ -0,0 +1,69 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "utils.h"
+
+#include <flutter_windows.h>
+#include <io.h>
+#include <stdio.h>
+#include <windows.h>
+
+#include <iostream>
+
+void CreateAndAttachConsole() {
+ if (::AllocConsole()) {
+ FILE *unused;
+ if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
+ _dup2(_fileno(stdout), 1);
+ }
+ if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
+ _dup2(_fileno(stdout), 2);
+ }
+ std::ios::sync_with_stdio();
+ FlutterDesktopResyncOutputStreams();
+ }
+}
+
+std::vector<std::string> GetCommandLineArguments() {
+ // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
+ int argc;
+ wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
+ if (argv == nullptr) {
+ return std::vector<std::string>();
+ }
+
+ std::vector<std::string> command_line_arguments;
+
+ // Skip the first argument as it's the binary name.
+ for (int i = 1; i < argc; i++) {
+ command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
+ }
+
+ ::LocalFree(argv);
+
+ return command_line_arguments;
+}
+
+std::string Utf8FromUtf16(const wchar_t* utf16_string) {
+ if (utf16_string == nullptr) {
+ return std::string();
+ }
+ unsigned int target_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ -1, nullptr, 0, nullptr, nullptr)
+ -1; // remove the trailing null character
+ int input_length = (int)wcslen(utf16_string);
+ std::string utf8_string;
+ if (target_length == 0 || target_length > utf8_string.max_size()) {
+ return utf8_string;
+ }
+ utf8_string.resize(target_length);
+ int converted_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ input_length, utf8_string.data(), target_length, nullptr, nullptr);
+ if (converted_length == 0) {
+ return std::string();
+ }
+ return utf8_string;
+}
diff --git a/dev/integration_tests/windowing_test/windows/runner/utils.h b/dev/integration_tests/windowing_test/windows/runner/utils.h
new file mode 100644
index 0000000..54414c9
--- /dev/null
+++ b/dev/integration_tests/windowing_test/windows/runner/utils.h
@@ -0,0 +1,23 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef RUNNER_UTILS_H_
+#define RUNNER_UTILS_H_
+
+#include <string>
+#include <vector>
+
+// Creates a console for the process, and redirects stdout and stderr to
+// it for both the runner and the Flutter library.
+void CreateAndAttachConsole();
+
+// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
+// encoded in UTF-8. Returns an empty std::string on failure.
+std::string Utf8FromUtf16(const wchar_t* utf16_string);
+
+// Gets the command line arguments passed in as a std::vector<std::string>,
+// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
+std::vector<std::string> GetCommandLineArguments();
+
+#endif // RUNNER_UTILS_H_
diff --git a/dev/integration_tests/windowing_test/windows/runner/win32_window.cpp b/dev/integration_tests/windowing_test/windows/runner/win32_window.cpp
new file mode 100644
index 0000000..14a183d
--- /dev/null
+++ b/dev/integration_tests/windowing_test/windows/runner/win32_window.cpp
@@ -0,0 +1,292 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "win32_window.h"
+
+#include <dwmapi.h>
+#include <flutter_windows.h>
+
+#include "resource.h"
+
+namespace {
+
+/// 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
+
+constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
+
+/// Registry key for app theme preference.
+///
+/// A value of 0 indicates apps should use dark mode. A non-zero or missing
+/// value indicates apps should use light mode.
+constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
+ L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
+constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
+
+// The number of Win32Window objects that currently exist.
+static int g_active_window_count = 0;
+
+using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
+
+// Scale helper to convert logical scaler values to physical using passed in
+// scale factor
+int Scale(int source, double scale_factor) {
+ return static_cast<int>(source * scale_factor);
+}
+
+// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
+// This API is only needed for PerMonitor V1 awareness mode.
+void EnableFullDpiSupportIfAvailable(HWND hwnd) {
+ HMODULE user32_module = LoadLibraryA("User32.dll");
+ if (!user32_module) {
+ return;
+ }
+ auto enable_non_client_dpi_scaling =
+ reinterpret_cast<EnableNonClientDpiScaling*>(
+ GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
+ if (enable_non_client_dpi_scaling != nullptr) {
+ enable_non_client_dpi_scaling(hwnd);
+ }
+ FreeLibrary(user32_module);
+}
+
+} // namespace
+
+// Manages the Win32Window's window class registration.
+class WindowClassRegistrar {
+ public:
+ ~WindowClassRegistrar() = default;
+
+ // Returns the singleton registrar instance.
+ static WindowClassRegistrar* GetInstance() {
+ if (!instance_) {
+ instance_ = new WindowClassRegistrar();
+ }
+ return instance_;
+ }
+
+ // Returns the name of the window class, registering the class if it hasn't
+ // previously been registered.
+ const wchar_t* GetWindowClass();
+
+ // Unregisters the window class. Should only be called if there are no
+ // instances of the window.
+ void UnregisterWindowClass();
+
+ private:
+ WindowClassRegistrar() = default;
+
+ static WindowClassRegistrar* instance_;
+
+ bool class_registered_ = false;
+};
+
+WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
+
+const wchar_t* WindowClassRegistrar::GetWindowClass() {
+ if (!class_registered_) {
+ WNDCLASS window_class{};
+ window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
+ window_class.lpszClassName = kWindowClassName;
+ window_class.style = CS_HREDRAW | CS_VREDRAW;
+ window_class.cbClsExtra = 0;
+ window_class.cbWndExtra = 0;
+ window_class.hInstance = GetModuleHandle(nullptr);
+ window_class.hIcon =
+ LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
+ window_class.hbrBackground = 0;
+ window_class.lpszMenuName = nullptr;
+ window_class.lpfnWndProc = Win32Window::WndProc;
+ RegisterClass(&window_class);
+ class_registered_ = true;
+ }
+ return kWindowClassName;
+}
+
+void WindowClassRegistrar::UnregisterWindowClass() {
+ UnregisterClass(kWindowClassName, nullptr);
+ class_registered_ = false;
+}
+
+Win32Window::Win32Window() {
+ ++g_active_window_count;
+}
+
+Win32Window::~Win32Window() {
+ --g_active_window_count;
+ Destroy();
+}
+
+bool Win32Window::Create(const std::wstring& title,
+ const Point& origin,
+ const Size& size) {
+ Destroy();
+
+ const wchar_t* window_class =
+ WindowClassRegistrar::GetInstance()->GetWindowClass();
+
+ const POINT target_point = {static_cast<LONG>(origin.x),
+ static_cast<LONG>(origin.y)};
+ HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
+ UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
+ double scale_factor = dpi / 96.0;
+
+ HWND window = CreateWindow(
+ window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
+ Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
+ Scale(size.width, scale_factor), Scale(size.height, scale_factor),
+ nullptr, nullptr, GetModuleHandle(nullptr), this);
+
+ if (!window) {
+ return false;
+ }
+
+ UpdateTheme(window);
+
+ return OnCreate();
+}
+
+bool Win32Window::Show() {
+ return ShowWindow(window_handle_, SW_SHOWNORMAL);
+}
+
+// static
+LRESULT CALLBACK Win32Window::WndProc(HWND const window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ if (message == WM_NCCREATE) {
+ auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
+ SetWindowLongPtr(window, GWLP_USERDATA,
+ reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
+
+ auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
+ EnableFullDpiSupportIfAvailable(window);
+ that->window_handle_ = window;
+ } else if (Win32Window* that = GetThisFromHandle(window)) {
+ return that->MessageHandler(window, message, wparam, lparam);
+ }
+
+ return DefWindowProc(window, message, wparam, lparam);
+}
+
+LRESULT
+Win32Window::MessageHandler(HWND hwnd,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ switch (message) {
+ case WM_DESTROY:
+ window_handle_ = nullptr;
+ Destroy();
+ if (quit_on_close_) {
+ PostQuitMessage(0);
+ }
+ return 0;
+
+ case WM_DPICHANGED: {
+ auto newRectSize = reinterpret_cast<RECT*>(lparam);
+ LONG newWidth = newRectSize->right - newRectSize->left;
+ LONG newHeight = newRectSize->bottom - newRectSize->top;
+
+ SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
+ newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
+
+ return 0;
+ }
+ case WM_SIZE: {
+ RECT rect = GetClientArea();
+ if (child_content_ != nullptr) {
+ // Size and position the child window.
+ MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
+ rect.bottom - rect.top, TRUE);
+ }
+ return 0;
+ }
+
+ case WM_ACTIVATE:
+ if (child_content_ != nullptr) {
+ SetFocus(child_content_);
+ }
+ return 0;
+
+ case WM_DWMCOLORIZATIONCOLORCHANGED:
+ UpdateTheme(hwnd);
+ return 0;
+ }
+
+ return DefWindowProc(window_handle_, message, wparam, lparam);
+}
+
+void Win32Window::Destroy() {
+ OnDestroy();
+
+ if (window_handle_) {
+ DestroyWindow(window_handle_);
+ window_handle_ = nullptr;
+ }
+ if (g_active_window_count == 0) {
+ WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
+ }
+}
+
+Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
+ return reinterpret_cast<Win32Window*>(
+ GetWindowLongPtr(window, GWLP_USERDATA));
+}
+
+void Win32Window::SetChildContent(HWND content) {
+ child_content_ = content;
+ SetParent(content, window_handle_);
+ RECT frame = GetClientArea();
+
+ MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
+ frame.bottom - frame.top, true);
+
+ SetFocus(child_content_);
+}
+
+RECT Win32Window::GetClientArea() {
+ RECT frame;
+ GetClientRect(window_handle_, &frame);
+ return frame;
+}
+
+HWND Win32Window::GetHandle() {
+ return window_handle_;
+}
+
+void Win32Window::SetQuitOnClose(bool quit_on_close) {
+ quit_on_close_ = quit_on_close;
+}
+
+bool Win32Window::OnCreate() {
+ // No-op; provided for subclasses.
+ return true;
+}
+
+void Win32Window::OnDestroy() {
+ // No-op; provided for subclasses.
+}
+
+void Win32Window::UpdateTheme(HWND const window) {
+ DWORD light_mode;
+ DWORD light_mode_size = sizeof(light_mode);
+ LSTATUS 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));
+ }
+}
diff --git a/dev/integration_tests/windowing_test/windows/runner/win32_window.h b/dev/integration_tests/windowing_test/windows/runner/win32_window.h
new file mode 100644
index 0000000..bb93e88
--- /dev/null
+++ b/dev/integration_tests/windowing_test/windows/runner/win32_window.h
@@ -0,0 +1,106 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef RUNNER_WIN32_WINDOW_H_
+#define RUNNER_WIN32_WINDOW_H_
+
+#include <windows.h>
+
+#include <functional>
+#include <memory>
+#include <string>
+
+// A class abstraction for a high DPI-aware Win32 Window. Intended to be
+// inherited from by classes that wish to specialize with custom
+// rendering and input handling
+class Win32Window {
+ public:
+ struct Point {
+ unsigned int x;
+ unsigned int y;
+ Point(unsigned int x, unsigned int y) : x(x), y(y) {}
+ };
+
+ struct Size {
+ unsigned int width;
+ unsigned int height;
+ Size(unsigned int width, unsigned int height)
+ : width(width), height(height) {}
+ };
+
+ Win32Window();
+ virtual ~Win32Window();
+
+ // Creates a win32 window with |title| that is positioned and sized using
+ // |origin| and |size|. New windows are created on the default monitor. Window
+ // sizes are specified to the OS in physical pixels, hence to ensure a
+ // consistent size this function will scale the inputted width and height as
+ // as appropriate for the default monitor. The window is invisible until
+ // |Show| is called. Returns true if the window was created successfully.
+ bool Create(const std::wstring& title, const Point& origin, const Size& size);
+
+ // Show the current window. Returns true if the window was successfully shown.
+ bool Show();
+
+ // Release OS resources associated with window.
+ void Destroy();
+
+ // Inserts |content| into the window tree.
+ void SetChildContent(HWND content);
+
+ // Returns the backing Window handle to enable clients to set icon and other
+ // window properties. Returns nullptr if the window has been destroyed.
+ HWND GetHandle();
+
+ // If true, closing this window will quit the application.
+ void SetQuitOnClose(bool quit_on_close);
+
+ // Return a RECT representing the bounds of the current client area.
+ RECT GetClientArea();
+
+ protected:
+ // Processes and route salient window messages for mouse handling,
+ // size change and DPI. Delegates handling of these to member overloads that
+ // inheriting classes can handle.
+ virtual LRESULT MessageHandler(HWND window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept;
+
+ // Called when CreateAndShow is called, allowing subclass window-related
+ // setup. Subclasses should return false if setup fails.
+ virtual bool OnCreate();
+
+ // Called when Destroy is called.
+ virtual void OnDestroy();
+
+ private:
+ friend class WindowClassRegistrar;
+
+ // OS callback called by message pump. Handles the WM_NCCREATE message which
+ // is passed when the non-client area is being created and enables automatic
+ // non-client DPI scaling so that the non-client area automatically
+ // responds to changes in DPI. All other messages are handled by
+ // MessageHandler.
+ static LRESULT CALLBACK WndProc(HWND const window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept;
+
+ // Retrieves a class instance pointer for |window|
+ static Win32Window* GetThisFromHandle(HWND const window) noexcept;
+
+ // Update the window frame's theme to match the system theme.
+ static void UpdateTheme(HWND const window);
+
+ bool quit_on_close_ = false;
+
+ // window handle for top level window.
+ HWND window_handle_ = nullptr;
+
+ // window handle for hosted content.
+ HWND child_content_ = nullptr;
+};
+
+#endif // RUNNER_WIN32_WINDOW_H_
diff --git a/examples/multiple_windows/.gitignore b/examples/multiple_windows/.gitignore
new file mode 100644
index 0000000..79c113f
--- /dev/null
+++ b/examples/multiple_windows/.gitignore
@@ -0,0 +1,45 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/examples/multiple_windows/.metadata b/examples/multiple_windows/.metadata
new file mode 100644
index 0000000..c9660e9
--- /dev/null
+++ b/examples/multiple_windows/.metadata
@@ -0,0 +1,30 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "f07dbe9f9b40ecc5557632d6feb70a198dab5668"
+ channel: "[user-branch]"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: f07dbe9f9b40ecc5557632d6feb70a198dab5668
+ base_revision: f07dbe9f9b40ecc5557632d6feb70a198dab5668
+ - platform: macos
+ create_revision: f07dbe9f9b40ecc5557632d6feb70a198dab5668
+ base_revision: f07dbe9f9b40ecc5557632d6feb70a198dab5668
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/examples/multiple_windows/README.md b/examples/multiple_windows/README.md
new file mode 100644
index 0000000..23c3d31
--- /dev/null
+++ b/examples/multiple_windows/README.md
@@ -0,0 +1,11 @@
+# multiple_windows
+
+A reference application demonstrating multi-window support for Flutter using a
+rich semantics windowing API. At the moment, only the Windows platform is
+supported.
+
+## Running
+To run the example application, you must:
+
+1. Be on Flutter's main release channel
+2. Provide the `--enable-windowing` flag to flutter
diff --git a/examples/multiple_windows/analysis_options.yaml b/examples/multiple_windows/analysis_options.yaml
new file mode 100644
index 0000000..0d29021
--- /dev/null
+++ b/examples/multiple_windows/analysis_options.yaml
@@ -0,0 +1,28 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at https://dart.dev/lints.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/examples/multiple_windows/lib/app/main_window.dart b/examples/multiple_windows/lib/app/main_window.dart
new file mode 100644
index 0000000..c33e1d1
--- /dev/null
+++ b/examples/multiple_windows/lib/app/main_window.dart
@@ -0,0 +1,191 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore_for_file: invalid_use_of_internal_member
+// ignore_for_file: implementation_imports
+
+import 'package:flutter/material.dart';
+import 'package:flutter/src/widgets/_window.dart';
+
+import 'regular_window_content.dart';
+import 'window_settings_dialog.dart';
+import 'models.dart';
+import 'regular_window_edit_dialog.dart';
+
+class MainWindow extends StatelessWidget {
+ const MainWindow({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('Multi Window Reference App')),
+ body: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Expanded(
+ flex: 60,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Expanded(
+ child: SingleChildScrollView(
+ scrollDirection: Axis.vertical,
+ child: _WindowsTable(),
+ ),
+ ),
+ ],
+ ),
+ ),
+ Expanded(
+ flex: 40,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [Expanded(child: _WindowCreatorCard())],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _WindowsTable extends StatelessWidget {
+ List<DataRow> _buildRows(WindowManager windowManager, BuildContext context) {
+ List<DataRow> rows = [];
+ for (KeyedWindow controller in windowManager.windows) {
+ rows.add(
+ DataRow(
+ key: controller.key,
+ color: WidgetStateColor.resolveWith((states) {
+ if (states.contains(WidgetState.selected)) {
+ return Theme.of(context).colorScheme.primary.withAlpha(20);
+ }
+ return Colors.transparent;
+ }),
+ cells: [
+ DataCell(Text('${controller.controller.rootView.viewId}')),
+ DataCell(Text(_getWindowTypeName(controller.controller))),
+ DataCell(
+ Row(
+ children: [
+ IconButton(
+ icon: const Icon(Icons.edit_outlined),
+ onPressed: () => _showWindowEditDialog(controller, context),
+ ),
+ IconButton(
+ icon: const Icon(Icons.delete_outlined),
+ onPressed: () async {
+ controller.controller.destroy();
+ },
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ return rows;
+ }
+
+ void _showWindowEditDialog(KeyedWindow controller, BuildContext context) {
+ return switch (controller.controller) {
+ final RegularWindowController regular => showRegularWindowEditDialog(
+ context: context,
+ controller: regular,
+ ),
+ };
+ }
+
+ static String _getWindowTypeName(BaseWindowController controller) {
+ return switch (controller) {
+ RegularWindowController() => 'Regular',
+ };
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final WindowManager windowManager = WindowManagerAccessor.of(context);
+ return DataTable(
+ showBottomBorder: true,
+ columns: const [
+ DataColumn(
+ label: SizedBox(
+ width: 20,
+ child: Text('ID', style: TextStyle(fontSize: 16)),
+ ),
+ ),
+ DataColumn(
+ label: SizedBox(
+ width: 120,
+ child: Text('Type', style: TextStyle(fontSize: 16)),
+ ),
+ ),
+ DataColumn(label: SizedBox(width: 20, child: Text('')), numeric: true),
+ ],
+ rows: _buildRows(windowManager, context),
+ );
+ }
+}
+
+class _WindowCreatorCard extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ final WindowManager windowManager = WindowManagerAccessor.of(context);
+ final WindowSettings windowSettings = WindowSettingsAccessor.of(context);
+ return Card.outlined(
+ margin: const EdgeInsets.symmetric(horizontal: 25),
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(25, 0, 25, 5),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ const Padding(
+ padding: EdgeInsets.only(top: 10, bottom: 10),
+ child: Text(
+ 'New Window',
+ style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0),
+ ),
+ ),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ OutlinedButton(
+ onPressed: () async {
+ final UniqueKey key = UniqueKey();
+ windowManager.add(
+ KeyedWindow(
+ key: key,
+ controller: RegularWindowController(
+ delegate: CallbackRegularWindowControllerDelegate(
+ onDestroyed: () => windowManager.remove(key),
+ ),
+ title: 'Regular',
+ preferredSize: windowSettings.regularSize,
+ ),
+ ),
+ );
+ },
+ child: const Text('Regular'),
+ ),
+ const SizedBox(height: 8),
+ Container(
+ alignment: Alignment.bottomRight,
+ child: TextButton(
+ child: const Text('SETTINGS'),
+ onPressed: () {
+ showWindowSettingsDialog(context, windowSettings);
+ },
+ ),
+ ),
+ const SizedBox(width: 8),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/examples/multiple_windows/lib/app/models.dart b/examples/multiple_windows/lib/app/models.dart
new file mode 100644
index 0000000..2465c21
--- /dev/null
+++ b/examples/multiple_windows/lib/app/models.dart
@@ -0,0 +1,93 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore_for_file: invalid_use_of_internal_member
+// ignore_for_file: implementation_imports
+
+import 'package:flutter/widgets.dart';
+import 'package:flutter/src/widgets/_window.dart';
+
+class KeyedWindow {
+ KeyedWindow({
+ this.parent,
+ this.isMainWindow = false,
+ required this.key,
+ required this.controller,
+ });
+
+ final BaseWindowController? parent;
+ final bool isMainWindow;
+ final UniqueKey key;
+ final BaseWindowController controller;
+}
+
+/// Provides access to the windows created by the application.
+///
+/// The window manager manages a flat list of all of the [BaseWindowController]s
+/// that have been created by the application as well as which controller is
+/// currently selected by the UI.
+class WindowManager extends ChangeNotifier {
+ WindowManager({required List<KeyedWindow> initialWindows})
+ : _windows = initialWindows;
+
+ final List<KeyedWindow> _windows;
+ List<KeyedWindow> get windows => _windows;
+
+ void add(KeyedWindow window) {
+ _windows.add(window);
+ notifyListeners();
+ }
+
+ void remove(UniqueKey key) {
+ _windows.removeWhere((KeyedWindow window) => window.key == key);
+ notifyListeners();
+ }
+}
+
+/// Provides access to the [WindowManager] from the widget tree.
+class WindowManagerAccessor extends InheritedNotifier<WindowManager> {
+ const WindowManagerAccessor({
+ super.key,
+ required super.child,
+ required WindowManager windowManager,
+ }) : super(notifier: windowManager);
+
+ static WindowManager of(BuildContext context) {
+ final WindowManagerAccessor? result = context
+ .dependOnInheritedWidgetOfExactType<WindowManagerAccessor>();
+ assert(result != null, 'No WindowManager found in context');
+ return result!.notifier!;
+ }
+}
+
+/// Settings that control the behavior of newly created windows.
+class WindowSettings {
+ WindowSettings({this.regularSize = const Size(400, 300)});
+
+ /// The initial size for newly created regular windows.
+ Size regularSize;
+}
+
+/// Provides access to the [WindowSettings] from the widget tree.
+class WindowSettingsAccessor extends InheritedWidget {
+ const WindowSettingsAccessor({
+ super.key,
+ required super.child,
+ required this.windowSettings,
+ });
+
+ final WindowSettings windowSettings;
+
+ static WindowSettings of(BuildContext context) {
+ final WindowSettingsAccessor? result = context
+ .dependOnInheritedWidgetOfExactType<WindowSettingsAccessor>();
+ assert(result != null, 'No WindowSettings found in context');
+ return result!.windowSettings;
+ }
+
+ @override
+ bool updateShouldNotify(WindowSettingsAccessor oldWidget) {
+ return windowSettings != oldWidget.windowSettings;
+ }
+}
diff --git a/examples/multiple_windows/lib/app/regular_window_content.dart b/examples/multiple_windows/lib/app/regular_window_content.dart
new file mode 100644
index 0000000..150c1cc
--- /dev/null
+++ b/examples/multiple_windows/lib/app/regular_window_content.dart
@@ -0,0 +1,122 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore_for_file: invalid_use_of_internal_member
+// ignore_for_file: implementation_imports
+
+import 'package:flutter/material.dart';
+import 'window_content.dart';
+import 'models.dart';
+import 'rotated_wire_cube.dart';
+import 'dart:math';
+import 'package:flutter/src/widgets/_window.dart';
+
+class RegularWindowContent extends StatelessWidget {
+ RegularWindowContent({super.key, required this.window})
+ : cubeColor = _generateRandomDarkColor();
+
+ final RegularWindowController window;
+ final Color cubeColor;
+
+ static Color _generateRandomDarkColor() {
+ final random = Random();
+ const int lowerBound = 32;
+ const int span = 160;
+ int red = lowerBound + random.nextInt(span);
+ int green = lowerBound + random.nextInt(span);
+ int blue = lowerBound + random.nextInt(span);
+ return Color.fromARGB(255, red, green, blue);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final dpr = MediaQuery.of(context).devicePixelRatio;
+ final windowSize = WindowScope.contentSizeOf(context);
+ final WindowManager windowManager = WindowManagerAccessor.of(context);
+ final WindowSettings windowSettings = WindowSettingsAccessor.of(context);
+
+ final child = Scaffold(
+ appBar: AppBar(title: Text('Regular Window')),
+ body: Center(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [RotatedWireCube(cubeColor: cubeColor)],
+ ),
+ Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ ElevatedButton(
+ onPressed: () {
+ final UniqueKey key = UniqueKey();
+ windowManager.add(
+ KeyedWindow(
+ key: key,
+ controller: RegularWindowController(
+ preferredSize: windowSettings.regularSize,
+ delegate: CallbackRegularWindowControllerDelegate(
+ onDestroyed: () => windowManager.remove(key),
+ ),
+ title: 'Regular',
+ ),
+ ),
+ );
+ },
+ child: const Text('Create Regular Window'),
+ ),
+ const SizedBox(height: 20),
+ Text(
+ 'View #${window.rootView.viewId}\n'
+ 'Size: ${(windowSize.width).toStringAsFixed(1)}\u00D7${(windowSize.height).toStringAsFixed(1)}\n'
+ 'Device Pixel Ratio: $dpr',
+ textAlign: TextAlign.center,
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+
+ return ViewAnchor(
+ view: ListenableBuilder(
+ listenable: windowManager,
+ builder: (BuildContext context, Widget? child) {
+ final List<Widget> childViews = <Widget>[];
+ for (final KeyedWindow window in windowManager.windows) {
+ if (window.parent == window.controller) {
+ childViews.add(
+ WindowContent(
+ controller: window.controller,
+ windowKey: window.key,
+ onDestroyed: () => windowManager.remove(window.key),
+ onError: () => windowManager.remove(window.key),
+ ),
+ );
+ }
+ }
+
+ return ViewCollection(views: childViews);
+ },
+ ),
+ child: child,
+ );
+ }
+}
+
+class CallbackRegularWindowControllerDelegate
+ with RegularWindowControllerDelegate {
+ CallbackRegularWindowControllerDelegate({required this.onDestroyed});
+
+ @override
+ void onWindowDestroyed() {
+ onDestroyed();
+ super.onWindowDestroyed();
+ }
+
+ final VoidCallback onDestroyed;
+}
diff --git a/examples/multiple_windows/lib/app/regular_window_edit_dialog.dart b/examples/multiple_windows/lib/app/regular_window_edit_dialog.dart
new file mode 100644
index 0000000..7a5a16f
--- /dev/null
+++ b/examples/multiple_windows/lib/app/regular_window_edit_dialog.dart
@@ -0,0 +1,201 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore_for_file: invalid_use_of_internal_member
+// ignore_for_file: implementation_imports
+
+import 'package:flutter/material.dart';
+import 'package:flutter/src/widgets/_window.dart';
+
+void showRegularWindowEditDialog({
+ required BuildContext context,
+ required RegularWindowController controller,
+}) {
+ showDialog(
+ context: context,
+ builder: (context) => _RegularWindowEditDialog(
+ controller: controller,
+ onClose: () => Navigator.pop(context),
+ ),
+ );
+}
+
+class _RegularWindowEditDialog extends StatefulWidget {
+ const _RegularWindowEditDialog({
+ required this.controller,
+ required this.onClose,
+ });
+
+ final RegularWindowController controller;
+ final VoidCallback onClose;
+
+ @override
+ State<StatefulWidget> createState() => _RegularWindowEditDialogState();
+}
+
+class _RegularWindowEditDialogState extends State<_RegularWindowEditDialog> {
+ late Size initialSize;
+ late String initialTitle;
+ late bool initialFullscreen;
+ late bool initialMaximized;
+ late bool initialMinimized;
+
+ late final TextEditingController widthController;
+ late final TextEditingController heightController;
+ late final TextEditingController titleController;
+
+ bool? nextIsFullscreen;
+ bool? nextIsMaximized;
+ bool? nextIsMinized;
+
+ void _init() {
+ widget.controller.addListener(_onNotification);
+ initialSize = widget.controller.contentSize;
+ initialTitle = widget.controller.title;
+ initialFullscreen = widget.controller.isFullscreen;
+ initialMaximized = widget.controller.isMaximized;
+ initialMinimized = widget.controller.isMinimized;
+
+ widthController = TextEditingController(text: initialSize.width.toString());
+ heightController = TextEditingController(
+ text: initialSize.height.toString(),
+ );
+ titleController = TextEditingController(text: initialTitle);
+ nextIsFullscreen = null;
+ nextIsMaximized = null;
+ nextIsMinized = null;
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _init();
+ }
+
+ @override
+ void didUpdateWidget(covariant _RegularWindowEditDialog oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.controller != widget.controller) {
+ oldWidget.controller.removeListener(_onNotification);
+ _init();
+ }
+ }
+
+ void _onNotification() {
+ // We listen on the state of the controller. If a value that the user
+ // can edit changes from what it was initially set to, we invalidate
+ // their current change and store the new "initial" value.
+ if (widget.controller.contentSize != initialSize) {
+ initialSize = widget.controller.contentSize;
+ widthController.text = widget.controller.contentSize.width.toString();
+ heightController.text = widget.controller.contentSize.height.toString();
+ }
+ if (widget.controller.isFullscreen != initialFullscreen) {
+ setState(() {
+ initialFullscreen = widget.controller.isFullscreen;
+ nextIsFullscreen = null;
+ });
+ }
+ if (widget.controller.isMaximized != initialMaximized) {
+ setState(() {
+ initialMaximized = widget.controller.isMaximized;
+ nextIsMaximized = null;
+ });
+ }
+ if (widget.controller.isMinimized != initialMinimized) {
+ setState(() {
+ initialMinimized = widget.controller.isMinimized;
+ nextIsMinized = null;
+ });
+ }
+ }
+
+ @override
+ void dispose() {
+ widget.controller.removeListener(_onNotification);
+ super.dispose();
+ }
+
+ void _onSave() {
+ double? width = double.tryParse(widthController.text);
+ double? height = double.tryParse(heightController.text);
+ String? title = titleController.text.isEmpty ? null : titleController.text;
+ if (width != null &&
+ height != null &&
+ (width != initialSize.width || height != initialSize.height)) {
+ widget.controller.setSize(Size(width, height));
+ }
+ if (title != null && title != initialTitle) {
+ widget.controller.setTitle(title);
+ }
+ if (nextIsFullscreen != null && nextIsFullscreen != initialFullscreen) {
+ widget.controller.setFullscreen(nextIsFullscreen!);
+ }
+ if (nextIsMaximized != null && nextIsMaximized != initialMaximized) {
+ widget.controller.setMaximized(nextIsMaximized!);
+ }
+ if (nextIsMinized != null && nextIsMinized != initialMinimized) {
+ widget.controller.setMinimized(nextIsMinized!);
+ }
+
+ widget.onClose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ title: Text('Edit Window Properties'),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ TextField(
+ controller: widthController,
+ keyboardType: TextInputType.number,
+ decoration: InputDecoration(labelText: 'Width'),
+ ),
+ TextField(
+ controller: heightController,
+ keyboardType: TextInputType.number,
+ decoration: InputDecoration(labelText: 'Height'),
+ ),
+ TextField(
+ controller: titleController,
+ decoration: InputDecoration(labelText: 'Title'),
+ ),
+ CheckboxListTile(
+ title: const Text('Fullscreen'),
+ value: nextIsFullscreen ?? initialFullscreen,
+ onChanged: (bool? value) {
+ if (value != null) {
+ setState(() => nextIsFullscreen = value);
+ }
+ },
+ ),
+ CheckboxListTile(
+ title: const Text('Maximized'),
+ value: nextIsMaximized ?? initialMaximized,
+ onChanged: (bool? value) {
+ if (value != null) {
+ setState(() => nextIsMaximized = value);
+ }
+ },
+ ),
+ CheckboxListTile(
+ title: const Text('Minimized'),
+ value: nextIsMinized ?? initialMinimized,
+ onChanged: (bool? value) {
+ if (value != null) {
+ setState(() => nextIsMinized = value);
+ }
+ },
+ ),
+ ],
+ ),
+ actions: [
+ TextButton(onPressed: () => widget.onClose(), child: Text('Cancel')),
+ TextButton(onPressed: () => _onSave(), child: Text('Save')),
+ ],
+ );
+ }
+}
diff --git a/examples/multiple_windows/lib/app/rotated_wire_cube.dart b/examples/multiple_windows/lib/app/rotated_wire_cube.dart
new file mode 100644
index 0000000..f5e2212
--- /dev/null
+++ b/examples/multiple_windows/lib/app/rotated_wire_cube.dart
@@ -0,0 +1,108 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:vector_math/vector_math_64.dart';
+import 'dart:math';
+
+class RotatedWireCube extends StatefulWidget {
+ const RotatedWireCube({required this.cubeColor, super.key});
+
+ final Color cubeColor;
+
+ @override
+ State<StatefulWidget> createState() => _RotatedWireCubeState();
+}
+
+class _RotatedWireCubeState extends State<RotatedWireCube>
+ with SingleTickerProviderStateMixin {
+ late final AnimationController _animation;
+
+ @override
+ void initState() {
+ super.initState();
+ _animation = AnimationController(
+ vsync: this,
+ lowerBound: 0,
+ upperBound: 2 * pi,
+ duration: const Duration(seconds: 15),
+ )..repeat();
+ }
+
+ @override
+ void dispose() {
+ _animation.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return AnimatedBuilder(
+ animation: _animation,
+ builder: (context, child) {
+ return CustomPaint(
+ size: const Size(200, 200),
+ painter: _RotatedWireCubePainter(
+ angle: _animation.value,
+ color: widget.cubeColor,
+ ),
+ );
+ },
+ );
+ }
+}
+
+class _RotatedWireCubePainter extends CustomPainter {
+ static List<Vector3> vertices = [
+ Vector3(-0.5, -0.5, -0.5),
+ Vector3(0.5, -0.5, -0.5),
+ Vector3(0.5, 0.5, -0.5),
+ Vector3(-0.5, 0.5, -0.5),
+ Vector3(-0.5, -0.5, 0.5),
+ Vector3(0.5, -0.5, 0.5),
+ Vector3(0.5, 0.5, 0.5),
+ Vector3(-0.5, 0.5, 0.5),
+ ];
+
+ static const List<List<int>> edges = [
+ [0, 1], [1, 2], [2, 3], [3, 0], // Front face
+ [4, 5], [5, 6], [6, 7], [7, 4], // Back face
+ [0, 4], [1, 5], [2, 6], [3, 7], // Connecting front and back
+ ];
+
+ final double angle;
+ final Color color;
+
+ _RotatedWireCubePainter({required this.angle, required this.color});
+
+ Offset scaleAndCenter(Vector3 point, double size, Offset center) {
+ final scale = size / 2;
+ return Offset(center.dx + point.x * scale, center.dy - point.y * scale);
+ }
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ final rotatedVertices = vertices
+ .map((vertex) => Matrix4.rotationX(angle).transformed3(vertex))
+ .map((vertex) => Matrix4.rotationY(angle).transformed3(vertex))
+ .map((vertex) => Matrix4.rotationZ(angle).transformed3(vertex))
+ .toList();
+
+ final center = Offset(size.width / 2, size.height / 2);
+
+ final paint = Paint()
+ ..color = color
+ ..style = PaintingStyle.stroke
+ ..strokeWidth = 2;
+
+ for (var edge in edges) {
+ final p1 = scaleAndCenter(rotatedVertices[edge[0]], size.width, center);
+ final p2 = scaleAndCenter(rotatedVertices[edge[1]], size.width, center);
+ canvas.drawLine(p1, p2, paint);
+ }
+ }
+
+ @override
+ bool shouldRepaint(_RotatedWireCubePainter oldDelegate) => true;
+}
diff --git a/examples/multiple_windows/lib/app/window_content.dart b/examples/multiple_windows/lib/app/window_content.dart
new file mode 100644
index 0000000..72ea7fd
--- /dev/null
+++ b/examples/multiple_windows/lib/app/window_content.dart
@@ -0,0 +1,38 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore_for_file: invalid_use_of_internal_member
+// ignore_for_file: implementation_imports
+
+import 'package:flutter/material.dart';
+import 'regular_window_content.dart';
+import 'package:flutter/src/widgets/_window.dart';
+
+/// Responsible for rendering the appropriate content for a window based on
+/// the type of the window.
+class WindowContent extends StatelessWidget {
+ const WindowContent({
+ required this.windowKey,
+ required this.controller,
+ required this.onDestroyed,
+ required this.onError,
+ super.key,
+ });
+
+ final Key windowKey;
+ final BaseWindowController controller;
+ final VoidCallback onDestroyed;
+ final VoidCallback onError;
+
+ @override
+ Widget build(BuildContext context) {
+ return switch (controller) {
+ final RegularWindowController regular => RegularWindow(
+ key: windowKey,
+ controller: regular,
+ child: MaterialApp(home: RegularWindowContent(window: regular)),
+ ),
+ };
+ }
+}
diff --git a/examples/multiple_windows/lib/app/window_settings_dialog.dart b/examples/multiple_windows/lib/app/window_settings_dialog.dart
new file mode 100644
index 0000000..4e31dfc
--- /dev/null
+++ b/examples/multiple_windows/lib/app/window_settings_dialog.dart
@@ -0,0 +1,94 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'models.dart';
+
+Future<void> showWindowSettingsDialog(
+ BuildContext context,
+ WindowSettings settings,
+) async {
+ return await showDialog(
+ barrierDismissible: true,
+ context: context,
+ builder: (BuildContext ctx) {
+ return _WindowSettingsEditor(
+ settings: settings,
+ onClose: () => Navigator.of(context, rootNavigator: true).pop(),
+ );
+ },
+ );
+}
+
+class _WindowSettingsEditor extends StatefulWidget {
+ const _WindowSettingsEditor({required this.settings, required this.onClose});
+
+ final WindowSettings settings;
+ final void Function() onClose;
+
+ @override
+ State<_WindowSettingsEditor> createState() => _WindowSettingsEditorState();
+}
+
+class _WindowSettingsEditorState extends State<_WindowSettingsEditor> {
+ final TextEditingController _regularWidthController = TextEditingController();
+ final TextEditingController _regularHeightController =
+ TextEditingController();
+
+ @override
+ void initState() {
+ super.initState();
+ _regularWidthController.text = widget.settings.regularSize.width.toString();
+ _regularHeightController.text = widget.settings.regularSize.height
+ .toString();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SimpleDialog(
+ contentPadding: const EdgeInsets.all(4),
+ titlePadding: const EdgeInsets.fromLTRB(24, 10, 24, 0),
+ title: const Center(child: Text('Window Settings')),
+ children: [
+ ListTile(
+ title: const Text('Regular'),
+ subtitle: Row(
+ children: [
+ Expanded(
+ child: TextFormField(
+ controller: _regularWidthController,
+ decoration: const InputDecoration(labelText: 'Initial width'),
+ ),
+ ),
+ const SizedBox(width: 20),
+ Expanded(
+ child: TextFormField(
+ controller: _regularHeightController,
+ decoration: const InputDecoration(
+ labelText: 'Initial height',
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: TextButton(
+ onPressed: () {
+ widget.settings.regularSize = Size(
+ double.tryParse(_regularWidthController.text) ??
+ widget.settings.regularSize.width,
+ double.tryParse(_regularHeightController.text) ??
+ widget.settings.regularSize.height,
+ );
+ widget.onClose();
+ },
+ child: const Text('Apply'),
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/examples/multiple_windows/lib/main.dart b/examples/multiple_windows/lib/main.dart
new file mode 100644
index 0000000..b8a9d1e
--- /dev/null
+++ b/examples/multiple_windows/lib/main.dart
@@ -0,0 +1,102 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore_for_file: invalid_use_of_internal_member
+// ignore_for_file: implementation_imports
+
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'app/models.dart';
+import 'app/window_content.dart';
+import 'app/main_window.dart';
+import 'package:flutter/src/widgets/_window.dart';
+
+class MainControllerWindowDelegate with RegularWindowControllerDelegate {
+ @override
+ void onWindowDestroyed() {
+ super.onWindowDestroyed();
+ exit(0);
+ }
+}
+
+void main() {
+ WidgetsFlutterBinding.ensureInitialized();
+ runWidget(MultiWindowApp());
+}
+
+class MultiWindowApp extends StatefulWidget {
+ const MultiWindowApp({super.key});
+
+ @override
+ State<MultiWindowApp> createState() => _MultiWindowAppState();
+}
+
+class _MultiWindowAppState extends State<MultiWindowApp> {
+ final RegularWindowController controller = RegularWindowController(
+ preferredSize: const Size(800, 600),
+ title: 'Multi-Window Reference Application',
+ delegate: MainControllerWindowDelegate(),
+ );
+ final WindowSettings settings = WindowSettings();
+ late final WindowManager windowManager;
+
+ @override
+ void initState() {
+ super.initState();
+ windowManager = WindowManager(
+ initialWindows: <KeyedWindow>[
+ KeyedWindow(
+ isMainWindow: true,
+ key: UniqueKey(),
+ controller: controller,
+ ),
+ ],
+ );
+ }
+
+ @override
+ void dispose() {
+ controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final Widget mainWindowWidget = RegularWindow(
+ controller: controller,
+ child: MaterialApp(home: MainWindow()),
+ );
+ return WindowManagerAccessor(
+ windowManager: windowManager,
+ child: WindowSettingsAccessor(
+ windowSettings: settings,
+ child: ListenableBuilder(
+ listenable: windowManager,
+ builder: (BuildContext context, Widget? child) {
+ final List<Widget> childViews = <Widget>[mainWindowWidget];
+ for (final KeyedWindow window in windowManager.windows) {
+ // This check renders windows that are not nested below another window as
+ // a child window (e.g. a popup as a child of another window) in addition
+ // to the main window, which is special as it is the one that is currently
+ // being rendered.
+ if (window.parent == null && !window.isMainWindow) {
+ childViews.add(
+ WindowContent(
+ controller: window.controller,
+ windowKey: window.key,
+ onDestroyed: () => windowManager.remove(window.key),
+ onError: () => windowManager.remove(window.key),
+ ),
+ );
+ }
+ }
+
+ return ViewCollection(views: childViews);
+ },
+ ),
+ ),
+ );
+ }
+}
diff --git a/examples/multiple_windows/pubspec.yaml b/examples/multiple_windows/pubspec.yaml
new file mode 100644
index 0000000..2cd9b81
--- /dev/null
+++ b/examples/multiple_windows/pubspec.yaml
@@ -0,0 +1,18 @@
+name: multiple_windows
+
+environment:
+ sdk: ^3.8.0-0
+
+dependencies:
+ flutter:
+ sdk: flutter
+ vector_math: ^2.2.0
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+flutter:
+ uses-material-design: true
+
+# PUBSPEC CHECKSUM: lkvhan
diff --git a/examples/multiple_windows/windows/.gitignore b/examples/multiple_windows/windows/.gitignore
new file mode 100644
index 0000000..d492d0d
--- /dev/null
+++ b/examples/multiple_windows/windows/.gitignore
@@ -0,0 +1,17 @@
+flutter/ephemeral/
+
+# Visual Studio user-specific files.
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# Visual Studio build-related files.
+x64/
+x86/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
diff --git a/examples/multiple_windows/windows/CMakeLists.txt b/examples/multiple_windows/windows/CMakeLists.txt
new file mode 100644
index 0000000..a3413e9
--- /dev/null
+++ b/examples/multiple_windows/windows/CMakeLists.txt
@@ -0,0 +1,108 @@
+# Project-level configuration.
+cmake_minimum_required(VERSION 3.14)
+project(multiple_windows LANGUAGES CXX)
+
+# The name of the executable created for the application. Change this to change
+# the on-disk name of your application.
+set(BINARY_NAME "multiple_windows")
+
+# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
+# versions of CMake.
+cmake_policy(VERSION 3.14...3.25)
+
+# Define build configuration option.
+get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
+if(IS_MULTICONFIG)
+ set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
+ CACHE STRING "" FORCE)
+else()
+ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+ set(CMAKE_BUILD_TYPE "Debug" CACHE
+ STRING "Flutter build mode" FORCE)
+ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+ "Debug" "Profile" "Release")
+ endif()
+endif()
+# Define settings for the Profile build mode.
+set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
+set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
+set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
+set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
+
+# Use Unicode for all projects.
+add_definitions(-DUNICODE -D_UNICODE)
+
+# Compilation settings that should be applied to most targets.
+#
+# Be cautious about adding new options here, as plugins use this function by
+# default. In most cases, you should add new options to specific targets instead
+# of modifying this function.
+function(APPLY_STANDARD_SETTINGS TARGET)
+ target_compile_features(${TARGET} PUBLIC cxx_std_17)
+ target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
+ target_compile_options(${TARGET} PRIVATE /EHsc)
+ target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
+ target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
+endfunction()
+
+# Flutter library and tool build rules.
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# Application build; see runner/CMakeLists.txt.
+add_subdirectory("runner")
+
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# Support files are copied into place next to the executable, so that it can
+# run in place. This is done instead of making a separate bundle (as on Linux)
+# so that building and running from within Visual Studio will work.
+set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
+# Make the "install" step default, as it's required to run.
+set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+ set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+if(PLUGIN_BUNDLED_LIBRARIES)
+ install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endif()
+
+# Copy the native assets provided by the build.dart from all packages.
+set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
+install(DIRECTORY "${NATIVE_ASSETS_DIR}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+ file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+ " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+ DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ CONFIGURATIONS Profile;Release
+ COMPONENT Runtime)
diff --git a/examples/multiple_windows/windows/flutter/CMakeLists.txt b/examples/multiple_windows/windows/flutter/CMakeLists.txt
new file mode 100644
index 0000000..a71c6e2
--- /dev/null
+++ b/examples/multiple_windows/windows/flutter/CMakeLists.txt
@@ -0,0 +1,109 @@
+# This file controls Flutter-level build steps. It should not be edited.
+cmake_minimum_required(VERSION 3.14)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+set(CMAKE_CXX_STANDARD 20)
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
+
+# Set fallback configurations for older versions of the flutter tool.
+if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
+ set(FLUTTER_TARGET_PLATFORM "windows-x64")
+endif()
+
+# === Flutter Library ===
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+ "flutter_export.h"
+ "flutter_windows.h"
+ "flutter_messenger.h"
+ "flutter_plugin_registrar.h"
+ "flutter_texture_registrar.h"
+)
+list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+ "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
+add_dependencies(flutter flutter_assemble)
+
+# === Wrapper ===
+list(APPEND CPP_WRAPPER_SOURCES_CORE
+ "core_implementations.cc"
+ "standard_codec.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
+ "plugin_registrar.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_APP
+ "flutter_engine.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
+
+# Wrapper sources needed for a plugin.
+add_library(flutter_wrapper_plugin STATIC
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_PLUGIN}
+)
+apply_standard_settings(flutter_wrapper_plugin)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+ POSITION_INDEPENDENT_CODE ON)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+ CXX_VISIBILITY_PRESET hidden)
+target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
+target_include_directories(flutter_wrapper_plugin PUBLIC
+ "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_plugin flutter_assemble)
+
+# Wrapper sources needed for the runner.
+add_library(flutter_wrapper_app STATIC
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_APP}
+)
+apply_standard_settings(flutter_wrapper_app)
+target_link_libraries(flutter_wrapper_app PUBLIC flutter)
+target_include_directories(flutter_wrapper_app PUBLIC
+ "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_app flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
+set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
+add_custom_command(
+ OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+ ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
+ ${CPP_WRAPPER_SOURCES_APP}
+ ${PHONY_OUTPUT}
+ COMMAND ${CMAKE_COMMAND} -E env
+ ${FLUTTER_TOOL_ENVIRONMENT}
+ "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
+ ${FLUTTER_TARGET_PLATFORM} $<CONFIG>
+ VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+ "${FLUTTER_LIBRARY}"
+ ${FLUTTER_LIBRARY_HEADERS}
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_PLUGIN}
+ ${CPP_WRAPPER_SOURCES_APP}
+)
diff --git a/examples/multiple_windows/windows/runner/CMakeLists.txt b/examples/multiple_windows/windows/runner/CMakeLists.txt
new file mode 100644
index 0000000..697f434
--- /dev/null
+++ b/examples/multiple_windows/windows/runner/CMakeLists.txt
@@ -0,0 +1,37 @@
+cmake_minimum_required(VERSION 3.14)
+project(runner LANGUAGES CXX)
+
+# Define the application target. To change its name, change BINARY_NAME in the
+# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
+# work.
+#
+# Any new source files that you add to the application should be added here.
+add_executable(${BINARY_NAME} WIN32
+ "main.cpp"
+ "utils.cpp"
+ "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+ "Runner.rc"
+ "runner.exe.manifest"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Add preprocessor definitions for the build version.
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
+
+# Disable Windows macros that collide with C++ standard library functions.
+target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
+
+# Add dependency libraries and include directories. Add any application-specific
+# dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
+target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)
diff --git a/examples/multiple_windows/windows/runner/Runner.rc b/examples/multiple_windows/windows/runner/Runner.rc
new file mode 100644
index 0000000..d9d3832
--- /dev/null
+++ b/examples/multiple_windows/windows/runner/Runner.rc
@@ -0,0 +1,111 @@
+// Microsoft Visual C++ generated resource script.
+//
+#pragma code_page(65001)
+#include "resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winres.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
+#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
+#else
+#define VERSION_AS_NUMBER 1,0,0,0
+#endif
+
+#if defined(FLUTTER_VERSION)
+#define VERSION_AS_STRING FLUTTER_VERSION
+#else
+#define VERSION_AS_STRING "1.0.0"
+#endif
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION VERSION_AS_NUMBER
+ PRODUCTVERSION VERSION_AS_NUMBER
+ FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+ FILEFLAGS VS_FF_DEBUG
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_APP
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904e4"
+ BEGIN
+ VALUE "CompanyName", "The Flutter Authors" "\0"
+ VALUE "FileDescription", "A reference application demonstrating Flutter's multi-window API." "\0"
+ VALUE "FileVersion", VERSION_AS_STRING "\0"
+ VALUE "InternalName", "Flutter Multi-Window Reference App" "\0"
+ VALUE "LegalCopyright", "Copyright 2014 The Flutter Authors. All rights reserved." "\0"
+ VALUE "OriginalFilename", "multiple_windows.exe" "\0"
+ VALUE "ProductName", "Flutter Multi-Window Reference App" "\0"
+ VALUE "ProductVersion", VERSION_AS_STRING "\0"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1252
+ END
+END
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
diff --git a/examples/multiple_windows/windows/runner/main.cpp b/examples/multiple_windows/windows/runner/main.cpp
new file mode 100644
index 0000000..11a76c8
--- /dev/null
+++ b/examples/multiple_windows/windows/runner/main.cpp
@@ -0,0 +1,41 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <flutter/dart_project.h>
+#include <flutter/flutter_engine.h>
+#include <flutter/generated_plugin_registrant.h>
+
+#include "utils.h"
+
+int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
+ _In_ wchar_t* command_line, _In_ int show_command) {
+ // Attach to console when present (e.g., 'flutter run') or create a
+ // new console when running with a debugger.
+ if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
+ CreateAndAttachConsole();
+ }
+
+ // Initialize COM, so that it is available for use in the library and/or
+ // plugins.
+ ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
+
+ flutter::DartProject project(L"data");
+
+ auto command_line_arguments{GetCommandLineArguments()};
+
+ project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
+
+ auto const engine{std::make_shared<flutter::FlutterEngine>(project)};
+ RegisterPlugins(engine.get());
+ engine->Run();
+
+ ::MSG msg;
+ while (::GetMessage(&msg, nullptr, 0, 0)) {
+ ::TranslateMessage(&msg);
+ ::DispatchMessage(&msg);
+ }
+
+ ::CoUninitialize();
+ return EXIT_SUCCESS;
+}
diff --git a/examples/multiple_windows/windows/runner/resource.h b/examples/multiple_windows/windows/runner/resource.h
new file mode 100644
index 0000000..69cacf3
--- /dev/null
+++ b/examples/multiple_windows/windows/runner/resource.h
@@ -0,0 +1,19 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by Runner.rc
+//
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 102
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1001
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/examples/multiple_windows/windows/runner/runner.exe.manifest b/examples/multiple_windows/windows/runner/runner.exe.manifest
new file mode 100644
index 0000000..153653e
--- /dev/null
+++ b/examples/multiple_windows/windows/runner/runner.exe.manifest
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+ <application xmlns="urn:schemas-microsoft-com:asm.v3">
+ <windowsSettings>
+ <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
+ </windowsSettings>
+ </application>
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <!-- Windows 10 and Windows 11 -->
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
+ </application>
+ </compatibility>
+</assembly>
diff --git a/examples/multiple_windows/windows/runner/utils.cpp b/examples/multiple_windows/windows/runner/utils.cpp
new file mode 100644
index 0000000..6abcd65
--- /dev/null
+++ b/examples/multiple_windows/windows/runner/utils.cpp
@@ -0,0 +1,68 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "utils.h"
+
+#include <flutter_windows.h>
+#include <io.h>
+#include <stdio.h>
+#include <windows.h>
+
+#include <iostream>
+
+void CreateAndAttachConsole() {
+ if (::AllocConsole()) {
+ FILE *unused;
+ if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
+ _dup2(_fileno(stdout), 1);
+ }
+ if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
+ _dup2(_fileno(stdout), 2);
+ }
+ std::ios::sync_with_stdio();
+ FlutterDesktopResyncOutputStreams();
+ }
+}
+
+std::vector<std::string> GetCommandLineArguments() {
+ // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
+ int argc;
+ wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
+ if (argv == nullptr) {
+ return std::vector<std::string>();
+ }
+
+ std::vector<std::string> command_line_arguments;
+
+ // Skip the first argument as it's the binary name.
+ for (int i = 1; i < argc; i++) {
+ command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
+ }
+
+ ::LocalFree(argv);
+
+ return command_line_arguments;
+}
+
+std::string Utf8FromUtf16(const wchar_t* utf16_string) {
+ if (utf16_string == nullptr) {
+ return std::string();
+ }
+ unsigned int target_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ -1, nullptr, 0, nullptr, nullptr)
+ -1; // remove the trailing null character
+ std::string utf8_string;
+ if (target_length == 0 || target_length > utf8_string.max_size()) {
+ return utf8_string;
+ }
+ utf8_string.resize(target_length);
+ int converted_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ -1, utf8_string.data(), target_length, nullptr, nullptr);
+ if (converted_length == 0) {
+ return std::string();
+ }
+ return utf8_string;
+}
diff --git a/examples/multiple_windows/windows/runner/utils.h b/examples/multiple_windows/windows/runner/utils.h
new file mode 100644
index 0000000..54414c9
--- /dev/null
+++ b/examples/multiple_windows/windows/runner/utils.h
@@ -0,0 +1,23 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef RUNNER_UTILS_H_
+#define RUNNER_UTILS_H_
+
+#include <string>
+#include <vector>
+
+// Creates a console for the process, and redirects stdout and stderr to
+// it for both the runner and the Flutter library.
+void CreateAndAttachConsole();
+
+// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
+// encoded in UTF-8. Returns an empty std::string on failure.
+std::string Utf8FromUtf16(const wchar_t* utf16_string);
+
+// Gets the command line arguments passed in as a std::vector<std::string>,
+// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
+std::vector<std::string> GetCommandLineArguments();
+
+#endif // RUNNER_UTILS_H_
diff --git a/packages/flutter/lib/src/foundation/_features.dart b/packages/flutter/lib/src/foundation/_features.dart
index 283d43b..f9a6aca 100644
--- a/packages/flutter/lib/src/foundation/_features.dart
+++ b/packages/flutter/lib/src/foundation/_features.dart
@@ -14,6 +14,9 @@
/// files will throw an `UnsupportedError`:
///
/// 1. packages/flutter/lib/src/widgets/_window.dart
+/// 2. packages/flutter/lib/src/widgets/_window_io.dart
+/// 3. packages/flutter/lib/src/widgets/_window_web.dart
+/// 4. packages/flutter/lib/src/widgets/_window_win32.dart
///
/// See: https://github.com/flutter/flutter/issues/30701.
@internal
@@ -23,6 +26,6 @@
///
/// Do not use this API. Flutter can and will make breaking changes to this API.
@internal
-Set<String> debugEnabledFeatureFlags = <String>{
+final Set<String> debugEnabledFeatureFlags = <String>{
...const String.fromEnvironment('FLUTTER_ENABLED_FEATURE_FLAGS').split(','),
};
diff --git a/packages/flutter/lib/src/widgets/_window.dart b/packages/flutter/lib/src/widgets/_window.dart
index 9aa703a..5540b8e 100644
--- a/packages/flutter/lib/src/widgets/_window.dart
+++ b/packages/flutter/lib/src/widgets/_window.dart
@@ -10,19 +10,21 @@
//
// 1. Have the `@internal` attribute.
// 2. Throw an `UnsupportedError` if `isWindowingEnabled`
-// is `false.
+// is `false`.
//
// See: https://github.com/flutter/flutter/issues/30701.
import 'dart:ui' show Display, FlutterView;
import 'package:flutter/foundation.dart';
-import 'package:flutter/rendering.dart';
import '../foundation/_features.dart';
+import '_window_io.dart' if (dart.library.js_interop) '_window_web.dart' as window_impl;
+import 'basic.dart';
import 'binding.dart';
import 'framework.dart';
import 'inherited_model.dart';
+import 'transitions.dart';
import 'view.dart';
const String _kWindowingDisabledErrorMessage = '''
@@ -60,7 +62,7 @@
///
/// * [RegularWindowController], the controller for regular top-level windows.
@internal
-sealed class BaseWindowController {
+sealed class BaseWindowController extends ChangeNotifier {
/// The current size of the drawable area of the window.
///
/// This might differ from the requested size.
@@ -137,12 +139,6 @@
if (!isWindowingEnabled) {
throw UnsupportedError(_kWindowingDisabledErrorMessage);
}
-
- final WindowingOwner owner = WidgetsBinding.instance.windowingOwner;
- if (!owner.hasTopLevelWindows()) {
- // TODO(mattkae): close the application if this is the last window
- // via ServicesBinding.instance.exitApplication(AppExitType.cancelable);
- }
}
}
@@ -246,12 +242,18 @@
);
}
- /// Creates an empty [RegularWindowController] for testing purposes.
+ /// Creates an empty [RegularWindowController].
+ ///
+ /// This method is only intended to be used by subclasses of the
+ /// [RegularWindowController].
+ ///
+ /// Users who want to instantiate a new [RegularWindowController] should
+ /// always use the factory method to create a controller that is valid
+ /// for their particular platform.
///
/// {@macro flutter.widgets.windowing.experimental}
@internal
@protected
- @visibleForTesting
RegularWindowController.empty();
/// The current title of the window.
@@ -398,19 +400,23 @@
/// {@macro flutter.widgets.windowing.experimental}
@internal
bool hasTopLevelWindows();
+}
- /// Creates default windowing owner for standard desktop embedders.
- ///
- /// {@macro flutter.widgets.windowing.experimental}
- @internal
- static WindowingOwner createDefaultOwner() {
- if (!isWindowingEnabled) {
- return _WindowingOwnerUnsupported(errorMessage: _kWindowingDisabledErrorMessage);
- }
-
- // TODO(mattkae): Implement windowing owners for desktop platforms.
- return _WindowingOwnerUnsupported(errorMessage: 'Windowing is unsupported on this platform.');
+/// Creates default windowing owner for standard desktop embedders.
+///
+/// {@macro flutter.widgets.windowing.experimental}
+@internal
+WindowingOwner createDefaultWindowingOwner() {
+ if (!isWindowingEnabled) {
+ return _WindowingOwnerUnsupported(errorMessage: _kWindowingDisabledErrorMessage);
}
+
+ final WindowingOwner? owner = window_impl.createDefaultOwner();
+ if (owner != null) {
+ return owner;
+ }
+
+ return _WindowingOwnerUnsupported(errorMessage: 'Windowing is unsupported on this platform.');
}
/// Windowing delegate used on platforms that do not support windowing.
@@ -507,9 +513,12 @@
@internal
@override
Widget build(BuildContext context) {
- return WindowScope(
- controller: controller,
- child: View(view: controller.rootView, child: child),
+ return ListenableBuilder(
+ listenable: controller,
+ builder: (BuildContext context, Widget? widget) => WindowScope(
+ controller: controller,
+ child: View(view: controller.rootView, child: child),
+ ),
);
}
}
diff --git a/packages/flutter/lib/src/widgets/_window_io.dart b/packages/flutter/lib/src/widgets/_window_io.dart
new file mode 100644
index 0000000..071a58b
--- /dev/null
+++ b/packages/flutter/lib/src/widgets/_window_io.dart
@@ -0,0 +1,37 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Do not import this file in production applications or packages published
+// to pub.dev. Flutter will make breaking changes to this file, even in patch
+// versions.
+//
+// All APIs in this file must be private or must:
+//
+// 1. Have the `@internal` attribute.
+// 2. Throw an `UnsupportedError` if `isWindowingEnabled`
+// is `false`.
+//
+// See: https://github.com/flutter/flutter/issues/30701.
+
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+
+import '_window.dart';
+import '_window_win32.dart';
+
+/// Creates a default [WindowingOwner] for the current platform.
+///
+/// Returns null if windowing is not supported on the current platform.
+/// Only supported on desktop platforms.
+///
+/// {@macro flutter.widgets.windowing.experimental}
+@internal
+WindowingOwner? createDefaultOwner() {
+ if (Platform.isWindows) {
+ return WindowingOwnerWin32();
+ } else {
+ return null;
+ }
+}
diff --git a/packages/flutter/lib/src/widgets/_window_web.dart b/packages/flutter/lib/src/widgets/_window_web.dart
new file mode 100644
index 0000000..da3ddc7
--- /dev/null
+++ b/packages/flutter/lib/src/widgets/_window_web.dart
@@ -0,0 +1,29 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Do not import this file in production applications or packages published
+// to pub.dev. Flutter will make breaking changes to this file, even in patch
+// versions.
+//
+// All APIs in this file must be private or must:
+//
+// 1. Have the `@internal` attribute.
+// 2. Throw an `UnsupportedError` if `isWindowingEnabled`
+// is `false`.
+//
+// See: https://github.com/flutter/flutter/issues/30701.
+
+import 'package:flutter/foundation.dart';
+
+import '_window.dart';
+
+/// Creates a default [WindowingOwner] for web.
+///
+/// Returns `null` as web does not support multiple windows.
+///
+/// {@macro flutter.widgets.windowing.experimental}
+@internal
+WindowingOwner? createDefaultOwner() {
+ return null;
+}
diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart
new file mode 100644
index 0000000..1d6a81c
--- /dev/null
+++ b/packages/flutter/lib/src/widgets/_window_win32.dart
@@ -0,0 +1,856 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Do not import this file in production applications or packages published
+// to pub.dev. Flutter will make breaking changes to this file, even in patch
+// versions.
+//
+// All APIs in this file must be private or must:
+//
+// 1. Have the `@internal` attribute.
+// 2. Throw an `UnsupportedError` if `isWindowingEnabled`
+// is `false`.
+//
+// See: https://github.com/flutter/flutter/issues/30701.
+
+import 'dart:ffi' as ffi;
+import 'dart:io';
+import 'dart:typed_data';
+import 'dart:ui' show Display, FlutterView;
+import 'package:flutter/foundation.dart';
+import 'package:flutter/rendering.dart';
+
+import '../foundation/_features.dart';
+import '_window.dart';
+
+/// A Win32 window handle.
+///
+/// {@macro flutter.widgets.windowing.experimental}
+@internal
+typedef HWND = ffi.Pointer<ffi.Void>;
+
+const int _WM_SIZE = 0x0005;
+const int _WM_ACTIVATE = 0x0006;
+const int _WM_CLOSE = 0x0010;
+
+const int _SW_RESTORE = 9;
+const int _SW_MAXIMIZE = 3;
+const int _SW_MINIMIZE = 6;
+
+const String _kWindowingDisabledErrorMessage = '''
+Windowing APIs are not enabled.
+
+Windowing APIs are currently experimental. Do not use windowing APIs in
+production applications or plugins published to pub.dev.
+
+To try experimental windowing APIs:
+1. Switch to Flutter's main release channel.
+2. Turn on the windowing feature flag.
+
+See: https://github.com/flutter/flutter/issues/30701.
+''';
+
+/// Abstract handler class for Windows messages.
+///
+/// Implementations of this class should register with
+/// [WindowingOwnerWin32.addMessageHandler] to begin receiving messages.
+/// When finished handling messages, implementations should deregister
+/// themselves with [WindowingOwnerWin32.removeMessageHandler].
+abstract class _WindowsMessageHandler {
+ /// Handles a window message.
+ ///
+ /// Returned value, if not null will be returned to the system as LRESULT
+ /// and will stop all other handlers from being called. See
+ /// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-wndproc
+ /// for more information.
+ ///
+ /// {@macro flutter.widgets.windowing.experimental}
+ @internal
+ int? handleWindowsMessage(
+ FlutterView view,
+ HWND windowHandle,
+ int message,
+ int wParam,
+ int lParam,
+ );
+}
+
+/// [WindowingOwner] implementation for Windows.
+///
+/// If [Platform.isWindows] is false, then the constructor will throw an
+/// [UnsupportedError].
+///
+/// {@macro flutter.widgets.windowing.experimental}
+///
+/// See also:
+///
+/// * [WindowingOwner], the abstract class that manages native windows.
+@internal
+class WindowingOwnerWin32 extends WindowingOwner {
+ /// Creates a new [WindowingOwnerWin32] instance.
+ ///
+ /// If [Platform.isWindows] is false, then this constructor will throw an
+ /// [UnsupportedError]
+ ///
+ ///
+ /// {@macro flutter.widgets.windowing.experimental}
+ ///
+ /// See also:
+ ///
+ /// * [WindowingOwner], the abstract class that manages native windows.
+ @internal
+ WindowingOwnerWin32() : allocator = _CallocAllocator() {
+ if (!isWindowingEnabled) {
+ throw UnsupportedError(_kWindowingDisabledErrorMessage);
+ }
+
+ if (!Platform.isWindows) {
+ throw UnsupportedError('Only available on the Win32 platform');
+ }
+
+ assert(
+ PlatformDispatcher.instance.engineId != null,
+ 'WindowingOwnerWin32 must be created after the engine has been initialized.',
+ );
+
+ _Win32PlatformInterface.initializeWindowing(
+ allocator,
+ PlatformDispatcher.instance.engineId!,
+ _onMessage,
+ );
+ }
+
+ final List<_WindowsMessageHandler> _messageHandlers = <_WindowsMessageHandler>[];
+
+ /// The [Allocator] used for allocating native memory in this owner.
+ ///
+ /// This can be overridden via the [WindowingOwnerWin32.test] constructor.
+ ///
+ /// {@macro flutter.widgets.windowing.experimental}
+ @internal
+ final ffi.Allocator allocator;
+
+ @internal
+ @override
+ RegularWindowController createRegularWindowController({
+ Size? preferredSize,
+ BoxConstraints? preferredConstraints,
+ String? title,
+ required RegularWindowControllerDelegate delegate,
+ }) {
+ return RegularWindowControllerWin32(
+ owner: this,
+ delegate: delegate,
+ preferredSize: preferredSize,
+ preferredConstraints: preferredConstraints,
+ title: title,
+ );
+ }
+
+ /// Register a new [WindowsMessageHandler].
+ ///
+ /// The handler will be triggered for unhandled messages for all top level
+ /// windows.
+ ///
+ /// Adding a handler multiple times has no effect.
+ ///
+ /// Handlers are called in the order that they are added.
+ ///
+ /// Callers must remove their message handlers using
+ /// [WindowingOwnerWin32._removeMessageHandler].
+ void _addMessageHandler(_WindowsMessageHandler handler) {
+ if (_messageHandlers.contains(handler)) {
+ return;
+ }
+
+ _messageHandlers.add(handler);
+ }
+
+ /// Unregister a [WindowsMessageHandler].
+ ///
+ /// If the handler has not been registered, this method has no effect.
+ void _removeMessageHandler(_WindowsMessageHandler handler) {
+ _messageHandlers.remove(handler);
+ }
+
+ void _onMessage(ffi.Pointer<_WindowsMessage> message) {
+ final FlutterView flutterView = PlatformDispatcher.instance.views.firstWhere(
+ (FlutterView view) => view.viewId == message.ref.viewId,
+ );
+
+ final int handlesLength = _messageHandlers.length;
+ for (final _WindowsMessageHandler handler in _messageHandlers) {
+ assert(
+ _messageHandlers.length == handlesLength,
+ 'Message handler list changed while processing message: $message',
+ );
+ final int? result = handler.handleWindowsMessage(
+ flutterView,
+ message.ref.windowHandle,
+ message.ref.message,
+ message.ref.wParam,
+ message.ref.lParam,
+ );
+ if (result != null) {
+ message.ref.handled = true;
+ message.ref.lResult = result;
+ return;
+ }
+ }
+ }
+
+ @internal
+ @override
+ bool hasTopLevelWindows() {
+ return _Win32PlatformInterface.hasTopLevelWindows(PlatformDispatcher.instance.engineId!);
+ }
+}
+
+class _RegularWindowMesageHandler implements _WindowsMessageHandler {
+ _RegularWindowMesageHandler({required this.controller});
+
+ final RegularWindowControllerWin32 controller;
+
+ @override
+ int? handleWindowsMessage(
+ FlutterView view,
+ HWND windowHandle,
+ int message,
+ int wParam,
+ int lParam,
+ ) {
+ return controller._handleWindowsMessage(view, windowHandle, message, wParam, lParam);
+ }
+}
+
+/// Implementation of [RegularWindowController] for the Windows platform.
+///
+/// {@macro flutter.widgets.windowing.experimental}
+///
+/// See also:
+///
+/// * [RegularWindowController], the base class for regular windows.
+class RegularWindowControllerWin32 extends RegularWindowController {
+ /// Creates a new regular window controller for Win32.
+ ///
+ /// When this constructor completes the native window has been created and
+ /// has a view associated with it.
+ ///
+ /// {@macro flutter.widgets.windowing.experimental}
+ ///
+ /// See also:
+ ///
+ /// * [RegularWindowController], the base class for regular windows.
+ @internal
+ RegularWindowControllerWin32({
+ required WindowingOwnerWin32 owner,
+ required RegularWindowControllerDelegate delegate,
+ Size? preferredSize,
+ BoxConstraints? preferredConstraints,
+ String? title,
+ }) : _owner = owner,
+ _delegate = delegate,
+ super.empty() {
+ if (!isWindowingEnabled) {
+ throw UnsupportedError(_kWindowingDisabledErrorMessage);
+ }
+
+ _handler = _RegularWindowMesageHandler(controller: this);
+ owner._addMessageHandler(_handler);
+ final int viewId = _Win32PlatformInterface.createWindow(
+ _owner.allocator,
+ PlatformDispatcher.instance.engineId!,
+ preferredSize,
+ preferredConstraints,
+ title,
+ );
+ final FlutterView flutterView = PlatformDispatcher.instance.views.firstWhere(
+ (FlutterView view) => view.viewId == viewId,
+ );
+ rootView = flutterView;
+ }
+
+ final WindowingOwnerWin32 _owner;
+ final RegularWindowControllerDelegate _delegate;
+ late final _RegularWindowMesageHandler _handler;
+ bool _destroyed = false;
+
+ @override
+ @internal
+ Size get contentSize {
+ _ensureNotDestroyed();
+ final _ActualContentSize size = _Win32PlatformInterface.getWindowContentSize(getWindowHandle());
+ final Size result = Size(size.width, size.height);
+ return result;
+ }
+
+ @override
+ @internal
+ String get title {
+ _ensureNotDestroyed();
+ return _Win32PlatformInterface.getWindowTitle(_owner.allocator, getWindowHandle());
+ }
+
+ @override
+ @internal
+ bool get isActivated {
+ _ensureNotDestroyed();
+ return _Win32PlatformInterface.getForegroundWindow() == getWindowHandle();
+ }
+
+ @override
+ @internal
+ bool get isMaximized {
+ _ensureNotDestroyed();
+ return _Win32PlatformInterface.isZoomed(getWindowHandle()) != 0;
+ }
+
+ @override
+ @internal
+ bool get isMinimized {
+ _ensureNotDestroyed();
+ return _Win32PlatformInterface.isIconic(getWindowHandle()) != 0;
+ }
+
+ @override
+ @internal
+ bool get isFullscreen {
+ _ensureNotDestroyed();
+ return _Win32PlatformInterface.getFullscreen(getWindowHandle());
+ }
+
+ @override
+ @internal
+ void setSize(Size? size) {
+ _ensureNotDestroyed();
+ _Win32PlatformInterface.setWindowContentSize(_owner.allocator, getWindowHandle(), size);
+ }
+
+ @override
+ @internal
+ void setConstraints(BoxConstraints constraints) {
+ _ensureNotDestroyed();
+ _Win32PlatformInterface.setWindowConstraints(_owner.allocator, getWindowHandle(), constraints);
+ notifyListeners();
+ }
+
+ @override
+ @internal
+ void setTitle(String title) {
+ _ensureNotDestroyed();
+ _Win32PlatformInterface.setWindowTitle(_owner.allocator, getWindowHandle(), title);
+ notifyListeners();
+ }
+
+ @override
+ @internal
+ void activate() {
+ _ensureNotDestroyed();
+ _Win32PlatformInterface.showWindow(getWindowHandle(), _SW_RESTORE);
+ }
+
+ @override
+ @internal
+ void setMaximized(bool maximized) {
+ _ensureNotDestroyed();
+ if (maximized) {
+ _Win32PlatformInterface.showWindow(getWindowHandle(), _SW_MAXIMIZE);
+ } else {
+ _Win32PlatformInterface.showWindow(getWindowHandle(), _SW_RESTORE);
+ }
+ }
+
+ @override
+ @internal
+ void setMinimized(bool minimized) {
+ _ensureNotDestroyed();
+ if (minimized) {
+ _Win32PlatformInterface.showWindow(getWindowHandle(), _SW_MINIMIZE);
+ } else {
+ _Win32PlatformInterface.showWindow(getWindowHandle(), _SW_RESTORE);
+ }
+ }
+
+ @override
+ @internal
+ void setFullscreen(bool fullscreen, {Display? display}) {
+ _Win32PlatformInterface.setFullscreen(
+ _owner.allocator,
+ getWindowHandle(),
+ fullscreen,
+ display: display,
+ );
+ }
+
+ /// Returns HWND pointer to the top level window.
+ @internal
+ HWND getWindowHandle() {
+ _ensureNotDestroyed();
+ return _Win32PlatformInterface.getWindowHandle(
+ PlatformDispatcher.instance.engineId!,
+ rootView.viewId,
+ );
+ }
+
+ void _ensureNotDestroyed() {
+ if (_destroyed) {
+ throw StateError('Window has been destroyed.');
+ }
+ }
+
+ @override
+ void destroy() {
+ if (_destroyed) {
+ return;
+ }
+ _Win32PlatformInterface.destroyWindow(getWindowHandle());
+ _destroyed = true;
+ _owner._removeMessageHandler(_handler);
+ _delegate.onWindowDestroyed();
+ }
+
+ int? _handleWindowsMessage(
+ FlutterView view,
+ HWND windowHandle,
+ int message,
+ int wParam,
+ int lParam,
+ ) {
+ if (view.viewId != rootView.viewId) {
+ return null;
+ }
+
+ if (message == _WM_CLOSE) {
+ _delegate.onWindowCloseRequested(this);
+ return 0;
+ } else if (message == _WM_SIZE || message == _WM_ACTIVATE) {
+ notifyListeners();
+ }
+ return null;
+ }
+}
+
+class _Win32PlatformInterface {
+ @ffi.Native<ffi.Bool Function(ffi.Int64)>(
+ symbol: 'InternalFlutterWindows_WindowManager_HasTopLevelWindows',
+ )
+ external static bool hasTopLevelWindows(int engineId);
+
+ static void initializeWindowing(
+ ffi.Allocator allocator,
+ int engineId,
+ void Function(ffi.Pointer<_WindowsMessage>) onMessage,
+ ) {
+ final ffi.Pointer<_WindowingInitRequest> request = allocator<_WindowingInitRequest>();
+ try {
+ request.ref.onMessage =
+ ffi.NativeCallable<ffi.Void Function(ffi.Pointer<_WindowsMessage>)>.isolateLocal(
+ onMessage,
+ ).nativeFunction;
+ _initializeWindowing(engineId, request);
+ } finally {
+ allocator.free(request);
+ }
+ }
+
+ @ffi.Native<ffi.Void Function(ffi.Int64, ffi.Pointer<_WindowingInitRequest>)>(
+ symbol: 'InternalFlutterWindows_WindowManager_Initialize',
+ )
+ external static void _initializeWindowing(
+ int engineId,
+ ffi.Pointer<_WindowingInitRequest> request,
+ );
+
+ static int createWindow(
+ ffi.Allocator allocator,
+ int engineId,
+ Size? preferredSize,
+ BoxConstraints? preferredConstraints,
+ String? title,
+ ) {
+ final ffi.Pointer<_WindowCreationRequest> request = allocator<_WindowCreationRequest>();
+ try {
+ request.ref.preferredSize.from(preferredSize);
+ request.ref.preferredConstraints.from(preferredConstraints);
+ request.ref.title = (title ?? 'Regular window').toNativeUtf16(allocator: allocator);
+ return _createWindow(engineId, request);
+ } finally {
+ allocator.free(request);
+ }
+ }
+
+ @ffi.Native<ffi.Int64 Function(ffi.Int64, ffi.Pointer<_WindowCreationRequest>)>(
+ symbol: 'InternalFlutterWindows_WindowManager_CreateRegularWindow',
+ )
+ external static int _createWindow(int engineId, ffi.Pointer<_WindowCreationRequest> request);
+
+ @ffi.Native<HWND Function(ffi.Int64, ffi.Int64)>(
+ symbol: 'InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle',
+ )
+ external static HWND getWindowHandle(int engineId, int viewId);
+
+ @ffi.Native<ffi.Void Function(HWND)>(symbol: 'DestroyWindow')
+ external static void destroyWindow(HWND windowHandle);
+
+ @ffi.Native<_ActualContentSize Function(HWND)>(
+ symbol: 'InternalFlutterWindows_WindowManager_GetWindowContentSize',
+ )
+ external static _ActualContentSize getWindowContentSize(HWND windowHandle);
+
+ static void setWindowTitle(ffi.Allocator allocator, HWND windowHandle, String title) {
+ final ffi.Pointer<_Utf16> titlePointer = title.toNativeUtf16(allocator: allocator);
+ try {
+ _setWindowTitle(windowHandle, titlePointer);
+ } finally {
+ allocator.free(titlePointer);
+ }
+ }
+
+ @ffi.Native<ffi.Void Function(HWND, ffi.Pointer<_Utf16>)>(symbol: 'SetWindowTextW')
+ external static void _setWindowTitle(HWND windowHandle, ffi.Pointer<_Utf16> title);
+
+ static void setWindowContentSize(ffi.Allocator allocator, HWND windowHandle, Size? size) {
+ final ffi.Pointer<_WindowSizeRequest> request = allocator<_WindowSizeRequest>();
+ try {
+ request.ref.from(size);
+ _setWindowContentSize(windowHandle, request);
+ } finally {
+ allocator.free(request);
+ }
+ }
+
+ @ffi.Native<ffi.Void Function(HWND, ffi.Pointer<_WindowSizeRequest>)>(
+ symbol: 'InternalFlutterWindows_WindowManager_SetWindowSize',
+ )
+ external static void _setWindowContentSize(
+ HWND windowHandle,
+ ffi.Pointer<_WindowSizeRequest> size,
+ );
+
+ static void setWindowConstraints(
+ ffi.Allocator allocator,
+ HWND windowHandle,
+ BoxConstraints? constraints,
+ ) {
+ final ffi.Pointer<_WindowConstraintsRequest> request = allocator<_WindowConstraintsRequest>();
+ try {
+ request.ref.from(constraints);
+ _setWindowConstraints(windowHandle, request);
+ } finally {
+ allocator.free(request);
+ }
+ }
+
+ @ffi.Native<ffi.Void Function(HWND, ffi.Pointer<_WindowConstraintsRequest>)>(
+ symbol: 'InternalFlutterWindows_WindowManager_SetWindowConstraints',
+ )
+ external static void _setWindowConstraints(
+ HWND windowHandle,
+ ffi.Pointer<_WindowConstraintsRequest> constraints,
+ );
+
+ @ffi.Native<ffi.Void Function(HWND, ffi.Int32)>(symbol: 'ShowWindow')
+ external static void showWindow(HWND windowHandle, int command);
+
+ @ffi.Native<ffi.Int32 Function(HWND)>(symbol: 'IsIconic')
+ external static int isIconic(HWND windowHandle);
+
+ @ffi.Native<ffi.Int32 Function(HWND)>(symbol: 'IsZoomed')
+ external static int isZoomed(HWND windowHandle);
+
+ static void setFullscreen(
+ ffi.Allocator allocator,
+ HWND windowHandle,
+ bool fullscreen, {
+ Display? display,
+ }) {
+ final ffi.Pointer<_WindowFullscreenRequest> request = allocator<_WindowFullscreenRequest>();
+ try {
+ request.ref.fullscreen = fullscreen;
+ request.ref.hasDisplayId = display != null;
+ request.ref.displayId = display?.id ?? 0;
+ _setFullscreen(windowHandle, request);
+ } finally {
+ allocator.free(request);
+ }
+ }
+
+ @ffi.Native<ffi.Void Function(HWND, ffi.Pointer<_WindowFullscreenRequest>)>(
+ symbol: 'InternalFlutterWindows_WindowManager_SetFullscreen',
+ )
+ external static void _setFullscreen(
+ HWND windowHandle,
+ ffi.Pointer<_WindowFullscreenRequest> request,
+ );
+
+ @ffi.Native<ffi.Bool Function(HWND)>(symbol: 'InternalFlutterWindows_WindowManager_GetFullscreen')
+ external static bool getFullscreen(HWND windowHandle);
+
+ @ffi.Native<ffi.Int32 Function(HWND)>(symbol: 'GetWindowTextLengthW')
+ external static int _getWindowTextLength(HWND windowHandle);
+
+ @ffi.Native<ffi.Int32 Function(HWND, ffi.Pointer<_Utf16>, ffi.Int32)>(symbol: 'GetWindowTextW')
+ external static int _getWindowText(
+ HWND windowHandle,
+ ffi.Pointer<_Utf16> lpString,
+ int maxLength,
+ );
+
+ static String getWindowTitle(ffi.Allocator allocator, HWND windowHandle) {
+ final int length = _getWindowTextLength(windowHandle);
+ if (length == 0) {
+ return '';
+ }
+
+ final ffi.Pointer<ffi.Uint16> data = allocator<ffi.Uint16>(length + 1);
+ try {
+ final ffi.Pointer<_Utf16> buffer = data.cast<_Utf16>();
+ _getWindowText(windowHandle, buffer, length + 1);
+ return buffer.toDartString();
+ } finally {
+ allocator.free(data);
+ }
+ }
+
+ @ffi.Native<HWND Function()>(symbol: 'GetForegroundWindow')
+ external static HWND getForegroundWindow();
+}
+
+/// Payload for the creation method used by [_Win32PlatformInterface.createWindow].
+final class _WindowCreationRequest extends ffi.Struct {
+ external _WindowSizeRequest preferredSize;
+ external _WindowConstraintsRequest preferredConstraints;
+ external ffi.Pointer<_Utf16> title;
+}
+
+/// Payload for the initialization request for the windowing subsystem used
+/// by the constructor for [WindowingOwnerWin32].
+final class _WindowingInitRequest extends ffi.Struct {
+ external ffi.Pointer<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<_WindowsMessage>)>>
+ onMessage;
+}
+
+/// Payload for the size of a window used by [_WindowCreationRequest] and
+/// [_Win32PlatformInterface.setWindowContentSize].
+final class _WindowSizeRequest extends ffi.Struct {
+ @ffi.Bool()
+ external bool hasSize;
+
+ @ffi.Double()
+ external double width;
+
+ @ffi.Double()
+ external double height;
+
+ void from(Size? size) {
+ hasSize = size != null;
+ width = size?.width ?? 0;
+ height = size?.height ?? 0;
+ }
+}
+
+/// Payload for the constraints of a window used by [_WindowCreationRequest] and
+/// [_Win32PlatformInterface.setWindowConstraints].
+final class _WindowConstraintsRequest extends ffi.Struct {
+ @ffi.Bool()
+ external bool hasConstraints;
+
+ @ffi.Double()
+ external double minWidth;
+
+ @ffi.Double()
+ external double minHeight;
+
+ @ffi.Double()
+ external double maxWidth;
+
+ @ffi.Double()
+ external double maxHeight;
+
+ void from(BoxConstraints? constraints) {
+ hasConstraints = constraints != null;
+ minWidth = constraints?.minWidth ?? 0;
+ minHeight = constraints?.minHeight ?? 0;
+ maxWidth = constraints?.maxWidth ?? double.maxFinite;
+ maxHeight = constraints?.maxHeight ?? double.maxFinite;
+ }
+}
+
+/// A message received for all toplevel windows, used by [_WindowingInitRequest].
+final class _WindowsMessage extends ffi.Struct {
+ @ffi.Int64()
+ external int viewId;
+
+ external HWND windowHandle;
+
+ @ffi.Int32()
+ external int message;
+
+ @ffi.Int64()
+ external int wParam;
+
+ @ffi.Int64()
+ external int lParam;
+
+ @ffi.Int64()
+ external int lResult;
+
+ @ffi.Bool()
+ external bool handled;
+}
+
+/// Holds the real size of a window as retrieved from
+/// [_Win32PlatformInterface.getWindowContentSize].
+final class _ActualContentSize extends ffi.Struct {
+ @ffi.Double()
+ external double width;
+
+ @ffi.Double()
+ external double height;
+}
+
+/// Payload for the [_Win32PlatformInterface.setFullscreen] request.
+final class _WindowFullscreenRequest extends ffi.Struct {
+ @ffi.Bool()
+ external bool fullscreen;
+
+ @ffi.Bool()
+ external bool hasDisplayId;
+
+ @ffi.Uint64()
+ external int displayId;
+}
+
+/// The contents of a native zero-terminated array of UTF-16 code units.
+///
+/// The Utf16 type itself has no functionality, it's only intended to be used
+/// through a `Pointer<Utf16>` representing the entire array. This pointer is
+/// the equivalent of a char pointer (`const wchar_t*`) in C code. The
+/// individual UTF-16 code units are stored in native byte order.
+final class _Utf16 extends ffi.Opaque {}
+
+/// Extension method for converting a`Pointer<Utf16>` to a [String].
+extension _Utf16Pointer on ffi.Pointer<_Utf16> {
+ /// Converts this UTF-16 encoded string to a Dart string.
+ ///
+ /// Decodes the UTF-16 code units of this zero-terminated code unit array as
+ /// Unicode code points and creates a Dart string containing those code
+ /// points.
+ ///
+ /// If [length] is provided, zero-termination is ignored and the result can
+ /// contain NUL characters.
+ ///
+ /// If [length] is not provided, the returned string is the string up til
+ /// but not including the first NUL character.
+ String toDartString({int? length}) {
+ _ensureNotNullptr('toDartString');
+ final ffi.Pointer<ffi.Uint16> codeUnits = cast<ffi.Uint16>();
+ if (length == null) {
+ return _toUnknownLengthString(codeUnits);
+ } else {
+ RangeError.checkNotNegative(length, 'length');
+ return _toKnownLengthString(codeUnits, length);
+ }
+ }
+
+ static String _toKnownLengthString(ffi.Pointer<ffi.Uint16> codeUnits, int length) =>
+ String.fromCharCodes(codeUnits.asTypedList(length));
+
+ static String _toUnknownLengthString(ffi.Pointer<ffi.Uint16> codeUnits) {
+ final StringBuffer buffer = StringBuffer();
+ int i = 0;
+ while (true) {
+ final int char = (codeUnits + i).value;
+ if (char == 0) {
+ return buffer.toString();
+ }
+ buffer.writeCharCode(char);
+ i++;
+ }
+ }
+
+ void _ensureNotNullptr(String operation) {
+ if (this == ffi.nullptr) {
+ throw UnsupportedError("Operation '$operation' not allowed on a 'nullptr'.");
+ }
+ }
+}
+
+/// Extension method for converting a [String] to a `Pointer<Utf16>`.
+extension _StringUtf16Pointer on String {
+ /// Creates a zero-terminated [Utf16] code-unit array from this String.
+ ///
+ /// If this [String] contains NUL characters, converting it back to a string
+ /// using [Utf16Pointer.toDartString] will truncate the result if a length is
+ /// not passed.
+ ///
+ /// Returns an [allocator]-allocated pointer to the result.
+ ffi.Pointer<_Utf16> toNativeUtf16({required ffi.Allocator allocator}) {
+ final List<int> units = codeUnits;
+ final ffi.Pointer<ffi.Uint16> result = allocator<ffi.Uint16>(units.length + 1);
+ final Uint16List nativeString = result.asTypedList(units.length + 1);
+ nativeString.setRange(0, units.length, units);
+ nativeString[units.length] = 0;
+ return result.cast();
+ }
+}
+
+typedef _WinCoTaskMemAllocNative = ffi.Pointer<ffi.NativeType> Function(ffi.Size);
+typedef _WinCoTaskMemAlloc = ffi.Pointer<ffi.NativeType> Function(int);
+typedef _WinCoTaskMemFreeNative = ffi.Void Function(ffi.Pointer<ffi.NativeType>);
+typedef _WinCoTaskMemFree = void Function(ffi.Pointer<ffi.NativeType>);
+
+final class _CallocAllocator implements ffi.Allocator {
+ _CallocAllocator() {
+ _ole32lib = ffi.DynamicLibrary.open('ole32.dll');
+ _winCoTaskMemAlloc = _ole32lib.lookupFunction<_WinCoTaskMemAllocNative, _WinCoTaskMemAlloc>(
+ 'CoTaskMemAlloc',
+ );
+ _winCoTaskMemFreePointer = _ole32lib.lookup('CoTaskMemFree');
+ _winCoTaskMemFree = _winCoTaskMemFreePointer.asFunction();
+ }
+
+ late final ffi.DynamicLibrary _ole32lib;
+ late final _WinCoTaskMemAlloc _winCoTaskMemAlloc;
+ late final ffi.Pointer<ffi.NativeFunction<_WinCoTaskMemFreeNative>> _winCoTaskMemFreePointer;
+ late final _WinCoTaskMemFree _winCoTaskMemFree;
+
+ /// Fills a block of memory with a specified value.
+ // ignore: always_specify_types
+ void _fillMemory(ffi.Pointer destination, int length, int fill) {
+ final ffi.Pointer<ffi.Uint8> ptr = destination.cast<ffi.Uint8>();
+ for (int i = 0; i < length; i++) {
+ ptr[i] = fill;
+ }
+ }
+
+ /// Fills a block of memory with zeros.
+ // ignore: always_specify_types
+ void _zeroMemory(ffi.Pointer destination, int length) => _fillMemory(destination, length, 0);
+
+ /// Allocates [byteCount] bytes of zero-initialized of memory on the native
+ /// heap.
+ @override
+ ffi.Pointer<T> allocate<T extends ffi.NativeType>(int byteCount, {int? alignment}) {
+ ffi.Pointer<T> result;
+ result = _winCoTaskMemAlloc(byteCount).cast();
+ if (result.address == 0) {
+ throw ArgumentError('Could not allocate $byteCount bytes.');
+ }
+ if (Platform.isWindows) {
+ _zeroMemory(result, byteCount);
+ }
+ return result;
+ }
+
+ /// Releases memory allocated on the native heap.
+ @override
+ // ignore: always_specify_types
+ void free(ffi.Pointer pointer) {
+ _winCoTaskMemFree(pointer);
+ }
+
+ /// Returns a pointer to a native free function.
+ ffi.Pointer<ffi.NativeFinalizerFunction> get nativeFree => _winCoTaskMemFreePointer;
+}
diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart
index 61e8030..4152d0f 100644
--- a/packages/flutter/lib/src/widgets/binding.dart
+++ b/packages/flutter/lib/src/widgets/binding.dart
@@ -461,7 +461,7 @@
return true;
}());
platformMenuDelegate = DefaultPlatformMenuDelegate();
- _windowingOwner = WindowingOwner.createDefaultOwner();
+ _windowingOwner = createDefaultWindowingOwner();
}
/// The current [WidgetsBinding], if one has been created.
diff --git a/packages/flutter/test/widgets/windowing_test.dart b/packages/flutter/test/widgets/windowing_test.dart
index aec8eef..ca8995b 100644
--- a/packages/flutter/test/widgets/windowing_test.dart
+++ b/packages/flutter/test/widgets/windowing_test.dart
@@ -11,7 +11,8 @@
RegularWindowController,
RegularWindowControllerDelegate,
WindowScope,
- WindowingOwner;
+ WindowingOwner,
+ createDefaultWindowingOwner;
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -73,12 +74,12 @@
});
test('createDefaultOwner returns a WindowingOwner', () {
- final WindowingOwner owner = WindowingOwner.createDefaultOwner();
+ final WindowingOwner owner = createDefaultWindowingOwner();
expect(owner, isA<WindowingOwner>());
});
test('default WindowingOwner throws when accessing createRegularWindowController', () {
- final WindowingOwner owner = WindowingOwner.createDefaultOwner();
+ final WindowingOwner owner = createDefaultWindowingOwner();
expect(
() => owner.createRegularWindowController(delegate: RegularWindowControllerDelegate()),
throwsUnsupportedError,
@@ -86,7 +87,7 @@
});
test('default WindowingOwner throws when accessing hasTopLevelWindows', () {
- final WindowingOwner owner = WindowingOwner.createDefaultOwner();
+ final WindowingOwner owner = createDefaultWindowingOwner();
expect(() => owner.hasTopLevelWindows(), throwsUnsupportedError);
});
@@ -110,23 +111,22 @@
isWindowingEnabled = true;
});
- test('createDefaultOwner returns a WindowingOwner', () {
- final WindowingOwner owner = WindowingOwner.createDefaultOwner();
- expect(owner, isA<WindowingOwner>());
- });
-
testWidgets('RegularWindow does not throw', (WidgetTester tester) async {
+ final _StubWindowController controller = _StubWindowController(tester);
+ addTearDown(controller.dispose);
await tester.pumpWidget(
wrapWithView: false,
- RegularWindow(controller: _StubWindowController(tester), child: Container()),
+ RegularWindow(controller: controller, child: Container()),
);
});
testWidgets('Can access WindowScope.of', (WidgetTester tester) async {
+ final _StubWindowController controller = _StubWindowController(tester);
+ addTearDown(controller.dispose);
await tester.pumpWidget(
wrapWithView: false,
RegularWindow(
- controller: _StubWindowController(tester),
+ controller: controller,
child: Builder(
builder: (BuildContext context) {
final BaseWindowController scope = WindowScope.of(context);
@@ -139,10 +139,12 @@
});
testWidgets('Can access WindowScope.maybeOf', (WidgetTester tester) async {
+ final _StubWindowController controller = _StubWindowController(tester);
+ addTearDown(controller.dispose);
await tester.pumpWidget(
wrapWithView: false,
RegularWindow(
- controller: _StubWindowController(tester),
+ controller: controller,
child: Builder(
builder: (BuildContext context) {
final BaseWindowController? scope = WindowScope.maybeOf(context);
@@ -155,10 +157,12 @@
});
testWidgets('Can access WindowScope.contentSizeOf', (WidgetTester tester) async {
+ final _StubWindowController controller = _StubWindowController(tester);
+ addTearDown(controller.dispose);
await tester.pumpWidget(
wrapWithView: false,
RegularWindow(
- controller: _StubWindowController(tester),
+ controller: controller,
child: Builder(
builder: (BuildContext context) {
final Size size = WindowScope.contentSizeOf(context);
@@ -171,10 +175,12 @@
});
testWidgets('Can access WindowScope.maybeContentSizeOf', (WidgetTester tester) async {
+ final _StubWindowController controller = _StubWindowController(tester);
+ addTearDown(controller.dispose);
await tester.pumpWidget(
wrapWithView: false,
RegularWindow(
- controller: _StubWindowController(tester),
+ controller: controller,
child: Builder(
builder: (BuildContext context) {
final Size? size = WindowScope.maybeContentSizeOf(context);
@@ -187,10 +193,12 @@
});
testWidgets('Can access WindowScope.titleOf', (WidgetTester tester) async {
+ final _StubWindowController controller = _StubWindowController(tester);
+ addTearDown(controller.dispose);
await tester.pumpWidget(
wrapWithView: false,
RegularWindow(
- controller: _StubWindowController(tester),
+ controller: controller,
child: Builder(
builder: (BuildContext context) {
final String title = WindowScope.titleOf(context);
@@ -203,10 +211,12 @@
});
testWidgets('Can access WindowScope.maybeTitleOf', (WidgetTester tester) async {
+ final _StubWindowController controller = _StubWindowController(tester);
+ addTearDown(controller.dispose);
await tester.pumpWidget(
wrapWithView: false,
RegularWindow(
- controller: _StubWindowController(tester),
+ controller: controller,
child: Builder(
builder: (BuildContext context) {
final String? title = WindowScope.maybeTitleOf(context);
@@ -219,10 +229,12 @@
});
testWidgets('Can access WindowScope.isActivatedOf', (WidgetTester tester) async {
+ final _StubWindowController controller = _StubWindowController(tester);
+ addTearDown(controller.dispose);
await tester.pumpWidget(
wrapWithView: false,
RegularWindow(
- controller: _StubWindowController(tester),
+ controller: controller,
child: Builder(
builder: (BuildContext context) {
final bool isActivated = WindowScope.isActivatedOf(context);
@@ -235,10 +247,12 @@
});
testWidgets('Can access WindowScope.maybeIsActivatedOf', (WidgetTester tester) async {
+ final _StubWindowController controller = _StubWindowController(tester);
+ addTearDown(controller.dispose);
await tester.pumpWidget(
wrapWithView: false,
RegularWindow(
- controller: _StubWindowController(tester),
+ controller: controller,
child: Builder(
builder: (BuildContext context) {
final bool? isActivated = WindowScope.maybeIsActivatedOf(context);
@@ -251,10 +265,12 @@
});
testWidgets('Can access WindowScope.isMinimizedOf', (WidgetTester tester) async {
+ final _StubWindowController controller = _StubWindowController(tester);
+ addTearDown(controller.dispose);
await tester.pumpWidget(
wrapWithView: false,
RegularWindow(
- controller: _StubWindowController(tester),
+ controller: controller,
child: Builder(
builder: (BuildContext context) {
final bool isMinimized = WindowScope.isMinimizedOf(context);
@@ -267,10 +283,12 @@
});
testWidgets('Can access WindowScope.maybeIsMinimizedOf', (WidgetTester tester) async {
+ final _StubWindowController controller = _StubWindowController(tester);
+ addTearDown(controller.dispose);
await tester.pumpWidget(
wrapWithView: false,
RegularWindow(
- controller: _StubWindowController(tester),
+ controller: controller,
child: Builder(
builder: (BuildContext context) {
final bool? isMinimized = WindowScope.maybeIsMinimizedOf(context);
@@ -283,10 +301,12 @@
});
testWidgets('Can access WindowScope.isMaximizedOf', (WidgetTester tester) async {
+ final _StubWindowController controller = _StubWindowController(tester);
+ addTearDown(controller.dispose);
await tester.pumpWidget(
wrapWithView: false,
RegularWindow(
- controller: _StubWindowController(tester),
+ controller: controller,
child: Builder(
builder: (BuildContext context) {
final bool isMaximized = WindowScope.isMaximizedOf(context);
@@ -299,10 +319,12 @@
});
testWidgets('Can access WindowScope.maybeIsMaximizedOf', (WidgetTester tester) async {
+ final _StubWindowController controller = _StubWindowController(tester);
+ addTearDown(controller.dispose);
await tester.pumpWidget(
wrapWithView: false,
RegularWindow(
- controller: _StubWindowController(tester),
+ controller: controller,
child: Builder(
builder: (BuildContext context) {
final bool? isMaximized = WindowScope.maybeIsMaximizedOf(context);
@@ -315,10 +337,12 @@
});
testWidgets('Can access WindowScope.isFullscreenOf', (WidgetTester tester) async {
+ final _StubWindowController controller = _StubWindowController(tester);
+ addTearDown(controller.dispose);
await tester.pumpWidget(
wrapWithView: false,
RegularWindow(
- controller: _StubWindowController(tester),
+ controller: controller,
child: Builder(
builder: (BuildContext context) {
final bool isFullscreen = WindowScope.isFullscreenOf(context);
@@ -331,10 +355,12 @@
});
testWidgets('Can access WindowScope.maybeIsFullscreenOf', (WidgetTester tester) async {
+ final _StubWindowController controller = _StubWindowController(tester);
+ addTearDown(controller.dispose);
await tester.pumpWidget(
wrapWithView: false,
RegularWindow(
- controller: _StubWindowController(tester),
+ controller: controller,
child: Builder(
builder: (BuildContext context) {
final bool? isFullscreen = WindowScope.maybeIsFullscreenOf(context);
diff --git a/packages/flutter_test/lib/src/window.dart b/packages/flutter_test/lib/src/window.dart
index 5148942..78e7af5 100644
--- a/packages/flutter_test/lib/src/window.dart
+++ b/packages/flutter_test/lib/src/window.dart
@@ -175,6 +175,9 @@
: null;
}
+ @override
+ int? get engineId => 1;
+
final Map<int, TestFlutterView> _testViews = <int, TestFlutterView>{};
final Map<int, TestDisplay> _testDisplays = <int, TestDisplay>{};