From 28347af927349c71f915fd799386760ed20c4045 Mon Sep 17 00:00:00 2001 From: x100111010 <167847953+x100111010@users.noreply.github.com> Date: Wed, 2 Oct 2024 23:28:04 +0200 Subject: [PATCH] add windows build --- .github/workflows/deploy.yml | 48 ++- windows/.gitignore | 17 ++ windows/CMakeLists.txt | 108 +++++++ windows/flutter/CMakeLists.txt | 109 +++++++ .../flutter/generated_plugin_registrant.cc | 29 ++ windows/flutter/generated_plugin_registrant.h | 15 + windows/flutter/generated_plugins.cmake | 29 ++ windows/runner/CMakeLists.txt | 40 +++ windows/runner/Runner.rc | 121 ++++++++ windows/runner/flutter_window.cpp | 71 +++++ windows/runner/flutter_window.h | 33 ++ windows/runner/main.cpp | 43 +++ windows/runner/resource.h | 16 + windows/runner/resources/spr_icon.ico | Bin 0 -> 180460 bytes windows/runner/runner.exe.manifest | 14 + windows/runner/utils.cpp | 65 ++++ windows/runner/utils.h | 19 ++ windows/runner/win32_window.cpp | 288 ++++++++++++++++++ windows/runner/win32_window.h | 102 +++++++ 19 files changed, 1166 insertions(+), 1 deletion(-) create mode 100644 windows/.gitignore create mode 100644 windows/CMakeLists.txt create mode 100644 windows/flutter/CMakeLists.txt create mode 100644 windows/flutter/generated_plugin_registrant.cc create mode 100644 windows/flutter/generated_plugin_registrant.h create mode 100644 windows/flutter/generated_plugins.cmake create mode 100644 windows/runner/CMakeLists.txt create mode 100644 windows/runner/Runner.rc create mode 100644 windows/runner/flutter_window.cpp create mode 100644 windows/runner/flutter_window.h create mode 100644 windows/runner/main.cpp create mode 100644 windows/runner/resource.h create mode 100644 windows/runner/resources/spr_icon.ico create mode 100644 windows/runner/runner.exe.manifest create mode 100644 windows/runner/utils.cpp create mode 100644 windows/runner/utils.h create mode 100644 windows/runner/win32_window.cpp create mode 100644 windows/runner/win32_window.h diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 48d4a5b..332e3f1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,7 +5,7 @@ on: types: [published] jobs: - build: + build-linux-android: runs-on: ubuntu-latest steps: @@ -78,3 +78,49 @@ jobs: files: | bin/*.apk bin/spectrum-${{ github.event.release.tag_name }}-linux.tar.gz + + + build-windows: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: 3.24.0 + - run: flutter --version + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + cache: 'gradle' + + - name: Get dependencies + run: flutter pub get + + - name: Run code generation + run: dart run build_runner build + + - name: Build Windows app + run: | + flutter build windows --release + + # Build MSIX Packaging + dart run msix:create + + # 'winbin' directory + mkdir winbin + + # create a tar.gz archive with all necessary files + powershell Compress-Archive -Path build/windows/x64/runner/Release -DestinationPath winbin/spectrum-${{ github.event.release.tag_name }}-windows.zip + + - name: Upload release assets + uses: softprops/action-gh-release@v2 + with: + files: winbin/*.zip \ No newline at end of file diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/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/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..83948b5 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(spectrum 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 "spectrum") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/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} $ + 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/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..cb703cd --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,29 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + LocalAuthPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("LocalAuthPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..68b4535 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,29 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + app_links + file_selector_windows + flutter_secure_storage_windows + local_auth_windows + share_plus + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/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/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..69d9349 --- /dev/null +++ b/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\\spr_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", "org.spectre" "\0" + VALUE "FileDescription", "spectrum" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "spectrum" "\0" + VALUE "LegalCopyright", "Copyright (C) 2024 org.spectre. All rights reserved." "\0" + VALUE "OriginalFilename", "spectrum.exe" "\0" + VALUE "ProductName", "spectrum" "\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/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#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( + 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 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/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#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_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..df3ed9a --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#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 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"spectrum", 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/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{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/windows/runner/resources/spr_icon.ico b/windows/runner/resources/spr_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7d139904c32d0da9d342095284f4f8571c0ce47b GIT binary patch literal 180460 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 E1E0Q#g#Z8m literal 0 HcmV?d00001 diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + 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/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#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(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( + 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(origin.x), + static_cast(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(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(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(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( + 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/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// 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_