From 988e8d4fc66e59008c3e8ef91ec30a56b6eeecbf Mon Sep 17 00:00:00 2001 From: alvocool Date: Sun, 11 Oct 2026 10:16:43 +0300 Subject: [PATCH] Add windows (part 1) Co-Authored-By: Claude Opus 5.5 (1M context) --- windows/.gitignore | 17 ++ windows/CMakeLists.txt | 108 +++++++++ windows/flutter/CMakeLists.txt | 109 +++++++++ windows/flutter/generated_plugins.cmake | 26 +++ 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/app_icon.ico | Bin 0 -> 23933 bytes windows/runner/runner.exe.manifest | 14 ++ windows/runner/utils.cpp | 69 ++++++ windows/runner/utils.h | 19 ++ windows/runner/win32_window.cpp | 288 ++++++++++++++++++++++++ windows/runner/win32_window.h | 102 +++++++++ 16 files changed, 1076 insertions(+) create mode 100644 windows/.gitignore create mode 100644 windows/CMakeLists.txt create mode 100644 windows/flutter/CMakeLists.txt 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/app_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/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..a7021b2 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(peach 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 "peach") + +# 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_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..4202d48 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,26 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + desktop_drop + flutter_secure_storage_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni +) + +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..5682394 --- /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\\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", "nya.application" "\0" + VALUE "FileDescription", "Peach" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "peach" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 nya.application. All rights reserved." "\0" + VALUE "OriginalFilename", "peach.exe" "\0" + VALUE "ProductName", "Peach" "\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..d613e1a --- /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"Peach", 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/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4bfa46c9f262aa8f897f2ec8588bc37ba51ad731 GIT binary patch literal 23933 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 CEd5OY 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..3cb7146 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,69 @@ +#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(); + } + // First, find the length of the string with a safe upper bound (CWE-126). + // UNICODE_STRING_MAX_CHARS (32767) is the maximum length of a UNICODE_STRING. + int input_length = static_cast(wcsnlen(utf16_string, UNICODE_STRING_MAX_CHARS)); + // Now use that bounded length to determine the required buffer size. + // When an explicit length is passed, WideCharToMultiByte does not include + // the null terminator in its returned size. + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || static_cast(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_