From 9ec8047767da442c2689a46bcdaebbb3e298532c Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Wed, 17 Apr 2024 11:04:32 -0700 Subject: [PATCH] Add multi-window sample --- LICENSE | 25 ++++++++ README.md | 21 +++---- lib/main.dart | 73 ++++------------------- lib/src/widgets.dart | 73 +++++++++++++++++++++++ windows/runner/flutter_window.cpp | 60 +++++++++---------- windows/runner/flutter_window.h | 14 ++--- windows/runner/flutter_windows_internal.h | 72 ++++++++++++++++++++++ windows/runner/main.cpp | 43 +++++++++---- windows/runner/win32_window.cpp | 2 +- 9 files changed, 254 insertions(+), 129 deletions(-) create mode 100644 LICENSE create mode 100644 lib/src/widgets.dart create mode 100644 windows/runner/flutter_windows_internal.h diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aea51a3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright 2014 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 401d356..e3142ff 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,9 @@ -# windows_c_multi_window +# Windows multi-window sample -A new Flutter project. +This repository contains an experimental Flutter Windows app that launches +multiple windows. This sample requires +[Flutter's `master` channel](https://docs.flutter.dev/release/upgrade#changing-channels). -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +This sample is aspirational, may not actually work, may be outdated, and/or have +other issues. This sample links against experimental Flutter Windows APIs that +may break at any time. DO NOT DEPEND ON ANYTHING IN THIS REPOSITORY. \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 8e94089..67fb7ed 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,37 +1,28 @@ +// 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 'src/widgets.dart'; void main() { - runApp(const MyApp()); + runWidget(MultiViewApp( + viewBuilder: (BuildContext context) => const Counter(), + )); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +class Counter extends StatelessWidget { + const Counter({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), + home: MyHomePage(title: 'Counter View ${View.of(context).viewId}'), ); } } @@ -39,15 +30,6 @@ class MyApp extends StatelessWidget { class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - final String title; @override @@ -59,50 +41,19 @@ class _MyHomePageState extends State { void _incrementCounter() { setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. _counter++; }); } @override Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( @@ -119,7 +70,7 @@ class _MyHomePageState extends State { onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. + ), ); } } diff --git a/lib/src/widgets.dart b/lib/src/widgets.dart new file mode 100644 index 0000000..5beed27 --- /dev/null +++ b/lib/src/widgets.dart @@ -0,0 +1,73 @@ +// 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:ui' show FlutterView; + +import 'package:flutter/widgets.dart'; + +/// Calls [viewBuilder] for every view added to the app to obtain the widget to +/// render into that view. The current view can be looked up with [View.of]. +class MultiViewApp extends StatefulWidget { + const MultiViewApp({super.key, required this.viewBuilder}); + + final WidgetBuilder viewBuilder; + + @override + State createState() => _MultiViewAppState(); +} + +class _MultiViewAppState extends State with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _updateViews(); + } + + @override + void didUpdateWidget(MultiViewApp oldWidget) { + super.didUpdateWidget(oldWidget); + // Need to re-evaluate the viewBuilder callback for all views. + _views.clear(); + _updateViews(); + } + + @override + void didChangeMetrics() { + _updateViews(); + } + + Map _views = {}; + + void _updateViews() { + final Map newViews = {}; + for (final FlutterView view in WidgetsBinding.instance.platformDispatcher.views) { + final Widget viewWidget = _views[view.viewId] ?? _createViewWidget(view); + newViews[view.viewId] = viewWidget; + } + setState(() { + _views = newViews; + }); + } + + Widget _createViewWidget(FlutterView view) { + return View( + view: view, + child: Builder( + builder: widget.viewBuilder, + ), + ); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ViewCollection(views: _views.values.toList(growable: false)); + } +} diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index 955ee30..f660593 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -3,9 +3,10 @@ #include #include "flutter/generated_plugin_registrant.h" +#include "flutter_windows_internal.h" -FlutterWindow::FlutterWindow(const flutter::DartProject& project) - : project_(project) {} +FlutterWindow::FlutterWindow(FlutterDesktopEngineRef engine) + : engine_(engine) {} FlutterWindow::~FlutterWindow() {} @@ -18,30 +19,23 @@ bool FlutterWindow::OnCreate() { // The size here must match the window dimensions to avoid unnecessary surface // creation / destruction in the startup path. - flutter_controller_ = std::make_unique( - 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()) { + FlutterDesktopViewControllerProperties properties = {}; + properties.width = frame.right - frame.left; + properties.height = frame.bottom - frame.top; + + controller_ = FlutterDesktopEngineCreateViewController(engine_, &properties); + if (controller_ == nullptr) { 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(); + FlutterDesktopViewRef view{ FlutterDesktopViewControllerGetView(controller_) }; + SetChildContent(FlutterDesktopViewGetHWND(view)); return true; } void FlutterWindow::OnDestroy() { - if (flutter_controller_) { - flutter_controller_ = nullptr; + if (controller_) { + FlutterDesktopViewControllerDestroy(controller_); } Win32Window::OnDestroy(); @@ -49,22 +43,28 @@ void FlutterWindow::OnDestroy() { LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { + WPARAM const wparam, + LPARAM const lparam) noexcept { // Give Flutter, including plugins, an opportunity to handle window messages. - if (flutter_controller_) { - std::optional result = - flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, - lparam); - if (result) { - return *result; + if (controller_) { + LRESULT result; + bool handled = FlutterDesktopViewControllerHandleTopLevelWindowProc( + controller_, + hwnd, + message, + wparam, + lparam, + &result); + + if (handled) { + return result; } } switch (message) { - case WM_FONTCHANGE: - flutter_controller_->engine()->ReloadSystemFonts(); - break; + case WM_FONTCHANGE: + FlutterDesktopEngineReloadSystemFonts(engine_); + break; } return Win32Window::MessageHandler(hwnd, message, wparam, lparam); diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h index 6da0652..fffa57d 100644 --- a/windows/runner/flutter_window.h +++ b/windows/runner/flutter_window.h @@ -1,9 +1,7 @@ #ifndef RUNNER_FLUTTER_WINDOW_H_ #define RUNNER_FLUTTER_WINDOW_H_ -#include -#include - +#include #include #include "win32_window.h" @@ -11,8 +9,7 @@ // 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); + explicit FlutterWindow(FlutterDesktopEngineRef engine); virtual ~FlutterWindow(); protected: @@ -23,11 +20,8 @@ class FlutterWindow : public Win32Window { LPARAM const lparam) noexcept override; private: - // The project to run. - flutter::DartProject project_; - - // The Flutter instance hosted by this window. - std::unique_ptr flutter_controller_; + FlutterDesktopEngineRef engine_ = nullptr; + FlutterDesktopViewControllerRef controller_ = nullptr; }; #endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/flutter_windows_internal.h b/windows/runner/flutter_windows_internal.h new file mode 100644 index 0000000..5772dd9 --- /dev/null +++ b/windows/runner/flutter_windows_internal.h @@ -0,0 +1,72 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_WINDOWS_INTERNAL_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_WINDOWS_INTERNAL_H_ + +#include + +#if defined(__cplusplus) +extern "C" { +#endif + +// Declare functions that are currently in-progress and shall be exposed to the +// public facing API upon completion. + +// Properties for configuring a Flutter view controller. +typedef struct { + // The view's initial width. + int width; + + // The view's initial height. + int height; +} FlutterDesktopViewControllerProperties; + +// Creates a view for the given engine. +// +// The |engine| will be started if it is not already running. +// +// The caller owns the returned reference, and is responsible for calling +// |FlutterDesktopViewControllerDestroy|. Returns a null pointer in the event of +// an error. +// +// Unlike |FlutterDesktopViewControllerCreate|, this does *not* take ownership +// of |engine| and |FlutterDesktopEngineDestroy| must be called to destroy +// the engine. +FLUTTER_EXPORT FlutterDesktopViewControllerRef +FlutterDesktopEngineCreateViewController( + FlutterDesktopEngineRef engine, + const FlutterDesktopViewControllerProperties* properties); + +typedef int64_t PlatformViewId; + +typedef struct { + size_t struct_size; + HWND parent_window; + const char* platform_view_type; + // user_data may hold any necessary additional information for creating a new + // platform view. For example, an instance of FlutterWindow. + void* user_data; + PlatformViewId platform_view_id; +} FlutterPlatformViewCreationParameters; + +typedef HWND (*FlutterPlatformViewFactory)( + const FlutterPlatformViewCreationParameters*); + +typedef struct { + size_t struct_size; + FlutterPlatformViewFactory factory; + void* user_data; // Arbitrary user data supplied to the creation struct. +} FlutterPlatformViewTypeEntry; + +FLUTTER_EXPORT void FlutterDesktopEngineRegisterPlatformViewType( + FlutterDesktopEngineRef engine, + const char* view_type_name, + FlutterPlatformViewTypeEntry view_type); + +#if defined(__cplusplus) +} +#endif + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_WINDOWS_INTERNAL_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 264530f..beb46d3 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -1,5 +1,6 @@ -#include -#include +#include +#include +#include #include #include "flutter_window.h" @@ -17,20 +18,36 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - flutter::DartProject project(L"data"); - std::vector 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"windows_c_multi_window", origin, size)) { + GetCommandLineArguments(); + std::vector entrypoint_argv; + std::transform( + command_line_arguments.begin(), command_line_arguments.end(), + std::back_inserter(entrypoint_argv), + [](const std::string& arg) -> const char* { return arg.c_str(); }); + FlutterDesktopEngineProperties engine_properties{ + /*assets_path=*/L"data\\flutter_assets", + /*icu_data_path=*/L"data\\icudtl.dat", + /*aot_library_path=*/L"data\\app.so", + /*dart_entrypoint=*/nullptr, + /*dart_entrypoint_argc=*/static_cast(entrypoint_argv.size()), + /*dart_entrypoint_argv=*/entrypoint_argv.empty() ? nullptr : entrypoint_argv.data(), + }; + + FlutterDesktopEngineRef engine{FlutterDesktopEngineCreate(&engine_properties)}; + + // RegisterPlugins(engine.get()); + + Win32Window::Point origin1{10, 10}; + Win32Window::Point origin2{50, 50}; + Win32Window::Size size{1280, 720}; + + FlutterWindow window1{engine}; + FlutterWindow window2{engine}; + if (!window1.Create(L"Window #1", origin1, size) + || !window2.Create(L"Window #2", origin2, size)) { return EXIT_FAILURE; } - window.SetQuitOnClose(true); ::MSG msg; while (::GetMessage(&msg, nullptr, 0, 0)) { diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp index 60608d0..b60b5ab 100644 --- a/windows/runner/win32_window.cpp +++ b/windows/runner/win32_window.cpp @@ -135,7 +135,7 @@ bool Win32Window::Create(const std::wstring& title, double scale_factor = dpi / 96.0; HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, 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);