From ec08a47de112839a675bcfbcc9d5f0cfafd52aa5 Mon Sep 17 00:00:00 2001 From: Rich Young Date: Wed, 4 Feb 2026 04:41:20 -0500 Subject: [PATCH 01/34] feat(flutter_tools): add linux-gtk template selector --- packages/flutter_tools/lib/src/commands/create.dart | 7 +++++++ .../flutter_tools/lib/src/commands/create_base.dart | 2 ++ packages/flutter_tools/lib/src/template.dart | 12 ++++++++++++ 3 files changed, 21 insertions(+) diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart index b18a4c6e6b5..385ac07213d 100644 --- a/packages/flutter_tools/lib/src/commands/create.dart +++ b/packages/flutter_tools/lib/src/commands/create.dart @@ -83,6 +83,12 @@ class CreateCommand extends FlutterCommand with CreateBase { help: 'The language to use for Android-specific code, either Kotlin (recommended) or Java (legacy).', ); + argParser.addOption( + 'linux-gtk', + defaultsTo: 'gtk4', + allowed: ['gtk4', 'gtk3'], + help: 'Select the GTK version for Linux templates (gtk4 default, gtk3 opt-in).', + ); argParser.addFlag( 'skip-name-checks', help: @@ -459,6 +465,7 @@ class CreateCommand extends FlutterCommand with CreateBase { darwin: includeDarwin, web: includeWeb, linux: includeLinux, + linuxGtkVersion: stringArg('linux-gtk') ?? 'gtk4', macos: includeMacos, windows: includeWindows, dartSdkVersionBounds: '^$dartSdk', diff --git a/packages/flutter_tools/lib/src/commands/create_base.dart b/packages/flutter_tools/lib/src/commands/create_base.dart index 2f10376c82e..271674d5035 100644 --- a/packages/flutter_tools/lib/src/commands/create_base.dart +++ b/packages/flutter_tools/lib/src/commands/create_base.dart @@ -319,6 +319,7 @@ mixin CreateBase on FlutterCommand { bool android = false, bool web = false, bool linux = false, + String linuxGtkVersion = 'gtk4', bool macos = false, bool windows = false, bool darwin = false, @@ -375,6 +376,7 @@ mixin CreateBase on FlutterCommand { 'android': android, 'web': web, 'linux': linux, + 'linuxGtkVersion': linuxGtkVersion, 'macos': macos, 'darwin': darwin, 'sharedDarwinSource': darwin, diff --git a/packages/flutter_tools/lib/src/template.dart b/packages/flutter_tools/lib/src/template.dart index 9f83b0b7299..df47c7e9864 100644 --- a/packages/flutter_tools/lib/src/template.dart +++ b/packages/flutter_tools/lib/src/template.dart @@ -258,6 +258,18 @@ class Template { } // Only build a Linux project if explicitly asked. final bool linux = (context['linux'] as bool?) ?? false; + final String linuxGtkVersion = (context['linuxGtkVersion'] as String?) ?? 'gtk4'; + final bool linuxGtk3Template = relativeDestinationPath.startsWith('linux-gtk3.tmpl'); + final bool linuxGtk4Template = relativeDestinationPath.startsWith('linux-gtk4.tmpl'); + if ((linuxGtk3Template || linuxGtk4Template) && !linux) { + return null; + } + if (linuxGtk3Template && linuxGtkVersion != 'gtk3') { + return null; + } + if (linuxGtk4Template && linuxGtkVersion != 'gtk4') { + return null; + } if (relativeDestinationPath.startsWith('linux.tmpl') && !linux) { return null; } From 7d323983a681bc3d161733f51e6fb3d78224bcb9 Mon Sep 17 00:00:00 2001 From: Rich Young Date: Wed, 4 Feb 2026 04:43:16 -0500 Subject: [PATCH 02/34] feat(templates/linux): split GTK3/GTK4 templates --- docs/gtk4-linux-plan.md | 44 ++++++ .../.gitignore | 0 .../CMakeLists.txt.tmpl | 0 .../flutter/CMakeLists.txt | 0 .../runner/CMakeLists.txt | 0 .../runner/main.cc | 0 .../runner/my_application.cc.tmpl | 0 .../runner/my_application.h | 0 .../templates/app/linux-gtk4.tmpl/.gitignore | 1 + .../app/linux-gtk4.tmpl/CMakeLists.txt.tmpl | 132 ++++++++++++++++ .../linux-gtk4.tmpl/flutter/CMakeLists.txt | 88 +++++++++++ .../app/linux-gtk4.tmpl/runner/CMakeLists.txt | 26 +++ .../app/linux-gtk4.tmpl/runner/main.cc | 6 + .../runner/my_application.cc.tmpl | 148 ++++++++++++++++++ .../linux-gtk4.tmpl/runner/my_application.h | 21 +++ .../CMakeLists.txt.tmpl | 0 .../pluginClassSnakeCase.h.tmpl | 0 .../pluginClassSnakeCase.cc.tmpl | 0 .../pluginClassSnakeCase_private.h.tmpl | 0 .../test/pluginClassSnakeCase_test.cc.tmpl | 0 .../linux-gtk4.tmpl/CMakeLists.txt.tmpl | 94 +++++++++++ .../pluginClassSnakeCase.h.tmpl | 26 +++ .../pluginClassSnakeCase.cc.tmpl | 76 +++++++++ .../pluginClassSnakeCase_private.h.tmpl | 10 ++ .../test/pluginClassSnakeCase_test.cc.tmpl | 31 ++++ .../CMakeLists.txt.tmpl | 0 .../linux-gtk4.tmpl/CMakeLists.txt.tmpl | 22 +++ .../templates/template_manifest.json | 57 ++++--- 28 files changed, 753 insertions(+), 29 deletions(-) create mode 100644 docs/gtk4-linux-plan.md rename packages/flutter_tools/templates/app/{linux.tmpl => linux-gtk3.tmpl}/.gitignore (100%) rename packages/flutter_tools/templates/app/{linux.tmpl => linux-gtk3.tmpl}/CMakeLists.txt.tmpl (100%) rename packages/flutter_tools/templates/app/{linux.tmpl => linux-gtk3.tmpl}/flutter/CMakeLists.txt (100%) rename packages/flutter_tools/templates/app/{linux.tmpl => linux-gtk3.tmpl}/runner/CMakeLists.txt (100%) rename packages/flutter_tools/templates/app/{linux.tmpl => linux-gtk3.tmpl}/runner/main.cc (100%) rename packages/flutter_tools/templates/app/{linux.tmpl => linux-gtk3.tmpl}/runner/my_application.cc.tmpl (100%) rename packages/flutter_tools/templates/app/{linux.tmpl => linux-gtk3.tmpl}/runner/my_application.h (100%) create mode 100644 packages/flutter_tools/templates/app/linux-gtk4.tmpl/.gitignore create mode 100644 packages/flutter_tools/templates/app/linux-gtk4.tmpl/CMakeLists.txt.tmpl create mode 100644 packages/flutter_tools/templates/app/linux-gtk4.tmpl/flutter/CMakeLists.txt create mode 100644 packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/CMakeLists.txt create mode 100644 packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/main.cc create mode 100644 packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/my_application.cc.tmpl create mode 100644 packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/my_application.h rename packages/flutter_tools/templates/plugin/{linux.tmpl => linux-gtk3.tmpl}/CMakeLists.txt.tmpl (100%) rename packages/flutter_tools/templates/plugin/{linux.tmpl => linux-gtk3.tmpl}/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl (100%) rename packages/flutter_tools/templates/plugin/{linux.tmpl => linux-gtk3.tmpl}/pluginClassSnakeCase.cc.tmpl (100%) rename packages/flutter_tools/templates/plugin/{linux.tmpl => linux-gtk3.tmpl}/pluginClassSnakeCase_private.h.tmpl (100%) rename packages/flutter_tools/templates/plugin/{linux.tmpl => linux-gtk3.tmpl}/test/pluginClassSnakeCase_test.cc.tmpl (100%) create mode 100644 packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/CMakeLists.txt.tmpl create mode 100644 packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl create mode 100644 packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/pluginClassSnakeCase.cc.tmpl create mode 100644 packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/pluginClassSnakeCase_private.h.tmpl create mode 100644 packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/test/pluginClassSnakeCase_test.cc.tmpl rename packages/flutter_tools/templates/plugin_ffi/{linux.tmpl => linux-gtk3.tmpl}/CMakeLists.txt.tmpl (100%) create mode 100644 packages/flutter_tools/templates/plugin_ffi/linux-gtk4.tmpl/CMakeLists.txt.tmpl diff --git a/docs/gtk4-linux-plan.md b/docs/gtk4-linux-plan.md new file mode 100644 index 00000000000..a262715d8f9 --- /dev/null +++ b/docs/gtk4-linux-plan.md @@ -0,0 +1,44 @@ +# GTK4 Linux Default Plan + +## Goal +Make the Linux desktop embedder use GTK4 by default while keeping a short transition path for existing GTK3-based apps and plugins. + +## Scope +- Engine/Embedder: GTK4-backed Linux shell and `flutter_linux` surface. +- Tooling/Templates: update generated Linux runner (`my_application.*`, CMake, manifest). +- Tests/CI: validate GTK4 in bots and add regression coverage. + +## Workstreams + +### 1) Embedder + Engine +- Add a GTK4 backend in `engine/src/flutter/shell/platform/linux` (or port the existing GTK3 implementation). +- Replace GTK3-only APIs with GTK4 equivalents (e.g., container/child APIs, event controllers, header bar wiring). +- Ensure IME, clipboard, accessibility, windowing, and input paths still function under GTK4. +- Update build configuration to link against `gtk4`/`gdk-4` via pkg-config and document system deps. + +### 2) Tooling + Templates +- Update Linux runner templates in `packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/`: + - `my_application.cc.tmpl` and `my_application.h` to GTK4 APIs. + - `CMakeLists.txt` to find and link GTK4 packages. +- Verify `template_manifest.json` stays consistent and new projects generate GTK4 code by default. +- Provide a migration note for existing apps: re-run `flutter create --platforms=linux .` or manually port the runner code. + +### 3) Plugins + Platform Interfaces +- Audit `packages/` Linux plugins for GTK3-specific types or assumptions. +- Update plugin examples and registrant usage if API surface changes in `flutter_linux`. + +### 4) Testing + CI +- Add/extend Linux integration tests in `dev/integration_tests/` to cover windowing, input, IME, and accessibility on GTK4. +- Wire a GTK4 Linux shard into `dev/bots/test.dart` (and `analyze.dart` if needed) so GTK4 is exercised in CI. +- Keep a temporary GTK3 shard for comparison until GTK4 stabilizes, then remove or demote. + +## Milestones +1. **Prototype**: GTK4 embedder builds and runs a minimal app (no regressions in windowing/input). +2. **Template switch**: new Linux apps generate GTK4 runner code by default. +3. **CI coverage**: GTK4 shard green for key test suites. +4. **Default rollout**: document GTK4 as default; keep GTK3 escape hatch for one cycle. + +## Risks & Open Questions +- GTK4 API differences (container hierarchy, event handling, header bars) may require non-trivial refactors. +- Plugin ecosystem breakage: need a compatibility story and clear migration guidance. +- Distribution requirements: confirm minimum distro versions that ship GTK4. diff --git a/packages/flutter_tools/templates/app/linux.tmpl/.gitignore b/packages/flutter_tools/templates/app/linux-gtk3.tmpl/.gitignore similarity index 100% rename from packages/flutter_tools/templates/app/linux.tmpl/.gitignore rename to packages/flutter_tools/templates/app/linux-gtk3.tmpl/.gitignore diff --git a/packages/flutter_tools/templates/app/linux.tmpl/CMakeLists.txt.tmpl b/packages/flutter_tools/templates/app/linux-gtk3.tmpl/CMakeLists.txt.tmpl similarity index 100% rename from packages/flutter_tools/templates/app/linux.tmpl/CMakeLists.txt.tmpl rename to packages/flutter_tools/templates/app/linux-gtk3.tmpl/CMakeLists.txt.tmpl diff --git a/packages/flutter_tools/templates/app/linux.tmpl/flutter/CMakeLists.txt b/packages/flutter_tools/templates/app/linux-gtk3.tmpl/flutter/CMakeLists.txt similarity index 100% rename from packages/flutter_tools/templates/app/linux.tmpl/flutter/CMakeLists.txt rename to packages/flutter_tools/templates/app/linux-gtk3.tmpl/flutter/CMakeLists.txt diff --git a/packages/flutter_tools/templates/app/linux.tmpl/runner/CMakeLists.txt b/packages/flutter_tools/templates/app/linux-gtk3.tmpl/runner/CMakeLists.txt similarity index 100% rename from packages/flutter_tools/templates/app/linux.tmpl/runner/CMakeLists.txt rename to packages/flutter_tools/templates/app/linux-gtk3.tmpl/runner/CMakeLists.txt diff --git a/packages/flutter_tools/templates/app/linux.tmpl/runner/main.cc b/packages/flutter_tools/templates/app/linux-gtk3.tmpl/runner/main.cc similarity index 100% rename from packages/flutter_tools/templates/app/linux.tmpl/runner/main.cc rename to packages/flutter_tools/templates/app/linux-gtk3.tmpl/runner/main.cc diff --git a/packages/flutter_tools/templates/app/linux.tmpl/runner/my_application.cc.tmpl b/packages/flutter_tools/templates/app/linux-gtk3.tmpl/runner/my_application.cc.tmpl similarity index 100% rename from packages/flutter_tools/templates/app/linux.tmpl/runner/my_application.cc.tmpl rename to packages/flutter_tools/templates/app/linux-gtk3.tmpl/runner/my_application.cc.tmpl diff --git a/packages/flutter_tools/templates/app/linux.tmpl/runner/my_application.h b/packages/flutter_tools/templates/app/linux-gtk3.tmpl/runner/my_application.h similarity index 100% rename from packages/flutter_tools/templates/app/linux.tmpl/runner/my_application.h rename to packages/flutter_tools/templates/app/linux-gtk3.tmpl/runner/my_application.h diff --git a/packages/flutter_tools/templates/app/linux-gtk4.tmpl/.gitignore b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/.gitignore new file mode 100644 index 00000000000..d3896c98444 --- /dev/null +++ b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/flutter_tools/templates/app/linux-gtk4.tmpl/CMakeLists.txt.tmpl b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/CMakeLists.txt.tmpl new file mode 100644 index 00000000000..f710aa3d4ef --- /dev/null +++ b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/CMakeLists.txt.tmpl @@ -0,0 +1,132 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner 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 "{{projectName}}") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "{{linuxIdentifier}}") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +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() + +# 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_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +{{#withPlatformChannelPluginHook}} +# Enable the test target. +set(include_{{pluginProjectName}}_tests TRUE) +{{/withPlatformChannelPluginHook}} + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +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) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +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. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/flutter_tools/templates/app/linux-gtk4.tmpl/flutter/CMakeLists.txt b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/flutter/CMakeLists.txt new file mode 100644 index 00000000000..d5bd01648a9 --- /dev/null +++ b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +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. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# 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/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter 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. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/CMakeLists.txt b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/CMakeLists.txt new file mode 100644 index 00000000000..e97dabc7028 --- /dev/null +++ b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +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} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# 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 application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/main.cc b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/main.cc new file mode 100644 index 00000000000..e7c5c543703 --- /dev/null +++ b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/my_application.cc.tmpl b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/my_application.cc.tmpl new file mode 100644 index 00000000000..3b8611caea9 --- /dev/null +++ b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/my_application.cc.tmpl @@ -0,0 +1,148 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "{{projectName}}"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "{{projectName}}"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/my_application.h b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/my_application.h new file mode 100644 index 00000000000..db16367a77d --- /dev/null +++ b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/my_application.h @@ -0,0 +1,21 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, + my_application, + MY, + APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/flutter_tools/templates/plugin/linux.tmpl/CMakeLists.txt.tmpl b/packages/flutter_tools/templates/plugin/linux-gtk3.tmpl/CMakeLists.txt.tmpl similarity index 100% rename from packages/flutter_tools/templates/plugin/linux.tmpl/CMakeLists.txt.tmpl rename to packages/flutter_tools/templates/plugin/linux-gtk3.tmpl/CMakeLists.txt.tmpl diff --git a/packages/flutter_tools/templates/plugin/linux.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl b/packages/flutter_tools/templates/plugin/linux-gtk3.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl similarity index 100% rename from packages/flutter_tools/templates/plugin/linux.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl rename to packages/flutter_tools/templates/plugin/linux-gtk3.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl diff --git a/packages/flutter_tools/templates/plugin/linux.tmpl/pluginClassSnakeCase.cc.tmpl b/packages/flutter_tools/templates/plugin/linux-gtk3.tmpl/pluginClassSnakeCase.cc.tmpl similarity index 100% rename from packages/flutter_tools/templates/plugin/linux.tmpl/pluginClassSnakeCase.cc.tmpl rename to packages/flutter_tools/templates/plugin/linux-gtk3.tmpl/pluginClassSnakeCase.cc.tmpl diff --git a/packages/flutter_tools/templates/plugin/linux.tmpl/pluginClassSnakeCase_private.h.tmpl b/packages/flutter_tools/templates/plugin/linux-gtk3.tmpl/pluginClassSnakeCase_private.h.tmpl similarity index 100% rename from packages/flutter_tools/templates/plugin/linux.tmpl/pluginClassSnakeCase_private.h.tmpl rename to packages/flutter_tools/templates/plugin/linux-gtk3.tmpl/pluginClassSnakeCase_private.h.tmpl diff --git a/packages/flutter_tools/templates/plugin/linux.tmpl/test/pluginClassSnakeCase_test.cc.tmpl b/packages/flutter_tools/templates/plugin/linux-gtk3.tmpl/test/pluginClassSnakeCase_test.cc.tmpl similarity index 100% rename from packages/flutter_tools/templates/plugin/linux.tmpl/test/pluginClassSnakeCase_test.cc.tmpl rename to packages/flutter_tools/templates/plugin/linux-gtk3.tmpl/test/pluginClassSnakeCase_test.cc.tmpl diff --git a/packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/CMakeLists.txt.tmpl b/packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/CMakeLists.txt.tmpl new file mode 100644 index 00000000000..2d8110fe75c --- /dev/null +++ b/packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/CMakeLists.txt.tmpl @@ -0,0 +1,94 @@ +# The Flutter tooling requires that developers have CMake 3.10 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.10) + +# Project-level configuration. +set(PROJECT_NAME "{{projectName}}") +project(${PROJECT_NAME} LANGUAGES CXX) + +# This value is used when generating builds using this plugin, so it must +# not be changed. +set(PLUGIN_NAME "{{projectName}}_plugin") + +# Any new source files that you add to the plugin should be added here. +list(APPEND PLUGIN_SOURCES + "{{pluginClassSnakeCase}}.cc" +) + +# Define the plugin library target. Its name must not be changed (see comment +# on PLUGIN_NAME above). +add_library(${PLUGIN_NAME} SHARED + ${PLUGIN_SOURCES} +) + +# Apply a standard set of build settings that are configured in the +# application-level CMakeLists.txt. This can be removed for plugins that want +# full control over build settings. +apply_standard_settings(${PLUGIN_NAME}) + +# Symbols are hidden by default to reduce the chance of accidental conflicts +# between plugins. This should not be removed; any symbols that should be +# exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro. +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) + +# Source include directories and library dependencies. Add any plugin-specific +# dependencies here. +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter) +target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK) + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set({{projectName}}_bundled_libraries + "" + PARENT_SCOPE +) + +# === Tests === +# These unit tests can be run from a terminal after building the example. + +# Only enable test builds when building the example (which sets this variable) +# so that plugin clients aren't building the tests. +if (${include_${PROJECT_NAME}_tests}) +if(${CMAKE_VERSION} VERSION_LESS "3.11.0") +message("Unit tests require CMake 3.11.0 or later") +else() +set(TEST_RUNNER "${PROJECT_NAME}_test") +enable_testing() + +# Add the Google Test dependency. +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/release-1.11.0.zip +) +# Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +# Disable install commands for gtest so it doesn't end up in the bundle. +set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE) + +FetchContent_MakeAvailable(googletest) + +# The plugin's exported API is not very useful for unit testing, so build the +# sources directly into the test binary rather than using the shared library. +add_executable(${TEST_RUNNER} + test/{{pluginClassSnakeCase}}_test.cc + ${PLUGIN_SOURCES} +) +apply_standard_settings(${TEST_RUNNER}) +target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(${TEST_RUNNER} PRIVATE flutter) +target_link_libraries(${TEST_RUNNER} PRIVATE PkgConfig::GTK) +target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock) + +# Enable automatic test discovery. +include(GoogleTest) +gtest_discover_tests(${TEST_RUNNER}) + +endif() # CMake version check +endif() # include_${PROJECT_NAME}_tests \ No newline at end of file diff --git a/packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl b/packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl new file mode 100644 index 00000000000..439b25a0dd1 --- /dev/null +++ b/packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl @@ -0,0 +1,26 @@ +#ifndef FLUTTER_PLUGIN_{{pluginClassCapitalSnakeCase}}_H_ +#define FLUTTER_PLUGIN_{{pluginClassCapitalSnakeCase}}_H_ + +#include + +G_BEGIN_DECLS + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define FLUTTER_PLUGIN_EXPORT +#endif + +typedef struct _{{pluginClass}} {{pluginClass}}; +typedef struct { + GObjectClass parent_class; +} {{pluginClass}}Class; + +FLUTTER_PLUGIN_EXPORT GType {{pluginClassSnakeCase}}_get_type(); + +FLUTTER_PLUGIN_EXPORT void {{pluginClassSnakeCase}}_register_with_registrar( + FlPluginRegistrar* registrar); + +G_END_DECLS + +#endif // FLUTTER_PLUGIN_{{pluginClassCapitalSnakeCase}}_H_ diff --git a/packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/pluginClassSnakeCase.cc.tmpl b/packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/pluginClassSnakeCase.cc.tmpl new file mode 100644 index 00000000000..4eeb03c7578 --- /dev/null +++ b/packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/pluginClassSnakeCase.cc.tmpl @@ -0,0 +1,76 @@ +#include "include/{{projectName}}/{{pluginClassSnakeCase}}.h" + +#include +#include +#include + +#include + +#include "{{pluginClassSnakeCase}}_private.h" + +#define {{pluginClassCapitalSnakeCase}}(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), {{pluginClassSnakeCase}}_get_type(), \ + {{pluginClass}})) + +struct _{{pluginClass}} { + GObject parent_instance; +}; + +G_DEFINE_TYPE({{pluginClass}}, {{pluginClassSnakeCase}}, g_object_get_type()) + +// Called when a method call is received from Flutter. +static void {{pluginClassSnakeCase}}_handle_method_call( + {{pluginClass}}* self, + FlMethodCall* method_call) { + g_autoptr(FlMethodResponse) response = nullptr; + + const gchar* method = fl_method_call_get_name(method_call); + + if (strcmp(method, "getPlatformVersion") == 0) { + response = get_platform_version(); + } else { + response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); + } + + fl_method_call_respond(method_call, response, nullptr); +} + +FlMethodResponse* get_platform_version() { + struct utsname uname_data = {}; + uname(&uname_data); + g_autofree gchar *version = g_strdup_printf("Linux %s", uname_data.version); + g_autoptr(FlValue) result = fl_value_new_string(version); + return FL_METHOD_RESPONSE(fl_method_success_response_new(result)); +} + +static void {{pluginClassSnakeCase}}_dispose(GObject* object) { + G_OBJECT_CLASS({{pluginClassSnakeCase}}_parent_class)->dispose(object); +} + +static void {{pluginClassSnakeCase}}_class_init({{pluginClass}}Class* klass) { + G_OBJECT_CLASS(klass)->dispose = {{pluginClassSnakeCase}}_dispose; +} + +static void {{pluginClassSnakeCase}}_init({{pluginClass}}* self) {} + +static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call, + gpointer user_data) { + {{pluginClass}}* plugin = {{pluginClassCapitalSnakeCase}}(user_data); + {{pluginClassSnakeCase}}_handle_method_call(plugin, method_call); +} + +void {{pluginClassSnakeCase}}_register_with_registrar(FlPluginRegistrar* registrar) { + {{pluginClass}}* plugin = {{pluginClassCapitalSnakeCase}}( + g_object_new({{pluginClassSnakeCase}}_get_type(), nullptr)); + + g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); + g_autoptr(FlMethodChannel) channel = + fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar), + "{{projectName}}", + FL_METHOD_CODEC(codec)); + fl_method_channel_set_method_call_handler(channel, method_call_cb, + g_object_ref(plugin), + g_object_unref); + + g_object_unref(plugin); +} diff --git a/packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/pluginClassSnakeCase_private.h.tmpl b/packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/pluginClassSnakeCase_private.h.tmpl new file mode 100644 index 00000000000..604a14e30ec --- /dev/null +++ b/packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/pluginClassSnakeCase_private.h.tmpl @@ -0,0 +1,10 @@ +#include + +#include "include/{{projectName}}/{{pluginClassSnakeCase}}.h" + +// This file exposes some plugin internals for unit testing. See +// https://github.com/flutter/flutter/issues/88724 for current limitations +// in the unit-testable API. + +// Handles the getPlatformVersion method call. +FlMethodResponse *get_platform_version(); diff --git a/packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/test/pluginClassSnakeCase_test.cc.tmpl b/packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/test/pluginClassSnakeCase_test.cc.tmpl new file mode 100644 index 00000000000..cf9160bb8ce --- /dev/null +++ b/packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/test/pluginClassSnakeCase_test.cc.tmpl @@ -0,0 +1,31 @@ +#include +#include +#include + +#include "include/{{projectName}}/{{pluginClassSnakeCase}}.h" +#include "{{pluginClassSnakeCase}}_private.h" + +// This demonstrates a simple unit test of the C portion of this plugin's +// implementation. +// +// Once you have built the plugin's example app, you can run these tests +// from the command line. For instance, for a plugin called my_plugin +// built for x64 debug, run: +// $ build/linux/x64/debug/plugins/my_plugin/my_plugin_test + +namespace {{projectName}} { +namespace test { + +TEST({{pluginClass}}, GetPlatformVersion) { + g_autoptr(FlMethodResponse) response = get_platform_version(); + ASSERT_NE(response, nullptr); + ASSERT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + FlValue* result = fl_method_success_response_get_result( + FL_METHOD_SUCCESS_RESPONSE(response)); + ASSERT_EQ(fl_value_get_type(result), FL_VALUE_TYPE_STRING); + // The full string varies, so just validate that it has the right format. + EXPECT_THAT(fl_value_get_string(result), testing::StartsWith("Linux ")); +} + +} // namespace test +} // namespace {{projectName}} diff --git a/packages/flutter_tools/templates/plugin_ffi/linux.tmpl/CMakeLists.txt.tmpl b/packages/flutter_tools/templates/plugin_ffi/linux-gtk3.tmpl/CMakeLists.txt.tmpl similarity index 100% rename from packages/flutter_tools/templates/plugin_ffi/linux.tmpl/CMakeLists.txt.tmpl rename to packages/flutter_tools/templates/plugin_ffi/linux-gtk3.tmpl/CMakeLists.txt.tmpl diff --git a/packages/flutter_tools/templates/plugin_ffi/linux-gtk4.tmpl/CMakeLists.txt.tmpl b/packages/flutter_tools/templates/plugin_ffi/linux-gtk4.tmpl/CMakeLists.txt.tmpl new file mode 100644 index 00000000000..6b8415f0cbb --- /dev/null +++ b/packages/flutter_tools/templates/plugin_ffi/linux-gtk4.tmpl/CMakeLists.txt.tmpl @@ -0,0 +1,22 @@ +# The Flutter tooling requires that developers have CMake 3.10 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.10) + +# Project-level configuration. +set(PROJECT_NAME "{{projectName}}") +project(${PROJECT_NAME} LANGUAGES CXX) + +# Invoke the build for native code shared with the other target platforms. +# This can be changed to accommodate different builds. +add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../src" "${CMAKE_CURRENT_BINARY_DIR}/shared") + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set({{projectName}}_bundled_libraries + # Defined in ../src/CMakeLists.txt. + # This can be changed to accommodate different builds. + $ + PARENT_SCOPE +) diff --git a/packages/flutter_tools/templates/template_manifest.json b/packages/flutter_tools/templates/template_manifest.json index bb25af1e2bf..f84e46604a9 100644 --- a/packages/flutter_tools/templates/template_manifest.json +++ b/packages/flutter_tools/templates/template_manifest.json @@ -76,13 +76,20 @@ "templates/app/ios.tmpl/Runner/SceneDelegate.swift", "templates/app/ios.tmpl/RunnerTests/RunnerTests.swift.tmpl", "templates/app/lib/main.dart.tmpl", - "templates/app/linux.tmpl/.gitignore", - "templates/app/linux.tmpl/CMakeLists.txt.tmpl", - "templates/app/linux.tmpl/flutter/CMakeLists.txt", - "templates/app/linux.tmpl/runner/CMakeLists.txt", - "templates/app/linux.tmpl/runner/main.cc", - "templates/app/linux.tmpl/runner/my_application.cc.tmpl", - "templates/app/linux.tmpl/runner/my_application.h", + "templates/app/linux-gtk4.tmpl/.gitignore", + "templates/app/linux-gtk3.tmpl/.gitignore", + "templates/app/linux-gtk4.tmpl/CMakeLists.txt.tmpl", + "templates/app/linux-gtk3.tmpl/CMakeLists.txt.tmpl", + "templates/app/linux-gtk4.tmpl/flutter/CMakeLists.txt", + "templates/app/linux-gtk3.tmpl/flutter/CMakeLists.txt", + "templates/app/linux-gtk4.tmpl/runner/CMakeLists.txt", + "templates/app/linux-gtk3.tmpl/runner/CMakeLists.txt", + "templates/app/linux-gtk4.tmpl/runner/main.cc", + "templates/app/linux-gtk3.tmpl/runner/main.cc", + "templates/app/linux-gtk4.tmpl/runner/my_application.cc.tmpl", + "templates/app/linux-gtk3.tmpl/runner/my_application.cc.tmpl", + "templates/app/linux-gtk4.tmpl/runner/my_application.h", + "templates/app/linux-gtk3.tmpl/runner/my_application.h", "templates/app/macos.tmpl/.gitignore", "templates/app/macos.tmpl/Flutter/Flutter-Debug.xcconfig", "templates/app/macos.tmpl/Flutter/Flutter-Release.xcconfig", @@ -135,14 +142,10 @@ "templates/app/windows.tmpl/runner/utils.h", "templates/app/windows.tmpl/runner/win32_window.cpp", "templates/app/windows.tmpl/runner/win32_window.h", - "templates/app_test_widget/test/widget_test.dart.tmpl", - "templates/app_integration_test/integration_test/plugin_integration_test.dart.tmpl", - "templates/cocoapods/Podfile-ios", "templates/cocoapods/Podfile-macos", - "templates/module/android/deferred_component/build.gradle.tmpl", "templates/module/android/deferred_component/src/main/AndroidManifest.xml.tmpl", "templates/module/android/gradle/build.gradle.tmpl", @@ -230,7 +233,6 @@ "templates/module/ios/library/Flutter.tmpl/podhelper.rb.tmpl", "templates/module/ios/library/Flutter.tmpl/README.md", "templates/module/README.md", - "templates/package/.gitignore.tmpl", "templates/package/.idea/libraries/Dart_SDK.xml.tmpl", "templates/package/.idea/modules.xml.tmpl", @@ -244,7 +246,6 @@ "templates/package/pubspec.yaml.tmpl", "templates/package/README.md.tmpl", "templates/package/test/projectName_test.dart.tmpl", - "templates/package_ffi/.gitignore.tmpl", "templates/package_ffi/.metadata.tmpl", "templates/package_ffi/analysis_options.yaml.tmpl", @@ -259,7 +260,6 @@ "templates/package_ffi/src.tmpl/projectName.c.tmpl", "templates/package_ffi/src.tmpl/projectName.h.tmpl", "templates/package_ffi/test/projectName_test.dart.tmpl", - "templates/plugin/android-java.tmpl/build.gradle.kts.tmpl", "templates/plugin/android-java.tmpl/projectName_android.iml.tmpl", "templates/plugin/android-java.tmpl/src/main/java/androidIdentifier/pluginClass.java.tmpl", @@ -278,11 +278,16 @@ "templates/plugin/lib/projectName.dart.tmpl", "templates/plugin/lib/projectName_platform_interface.dart.tmpl", "templates/plugin/lib/projectName_method_channel.dart.tmpl", - "templates/plugin/linux.tmpl/CMakeLists.txt.tmpl", - "templates/plugin/linux.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl", - "templates/plugin/linux.tmpl/pluginClassSnakeCase.cc.tmpl", - "templates/plugin/linux.tmpl/pluginClassSnakeCase_private.h.tmpl", - "templates/plugin/linux.tmpl/test/pluginClassSnakeCase_test.cc.tmpl", + "templates/plugin/linux-gtk4.tmpl/CMakeLists.txt.tmpl", + "templates/plugin/linux-gtk3.tmpl/CMakeLists.txt.tmpl", + "templates/plugin/linux-gtk4.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl", + "templates/plugin/linux-gtk3.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl", + "templates/plugin/linux-gtk4.tmpl/pluginClassSnakeCase.cc.tmpl", + "templates/plugin/linux-gtk3.tmpl/pluginClassSnakeCase.cc.tmpl", + "templates/plugin/linux-gtk4.tmpl/pluginClassSnakeCase_private.h.tmpl", + "templates/plugin/linux-gtk3.tmpl/pluginClassSnakeCase_private.h.tmpl", + "templates/plugin/linux-gtk4.tmpl/test/pluginClassSnakeCase_test.cc.tmpl", + "templates/plugin/linux-gtk3.tmpl/test/pluginClassSnakeCase_test.cc.tmpl", "templates/plugin/README.md.tmpl", "templates/plugin/test/projectName_test.dart.tmpl", "templates/plugin/test/projectName_method_channel_test.dart.tmpl", @@ -293,7 +298,6 @@ "templates/plugin/windows.tmpl/pluginClassSnakeCase.h.tmpl", "templates/plugin/windows.tmpl/pluginClassSnakeCase_c_api.cpp.tmpl", "templates/plugin/lib/projectName_web.dart.tmpl", - "templates/plugin_ffi/android.tmpl/build.gradle.tmpl", "templates/plugin_ffi/android.tmpl/projectName_android.iml.tmpl", "templates/plugin_ffi/ffigen.yaml.tmpl", @@ -302,15 +306,16 @@ "templates/plugin_ffi/ios.tmpl/projectName.podspec.tmpl", "templates/plugin_ffi/lib/projectName_bindings_generated.dart.tmpl", "templates/plugin_ffi/lib/projectName.dart.tmpl", - "templates/plugin_ffi/linux.tmpl/CMakeLists.txt.tmpl", - "templates/plugin_ffi/linux.tmpl/include/projectName.tmpl/plugin_ffiClassSnakeCase.h.tmpl", + "templates/plugin_ffi/linux-gtk4.tmpl/CMakeLists.txt.tmpl", + "templates/plugin_ffi/linux-gtk3.tmpl/CMakeLists.txt.tmpl", + "templates/plugin_ffi/linux-gtk4.tmpl/include/projectName.tmpl/plugin_ffiClassSnakeCase.h.tmpl", + "templates/plugin_ffi/linux-gtk3.tmpl/include/projectName.tmpl/plugin_ffiClassSnakeCase.h.tmpl", "templates/plugin_ffi/macos.tmpl/Classes/projectName.c.tmpl", "templates/plugin_ffi/README.md.tmpl", "templates/plugin_ffi/src.tmpl/CMakeLists.txt.tmpl", "templates/plugin_ffi/src.tmpl/projectName.c.tmpl", "templates/plugin_ffi/src.tmpl/projectName.h.tmpl", "templates/plugin_ffi/windows.tmpl/CMakeLists.txt.tmpl", - "templates/plugin_shared/.gitignore.tmpl", "templates/plugin_shared/.idea/libraries/Dart_SDK.xml.tmpl", "templates/plugin_shared/.idea/modules.xml.tmpl", @@ -326,24 +331,20 @@ "templates/plugin_shared/projectName.iml.tmpl", "templates/plugin_shared/pubspec.yaml.tmpl", "templates/plugin_shared/windows.tmpl/.gitignore", - "templates/plugin_darwin_cocoapods/darwin.tmpl/.gitignore", "templates/plugin_darwin_cocoapods/darwin.tmpl/Classes/pluginClass.swift.tmpl", "templates/plugin_darwin_cocoapods/darwin.tmpl/Resources/PrivacyInfo.xcprivacy", "templates/plugin_darwin_cocoapods/darwin.tmpl/projectName.podspec.tmpl", - "templates/plugin_darwin_spm/darwin.tmpl/projectName.tmpl/Sources/projectName.tmpl/pluginClass.swift.tmpl", "templates/plugin_darwin_spm/darwin.tmpl/projectName.tmpl/Package.swift.tmpl", "templates/plugin_darwin_spm/darwin.tmpl/projectName.tmpl/Sources/projectName.tmpl/PrivacyInfo.xcprivacy", "templates/plugin_darwin_spm/darwin.tmpl/projectName.tmpl/Sources/projectName.tmpl/Resources/.gitkeep", "templates/plugin_darwin_spm/darwin.tmpl/projectName.podspec.tmpl", - "templates/plugin_cocoapods/ios.tmpl/Classes/pluginClass.swift.tmpl", "templates/plugin_cocoapods/ios.tmpl/Assets/.gitkeep", "templates/plugin_cocoapods/ios.tmpl/Resources/PrivacyInfo.xcprivacy", "templates/plugin_cocoapods/macos.tmpl/Classes/pluginClass.swift.tmpl", "templates/plugin_cocoapods/macos.tmpl/Resources/PrivacyInfo.xcprivacy", - "templates/plugin_swift_package_manager/ios.tmpl/projectName.tmpl/Sources/projectName.tmpl/pluginClass.swift.tmpl", "templates/plugin_swift_package_manager/ios.tmpl/projectName.tmpl/Package.swift.tmpl", "templates/plugin_swift_package_manager/ios.tmpl/projectName.tmpl/Sources/projectName.tmpl/PrivacyInfo.xcprivacy", @@ -351,7 +352,6 @@ "templates/plugin_swift_package_manager/macos.tmpl/projectName.tmpl/Sources/projectName.tmpl/pluginClass.swift.tmpl", "templates/plugin_swift_package_manager/macos.tmpl/projectName.tmpl/Package.swift.tmpl", "templates/plugin_swift_package_manager/macos.tmpl/projectName.tmpl/Sources/projectName.tmpl/PrivacyInfo.xcprivacy", - "templates/widget_preview_scaffold/lib/main.dart.tmpl", "templates/widget_preview_scaffold/lib/src/widget_preview.dart.tmpl", "templates/widget_preview_scaffold/lib/src/widget_preview_inspector_service.dart.tmpl", @@ -375,7 +375,6 @@ "templates/widget_preview_scaffold/lib/src/utils.dart.tmpl", "templates/widget_preview_scaffold/pubspec.yaml.tmpl", "templates/widget_preview_scaffold/README.md.tmpl", - "templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/contents.xcworkspacedata", "templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/xcshareddata/IDEWorkspaceChecks.plist", "templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/xcshareddata/WorkspaceSettings.xcsettings", From b475801b537b9ccf92491312e21cc464e7efd6ac Mon Sep 17 00:00:00 2001 From: Rich Young Date: Wed, 4 Feb 2026 05:10:46 -0500 Subject: [PATCH 03/34] feat(engine/linux): add GTK4 build flag and config --- engine/src/flutter/shell/platform/linux/BUILD.gn | 12 ++++++++---- engine/src/flutter/shell/platform/linux/config.gni | 8 ++++++++ .../src/flutter/shell/platform/linux/config/BUILD.gn | 4 ++++ 3 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 engine/src/flutter/shell/platform/linux/config.gni diff --git a/engine/src/flutter/shell/platform/linux/BUILD.gn b/engine/src/flutter/shell/platform/linux/BUILD.gn index b9b1fb6657b..8af51718b80 100644 --- a/engine/src/flutter/shell/platform/linux/BUILD.gn +++ b/engine/src/flutter/shell/platform/linux/BUILD.gn @@ -6,6 +6,7 @@ assert(is_linux) import("//flutter/build/zip_bundle.gni") import("//flutter/shell/platform/glfw/config.gni") +import("//flutter/shell/platform/linux/config.gni") import("//flutter/testing/testing.gni") group("linux") { @@ -81,6 +82,9 @@ config("disable_warnings") { cflags_cc = warning_flags } +_gtk_config = use_gtk4 ? "//flutter/shell/platform/linux/config:gtk4" : "//flutter/shell/platform/linux/config:gtk" +_gtk_defines = use_gtk4 ? [ "FLUTTER_LINUX_GTK4" ] : [] + source_set("flutter_linux_sources") { public = _public_headers + [ "fl_binary_messenger_private.h", @@ -104,7 +108,7 @@ source_set("flutter_linux_sources") { "key_mapping.h", ] - configs += [ "//flutter/shell/platform/linux/config:gtk" ] + configs += [ _gtk_config ] public_configs = [ ":disable_warnings" ] @@ -179,7 +183,7 @@ source_set("flutter_linux_sources") { defines = [ "FLUTTER_LINUX_COMPILATION", "FLUTTER_ENGINE_NO_PROTOTYPES", - ] + ] + _gtk_defines deps = [ "$dart_src/runtime:dart_api", @@ -195,7 +199,7 @@ source_set("flutter_linux_sources") { source_set("flutter_linux") { configs += [ - "//flutter/shell/platform/linux/config:gtk", + _gtk_config, "//flutter/shell/platform/linux/config:epoxy", ] @@ -284,7 +288,7 @@ executable("flutter_linux_unittests") { public_configs = [ "//flutter:config" ] configs += [ - "//flutter/shell/platform/linux/config:gtk", + _gtk_config, "//flutter/shell/platform/linux/config:epoxy", ] diff --git a/engine/src/flutter/shell/platform/linux/config.gni b/engine/src/flutter/shell/platform/linux/config.gni new file mode 100644 index 00000000000..f2533c61ff1 --- /dev/null +++ b/engine/src/flutter/shell/platform/linux/config.gni @@ -0,0 +1,8 @@ +# 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. + +declare_args() { + # Build the Linux embedder against GTK4 instead of GTK3. + use_gtk4 = false +} diff --git a/engine/src/flutter/shell/platform/linux/config/BUILD.gn b/engine/src/flutter/shell/platform/linux/config/BUILD.gn index e6f5abbea33..92b0b0b4234 100644 --- a/engine/src/flutter/shell/platform/linux/config/BUILD.gn +++ b/engine/src/flutter/shell/platform/linux/config/BUILD.gn @@ -14,6 +14,10 @@ _pkg_configs = [ name = "gtk" package = "gtk+-3.0" }, + { + name = "gtk4" + package = "gtk4" + }, { name = "egl" package = "egl" From a58a0152cf6cf409b819705528f9f0487c41d60d Mon Sep 17 00:00:00 2001 From: Rich Young Date: Wed, 4 Feb 2026 05:11:16 -0500 Subject: [PATCH 04/34] docs(gtk4): add test plan --- docs/gtk4-linux-plan.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/gtk4-linux-plan.md b/docs/gtk4-linux-plan.md index a262715d8f9..6585ff78c58 100644 --- a/docs/gtk4-linux-plan.md +++ b/docs/gtk4-linux-plan.md @@ -32,6 +32,13 @@ Make the Linux desktop embedder use GTK4 by default while keeping a short transi - Wire a GTK4 Linux shard into `dev/bots/test.dart` (and `analyze.dart` if needed) so GTK4 is exercised in CI. - Keep a temporary GTK3 shard for comparison until GTK4 stabilizes, then remove or demote. +## Test Plan (Proposed) +- **Engine unit tests (GTK4)**: build with `use_gtk4=true` and run `flutter_linux_unittests`. +- **Engine unit tests (GTK3)**: keep a parity run with `use_gtk4=false` during transition. +- **Tooling sanity**: `flutter create --linux-gtk=gtk4` and `--linux-gtk=gtk3` generate and build runner code. +- **Integration coverage**: run targeted tests in `dev/integration_tests/` (windowing, text input, a11y) against GTK4 builds. +- **CI shard**: add a Linux GTK4 shard to run the above in LUCI, keeping GTK3 for comparison until stable. + ## Milestones 1. **Prototype**: GTK4 embedder builds and runs a minimal app (no regressions in windowing/input). 2. **Template switch**: new Linux apps generate GTK4 runner code by default. From 10739c8181d5e723a062a023239b925e0cb9e5e8 Mon Sep 17 00:00:00 2001 From: Rich Young Date: Wed, 4 Feb 2026 05:12:09 -0500 Subject: [PATCH 05/34] fix(engine/linux): guard GTK4 window child attachment --- engine/src/flutter/shell/platform/linux/fl_application.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/engine/src/flutter/shell/platform/linux/fl_application.cc b/engine/src/flutter/shell/platform/linux/fl_application.cc index 42ade0134d3..622defcf10b 100644 --- a/engine/src/flutter/shell/platform/linux/fl_application.cc +++ b/engine/src/flutter/shell/platform/linux/fl_application.cc @@ -76,7 +76,11 @@ static GtkWindow* fl_application_create_window(FlApplication* self, gtk_window_set_titlebar(GTK_WINDOW(window), GTK_WIDGET(header_bar)); } +#if FLUTTER_LINUX_GTK4 + gtk_window_set_child(GTK_WINDOW(window), GTK_WIDGET(view)); +#else gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); +#endif return GTK_WINDOW(window); } From 0bd68e5d49c849fe49aba6e5e3c0e9f8bc154aea Mon Sep 17 00:00:00 2001 From: Rich Young Date: Wed, 4 Feb 2026 05:14:34 -0500 Subject: [PATCH 06/34] docs(gtk4): document rendering files and options --- docs/gtk4-rendering-notes.md | 76 ++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 docs/gtk4-rendering-notes.md diff --git a/docs/gtk4-rendering-notes.md b/docs/gtk4-rendering-notes.md new file mode 100644 index 00000000000..7fe96c0c3b5 --- /dev/null +++ b/docs/gtk4-rendering-notes.md @@ -0,0 +1,76 @@ +# GTK4 Rendering Notes (Linux Embedder) + +## Overview: current rendering pipeline +- `FlEngine` selects the renderer (`kOpenGL` or `kSoftware`) and wires the embedder callbacks in `engine/src/flutter/shell/platform/linux/fl_engine.cc`. +- `FlView` owns the GTK widget tree, creates the GL context (or software path), and dispatches frames to the compositor in `engine/src/flutter/shell/platform/linux/fl_view.cc`. +- `FlCompositor` abstracts composition. Implementations: + - OpenGL path: `engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc` + - Software path: `engine/src/flutter/shell/platform/linux/fl_compositor_software.cc` +- Frame storage and interop live in: + - `engine/src/flutter/shell/platform/linux/fl_framebuffer.*` + - `engine/src/flutter/shell/platform/linux/fl_renderable.*` + - `engine/src/flutter/shell/platform/linux/fl_texture_gl.*` +- GL context lifecycle/sharing lives in `engine/src/flutter/shell/platform/linux/fl_opengl_manager.*`. + +## Files that are rendering-critical +- `engine/src/flutter/shell/platform/linux/fl_engine.cc`: renderer selection, embedder config, compositor callbacks. +- `engine/src/flutter/shell/platform/linux/fl_view.cc`: widget tree, GL context creation, render callbacks, window metrics, input/event plumbing. +- `engine/src/flutter/shell/platform/linux/fl_compositor.h`: compositor API (currently uses `GdkWindow*` in render path). +- `engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc`: GL shader setup, framebuffer compositing, EGL/GLX sharing decisions. +- `engine/src/flutter/shell/platform/linux/fl_compositor_software.cc`: Cairo-based software rendering. +- `engine/src/flutter/shell/platform/linux/fl_framebuffer.*`: OpenGL FBO/texture management. +- `engine/src/flutter/shell/platform/linux/fl_texture_gl.*`: GL texture interop with GTK contexts. +- `engine/src/flutter/shell/platform/linux/fl_window_state_monitor.*`: window state and scale factor (uses `GdkWindow*`). +- `engine/src/flutter/shell/platform/linux/fl_text_input_handler.cc`: IME integration uses `gtk_widget_get_window`. + +## GTK4 adaptation points (API shifts to address) +These are the main GTK3 -> GTK4 deltas that touch rendering and windowing: + +1) Window/surface APIs +- GTK3 uses `GdkWindow*` from `gtk_widget_get_window`. GTK4 replaces this with `GdkSurface*` accessed via `GtkNative`. +- Update compositor render signatures to use `GdkSurface*` (or an abstracted surface type) and replace calls like: + - `gdk_window_get_width/height/scale_factor/display` -> GTK4 surface equivalents. + +2) Child attachment and containers +- GTK4 removes `GtkContainer` APIs. Replace `gtk_container_add` with widget-specific APIs (e.g., `gtk_window_set_child`, `gtk_box_append`). +- In the embedder, this affects `fl_application.cc` and `fl_view.cc` widget trees. + +3) Event handling +- `GtkEventBox` is removed in GTK4. Replace with `GtkEventController*` and attach controllers to the widget that should receive input. +- Update input plumbing in `fl_view.cc` (motion, button, scroll, touch) to use GTK4 event controllers and gestures. + +4) GL/Vulkan context creation +- GTK3 uses `gdk_window_create_gl_context`. GTK4 uses surface-native context creation APIs. +- Update `fl_view.cc` and any GL interop helpers to use GTK4 context creation and to fetch the correct drawable/surface handle. + +5) Cursor + monitor queries +- Cursor setting and monitor lookup currently use `GdkWindow*`. Switch to surface-based APIs in `fl_view.cc` and `fl_window_state_monitor.cc`. + +## Rendering options (is there more than one?) +Yes. There are at least three viable rendering paths: + +1) **GTK4 + OpenGL (EGL/GLX)** +- Keep the existing OpenGL renderer and compositor but replace GTK3 APIs with GTK4 equivalents. +- Lowest risk, reuses current embedder code. Still benefits from GTK4 event/input improvements. + +2) **GTK4 + Vulkan (Impeller/Vulkan)** +- Requires adding Vulkan support to the Linux embedder. Today `fl_engine.cc` rejects `kVulkan`. +- GTK4 exposes Vulkan-capable surfaces; the embedder would need: + - Vulkan device/swapchain setup per surface. + - A Vulkan-backed compositor path (similar to `fl_compositor_opengl` but for Vulkan). + - Impeller/Vulkan integration in the embedder config. +- Highest upside if Vulkan stability/perf are a goal, but also the largest change. + +3) **Software (Cairo)** +- Already supported via `fl_compositor_software.cc` and remains a fallback for unsupported GPU paths. +- Useful for headless or minimal environments, but not performance-competitive. + +## Recommended approach order +- Start with **GTK4 + OpenGL** to land GTK4 without destabilizing rendering. +- Parallel-plan a **Vulkan/Impeller** track: add renderer selection, Vulkan context creation, and compositor. +- Keep **software** as a fallback path during transition. + +## Test scope (rendering-specific) +- `engine/src/flutter/shell/platform/linux/*_test.cc` for compositor, framebuffer, and view lifecycle. +- Add GTK4-only test runs by building `flutter_linux_unittests` with `use_gtk4=true`. +- Add GTK4 integration tests for window metrics, input, and first-frame behavior using `dev/integration_tests/`. From c4e4f26067cb225b133e30169d6271420a3a53c4 Mon Sep 17 00:00:00 2001 From: Rich Young Date: Wed, 4 Feb 2026 05:49:27 -0500 Subject: [PATCH 07/34] feat(engine/linux): add GTK surface helpers --- .../src/flutter/shell/platform/linux/BUILD.gn | 1 + .../shell/platform/linux/fl_compositor.cc | 4 +- .../shell/platform/linux/fl_compositor.h | 9 +- .../platform/linux/fl_compositor_opengl.cc | 14 +-- .../platform/linux/fl_compositor_software.cc | 10 +- .../src/flutter/shell/platform/linux/fl_gtk.h | 91 ++++++++++++++++ .../flutter/shell/platform/linux/fl_view.cc | 101 +++++++++++++++--- 7 files changed, 199 insertions(+), 31 deletions(-) create mode 100644 engine/src/flutter/shell/platform/linux/fl_gtk.h diff --git a/engine/src/flutter/shell/platform/linux/BUILD.gn b/engine/src/flutter/shell/platform/linux/BUILD.gn index 8af51718b80..b9a1b71b369 100644 --- a/engine/src/flutter/shell/platform/linux/BUILD.gn +++ b/engine/src/flutter/shell/platform/linux/BUILD.gn @@ -90,6 +90,7 @@ source_set("flutter_linux_sources") { "fl_binary_messenger_private.h", "fl_dart_project_private.h", "fl_engine_private.h", + "fl_gtk.h", "fl_key_channel_responder.h", "fl_key_embedder_responder.h", "fl_key_embedder_responder_private.h", diff --git a/engine/src/flutter/shell/platform/linux/fl_compositor.cc b/engine/src/flutter/shell/platform/linux/fl_compositor.cc index fef2db4bc2f..52d875cd0fe 100644 --- a/engine/src/flutter/shell/platform/linux/fl_compositor.cc +++ b/engine/src/flutter/shell/platform/linux/fl_compositor.cc @@ -22,7 +22,7 @@ gboolean fl_compositor_present_layers(FlCompositor* self, gboolean fl_compositor_render(FlCompositor* self, cairo_t* cr, - GdkWindow* window) { + FlGdkSurface* surface) { g_return_val_if_fail(FL_IS_COMPOSITOR(self), FALSE); - return FL_COMPOSITOR_GET_CLASS(self)->render(self, cr, window); + return FL_COMPOSITOR_GET_CLASS(self)->render(self, cr, surface); } diff --git a/engine/src/flutter/shell/platform/linux/fl_compositor.h b/engine/src/flutter/shell/platform/linux/fl_compositor.h index 3c213d54647..a633dd16ea9 100644 --- a/engine/src/flutter/shell/platform/linux/fl_compositor.h +++ b/engine/src/flutter/shell/platform/linux/fl_compositor.h @@ -8,6 +8,7 @@ #include #include +#include "flutter/shell/platform/linux/fl_gtk.h" #include "flutter/shell/platform/embedder/embedder.h" G_BEGIN_DECLS @@ -21,7 +22,9 @@ struct _FlCompositorClass { const FlutterLayer** layers, size_t layers_count); - gboolean (*render)(FlCompositor* compositor, cairo_t* cr, GdkWindow* window); + gboolean (*render)(FlCompositor* compositor, + cairo_t* cr, + FlGdkSurface* surface); }; /** @@ -48,7 +51,7 @@ gboolean fl_compositor_present_layers(FlCompositor* compositor, * fl_compositor_render: * @compositor: an #FlCompositor. * @cr: a Cairo rendering context. - * @window: window being rendered into. + * @surface: surface being rendered into. * * Renders the current frame. Called from the GTK thread. * @@ -56,7 +59,7 @@ gboolean fl_compositor_present_layers(FlCompositor* compositor, */ gboolean fl_compositor_render(FlCompositor* compositor, cairo_t* cr, - GdkWindow* window); + FlGdkSurface* surface); G_END_DECLS diff --git a/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc b/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc index 5ec5697d785..e51c6710a70 100644 --- a/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc +++ b/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc @@ -11,6 +11,7 @@ #include "flutter/shell/platform/embedder/embedder.h" #include "flutter/shell/platform/linux/fl_engine_private.h" #include "flutter/shell/platform/linux/fl_framebuffer.h" +#include "flutter/shell/platform/linux/fl_gtk.h" // Vertex shader to draw Flutter window contents. static const char* vertex_shader_src = @@ -363,7 +364,7 @@ static gboolean fl_compositor_opengl_present_layers(FlCompositor* compositor, static gboolean fl_compositor_opengl_render(FlCompositor* compositor, cairo_t* cr, - GdkWindow* window) { + FlGdkSurface* surface) { FlCompositorOpenGL* self = FL_COMPOSITOR_OPENGL(compositor); g_mutex_lock(&self->frame_mutex); @@ -373,9 +374,9 @@ static gboolean fl_compositor_opengl_render(FlCompositor* compositor, } // If frame not ready, then wait for it. - gint scale_factor = gdk_window_get_scale_factor(window); - size_t width = gdk_window_get_width(window) * scale_factor; - size_t height = gdk_window_get_height(window) * scale_factor; + gint scale_factor = fl_gtk_surface_get_scale_factor(surface); + size_t width = fl_gtk_surface_get_width(surface) * scale_factor; + size_t height = fl_gtk_surface_get_height(surface) * scale_factor; while (fl_framebuffer_get_width(self->framebuffer) != width || fl_framebuffer_get_height(self->framebuffer) != height) { g_mutex_unlock(&self->frame_mutex); @@ -386,7 +387,8 @@ static gboolean fl_compositor_opengl_render(FlCompositor* compositor, if (fl_framebuffer_get_shareable(self->framebuffer)) { g_autoptr(FlFramebuffer) sibling = fl_framebuffer_create_sibling(self->framebuffer); - gdk_cairo_draw_from_gl(cr, window, fl_framebuffer_get_texture_id(sibling), + gdk_cairo_draw_from_gl(cr, surface, + fl_framebuffer_get_texture_id(sibling), GL_TEXTURE, scale_factor, 0, 0, width, height); } else { GLint saved_texture_binding; @@ -398,7 +400,7 @@ static gboolean fl_compositor_opengl_render(FlCompositor* compositor, glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, self->pixels); - gdk_cairo_draw_from_gl(cr, window, texture_id, GL_TEXTURE, scale_factor, 0, + gdk_cairo_draw_from_gl(cr, surface, texture_id, GL_TEXTURE, scale_factor, 0, 0, width, height); glDeleteTextures(1, &texture_id); diff --git a/engine/src/flutter/shell/platform/linux/fl_compositor_software.cc b/engine/src/flutter/shell/platform/linux/fl_compositor_software.cc index 0795851a527..f042753ddd5 100644 --- a/engine/src/flutter/shell/platform/linux/fl_compositor_software.cc +++ b/engine/src/flutter/shell/platform/linux/fl_compositor_software.cc @@ -4,6 +4,8 @@ #include "fl_compositor_software.h" +#include "flutter/shell/platform/linux/fl_gtk.h" + struct _FlCompositorSoftware { FlCompositor parent_instance; @@ -70,7 +72,7 @@ static gboolean fl_compositor_software_present_layers( static gboolean fl_compositor_software_render(FlCompositor* compositor, cairo_t* cr, - GdkWindow* window) { + FlGdkSurface* surface) { FlCompositorSoftware* self = FL_COMPOSITOR_SOFTWARE(compositor); g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&self->frame_mutex); @@ -80,9 +82,9 @@ static gboolean fl_compositor_software_render(FlCompositor* compositor, } // If frame not ready, then wait for it. - gint scale_factor = gdk_window_get_scale_factor(window); - size_t width = gdk_window_get_width(window) * scale_factor; - size_t height = gdk_window_get_height(window) * scale_factor; + gint scale_factor = fl_gtk_surface_get_scale_factor(surface); + size_t width = fl_gtk_surface_get_width(surface) * scale_factor; + size_t height = fl_gtk_surface_get_height(surface) * scale_factor; while (self->width != width || self->height != height) { g_mutex_unlock(&self->frame_mutex); fl_task_runner_wait(self->task_runner); diff --git a/engine/src/flutter/shell/platform/linux/fl_gtk.h b/engine/src/flutter/shell/platform/linux/fl_gtk.h new file mode 100644 index 00000000000..304d15a5ac5 --- /dev/null +++ b/engine/src/flutter/shell/platform/linux/fl_gtk.h @@ -0,0 +1,91 @@ +// 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_LINUX_FL_GTK_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_GTK_H_ + +#include + +#if FLUTTER_LINUX_GTK4 +typedef GdkSurface FlGdkSurface; + +static inline FlGdkSurface* fl_gtk_widget_get_surface(GtkWidget* widget) { + GtkNative* native = gtk_widget_get_native(widget); + return native != nullptr ? gtk_native_get_surface(native) : nullptr; +} + +static inline GdkDisplay* fl_gtk_surface_get_display(FlGdkSurface* surface) { + return gdk_surface_get_display(surface); +} + +static inline gint fl_gtk_surface_get_scale_factor(FlGdkSurface* surface) { + return gdk_surface_get_scale_factor(surface); +} + +static inline gint fl_gtk_surface_get_width(FlGdkSurface* surface) { + return gdk_surface_get_width(surface); +} + +static inline gint fl_gtk_surface_get_height(FlGdkSurface* surface) { + return gdk_surface_get_height(surface); +} + +static inline GdkMonitor* fl_gtk_display_get_monitor_at_surface( + GdkDisplay* display, + FlGdkSurface* surface) { + return gdk_display_get_monitor_at_surface(display, surface); +} + +static inline void fl_gtk_surface_set_cursor(FlGdkSurface* surface, + GdkCursor* cursor) { + gdk_surface_set_cursor(surface, cursor); +} + +static inline GdkGLContext* fl_gtk_surface_create_gl_context( + FlGdkSurface* surface, + GError** error) { + return gdk_surface_create_gl_context(surface, error); +} +#else +typedef GdkWindow FlGdkSurface; + +static inline FlGdkSurface* fl_gtk_widget_get_surface(GtkWidget* widget) { + return gtk_widget_get_window(widget); +} + +static inline GdkDisplay* fl_gtk_surface_get_display(FlGdkSurface* surface) { + return gdk_window_get_display(surface); +} + +static inline gint fl_gtk_surface_get_scale_factor(FlGdkSurface* surface) { + return gdk_window_get_scale_factor(surface); +} + +static inline gint fl_gtk_surface_get_width(FlGdkSurface* surface) { + return gdk_window_get_width(surface); +} + +static inline gint fl_gtk_surface_get_height(FlGdkSurface* surface) { + return gdk_window_get_height(surface); +} + +static inline GdkMonitor* fl_gtk_display_get_monitor_at_surface( + GdkDisplay* display, + FlGdkSurface* surface) { + return gdk_display_get_monitor_at_window(display, surface); +} + +static inline void fl_gtk_surface_set_cursor(FlGdkSurface* surface, + GdkCursor* cursor) { + gdk_window_set_cursor(surface, cursor); +} + +static inline GdkGLContext* fl_gtk_surface_create_gl_context( + FlGdkSurface* surface, + GError** error) { + return gdk_window_create_gl_context(surface, error); +} +#endif // FLUTTER_LINUX_GTK4 + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_GTK_H_ diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index a9be94291bc..3be51e51f46 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc @@ -5,7 +5,11 @@ #include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h" #include +#if FLUTTER_LINUX_GTK4 +#include +#else #include +#endif #include #include @@ -15,6 +19,7 @@ #include "flutter/shell/platform/linux/fl_compositor_opengl.h" #include "flutter/shell/platform/linux/fl_compositor_software.h" #include "flutter/shell/platform/linux/fl_engine_private.h" +#include "flutter/shell/platform/linux/fl_gtk.h" #include "flutter/shell/platform/linux/fl_key_event.h" #include "flutter/shell/platform/linux/fl_opengl_manager.h" #include "flutter/shell/platform/linux/fl_plugin_registrar_private.h" @@ -113,6 +118,20 @@ static gboolean redraw_cb(gpointer user_data) { return FALSE; } +static GtkWidget* fl_view_get_toplevel_widget(FlView* self) { +#if FLUTTER_LINUX_GTK4 + GtkRoot* root = gtk_widget_get_root(GTK_WIDGET(self)); + return root != nullptr ? GTK_WIDGET(root) : nullptr; +#else + return gtk_widget_get_toplevel(GTK_WIDGET(self)); +#endif +} + +static FlGdkSurface* fl_view_get_toplevel_surface(FlView* self) { + GtkWidget* toplevel = fl_view_get_toplevel_widget(self); + return toplevel != nullptr ? fl_gtk_widget_get_surface(toplevel) : nullptr; +} + // Signal handler for GtkWidget::delete-event static gboolean window_delete_event_cb(FlView* self) { fl_engine_request_app_exit(self->engine); @@ -155,11 +174,13 @@ static void cursor_changed_cb(FlView* self) { FlMouseCursorHandler* handler = fl_engine_get_mouse_cursor_handler(self->engine); const gchar* cursor_name = fl_mouse_cursor_handler_get_cursor_name(handler); - GdkWindow* window = - gtk_widget_get_window(gtk_widget_get_toplevel(GTK_WIDGET(self))); - g_autoptr(GdkCursor) cursor = - gdk_cursor_new_from_name(gdk_window_get_display(window), cursor_name); - gdk_window_set_cursor(window, cursor); + FlGdkSurface* surface = fl_view_get_toplevel_surface(self); + if (surface == nullptr) { + return; + } + g_autoptr(GdkCursor) cursor = gdk_cursor_new_from_name( + fl_gtk_surface_get_display(surface), cursor_name); + fl_gtk_surface_set_cursor(surface, cursor); } // Set the mouse cursor. @@ -183,8 +204,7 @@ static void handle_geometry_changed(FlView* self) { // has changed, so moving between two monitors of the same scale doesn't // provide any information. - GdkWindow* window = - gtk_widget_get_window(gtk_widget_get_toplevel(GTK_WIDGET(self))); + FlGdkSurface* surface = fl_view_get_toplevel_surface(self); // NOTE(robert-ancell) If we haven't got a window we default to display 0. // This is probably indicating a problem with this code in that we // shouldn't be generating anything until the window is created. @@ -193,9 +213,9 @@ static void handle_geometry_changed(FlView* self) { // probably shouldn't call handle_geometry_changed after the view is // added but only when the window is realized. FlutterEngineDisplayId display_id = 0; - if (window != nullptr) { - GdkMonitor* monitor = gdk_display_get_monitor_at_window( - gtk_widget_get_display(GTK_WIDGET(self)), window); + if (surface != nullptr) { + GdkMonitor* monitor = fl_gtk_display_get_monitor_at_surface( + fl_gtk_surface_get_display(surface), surface); display_id = fl_display_monitor_get_display_id( fl_engine_get_display_monitor(self->engine), monitor); } @@ -284,6 +304,7 @@ static void sync_modifier_if_needed(FlView* self, GdkEvent* event) { fl_engine_get_keyboard_manager(self->engine), event_state, event_time); } +#if !FLUTTER_LINUX_GTK4 static void set_scrolling_position(FlView* self, gdouble x, gdouble y) { gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); fl_scrolling_manager_set_last_mouse_position( @@ -430,12 +451,19 @@ static void gesture_zoom_update_cb(FlView* self, gdouble scale) { static void gesture_zoom_end_cb(FlView* self) { fl_scrolling_manager_handle_zoom_end(self->scrolling_manager); } +#endif // !FLUTTER_LINUX_GTK4 static void setup_opengl(FlView* self) { g_autoptr(GError) error = nullptr; - self->render_context = gdk_window_create_gl_context( - gtk_widget_get_window(GTK_WIDGET(self->render_area)), &error); + FlGdkSurface* surface = + fl_gtk_widget_get_surface(GTK_WIDGET(self->render_area)); + if (surface == nullptr) { + g_warning("Failed to create OpenGL context: surface not available"); + return; + } + + self->render_context = fl_gtk_surface_create_gl_context(surface, &error); if (self->render_context == nullptr) { g_warning("Failed to create OpenGL context: %s", error->message); return; @@ -450,7 +478,7 @@ static void setup_opengl(FlView* self) { // from the Flutter context using EGLImage. If not (i.e. X11 using GLX) // then we have to copy the texture via the CPU. gboolean shareable = - GDK_IS_WAYLAND_DISPLAY(gtk_widget_get_display(GTK_WIDGET(self))); + GDK_IS_WAYLAND_DISPLAY(fl_gtk_surface_get_display(surface)); self->compositor = FL_COMPOSITOR(fl_compositor_opengl_new( fl_engine_get_task_runner(self->engine), fl_engine_get_opengl_manager(self->engine), shareable)); @@ -478,7 +506,7 @@ static void realize_cb(FlView* self) { return; } - GtkWidget* toplevel_window = gtk_widget_get_toplevel(GTK_WIDGET(self)); + GtkWidget* toplevel_window = fl_view_get_toplevel_widget(self); self->window_state_monitor = fl_window_state_monitor_new(fl_engine_get_binary_messenger(self->engine), @@ -526,9 +554,17 @@ static gboolean draw_cb(FlView* self, cairo_t* cr) { gdk_gl_context_make_current(self->render_context); } + FlGdkSurface* surface = + fl_gtk_widget_get_surface(GTK_WIDGET(self->render_area)); + if (surface == nullptr) { + if (self->render_context) { + gdk_gl_context_clear_current(); + } + return FALSE; + } + gboolean result = fl_compositor_render( - self->compositor, cr, - gtk_widget_get_window(GTK_WIDGET(self->render_area))); + self->compositor, cr, surface); if (self->render_context) { gdk_gl_context_clear_current(); @@ -537,6 +573,20 @@ static gboolean draw_cb(FlView* self, cairo_t* cr) { return result; } +#if FLUTTER_LINUX_GTK4 +static void draw_cb_gtk4(GtkDrawingArea* area, + cairo_t* cr, + int width, + int height, + gpointer user_data) { + (void)area; + (void)width; + (void)height; + FlView* self = FL_VIEW(user_data); + draw_cb(self, cr); +} +#endif // FLUTTER_LINUX_GTK4 + static void fl_view_notify(GObject* object, GParamSpec* pspec) { FlView* self = FL_VIEW(object); @@ -599,9 +649,12 @@ static void fl_view_realize(GtkWidget* widget) { GTK_WIDGET_CLASS(fl_view_parent_class)->realize(widget); // Realize the child widgets. +#if !FLUTTER_LINUX_GTK4 gtk_widget_realize(GTK_WIDGET(self->render_area)); +#endif } +#if !FLUTTER_LINUX_GTK4 static gboolean handle_key_event(FlView* self, GdkEventKey* key_event) { g_autoptr(FlKeyEvent) event = fl_key_event_new_from_gdk_event( gdk_event_copy(reinterpret_cast(key_event))); @@ -660,6 +713,7 @@ static gboolean fl_view_key_release_event(GtkWidget* widget, FlView* self = FL_VIEW(widget); return handle_key_event(self, key_event); } +#endif // !FLUTTER_LINUX_GTK4 static void fl_view_class_init(FlViewClass* klass) { GObjectClass* object_class = G_OBJECT_CLASS(klass); @@ -668,9 +722,11 @@ static void fl_view_class_init(FlViewClass* klass) { GtkWidgetClass* widget_class = GTK_WIDGET_CLASS(klass); widget_class->realize = fl_view_realize; +#if !FLUTTER_LINUX_GTK4 widget_class->focus_in_event = fl_view_focus_in_event; widget_class->key_press_event = fl_view_key_press_event; widget_class->key_release_event = fl_view_key_release_event; +#endif fl_view_signals[SIGNAL_FIRST_FRAME] = g_signal_new("first-frame", fl_view_get_type(), G_SIGNAL_RUN_LAST, 0, @@ -710,6 +766,7 @@ static void fl_view_init(FlView* self) { .red = 0.0, .green = 0.0, .blue = 0.0, .alpha = 1.0}; self->background_color = gdk_rgba_copy(&default_background); +#if !FLUTTER_LINUX_GTK4 self->event_box = gtk_event_box_new(); gtk_widget_set_hexpand(self->event_box, TRUE); gtk_widget_set_vexpand(self->event_box, TRUE); @@ -747,17 +804,29 @@ static void fl_view_init(FlView* self) { self); g_signal_connect_swapped(self->event_box, "touch-event", G_CALLBACK(touch_event_cb), self); +#else + // TODO(gtk4): Replace GtkEventBox/event signals with GTK4 controllers. +#endif // !FLUTTER_LINUX_GTK4 self->render_area = GTK_DRAWING_AREA(gtk_drawing_area_new()); gtk_widget_show(GTK_WIDGET(self->render_area)); +#if FLUTTER_LINUX_GTK4 + gtk_box_append(GTK_BOX(self), GTK_WIDGET(self->render_area)); +#else gtk_container_add(GTK_CONTAINER(self->event_box), GTK_WIDGET(self->render_area)); +#endif g_signal_connect_swapped(self->render_area, "realize", G_CALLBACK(realize_cb), self); g_signal_connect_swapped(self->render_area, "size-allocate", G_CALLBACK(size_allocate_cb), self); +#if FLUTTER_LINUX_GTK4 + gtk_drawing_area_set_draw_func(self->render_area, draw_cb_gtk4, self, + nullptr); +#else g_signal_connect_swapped(self->render_area, "draw", G_CALLBACK(draw_cb), self); +#endif } G_MODULE_EXPORT FlView* fl_view_new(FlDartProject* project) { From 2e062a89a9eb9d53f9254bd69fab4bef76b85543 Mon Sep 17 00:00:00 2001 From: Rich Young Date: Wed, 4 Feb 2026 05:49:42 -0500 Subject: [PATCH 08/34] fix(engine/linux): guard GTK3-only embedder paths --- engine/src/flutter/shell/platform/linux/BUILD.gn | 4 +++- .../flutter/shell/platform/linux/fl_application.cc | 8 ++++++++ .../shell/platform/linux/fl_opengl_manager.cc | 5 +++++ .../shell/platform/linux/fl_text_input_handler.cc | 12 ++++++++++-- .../shell/platform/linux/fl_window_state_monitor.cc | 12 +++++++++++- .../platform/linux/fl_window_state_monitor_test.cc | 4 +++- 6 files changed, 40 insertions(+), 5 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/BUILD.gn b/engine/src/flutter/shell/platform/linux/BUILD.gn index b9a1b71b369..c4bc4fe5312 100644 --- a/engine/src/flutter/shell/platform/linux/BUILD.gn +++ b/engine/src/flutter/shell/platform/linux/BUILD.gn @@ -222,7 +222,8 @@ copy("flutter_linux_gschemas") { outputs = [ "$target_gen_dir/assets/{{source_name_part}}/gschemas.compiled" ] } -executable("flutter_linux_unittests") { +if (!use_gtk4) { + executable("flutter_linux_unittests") { testonly = true sources = [ @@ -311,6 +312,7 @@ executable("flutter_linux_unittests") { "//flutter/shell/platform/embedder:embedder_test_utils", "//flutter/testing", ] + } } shared_library("flutter_linux_gtk") { diff --git a/engine/src/flutter/shell/platform/linux/fl_application.cc b/engine/src/flutter/shell/platform/linux/fl_application.cc index 622defcf10b..9077f7a0b1f 100644 --- a/engine/src/flutter/shell/platform/linux/fl_application.cc +++ b/engine/src/flutter/shell/platform/linux/fl_application.cc @@ -5,9 +5,11 @@ #include "flutter/shell/platform/linux/public/flutter_linux/fl_application.h" #include +#if !FLUTTER_LINUX_GTK4 #ifdef GDK_WINDOWING_X11 #include #endif +#endif #include "flutter/shell/platform/linux/fl_engine_private.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_dart_project.h" @@ -34,7 +36,11 @@ G_DEFINE_TYPE_WITH_CODE(FlApplication, // Called when the first frame is received. static void first_frame_cb(FlApplication* self, FlView* view) { +#if FLUTTER_LINUX_GTK4 + GtkWidget* window = gtk_widget_get_root(GTK_WIDGET(view)); +#else GtkWidget* window = gtk_widget_get_toplevel(GTK_WIDGET(view)); +#endif // Show the main window. if (window != nullptr && GTK_IS_WINDOW(window)) { @@ -61,6 +67,7 @@ static GtkWindow* fl_application_create_window(FlApplication* self, // if future cases occur). gboolean use_header_bar = TRUE; #ifdef GDK_WINDOWING_X11 +#if !FLUTTER_LINUX_GTK4 GdkScreen* screen = gtk_window_get_screen(GTK_WINDOW(window)); if (GDK_IS_X11_SCREEN(screen)) { const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); @@ -68,6 +75,7 @@ static GtkWindow* fl_application_create_window(FlApplication* self, use_header_bar = FALSE; } } +#endif #endif if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); diff --git a/engine/src/flutter/shell/platform/linux/fl_opengl_manager.cc b/engine/src/flutter/shell/platform/linux/fl_opengl_manager.cc index c6d7d62d30c..c254f275edc 100644 --- a/engine/src/flutter/shell/platform/linux/fl_opengl_manager.cc +++ b/engine/src/flutter/shell/platform/linux/fl_opengl_manager.cc @@ -3,8 +3,13 @@ // found in the LICENSE file. #include +#if FLUTTER_LINUX_GTK4 +#include +#include +#else #include #include +#endif #include "flutter/shell/platform/linux/fl_opengl_manager.h" diff --git a/engine/src/flutter/shell/platform/linux/fl_text_input_handler.cc b/engine/src/flutter/shell/platform/linux/fl_text_input_handler.cc index 18b9030b592..c006c5b04e6 100644 --- a/engine/src/flutter/shell/platform/linux/fl_text_input_handler.cc +++ b/engine/src/flutter/shell/platform/linux/fl_text_input_handler.cc @@ -347,8 +347,12 @@ static void update_im_cursor_position(FlTextInputHandler* self) { // Transform from Flutter view coordinates to GTK window coordinates. GdkRectangle preedit_rect = {}; - gtk_widget_translate_coordinates(self->widget, - gtk_widget_get_toplevel(self->widget), x, y, +#if FLUTTER_LINUX_GTK4 + GtkWidget* toplevel = GTK_WIDGET(gtk_widget_get_root(self->widget)); +#else + GtkWidget* toplevel = gtk_widget_get_toplevel(self->widget); +#endif + gtk_widget_translate_coordinates(self->widget, toplevel, x, y, &preedit_rect.x, &preedit_rect.y); // Set the cursor location in window coordinates so that GTK can position @@ -480,8 +484,12 @@ void fl_text_input_handler_set_widget(FlTextInputHandler* self, GtkWidget* widget) { g_return_if_fail(FL_IS_TEXT_INPUT_HANDLER(self)); self->widget = widget; +#if FLUTTER_LINUX_GTK4 + gtk_im_context_set_client_widget(self->im_context, widget); +#else gtk_im_context_set_client_window(self->im_context, gtk_widget_get_window(self->widget)); +#endif } GtkWidget* fl_text_input_handler_get_widget(FlTextInputHandler* self) { diff --git a/engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc b/engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc index 756704f2b1e..3470c4f2715 100644 --- a/engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc +++ b/engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc @@ -27,7 +27,9 @@ struct _FlWindowStateMonitor { GtkWindow* window; // Current state information. +#if !FLUTTER_LINUX_GTK4 GdkWindowState window_state; +#endif // Signal connection ID for window-state-changed gulong window_state_event_cb_id; @@ -51,11 +53,13 @@ static void send_lifecycle_state(FlWindowStateMonitor* self, message, nullptr, nullptr, nullptr); } +#if !FLUTTER_LINUX_GTK4 static gboolean is_hidden(GdkWindowState state) { return (state & GDK_WINDOW_STATE_WITHDRAWN) || (state & GDK_WINDOW_STATE_ICONIFIED); } - +#endif // !FLUTTER_LINUX_GTK4 +#if !FLUTTER_LINUX_GTK4 // Signal handler for GtkWindow::window-state-event static gboolean window_state_event_cb(FlWindowStateMonitor* self, GdkEvent* event) { @@ -81,6 +85,7 @@ static gboolean window_state_event_cb(FlWindowStateMonitor* self, return FALSE; } +#endif // !FLUTTER_LINUX_GTK4 static void fl_window_state_monitor_dispose(GObject* object) { FlWindowStateMonitor* self = FL_WINDOW_STATE_MONITOR(object); @@ -109,11 +114,16 @@ FlWindowStateMonitor* fl_window_state_monitor_new(FlBinaryMessenger* messenger, self->window = window; // Listen to window state changes. +#if !FLUTTER_LINUX_GTK4 self->window_state_event_cb_id = g_signal_connect_swapped(self->window, "window-state-event", G_CALLBACK(window_state_event_cb), self); self->window_state = gdk_window_get_state(gtk_widget_get_window(GTK_WIDGET(self->window))); +#else + self->window_state_event_cb_id = 0; + // TODO(gtk4): Reconnect lifecycle updates using GdkToplevel state. +#endif return self; } diff --git a/engine/src/flutter/shell/platform/linux/fl_window_state_monitor_test.cc b/engine/src/flutter/shell/platform/linux/fl_window_state_monitor_test.cc index 0db4e4f8e15..84de78122a7 100644 --- a/engine/src/flutter/shell/platform/linux/fl_window_state_monitor_test.cc +++ b/engine/src/flutter/shell/platform/linux/fl_window_state_monitor_test.cc @@ -10,6 +10,7 @@ #include "gtest/gtest.h" +#if !FLUTTER_LINUX_GTK4 TEST(FlWindowStateMonitorTest, GainFocus) { g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); ::testing::NiceMock mock_gtk; @@ -44,7 +45,6 @@ TEST(FlWindowStateMonitorTest, GainFocus) { fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); } - TEST(FlWindowStateMonitorTest, LoseFocus) { g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); ::testing::NiceMock mock_gtk; @@ -287,3 +287,5 @@ TEST(FlWindowStateMonitorTest, LeaveWithdrawnFocused) { fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); } + +#endif // !FLUTTER_LINUX_GTK4 From b333ec392b5ff3b1e710378c2eca41eaeb38f58c Mon Sep 17 00:00:00 2001 From: Rich Young Date: Wed, 4 Feb 2026 11:17:18 -0500 Subject: [PATCH 09/34] fix(engine/linux): update scroll handling for GTK4 events --- .../platform/linux/fl_scrolling_manager.cc | 4 +--- .../platform/linux/fl_scrolling_manager.h | 2 +- .../linux/fl_scrolling_manager_test.cc | 19 +++++++++++-------- .../flutter/shell/platform/linux/fl_view.cc | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/fl_scrolling_manager.cc b/engine/src/flutter/shell/platform/linux/fl_scrolling_manager.cc index e49924a2cae..eaf8355bcb6 100644 --- a/engine/src/flutter/shell/platform/linux/fl_scrolling_manager.cc +++ b/engine/src/flutter/shell/platform/linux/fl_scrolling_manager.cc @@ -69,7 +69,7 @@ void fl_scrolling_manager_set_last_mouse_position(FlScrollingManager* self, } void fl_scrolling_manager_handle_scroll_event(FlScrollingManager* self, - GdkEventScroll* scroll_event, + GdkEvent* event, gint scale_factor) { g_return_if_fail(FL_IS_SCROLLING_MANAGER(self)); @@ -78,8 +78,6 @@ void fl_scrolling_manager_handle_scroll_event(FlScrollingManager* self, return; } - GdkEvent* event = reinterpret_cast(scroll_event); - guint event_time = gdk_event_get_time(event); gdouble event_x = 0.0, event_y = 0.0; gdk_event_get_coords(event, &event_x, &event_y); diff --git a/engine/src/flutter/shell/platform/linux/fl_scrolling_manager.h b/engine/src/flutter/shell/platform/linux/fl_scrolling_manager.h index 551a38e8994..164a12f289b 100644 --- a/engine/src/flutter/shell/platform/linux/fl_scrolling_manager.h +++ b/engine/src/flutter/shell/platform/linux/fl_scrolling_manager.h @@ -52,7 +52,7 @@ void fl_scrolling_manager_set_last_mouse_position(FlScrollingManager* manager, * Inform the scrolling manager of a scroll event. */ void fl_scrolling_manager_handle_scroll_event(FlScrollingManager* manager, - GdkEventScroll* event, + GdkEvent* event, gint scale_factor); /** diff --git a/engine/src/flutter/shell/platform/linux/fl_scrolling_manager_test.cc b/engine/src/flutter/shell/platform/linux/fl_scrolling_manager_test.cc index 0f68a4432bf..2c8897e0797 100644 --- a/engine/src/flutter/shell/platform/linux/fl_scrolling_manager_test.cc +++ b/engine/src/flutter/shell/platform/linux/fl_scrolling_manager_test.cc @@ -12,6 +12,7 @@ #include "gtest/gtest.h" +#if !FLUTTER_LINUX_GTK4 TEST(FlScrollingManagerTest, DiscreteDirectional) { g_autoptr(FlDartProject) project = fl_dart_project_new(); g_autoptr(FlEngine) engine = fl_engine_new(project); @@ -44,7 +45,7 @@ TEST(FlScrollingManagerTest, DiscreteDirectional) { event->y = 8.0; event->device = mouse; event->direction = GDK_SCROLL_UP; - fl_scrolling_manager_handle_scroll_event(manager, event, 1.0); + fl_scrolling_manager_handle_scroll_event(manager, reinterpret_cast(event), 1.0); EXPECT_EQ(pointer_events.size(), 1u); EXPECT_EQ(pointer_events[0].x, 4.0); EXPECT_EQ(pointer_events[0].y, 8.0); @@ -54,7 +55,7 @@ TEST(FlScrollingManagerTest, DiscreteDirectional) { EXPECT_EQ(pointer_events[0].scroll_delta_x, 0); EXPECT_EQ(pointer_events[0].scroll_delta_y, 53 * -1.0); event->direction = GDK_SCROLL_DOWN; - fl_scrolling_manager_handle_scroll_event(manager, event, 1.0); + fl_scrolling_manager_handle_scroll_event(manager, reinterpret_cast(event), 1.0); EXPECT_EQ(pointer_events.size(), 2u); EXPECT_EQ(pointer_events[1].x, 4.0); EXPECT_EQ(pointer_events[1].y, 8.0); @@ -64,7 +65,7 @@ TEST(FlScrollingManagerTest, DiscreteDirectional) { EXPECT_EQ(pointer_events[1].scroll_delta_x, 0); EXPECT_EQ(pointer_events[1].scroll_delta_y, 53 * 1.0); event->direction = GDK_SCROLL_LEFT; - fl_scrolling_manager_handle_scroll_event(manager, event, 1.0); + fl_scrolling_manager_handle_scroll_event(manager, reinterpret_cast(event), 1.0); EXPECT_EQ(pointer_events.size(), 3u); EXPECT_EQ(pointer_events[2].x, 4.0); EXPECT_EQ(pointer_events[2].y, 8.0); @@ -74,7 +75,7 @@ TEST(FlScrollingManagerTest, DiscreteDirectional) { EXPECT_EQ(pointer_events[2].scroll_delta_x, 53 * -1.0); EXPECT_EQ(pointer_events[2].scroll_delta_y, 0); event->direction = GDK_SCROLL_RIGHT; - fl_scrolling_manager_handle_scroll_event(manager, event, 1.0); + fl_scrolling_manager_handle_scroll_event(manager, reinterpret_cast(event), 1.0); EXPECT_EQ(pointer_events.size(), 4u); EXPECT_EQ(pointer_events[3].x, 4.0); EXPECT_EQ(pointer_events[3].y, 8.0); @@ -119,7 +120,7 @@ TEST(FlScrollingManagerTest, DiscreteScrolling) { event->delta_y = 2.0; event->device = mouse; event->direction = GDK_SCROLL_SMOOTH; - fl_scrolling_manager_handle_scroll_event(manager, event, 1.0); + fl_scrolling_manager_handle_scroll_event(manager, reinterpret_cast(event), 1.0); EXPECT_EQ(pointer_events.size(), 1u); EXPECT_EQ(pointer_events[0].x, 4.0); EXPECT_EQ(pointer_events[0].y, 8.0); @@ -164,7 +165,7 @@ TEST(FlScrollingManagerTest, Panning) { event->delta_y = 2.0; event->device = touchpad; event->direction = GDK_SCROLL_SMOOTH; - fl_scrolling_manager_handle_scroll_event(manager, event, 1.0); + fl_scrolling_manager_handle_scroll_event(manager, reinterpret_cast(event), 1.0); EXPECT_EQ(pointer_events.size(), 2u); EXPECT_EQ(pointer_events[0].x, 4.0); EXPECT_EQ(pointer_events[0].y, 8.0); @@ -180,7 +181,7 @@ TEST(FlScrollingManagerTest, Panning) { EXPECT_EQ(pointer_events[1].pan_y, 53 * -2.0); EXPECT_EQ(pointer_events[1].scale, 1.0); EXPECT_EQ(pointer_events[1].rotation, 0.0); - fl_scrolling_manager_handle_scroll_event(manager, event, 1.0); + fl_scrolling_manager_handle_scroll_event(manager, reinterpret_cast(event), 1.0); EXPECT_EQ(pointer_events.size(), 3u); EXPECT_EQ(pointer_events[2].x, 4.0); EXPECT_EQ(pointer_events[2].y, 8.0); @@ -192,7 +193,7 @@ TEST(FlScrollingManagerTest, Panning) { EXPECT_EQ(pointer_events[2].scale, 1.0); EXPECT_EQ(pointer_events[2].rotation, 0.0); event->is_stop = true; - fl_scrolling_manager_handle_scroll_event(manager, event, 1.0); + fl_scrolling_manager_handle_scroll_event(manager, reinterpret_cast(event), 1.0); EXPECT_EQ(pointer_events.size(), 4u); EXPECT_EQ(pointer_events[3].x, 4.0); EXPECT_EQ(pointer_events[3].y, 8.0); @@ -429,3 +430,5 @@ TEST(FlScrollingManagerTest, UnsynchronizedZoomingAndRotating) { EXPECT_EQ(pointer_events[4].phase, kPanZoomEnd); EXPECT_GE(pointer_events[4].timestamp, pointer_events[3].timestamp); } + +#endif // !FLUTTER_LINUX_GTK4 diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index 3be51e51f46..7c84cc74585 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc @@ -364,7 +364,7 @@ static gboolean scroll_event_cb(FlView* self, GdkEventScroll* event) { // depend on GTK 3.24. fl_scrolling_manager_handle_scroll_event( - self->scrolling_manager, event, + self->scrolling_manager, reinterpret_cast(event), gtk_widget_get_scale_factor(GTK_WIDGET(self))); return TRUE; } From 0c1a02e4af95522ee8ea18700c42f2751aa4d201 Mon Sep 17 00:00:00 2001 From: Rich Young Date: Wed, 4 Feb 2026 11:17:34 -0500 Subject: [PATCH 10/34] feat(engine/linux): add GTK4 input controllers --- .../shell/platform/linux/fl_key_event.cc | 11 +- .../flutter/shell/platform/linux/fl_view.cc | 280 +++++++++++++++++- 2 files changed, 285 insertions(+), 6 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/fl_key_event.cc b/engine/src/flutter/shell/platform/linux/fl_key_event.cc index 045f6859859..3b7f2219f8f 100644 --- a/engine/src/flutter/shell/platform/linux/fl_key_event.cc +++ b/engine/src/flutter/shell/platform/linux/fl_key_event.cc @@ -70,8 +70,13 @@ FlKeyEvent* fl_key_event_new_from_gdk_event(GdkEvent* event) { self->keycode = keycode; self->keyval = keyval; self->state = state; +#if FLUTTER_LINUX_GTK4 + self->group = 0; + self->origin = gdk_event_ref(event); +#else self->group = event->key.group; - self->origin = event; + self->origin = gdk_event_copy(event); +#endif return self; } @@ -114,7 +119,11 @@ GdkEvent* fl_key_event_get_origin(FlKeyEvent* self) { static void fl_key_event_dispose(GObject* object) { FlKeyEvent* self = FL_KEY_EVENT(object); +#if FLUTTER_LINUX_GTK4 + g_clear_pointer(&self->origin, gdk_event_unref); +#else g_clear_pointer(&self->origin, gdk_event_free); +#endif G_OBJECT_CLASS(fl_key_event_parent_class)->dispose(object); } diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index 7c84cc74585..2ad5ae7ec8e 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc @@ -85,6 +85,15 @@ struct _FlView { guint cursor_changed_cb_id; GCancellable* cancellable; + +#if FLUTTER_LINUX_GTK4 + GtkEventControllerMotion* motion_controller; + GtkGestureClick* click_gesture; + GtkEventControllerScroll* scroll_controller; + GtkEventControllerKey* key_controller; + GtkGestureZoom* zoom_gesture; + GtkGestureRotate* rotate_gesture; +#endif }; enum { SIGNAL_FIRST_FRAME, LAST_SIGNAL }; @@ -304,13 +313,13 @@ static void sync_modifier_if_needed(FlView* self, GdkEvent* event) { fl_engine_get_keyboard_manager(self->engine), event_state, event_time); } -#if !FLUTTER_LINUX_GTK4 static void set_scrolling_position(FlView* self, gdouble x, gdouble y) { gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); fl_scrolling_manager_set_last_mouse_position( self->scrolling_manager, x * scale_factor, y * scale_factor); } +#if !FLUTTER_LINUX_GTK4 // Signal handler for GtkWidget::button-press-event static gboolean button_press_event_cb(FlView* self, GdkEventButton* button_event) { @@ -424,6 +433,194 @@ static gboolean leave_notify_event_cb(FlView* self, self->pointer_manager, gdk_event_get_time(event), get_device_kind(event), x * scale_factor, y * scale_factor); } +#endif // !FLUTTER_LINUX_GTK4 + +#if FLUTTER_LINUX_GTK4 +static guint32 get_event_time_or_now(GdkEvent* event) { + if (event != nullptr) { + return gdk_event_get_time(event); + } + return static_cast(g_get_real_time() / 1000); +} + +static GdkEvent* get_current_event(GtkEventController* controller) { + return gtk_event_controller_get_current_event(controller); +} + +static FlutterPointerDeviceKind get_device_kind_or_default(GdkEvent* event) { + if (event != nullptr) { + return get_device_kind(event); + } + return kFlutterPointerDeviceKindMouse; +} + +static void motion_event_cb(FlView* self, gdouble x, gdouble y) { + GdkEvent* event = get_current_event(GTK_EVENT_CONTROLLER( + self->motion_controller)); + if (event == nullptr) { + return; + } + + sync_modifier_if_needed(self, event); + + // return if touch event + auto event_type = gdk_event_get_event_type(event); + if (event_type == GDK_TOUCH_BEGIN || event_type == GDK_TOUCH_UPDATE || + event_type == GDK_TOUCH_END || event_type == GDK_TOUCH_CANCEL) { + return; + } + + gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); + fl_pointer_manager_handle_motion( + self->pointer_manager, gdk_event_get_time(event), + get_device_kind(event), x * scale_factor, y * scale_factor); +} + +static void enter_event_cb(FlView* self, gdouble x, gdouble y) { + GdkEvent* event = get_current_event(GTK_EVENT_CONTROLLER( + self->motion_controller)); + gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); + fl_pointer_manager_handle_enter( + self->pointer_manager, get_event_time_or_now(event), + get_device_kind_or_default(event), x * scale_factor, y * scale_factor); +} + +static void leave_event_cb(FlView* self, gdouble x, gdouble y) { + GdkEvent* event = get_current_event(GTK_EVENT_CONTROLLER( + self->motion_controller)); + gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); + fl_pointer_manager_handle_leave( + self->pointer_manager, get_event_time_or_now(event), + get_device_kind_or_default(event), x * scale_factor, y * scale_factor); +} + +static void click_pressed_cb(FlView* self, + gint n_press, + gdouble x, + gdouble y) { + if (n_press > 1) { + return; + } + + GdkEvent* event = get_current_event(GTK_EVENT_CONTROLLER(self->click_gesture)); + if (event == nullptr) { + return; + } + + guint button = gtk_gesture_single_get_current_button( + GTK_GESTURE_SINGLE(self->click_gesture)); + + set_scrolling_position(self, x, y); + sync_modifier_if_needed(self, event); + + gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); + fl_pointer_manager_handle_button_press( + self->pointer_manager, gdk_event_get_time(event), get_device_kind(event), + x * scale_factor, y * scale_factor, button); +} + +static void click_released_cb(FlView* self, + gint n_press, + gdouble x, + gdouble y) { + (void)n_press; + GdkEvent* event = get_current_event(GTK_EVENT_CONTROLLER(self->click_gesture)); + if (event == nullptr) { + return; + } + + guint button = gtk_gesture_single_get_current_button( + GTK_GESTURE_SINGLE(self->click_gesture)); + + set_scrolling_position(self, x, y); + sync_modifier_if_needed(self, event); + + gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); + fl_pointer_manager_handle_button_release( + self->pointer_manager, gdk_event_get_time(event), get_device_kind(event), + x * scale_factor, y * scale_factor, button); +} + +static gboolean scroll_event_cb(FlView* self, gdouble dx, gdouble dy) { + (void)dx; + (void)dy; + GdkEvent* event = + get_current_event(GTK_EVENT_CONTROLLER(self->scroll_controller)); + if (event == nullptr) { + return FALSE; + } + + fl_scrolling_manager_handle_scroll_event( + self->scrolling_manager, event, + gtk_widget_get_scale_factor(GTK_WIDGET(self))); + return TRUE; +} + +static gboolean handle_key_event(FlView* self, + GdkEvent* event, + gboolean is_press, + guint keyval, + guint keycode, + GdkModifierType state) { + g_autoptr(FlKeyEvent) key_event = nullptr; + if (event != nullptr) { + key_event = fl_key_event_new_from_gdk_event(event); + } else { + key_event = fl_key_event_new(get_event_time_or_now(event), is_press, + keycode, keyval, state, 0); + } + + fl_keyboard_manager_handle_event( + fl_engine_get_keyboard_manager(self->engine), key_event, + self->cancellable, + [](GObject* object, GAsyncResult* result, gpointer user_data) { + FlView* self = FL_VIEW(user_data); + + g_autoptr(FlKeyEvent) redispatch_event = nullptr; + g_autoptr(GError) error = nullptr; + if (!fl_keyboard_manager_handle_event_finish( + FL_KEYBOARD_MANAGER(object), result, &redispatch_event, + &error)) { + if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + return; + } + + g_warning("Failed to handle key event: %s", error->message); + } + + if (redispatch_event != nullptr) { + if (!fl_text_input_handler_filter_keypress( + fl_engine_get_text_input_handler(self->engine), + redispatch_event)) { + fl_keyboard_manager_add_redispatched_event( + fl_engine_get_keyboard_manager(self->engine), redispatch_event); + // TODO(gtk4): Redispatch unhandled events to GTK. + } + } + }, + self); + + return TRUE; +} + +static gboolean key_pressed_cb(FlView* self, + guint keyval, + guint keycode, + GdkModifierType state) { + GdkEvent* event = + get_current_event(GTK_EVENT_CONTROLLER(self->key_controller)); + return handle_key_event(self, event, TRUE, keyval, keycode, state); +} + +static gboolean key_released_cb(FlView* self, + guint keyval, + guint keycode, + GdkModifierType state) { + GdkEvent* event = + get_current_event(GTK_EVENT_CONTROLLER(self->key_controller)); + return handle_key_event(self, event, FALSE, keyval, keycode, state); +} +#endif // FLUTTER_LINUX_GTK4 static void gesture_rotation_begin_cb(FlView* self) { fl_scrolling_manager_handle_rotation_begin(self->scrolling_manager); @@ -451,7 +648,6 @@ static void gesture_zoom_update_cb(FlView* self, gdouble scale) { static void gesture_zoom_end_cb(FlView* self) { fl_scrolling_manager_handle_zoom_end(self->scrolling_manager); } -#endif // !FLUTTER_LINUX_GTK4 static void setup_opengl(FlView* self) { g_autoptr(GError) error = nullptr; @@ -636,6 +832,14 @@ static void fl_view_dispose(GObject* object) { g_clear_object(&self->scrolling_manager); g_clear_object(&self->pointer_manager); g_clear_object(&self->touch_manager); +#if FLUTTER_LINUX_GTK4 + g_clear_object(&self->motion_controller); + g_clear_object(&self->click_gesture); + g_clear_object(&self->scroll_controller); + g_clear_object(&self->key_controller); + g_clear_object(&self->zoom_gesture); + g_clear_object(&self->rotate_gesture); +#endif g_clear_object(&self->view_accessible); g_clear_object(&self->cancellable); @@ -657,7 +861,7 @@ static void fl_view_realize(GtkWidget* widget) { #if !FLUTTER_LINUX_GTK4 static gboolean handle_key_event(FlView* self, GdkEventKey* key_event) { g_autoptr(FlKeyEvent) event = fl_key_event_new_from_gdk_event( - gdk_event_copy(reinterpret_cast(key_event))); + reinterpret_cast(key_event)); fl_keyboard_manager_handle_event( fl_engine_get_keyboard_manager(self->engine), event, self->cancellable, @@ -747,6 +951,11 @@ static void setup_engine(FlView* self) { init_scrolling(self); init_touch(self); +#if FLUTTER_LINUX_GTK4 + fl_text_input_handler_set_widget( + fl_engine_get_text_input_handler(self->engine), + GTK_WIDGET(self)); +#endif self->on_pre_engine_restart_cb_id = g_signal_connect_swapped(self->engine, "on-pre-engine-restart", @@ -758,7 +967,11 @@ static void setup_engine(FlView* self) { static void fl_view_init(FlView* self) { self->cancellable = g_cancellable_new(); +#if FLUTTER_LINUX_GTK4 + gtk_widget_set_focusable(GTK_WIDGET(self), TRUE); +#else gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE); +#endif self->view_id = -1; @@ -804,13 +1017,13 @@ static void fl_view_init(FlView* self) { self); g_signal_connect_swapped(self->event_box, "touch-event", G_CALLBACK(touch_event_cb), self); -#else - // TODO(gtk4): Replace GtkEventBox/event signals with GTK4 controllers. #endif // !FLUTTER_LINUX_GTK4 self->render_area = GTK_DRAWING_AREA(gtk_drawing_area_new()); gtk_widget_show(GTK_WIDGET(self->render_area)); #if FLUTTER_LINUX_GTK4 + gtk_widget_set_hexpand(GTK_WIDGET(self->render_area), TRUE); + gtk_widget_set_vexpand(GTK_WIDGET(self->render_area), TRUE); gtk_box_append(GTK_BOX(self), GTK_WIDGET(self->render_area)); #else gtk_container_add(GTK_CONTAINER(self->event_box), @@ -827,6 +1040,63 @@ static void fl_view_init(FlView* self) { g_signal_connect_swapped(self->render_area, "draw", G_CALLBACK(draw_cb), self); #endif + +#if FLUTTER_LINUX_GTK4 + self->motion_controller = GTK_EVENT_CONTROLLER_MOTION( + gtk_event_controller_motion_new()); + g_signal_connect_swapped(self->motion_controller, "motion", + G_CALLBACK(motion_event_cb), self); + g_signal_connect_swapped(self->motion_controller, "enter", + G_CALLBACK(enter_event_cb), self); + g_signal_connect_swapped(self->motion_controller, "leave", + G_CALLBACK(leave_event_cb), self); + gtk_widget_add_controller(GTK_WIDGET(self->render_area), + GTK_EVENT_CONTROLLER(self->motion_controller)); + + self->click_gesture = GTK_GESTURE_CLICK(gtk_gesture_click_new()); + g_signal_connect_swapped(self->click_gesture, "pressed", + G_CALLBACK(click_pressed_cb), self); + g_signal_connect_swapped(self->click_gesture, "released", + G_CALLBACK(click_released_cb), self); + gtk_widget_add_controller(GTK_WIDGET(self->render_area), + GTK_EVENT_CONTROLLER(self->click_gesture)); + + self->scroll_controller = GTK_EVENT_CONTROLLER_SCROLL( + gtk_event_controller_scroll_new(GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES)); + g_signal_connect_swapped(self->scroll_controller, "scroll", + G_CALLBACK(scroll_event_cb), self); + gtk_widget_add_controller(GTK_WIDGET(self->render_area), + GTK_EVENT_CONTROLLER(self->scroll_controller)); + + self->key_controller = + GTK_EVENT_CONTROLLER_KEY(gtk_event_controller_key_new()); + g_signal_connect_swapped(self->key_controller, "key-pressed", + G_CALLBACK(key_pressed_cb), self); + g_signal_connect_swapped(self->key_controller, "key-released", + G_CALLBACK(key_released_cb), self); + gtk_widget_add_controller(GTK_WIDGET(self), + GTK_EVENT_CONTROLLER(self->key_controller)); + + self->zoom_gesture = GTK_GESTURE_ZOOM(gtk_gesture_zoom_new()); + g_signal_connect_swapped(self->zoom_gesture, "begin", + G_CALLBACK(gesture_zoom_begin_cb), self); + g_signal_connect_swapped(self->zoom_gesture, "scale-changed", + G_CALLBACK(gesture_zoom_update_cb), self); + g_signal_connect_swapped(self->zoom_gesture, "end", + G_CALLBACK(gesture_zoom_end_cb), self); + gtk_widget_add_controller(GTK_WIDGET(self->render_area), + GTK_EVENT_CONTROLLER(self->zoom_gesture)); + + self->rotate_gesture = GTK_GESTURE_ROTATE(gtk_gesture_rotate_new()); + g_signal_connect_swapped(self->rotate_gesture, "begin", + G_CALLBACK(gesture_rotation_begin_cb), self); + g_signal_connect_swapped(self->rotate_gesture, "angle-changed", + G_CALLBACK(gesture_rotation_update_cb), self); + g_signal_connect_swapped(self->rotate_gesture, "end", + G_CALLBACK(gesture_rotation_end_cb), self); + gtk_widget_add_controller(GTK_WIDGET(self->render_area), + GTK_EVENT_CONTROLLER(self->rotate_gesture)); +#endif // FLUTTER_LINUX_GTK4 } G_MODULE_EXPORT FlView* fl_view_new(FlDartProject* project) { From 33e14de6d8fa24b7ba61a1a1fc0bdec5df27810a Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 15:52:07 -0500 Subject: [PATCH 11/34] build(engine/linux): adjust GTK4 build config --- engine/src/flutter/BUILD.gn | 3 ++- .../src/flutter/shell/platform/linux/BUILD.gn | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/engine/src/flutter/BUILD.gn b/engine/src/flutter/BUILD.gn index ec7b55d24f1..88f1215d117 100644 --- a/engine/src/flutter/BUILD.gn +++ b/engine/src/flutter/BUILD.gn @@ -7,6 +7,7 @@ import("//flutter/common/config.gni") import("//flutter/examples/examples.gni") import("//flutter/shell/platform/config.gni") import("//flutter/shell/platform/glfw/config.gni") +import("//flutter/shell/platform/linux/config.gni") import("//flutter/testing/testing.gni") import("$dart_src/build/dart/copy_tree.gni") @@ -279,7 +280,7 @@ group("unittests") { public_deps += [ "//flutter/shell/platform/glfw/client_wrapper:client_wrapper_glfw_unittests" ] } - if (is_linux) { + if (is_linux && !use_gtk4) { public_deps += [ "//flutter/shell/platform/linux:flutter_linux_unittests" ] if (build_glfw_shell) { diff --git a/engine/src/flutter/shell/platform/linux/BUILD.gn b/engine/src/flutter/shell/platform/linux/BUILD.gn index c4bc4fe5312..62c6ce43e46 100644 --- a/engine/src/flutter/shell/platform/linux/BUILD.gn +++ b/engine/src/flutter/shell/platform/linux/BUILD.gn @@ -82,8 +82,13 @@ config("disable_warnings") { cflags_cc = warning_flags } -_gtk_config = use_gtk4 ? "//flutter/shell/platform/linux/config:gtk4" : "//flutter/shell/platform/linux/config:gtk" -_gtk_defines = use_gtk4 ? [ "FLUTTER_LINUX_GTK4" ] : [] +if (use_gtk4) { + _gtk_config = "//flutter/shell/platform/linux/config:gtk4" + _gtk_defines = [ "FLUTTER_LINUX_GTK4" ] +} else { + _gtk_config = "//flutter/shell/platform/linux/config:gtk" + _gtk_defines = [] +} source_set("flutter_linux_sources") { public = _public_headers + [ @@ -116,8 +121,6 @@ source_set("flutter_linux_sources") { sources = [ "fl_accessibility_channel.cc", "fl_accessibility_handler.cc", - "fl_accessible_node.cc", - "fl_accessible_text_field.cc", "fl_application.cc", "fl_basic_message_channel.cc", "fl_binary_codec.cc", @@ -161,7 +164,6 @@ source_set("flutter_linux_sources") { "fl_settings_channel.cc", "fl_settings_handler.cc", "fl_settings_portal.cc", - "fl_socket_accessible.cc", "fl_standard_message_codec.cc", "fl_standard_method_codec.cc", "fl_string_codec.cc", @@ -174,12 +176,20 @@ source_set("flutter_linux_sources") { "fl_touch_manager.cc", "fl_value.cc", "fl_view.cc", - "fl_view_accessible.cc", "fl_window_monitor.cc", "fl_window_state_monitor.cc", "key_mapping.g.cc", ] + if (!use_gtk4) { + sources += [ + "fl_accessible_node.cc", + "fl_accessible_text_field.cc", + "fl_socket_accessible.cc", + "fl_view_accessible.cc", + ] + } + # Set flag to stop headers being directly included (library users should not do this) defines = [ "FLUTTER_LINUX_COMPILATION", From ec5a872f41c3c898d7d9cb26695d7c97691b2ec7 Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 15:52:20 -0500 Subject: [PATCH 12/34] fix(engine/linux): update GTK4 input handling --- .../linux/fl_key_channel_responder.cc | 10 ++++- .../shell/platform/linux/fl_key_event.cc | 12 ++++-- .../platform/linux/fl_keyboard_manager.cc | 27 +++++++++++++ .../platform/linux/fl_scrolling_manager.cc | 35 ++++++++++++++++- .../platform/linux/fl_text_input_handler.cc | 38 +++++++++++++++---- .../shell/platform/linux/fl_touch_manager.cc | 10 +++++ .../shell/platform/linux/fl_touch_manager.h | 6 +++ .../shell/platform/linux/key_mapping.g.cc | 6 +++ 8 files changed, 129 insertions(+), 15 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/fl_key_channel_responder.cc b/engine/src/flutter/shell/platform/linux/fl_key_channel_responder.cc index faf43fdfb65..1177d48bfe4 100644 --- a/engine/src/flutter/shell/platform/linux/fl_key_channel_responder.cc +++ b/engine/src/flutter/shell/platform/linux/fl_key_channel_responder.cc @@ -107,8 +107,10 @@ void fl_key_channel_responder_handle_event(FlKeyChannelResponder* self, // interactions (for example, if shift-lock is on, tab traversal is broken). // Remove lock states from state mask. - guint state = - fl_key_event_get_state(event) & ~(GDK_LOCK_MASK | GDK_MOD2_MASK); + guint state = fl_key_event_get_state(event) & ~GDK_LOCK_MASK; +#if !FLUTTER_LINUX_GTK4 + state &= ~GDK_MOD2_MASK; +#endif static bool shift_lock_pressed = FALSE; static bool caps_lock_pressed = FALSE; @@ -128,7 +130,11 @@ void fl_key_channel_responder_handle_event(FlKeyChannelResponder* self, // Add back in the state matching the actual pressed state of the lock keys, // not the lock states. state |= (shift_lock_pressed || caps_lock_pressed) ? GDK_LOCK_MASK : 0x0; +#if !FLUTTER_LINUX_GTK4 state |= num_lock_pressed ? GDK_MOD2_MASK : 0x0; +#else + (void)num_lock_pressed; +#endif fl_key_event_channel_send( self->channel, type, scan_code, fl_key_event_get_keyval(event), state, diff --git a/engine/src/flutter/shell/platform/linux/fl_key_event.cc b/engine/src/flutter/shell/platform/linux/fl_key_event.cc index 3b7f2219f8f..965eec5ee7e 100644 --- a/engine/src/flutter/shell/platform/linux/fl_key_event.cc +++ b/engine/src/flutter/shell/platform/linux/fl_key_event.cc @@ -59,11 +59,17 @@ FlKeyEvent* fl_key_event_new_from_gdk_event(GdkEvent* event) { nullptr); guint16 keycode = 0; - gdk_event_get_keycode(event, &keycode); guint keyval = 0; - gdk_event_get_keyval(event, &keyval); GdkModifierType state = static_cast(0); +#if FLUTTER_LINUX_GTK4 + keycode = static_cast(gdk_key_event_get_keycode(event)); + keyval = gdk_key_event_get_keyval(event); + state = gdk_event_get_modifier_state(event); +#else + gdk_event_get_keycode(event, &keycode); + gdk_event_get_keyval(event, &keyval); gdk_event_get_state(event, &state); +#endif self->time = gdk_event_get_time(event); self->is_press = type == GDK_KEY_PRESS; @@ -71,7 +77,7 @@ FlKeyEvent* fl_key_event_new_from_gdk_event(GdkEvent* event) { self->keyval = keyval; self->state = state; #if FLUTTER_LINUX_GTK4 - self->group = 0; + self->group = static_cast(gdk_key_event_get_layout(event)); self->origin = gdk_event_ref(event); #else self->group = event->key.group; diff --git a/engine/src/flutter/shell/platform/linux/fl_keyboard_manager.cc b/engine/src/flutter/shell/platform/linux/fl_keyboard_manager.cc index 7a81da289ec..e8bad568e2f 100644 --- a/engine/src/flutter/shell/platform/linux/fl_keyboard_manager.cc +++ b/engine/src/flutter/shell/platform/linux/fl_keyboard_manager.cc @@ -111,9 +111,13 @@ struct _FlKeyboardManager { std::unique_ptr> logical_to_mandatory_goals; +#if FLUTTER_LINUX_GTK4 + GdkDisplay* display; +#else GdkKeymap* keymap; gulong keymap_keys_changed_cb_id; // Signal connection ID for // keymap-keys-changed +#endif GCancellable* cancellable; }; @@ -139,10 +143,12 @@ static gboolean event_is_redispatched(FlKeyboardManager* self, return FALSE; } +#if !FLUTTER_LINUX_GTK4 static void keymap_keys_changed_cb(FlKeyboardManager* self) { g_clear_object(&self->derived_layout); self->derived_layout = fl_keyboard_layout_new(); } +#endif static void complete_handle_event(FlKeyboardManager* self, GTask* task) { HandleEventData* data = @@ -219,7 +225,19 @@ static uint16_t convert_key_to_char(FlKeyboardManager* self, if (self->lookup_key_handler != nullptr) { origin = self->lookup_key_handler(&key, self->lookup_key_handler_user_data); } else { +#if FLUTTER_LINUX_GTK4 + GdkModifierType state = + level == 0 ? static_cast(0) : GDK_SHIFT_MASK; + guint keyval = 0; + if (gdk_display_translate_key(self->display, keycode, state, group, &keyval, + nullptr, nullptr, nullptr)) { + origin = keyval; + } else { + origin = 0; + } +#else origin = gdk_keymap_lookup_key(self->keymap, &key); +#endif } return origin < kBmpMax ? origin : 0xFFFF; } @@ -318,10 +336,14 @@ static void fl_keyboard_manager_dispose(GObject* object) { g_clear_object(&self->key_embedder_responder); g_clear_object(&self->key_channel_responder); g_clear_object(&self->derived_layout); +#if FLUTTER_LINUX_GTK4 + g_clear_object(&self->display); +#else if (self->keymap_keys_changed_cb_id != 0) { g_signal_handler_disconnect(self->keymap, self->keymap_keys_changed_cb_id); self->keymap_keys_changed_cb_id = 0; } +#endif g_clear_object(&self->cancellable); G_OBJECT_CLASS(fl_keyboard_manager_parent_class)->dispose(object); @@ -347,9 +369,14 @@ static void fl_keyboard_manager_init(FlKeyboardManager* self) { } } +#if FLUTTER_LINUX_GTK4 + self->display = + GDK_DISPLAY(g_object_ref(gdk_display_get_default())); +#else self->keymap = gdk_keymap_get_for_display(gdk_display_get_default()); self->keymap_keys_changed_cb_id = g_signal_connect_swapped( self->keymap, "keys-changed", G_CALLBACK(keymap_keys_changed_cb), self); +#endif self->cancellable = g_cancellable_new(); } diff --git a/engine/src/flutter/shell/platform/linux/fl_scrolling_manager.cc b/engine/src/flutter/shell/platform/linux/fl_scrolling_manager.cc index eaf8355bcb6..628f3c3c0a3 100644 --- a/engine/src/flutter/shell/platform/linux/fl_scrolling_manager.cc +++ b/engine/src/flutter/shell/platform/linux/fl_scrolling_manager.cc @@ -80,9 +80,31 @@ void fl_scrolling_manager_handle_scroll_event(FlScrollingManager* self, guint event_time = gdk_event_get_time(event); gdouble event_x = 0.0, event_y = 0.0; +#if FLUTTER_LINUX_GTK4 + gdk_event_get_position(event, &event_x, &event_y); +#else gdk_event_get_coords(event, &event_x, &event_y); +#endif gdouble scroll_delta_x = 0.0, scroll_delta_y = 0.0; GdkScrollDirection event_direction = GDK_SCROLL_SMOOTH; +#if FLUTTER_LINUX_GTK4 + event_direction = gdk_scroll_event_get_direction(event); + if (event_direction == GDK_SCROLL_UP) { + scroll_delta_x = 0; + scroll_delta_y = -1; + } else if (event_direction == GDK_SCROLL_DOWN) { + scroll_delta_x = 0; + scroll_delta_y = 1; + } else if (event_direction == GDK_SCROLL_LEFT) { + scroll_delta_x = -1; + scroll_delta_y = 0; + } else if (event_direction == GDK_SCROLL_RIGHT) { + scroll_delta_x = 1; + scroll_delta_y = 0; + } else { + gdk_scroll_event_get_deltas(event, &scroll_delta_x, &scroll_delta_y); + } +#else if (gdk_event_get_scroll_direction(event, &event_direction)) { if (event_direction == GDK_SCROLL_UP) { scroll_delta_x = 0; @@ -100,6 +122,7 @@ void fl_scrolling_manager_handle_scroll_event(FlScrollingManager* self, } else { gdk_event_get_scroll_deltas(event, &scroll_delta_x, &scroll_delta_y); } +#endif // The multiplier is taken from the Chromium source // (ui/events/x/events_x_utils.cc). @@ -107,11 +130,19 @@ void fl_scrolling_manager_handle_scroll_event(FlScrollingManager* self, scroll_delta_x *= kScrollOffsetMultiplier * scale_factor; scroll_delta_y *= kScrollOffsetMultiplier * scale_factor; - if (gdk_device_get_source(gdk_event_get_source_device(event)) == - GDK_SOURCE_TOUCHPAD) { +#if FLUTTER_LINUX_GTK4 + GdkDevice* source_device = gdk_event_get_device(event); +#else + GdkDevice* source_device = gdk_event_get_source_device(event); +#endif + if (gdk_device_get_source(source_device) == GDK_SOURCE_TOUCHPAD) { scroll_delta_x *= -1; scroll_delta_y *= -1; +#if FLUTTER_LINUX_GTK4 + if (gdk_scroll_event_is_stop(event)) { +#else if (gdk_event_is_scroll_stop_event(event)) { +#endif fl_engine_send_pointer_pan_zoom_event( engine, self->view_id, event_time * kMicrosecondsPerMillisecond, event_x * scale_factor, event_y * scale_factor, kPanZoomEnd, diff --git a/engine/src/flutter/shell/platform/linux/fl_text_input_handler.cc b/engine/src/flutter/shell/platform/linux/fl_text_input_handler.cc index c006c5b04e6..af3d9ed2040 100644 --- a/engine/src/flutter/shell/platform/linux/fl_text_input_handler.cc +++ b/engine/src/flutter/shell/platform/linux/fl_text_input_handler.cc @@ -216,9 +216,16 @@ static void im_preedit_end_cb(FlTextInputHandler* self) { // Signal handler for GtkIMContext::retrieve-surrounding static gboolean im_retrieve_surrounding_cb(FlTextInputHandler* self) { auto text = self->text_model->GetText(); + flutter::TextRange selection = self->text_model->selection(); size_t cursor_offset = self->text_model->GetCursorOffset(); +#if FLUTTER_LINUX_GTK4 + gtk_im_context_set_surrounding_with_selection( + self->im_context, text.c_str(), -1, static_cast(cursor_offset), + static_cast(selection.base())); +#else gtk_im_context_set_surrounding(self->im_context, text.c_str(), -1, cursor_offset); +#endif return TRUE; } @@ -338,12 +345,12 @@ static void update_im_cursor_position(FlTextInputHandler* self) { // Transform the x, y positions of the cursor from local coordinates to // Flutter view coordinates. - gint x = self->composing_rect.x * self->editabletext_transform[0][0] + - self->composing_rect.y * self->editabletext_transform[1][0] + - self->editabletext_transform[3][0] + self->composing_rect.width; - gint y = self->composing_rect.x * self->editabletext_transform[0][1] + - self->composing_rect.y * self->editabletext_transform[1][1] + - self->editabletext_transform[3][1] + self->composing_rect.height; + double x = self->composing_rect.x * self->editabletext_transform[0][0] + + self->composing_rect.y * self->editabletext_transform[1][0] + + self->editabletext_transform[3][0] + self->composing_rect.width; + double y = self->composing_rect.x * self->editabletext_transform[0][1] + + self->composing_rect.y * self->editabletext_transform[1][1] + + self->editabletext_transform[3][1] + self->composing_rect.height; // Transform from Flutter view coordinates to GTK window coordinates. GdkRectangle preedit_rect = {}; @@ -352,8 +359,19 @@ static void update_im_cursor_position(FlTextInputHandler* self) { #else GtkWidget* toplevel = gtk_widget_get_toplevel(self->widget); #endif - gtk_widget_translate_coordinates(self->widget, toplevel, x, y, - &preedit_rect.x, &preedit_rect.y); +#if FLUTTER_LINUX_GTK4 + double dest_x = 0.0; + double dest_y = 0.0; + gtk_widget_translate_coordinates(self->widget, toplevel, x, y, &dest_x, + &dest_y); + preedit_rect.x = static_cast(dest_x); + preedit_rect.y = static_cast(dest_y); +#else + gtk_widget_translate_coordinates(self->widget, toplevel, + static_cast(x), + static_cast(y), &preedit_rect.x, + &preedit_rect.y); +#endif // Set the cursor location in window coordinates so that GTK can position // any system input method windows. @@ -507,7 +525,11 @@ gboolean fl_text_input_handler_filter_keypress(FlTextInputHandler* self, if (gtk_im_context_filter_keypress( self->im_context, +#if FLUTTER_LINUX_GTK4 + fl_key_event_get_origin(event))) { +#else reinterpret_cast(fl_key_event_get_origin(event)))) { +#endif return TRUE; } diff --git a/engine/src/flutter/shell/platform/linux/fl_touch_manager.cc b/engine/src/flutter/shell/platform/linux/fl_touch_manager.cc index eca5576c2a8..9ddef630085 100644 --- a/engine/src/flutter/shell/platform/linux/fl_touch_manager.cc +++ b/engine/src/flutter/shell/platform/linux/fl_touch_manager.cc @@ -129,7 +129,11 @@ static void release_number(_FlTouchManager* self, uint32_t number) { } void fl_touch_manager_handle_touch_event(FlTouchManager* self, +#if FLUTTER_LINUX_GTK4 + GdkEvent* event, +#else GdkEventTouch* touch_event, +#endif gint scale_factor) { g_return_if_fail(FL_IS_TOUCH_MANAGER(self)); @@ -138,7 +142,9 @@ void fl_touch_manager_handle_touch_event(FlTouchManager* self, return; } +#if !FLUTTER_LINUX_GTK4 GdkEvent* event = reinterpret_cast(touch_event); +#endif // get sequence id from GdkEvent GdkEventSequence* seq = gdk_event_get_event_sequence(event); // cast pointer to int to get unique id @@ -150,7 +156,11 @@ void fl_touch_manager_handle_touch_event(FlTouchManager* self, static_cast(kFlutterPointerDeviceKindTouch) << 28 | touch_id; gdouble event_x = 0.0, event_y = 0.0; +#if FLUTTER_LINUX_GTK4 + gdk_event_get_position(event, &event_x, &event_y); +#else gdk_event_get_coords(event, &event_x, &event_y); +#endif double x = event_x * scale_factor; double y = event_y * scale_factor; diff --git a/engine/src/flutter/shell/platform/linux/fl_touch_manager.h b/engine/src/flutter/shell/platform/linux/fl_touch_manager.h index d79e1772675..1f82ec06210 100644 --- a/engine/src/flutter/shell/platform/linux/fl_touch_manager.h +++ b/engine/src/flutter/shell/platform/linux/fl_touch_manager.h @@ -35,9 +35,15 @@ FlTouchManager* fl_touch_manager_new(FlEngine* engine, FlutterViewId view_id); * @event: the touch event. * @scale_factor: the GTK scaling factor of the window. */ +#if FLUTTER_LINUX_GTK4 +void fl_touch_manager_handle_touch_event(FlTouchManager* manager, + GdkEvent* event, + gint scale_factor); +#else void fl_touch_manager_handle_touch_event(FlTouchManager* manager, GdkEventTouch* event, gint scale_factor); +#endif G_END_DECLS diff --git a/engine/src/flutter/shell/platform/linux/key_mapping.g.cc b/engine/src/flutter/shell/platform/linux/key_mapping.g.cc index 9770bc7a5ba..fba81bc9472 100644 --- a/engine/src/flutter/shell/platform/linux/key_mapping.g.cc +++ b/engine/src/flutter/shell/platform/linux/key_mapping.g.cc @@ -429,7 +429,11 @@ void initialize_modifier_bit_to_checked_keys(GHashTable* table) { data->secondary_logical_key = 0x00200000101; // controlRight data = g_new(FlKeyEmbedderCheckedKey, 1); +#if FLUTTER_LINUX_GTK4 + g_hash_table_insert(table, GUINT_TO_POINTER(GDK_ALT_MASK), data); +#else g_hash_table_insert(table, GUINT_TO_POINTER(GDK_MOD1_MASK), data); +#endif data->is_caps_lock = false; data->primary_physical_key = 0x0000700e2; // altLeft data->primary_logical_key = 0x00200000104; // altLeft @@ -452,11 +456,13 @@ void initialize_lock_bit_to_checked_keys(GHashTable* table) { data->primary_physical_key = 0x000070039; // capsLock data->primary_logical_key = 0x00100000104; // capsLock +#if !FLUTTER_LINUX_GTK4 data = g_new(FlKeyEmbedderCheckedKey, 1); g_hash_table_insert(table, GUINT_TO_POINTER(GDK_MOD2_MASK), data); data->is_caps_lock = false; data->primary_physical_key = 0x000070053; // numLock data->primary_logical_key = 0x0010000010a; // numLock +#endif } const std::vector layout_goals = { From 2f067fd481aa308e828f7725ea80fdfb661c0ade Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 15:52:37 -0500 Subject: [PATCH 13/34] fix(engine/linux): adapt windowing and accessibility for GTK4 --- .../linux/fl_accessibility_handler.cc | 11 ++++ .../shell/platform/linux/fl_application.cc | 7 ++- .../platform/linux/fl_display_monitor.cc | 16 +++++- .../platform/linux/fl_platform_handler.cc | 44 +++++++++++++++ .../flutter/shell/platform/linux/fl_view.cc | 55 +++++++++++++++++++ .../shell/platform/linux/fl_view_private.h | 5 +- .../shell/platform/linux/fl_window_monitor.cc | 4 ++ .../platform/linux/fl_window_state_monitor.cc | 4 ++ 8 files changed, 143 insertions(+), 3 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/fl_accessibility_handler.cc b/engine/src/flutter/shell/platform/linux/fl_accessibility_handler.cc index 5e59795405c..65acbc6f0bf 100644 --- a/engine/src/flutter/shell/platform/linux/fl_accessibility_handler.cc +++ b/engine/src/flutter/shell/platform/linux/fl_accessibility_handler.cc @@ -6,7 +6,9 @@ #include "flutter/shell/platform/linux/fl_accessibility_channel.h" #include "flutter/shell/platform/linux/fl_engine_private.h" +#if !FLUTTER_LINUX_GTK4 #include "flutter/shell/platform/linux/fl_view_private.h" +#endif typedef struct { GWeakRef engine; @@ -22,6 +24,14 @@ static void send_announcement(int64_t view_id, FlTextDirection text_direction, FlAssertiveness assertiveness, gpointer user_data) { +#if FLUTTER_LINUX_GTK4 + (void)view_id; + (void)message; + (void)text_direction; + (void)assertiveness; + (void)user_data; + return; +#else FlAccessibilityHandler* self = FL_ACCESSIBILITY_HANDLER(user_data); FlAccessibilityHandlerPrivate* priv = reinterpret_cast( @@ -41,6 +51,7 @@ static void send_announcement(int64_t view_id, FlViewAccessible* accessible = fl_view_get_accessible(view); fl_view_accessible_send_announcement( accessible, message, assertiveness == FL_ASSERTIVENESS_ASSERTIVE); +#endif } static void fl_accessibility_handler_dispose(GObject* object) { diff --git a/engine/src/flutter/shell/platform/linux/fl_application.cc b/engine/src/flutter/shell/platform/linux/fl_application.cc index 9077f7a0b1f..71e95cbb97b 100644 --- a/engine/src/flutter/shell/platform/linux/fl_application.cc +++ b/engine/src/flutter/shell/platform/linux/fl_application.cc @@ -37,7 +37,8 @@ G_DEFINE_TYPE_WITH_CODE(FlApplication, // Called when the first frame is received. static void first_frame_cb(FlApplication* self, FlView* view) { #if FLUTTER_LINUX_GTK4 - GtkWidget* window = gtk_widget_get_root(GTK_WIDGET(view)); + GtkRoot* root = gtk_widget_get_root(GTK_WIDGET(view)); + GtkWidget* window = root != nullptr ? GTK_WIDGET(root) : nullptr; #else GtkWidget* window = gtk_widget_get_toplevel(GTK_WIDGET(view)); #endif @@ -80,7 +81,11 @@ static GtkWindow* fl_application_create_window(FlApplication* self, if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); +#if FLUTTER_LINUX_GTK4 + gtk_header_bar_set_show_title_buttons(header_bar, TRUE); +#else gtk_header_bar_set_show_close_button(header_bar, TRUE); +#endif gtk_window_set_titlebar(GTK_WINDOW(window), GTK_WIDGET(header_bar)); } diff --git a/engine/src/flutter/shell/platform/linux/fl_display_monitor.cc b/engine/src/flutter/shell/platform/linux/fl_display_monitor.cc index bea40b2cbd1..e6f12f69ace 100644 --- a/engine/src/flutter/shell/platform/linux/fl_display_monitor.cc +++ b/engine/src/flutter/shell/platform/linux/fl_display_monitor.cc @@ -30,13 +30,23 @@ static void notify_display_update(FlDisplayMonitor* self) { return; } +#if FLUTTER_LINUX_GTK4 + GListModel* monitors = gdk_display_get_monitors(self->display); + guint n_monitors = g_list_model_get_n_items(monitors); +#else int n_monitors = gdk_display_get_n_monitors(self->display); +#endif g_autofree FlutterEngineDisplay* displays = g_new0(FlutterEngineDisplay, n_monitors); - for (int i = 0; i < n_monitors; i++) { + for (guint i = 0; i < n_monitors; i++) { FlutterEngineDisplay* display = &displays[i]; +#if FLUTTER_LINUX_GTK4 + GdkMonitor* monitor = + GDK_MONITOR(g_list_model_get_item(monitors, i)); +#else GdkMonitor* monitor = gdk_display_get_monitor(self->display, i); +#endif FlutterEngineDisplayId display_id = GPOINTER_TO_INT( g_hash_table_lookup(self->display_ids_by_monitor, monitor)); if (display_id == 0) { @@ -56,6 +66,10 @@ static void notify_display_update(FlDisplayMonitor* self) { display->width = geometry.width; display->height = geometry.height; display->device_pixel_ratio = gdk_monitor_get_scale_factor(monitor); + +#if FLUTTER_LINUX_GTK4 + g_object_unref(monitor); +#endif } fl_engine_notify_display_update(engine, displays, n_monitors); diff --git a/engine/src/flutter/shell/platform/linux/fl_platform_handler.cc b/engine/src/flutter/shell/platform/linux/fl_platform_handler.cc index 6be22e94363..e3bdd69b8d6 100644 --- a/engine/src/flutter/shell/platform/linux/fl_platform_handler.cc +++ b/engine/src/flutter/shell/platform/linux/fl_platform_handler.cc @@ -34,6 +34,28 @@ struct _FlPlatformHandler { G_DEFINE_TYPE(FlPlatformHandler, fl_platform_handler, G_TYPE_OBJECT) +#if FLUTTER_LINUX_GTK4 +// Called when clipboard text received. +static void clipboard_text_cb(GObject* object, + GAsyncResult* result, + gpointer user_data) { + g_autoptr(FlMethodCall) method_call = FL_METHOD_CALL(user_data); + g_autofree gchar* text = + gdk_clipboard_read_text_finish(GDK_CLIPBOARD(object), result, nullptr); + fl_platform_channel_respond_clipboard_get_data(method_call, text); +} + +// Called when clipboard text received during has_strings. +static void clipboard_text_has_strings_cb(GObject* object, + GAsyncResult* result, + gpointer user_data) { + g_autoptr(FlMethodCall) method_call = FL_METHOD_CALL(user_data); + g_autofree gchar* text = + gdk_clipboard_read_text_finish(GDK_CLIPBOARD(object), result, nullptr); + fl_platform_channel_respond_clipboard_has_strings( + method_call, text != nullptr && strlen(text) > 0); +} +#else // Called when clipboard text received. static void clipboard_text_cb(GtkClipboard* clipboard, const gchar* text, @@ -50,14 +72,21 @@ static void clipboard_text_has_strings_cb(GtkClipboard* clipboard, fl_platform_channel_respond_clipboard_has_strings( method_call, text != nullptr && strlen(text) > 0); } +#endif // Called when Flutter wants to copy to the clipboard. static FlMethodResponse* clipboard_set_data(FlMethodCall* method_call, const gchar* text, gpointer user_data) { +#if FLUTTER_LINUX_GTK4 + GdkClipboard* clipboard = + gdk_display_get_clipboard(gdk_display_get_default()); + gdk_clipboard_set_text(clipboard, text); +#else GtkClipboard* clipboard = gtk_clipboard_get_default(gdk_display_get_default()); gtk_clipboard_set_text(clipboard, text, -1); +#endif return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } @@ -72,10 +101,17 @@ static FlMethodResponse* clipboard_get_data(FlMethodCall* method_call, nullptr)); } +#if FLUTTER_LINUX_GTK4 + GdkClipboard* clipboard = + gdk_display_get_clipboard(gdk_display_get_default()); + gdk_clipboard_read_text_async(clipboard, nullptr, clipboard_text_cb, + g_object_ref(method_call)); +#else GtkClipboard* clipboard = gtk_clipboard_get_default(gdk_display_get_default()); gtk_clipboard_request_text(clipboard, clipboard_text_cb, g_object_ref(method_call)); +#endif // Will respond later. return nullptr; @@ -85,10 +121,18 @@ static FlMethodResponse* clipboard_get_data(FlMethodCall* method_call, // be pasted, without actually accessing the clipboard content itself. static FlMethodResponse* clipboard_has_strings(FlMethodCall* method_call, gpointer user_data) { +#if FLUTTER_LINUX_GTK4 + GdkClipboard* clipboard = + gdk_display_get_clipboard(gdk_display_get_default()); + gdk_clipboard_read_text_async(clipboard, nullptr, + clipboard_text_has_strings_cb, + g_object_ref(method_call)); +#else GtkClipboard* clipboard = gtk_clipboard_get_default(gdk_display_get_default()); gtk_clipboard_request_text(clipboard, clipboard_text_has_strings_cb, g_object_ref(method_call)); +#endif // Will respond later. return nullptr; diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index 2ad5ae7ec8e..7b886c3a787 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc @@ -4,18 +4,24 @@ #include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h" +#if !FLUTTER_LINUX_GTK4 #include +#endif #if FLUTTER_LINUX_GTK4 #include #else #include #endif +#if !FLUTTER_LINUX_GTK4 #include +#endif #include #include "flutter/common/constants.h" +#if !FLUTTER_LINUX_GTK4 #include "flutter/shell/platform/linux/fl_accessible_node.h" +#endif #include "flutter/shell/platform/linux/fl_compositor_opengl.h" #include "flutter/shell/platform/linux/fl_compositor_software.h" #include "flutter/shell/platform/linux/fl_engine_private.h" @@ -25,9 +31,13 @@ #include "flutter/shell/platform/linux/fl_plugin_registrar_private.h" #include "flutter/shell/platform/linux/fl_pointer_manager.h" #include "flutter/shell/platform/linux/fl_scrolling_manager.h" +#if !FLUTTER_LINUX_GTK4 #include "flutter/shell/platform/linux/fl_socket_accessible.h" +#endif #include "flutter/shell/platform/linux/fl_touch_manager.h" +#if !FLUTTER_LINUX_GTK4 #include "flutter/shell/platform/linux/fl_view_accessible.h" +#endif #include "flutter/shell/platform/linux/fl_view_private.h" #include "flutter/shell/platform/linux/fl_window_state_monitor.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_engine.h" @@ -78,8 +88,10 @@ struct _FlView { // Manages touch events. FlTouchManager* touch_manager; +#if !FLUTTER_LINUX_GTK4 // Accessible tree from Flutter, exposed as an AtkPlug. FlViewAccessible* view_accessible; +#endif // Signal subscripton for cursor changes. guint cursor_changed_cb_id; @@ -160,8 +172,26 @@ static void init_touch(FlView* self) { } static FlutterPointerDeviceKind get_device_kind(GdkEvent* event) { +#if FLUTTER_LINUX_GTK4 + GdkDevice* device = gdk_event_get_device(event); +#else GdkDevice* device = gdk_event_get_source_device(event); +#endif GdkInputSource source = gdk_device_get_source(device); +#if FLUTTER_LINUX_GTK4 + switch (source) { + case GDK_SOURCE_PEN: + case GDK_SOURCE_TABLET_PAD: + return kFlutterPointerDeviceKindStylus; + case GDK_SOURCE_TOUCHSCREEN: + return kFlutterPointerDeviceKindTouch; + case GDK_SOURCE_TOUCHPAD: + case GDK_SOURCE_TRACKPOINT: + case GDK_SOURCE_KEYBOARD: + case GDK_SOURCE_MOUSE: + return kFlutterPointerDeviceKindMouse; + } +#else switch (source) { case GDK_SOURCE_PEN: case GDK_SOURCE_ERASER: @@ -176,6 +206,8 @@ static FlutterPointerDeviceKind get_device_kind(GdkEvent* event) { case GDK_SOURCE_MOUSE: return kFlutterPointerDeviceKindMouse; } +#endif + return kFlutterPointerDeviceKindMouse; } // Called when the mouse cursor changes. @@ -187,8 +219,12 @@ static void cursor_changed_cb(FlView* self) { if (surface == nullptr) { return; } +#if FLUTTER_LINUX_GTK4 + g_autoptr(GdkCursor) cursor = gdk_cursor_new_from_name(cursor_name, nullptr); +#else g_autoptr(GdkCursor) cursor = gdk_cursor_new_from_name( fl_gtk_surface_get_display(surface), cursor_name); +#endif fl_gtk_surface_set_cursor(surface, cursor); } @@ -260,7 +296,12 @@ static void update_semantics_cb(FlView* self, return; } +#if !FLUTTER_LINUX_GTK4 fl_view_accessible_handle_update_semantics(self->view_accessible, update); +#else + (void)self; + (void)update; +#endif } // Invoked by the engine right before the engine is restarted. @@ -307,8 +348,12 @@ static void fl_view_plugin_registry_iface_init( static void sync_modifier_if_needed(FlView* self, GdkEvent* event) { guint event_time = gdk_event_get_time(event); +#if FLUTTER_LINUX_GTK4 + GdkModifierType event_state = gdk_event_get_modifier_state(event); +#else GdkModifierType event_state = static_cast(0); gdk_event_get_state(event, &event_state); +#endif fl_keyboard_manager_sync_modifier_if_needed( fl_engine_get_keyboard_manager(self->engine), event_state, event_time); } @@ -840,7 +885,9 @@ static void fl_view_dispose(GObject* object) { g_clear_object(&self->zoom_gesture); g_clear_object(&self->rotate_gesture); #endif +#if !FLUTTER_LINUX_GTK4 g_clear_object(&self->view_accessible); +#endif g_clear_object(&self->cancellable); G_OBJECT_CLASS(fl_view_parent_class)->dispose(object); @@ -848,7 +895,9 @@ static void fl_view_dispose(GObject* object) { // Implements GtkWidget::realize. static void fl_view_realize(GtkWidget* widget) { +#if !FLUTTER_LINUX_GTK4 FlView* self = FL_VIEW(widget); +#endif GTK_WIDGET_CLASS(fl_view_parent_class)->realize(widget); @@ -936,16 +985,20 @@ static void fl_view_class_init(FlViewClass* klass) { g_signal_new("first-frame", fl_view_get_type(), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); +#if !FLUTTER_LINUX_GTK4 gtk_widget_class_set_accessible_type(GTK_WIDGET_CLASS(klass), fl_socket_accessible_get_type()); +#endif } // Engine related construction. static void setup_engine(FlView* self) { +#if !FLUTTER_LINUX_GTK4 self->view_accessible = fl_view_accessible_new(self->engine, self->view_id); fl_socket_accessible_embed( FL_SOCKET_ACCESSIBLE(gtk_widget_get_accessible(GTK_WIDGET(self))), atk_plug_get_id(ATK_PLUG(self->view_accessible))); +#endif self->pointer_manager = fl_pointer_manager_new(self->view_id, self->engine); @@ -1144,7 +1197,9 @@ G_MODULE_EXPORT void fl_view_set_background_color(FlView* self, self->background_color = gdk_rgba_copy(color); } +#if !FLUTTER_LINUX_GTK4 FlViewAccessible* fl_view_get_accessible(FlView* self) { g_return_val_if_fail(FL_IS_VIEW(self), nullptr); return self->view_accessible; } +#endif diff --git a/engine/src/flutter/shell/platform/linux/fl_view_private.h b/engine/src/flutter/shell/platform/linux/fl_view_private.h index cbdcef2ba3e..3f630527b01 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view_private.h +++ b/engine/src/flutter/shell/platform/linux/fl_view_private.h @@ -5,11 +5,13 @@ #ifndef FLUTTER_SHELL_PLATFORM_LINUX_FL_VIEW_PRIVATE_H_ #define FLUTTER_SHELL_PLATFORM_LINUX_FL_VIEW_PRIVATE_H_ -#include "flutter/shell/platform/linux/fl_view_accessible.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h" G_BEGIN_DECLS +#if !FLUTTER_LINUX_GTK4 +#include "flutter/shell/platform/linux/fl_view_accessible.h" + /** * fl_view_get_accessible: * @view: an #FlView. @@ -19,6 +21,7 @@ G_BEGIN_DECLS * Returns: an #FlViewAccessible. */ FlViewAccessible* fl_view_get_accessible(FlView* view); +#endif G_END_DECLS diff --git a/engine/src/flutter/shell/platform/linux/fl_window_monitor.cc b/engine/src/flutter/shell/platform/linux/fl_window_monitor.cc index 24ae915a647..b3c09bfe4c2 100644 --- a/engine/src/flutter/shell/platform/linux/fl_window_monitor.cc +++ b/engine/src/flutter/shell/platform/linux/fl_window_monitor.cc @@ -27,6 +27,7 @@ struct _FlWindowMonitor { G_DEFINE_TYPE(FlWindowMonitor, fl_window_monitor, G_TYPE_OBJECT) +#if !FLUTTER_LINUX_GTK4 static gboolean configure_event_cb(FlWindowMonitor* self, GdkEventConfigure* event) { flutter::IsolateScope scope(self->isolate); @@ -42,6 +43,7 @@ static gboolean window_state_event_cb(FlWindowMonitor* self, return FALSE; } +#endif static void is_active_notify_cb(FlWindowMonitor* self) { flutter::IsolateScope scope(self->isolate); @@ -102,10 +104,12 @@ G_MODULE_EXPORT FlWindowMonitor* fl_window_monitor_new( self->on_title_notify = on_title_notify; self->on_close = on_close; self->on_destroy = on_destroy; +#if !FLUTTER_LINUX_GTK4 g_signal_connect_swapped(window, "configure-event", G_CALLBACK(configure_event_cb), self); g_signal_connect_swapped(window, "window-state-event", G_CALLBACK(window_state_event_cb), self); +#endif g_signal_connect_swapped(window, "notify::is-active", G_CALLBACK(is_active_notify_cb), self); g_signal_connect_swapped(window, "notify::title", G_CALLBACK(title_notify_cb), diff --git a/engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc b/engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc index 3470c4f2715..6318b9e6007 100644 --- a/engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc +++ b/engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc @@ -8,6 +8,7 @@ #include "flutter/shell/platform/linux/public/flutter_linux/fl_string_codec.h" +#if !FLUTTER_LINUX_GTK4 static constexpr const char* kFlutterLifecycleChannel = "flutter/lifecycle"; static constexpr const char* kAppLifecycleStateResumed = @@ -16,6 +17,7 @@ static constexpr const char* kAppLifecycleStateInactive = "AppLifecycleState.inactive"; static constexpr const char* kAppLifecycleStateHidden = "AppLifecycleState.hidden"; +#endif struct _FlWindowStateMonitor { GObject parent_instance; @@ -37,6 +39,7 @@ struct _FlWindowStateMonitor { G_DEFINE_TYPE(FlWindowStateMonitor, fl_window_state_monitor, G_TYPE_OBJECT); +#if !FLUTTER_LINUX_GTK4 static void send_lifecycle_state(FlWindowStateMonitor* self, const gchar* lifecycle_state) { g_autoptr(FlValue) value = fl_value_new_string(lifecycle_state); @@ -52,6 +55,7 @@ static void send_lifecycle_state(FlWindowStateMonitor* self, fl_binary_messenger_send_on_channel(self->messenger, kFlutterLifecycleChannel, message, nullptr, nullptr, nullptr); } +#endif #if !FLUTTER_LINUX_GTK4 static gboolean is_hidden(GdkWindowState state) { From c74f860472e1869dab92f8fb369a56f844bf5796 Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 15:52:46 -0500 Subject: [PATCH 14/34] fix(engine/linux): handle GTK4 deprecations in GL compositor --- .../shell/platform/linux/fl_compositor_opengl.cc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc b/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc index e51c6710a70..d5b6f27b05c 100644 --- a/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc +++ b/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc @@ -387,9 +387,15 @@ static gboolean fl_compositor_opengl_render(FlCompositor* compositor, if (fl_framebuffer_get_shareable(self->framebuffer)) { g_autoptr(FlFramebuffer) sibling = fl_framebuffer_create_sibling(self->framebuffer); +#if FLUTTER_LINUX_GTK4 + G_GNUC_BEGIN_IGNORE_DEPRECATIONS +#endif gdk_cairo_draw_from_gl(cr, surface, fl_framebuffer_get_texture_id(sibling), GL_TEXTURE, scale_factor, 0, 0, width, height); +#if FLUTTER_LINUX_GTK4 + G_GNUC_END_IGNORE_DEPRECATIONS +#endif } else { GLint saved_texture_binding; glGetIntegerv(GL_TEXTURE_BINDING_2D, &saved_texture_binding); @@ -400,8 +406,14 @@ static gboolean fl_compositor_opengl_render(FlCompositor* compositor, glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, self->pixels); +#if FLUTTER_LINUX_GTK4 + G_GNUC_BEGIN_IGNORE_DEPRECATIONS +#endif gdk_cairo_draw_from_gl(cr, surface, texture_id, GL_TEXTURE, scale_factor, 0, 0, width, height); +#if FLUTTER_LINUX_GTK4 + G_GNUC_END_IGNORE_DEPRECATIONS +#endif glDeleteTextures(1, &texture_id); From 611a356370fd8a4ac6e0c6d85a2d389943edd9de Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 15:52:54 -0500 Subject: [PATCH 15/34] docs(agents): add GTK4 build notes --- AGENTS.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..0894b6549f8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,37 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `packages/`: Flutter framework and core Dart packages (`flutter`, `flutter_test`, `flutter_tools`, etc.). +- `engine/`: C++/Skia-based engine sources and build files (see `engine/README.md` for engine build steps). +- `bin/`: Flutter CLI entrypoints (`bin/flutter`) and cached tooling. +- `dev/`: Internal tooling, CI scripts, and large test suites (e.g., `dev/bots`, `dev/devicelab`). +- `examples/`: Sample apps and integration examples. +- `docs/`: Project documentation and contributing references. + +## Build, Test, and Development Commands +- `./bin/flutter --version`: Bootstraps the repo toolchain (downloads the Dart SDK if needed). +- `./bin/flutter test`: Run tests for a package (run from the package directory, e.g., `packages/flutter`). +- `bin/cache/dart-sdk/bin/dart dev/bots/test.dart`: CI-style test runner; use shards like `SHARD=framework_tests`. +- `bin/cache/dart-sdk/bin/dart --enable-asserts dev/bots/analyze.dart`: Repository-wide analysis and lint checks. +- `./dev/tools/format.sh`: Applies Dart formatting used by CI. + +## Coding Style & Naming Conventions +- Dart uses 2-space indentation; rely on the formatter (`./dev/tools/format.sh`) instead of manual alignment. +- Follow `analysis_options.yaml` at the repo root (and in `dev/`) for lints and analyzer settings. +- File names are lower_snake_case; test files must end with `_test.dart`. + +## Testing Guidelines +- Unit/widget tests live in `*/test/` and are run with `flutter test`. +- CI uses `dev/bots/test.dart`; prefer shards when validating a focused area. +- New features or bug fixes should include tests near the affected package or tool. + +## Commit & Pull Request Guidelines +- Recent commits use a concise summary with a PR number suffix, e.g. `Roll Skia … (#181780)`. +- Keep commit subjects imperative and scoped to the change. +- PRs should include: a clear description, linked issue (if any), and the tests run. +- Follow `CONTRIBUTING.md` and `CODE_OF_CONDUCT.md` for process and community rules. + +## Agent Notes (GTK4 Porting) +- Engine deps are synced from the repo root using `.gclient` copied from `engine/scripts/standard.gclient`; do not use the archived `https://github.com/flutter/engine.git` mirror. +- Run `gclient sync` at the repo root; it may take a long time and needs network access. +- GN/Ninja builds can take >10 minutes; use longer timeouts (e.g., 600s) when running `ninja -C engine/src/out/host_debug_unopt` via automation. From 2c297b5f337079f8f5f36abb16e528bc876f8cac Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 16:16:03 -0500 Subject: [PATCH 16/34] fix(templates/linux): use gtk4 pkg-config --- .../templates/app/linux-gtk4.tmpl/CMakeLists.txt.tmpl | 2 +- .../templates/app/linux-gtk4.tmpl/flutter/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter_tools/templates/app/linux-gtk4.tmpl/CMakeLists.txt.tmpl b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/CMakeLists.txt.tmpl index f710aa3d4ef..c1617eb9271 100644 --- a/packages/flutter_tools/templates/app/linux-gtk4.tmpl/CMakeLists.txt.tmpl +++ b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/CMakeLists.txt.tmpl @@ -52,7 +52,7 @@ add_subdirectory(${FLUTTER_MANAGED_DIR}) # System-level dependencies. find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk4) # Application build; see runner/CMakeLists.txt. add_subdirectory("runner") diff --git a/packages/flutter_tools/templates/app/linux-gtk4.tmpl/flutter/CMakeLists.txt b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/flutter/CMakeLists.txt index d5bd01648a9..493a3fb92b5 100644 --- a/packages/flutter_tools/templates/app/linux-gtk4.tmpl/flutter/CMakeLists.txt +++ b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/flutter/CMakeLists.txt @@ -22,7 +22,7 @@ endfunction() # === Flutter Library === # System-level dependencies. find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk4) pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) From 1044ef13de2a66d3b33a6b07b3c8789d12cda167 Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 16:16:13 -0500 Subject: [PATCH 17/34] fix(templates/linux): update GTK4 runner APIs --- .../runner/my_application.cc.tmpl | 43 ++++++------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/my_application.cc.tmpl b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/my_application.cc.tmpl index 3b8611caea9..94d460068eb 100644 --- a/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/my_application.cc.tmpl +++ b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/my_application.cc.tmpl @@ -1,9 +1,6 @@ #include "my_application.h" #include -#ifdef GDK_WINDOWING_X11 -#include -#endif #include "flutter/generated_plugin_registrant.h" @@ -16,7 +13,12 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) // Called when first Flutter frame received. static void first_frame_cb(MyApplication* self, FlView* view) { - gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); + (void)self; + GtkRoot* root = gtk_widget_get_root(GTK_WIDGET(view)); + if (root == nullptr) { + return; + } + gtk_window_present(GTK_WINDOW(root)); } // Implements GApplication::activate. @@ -25,32 +27,11 @@ static void my_application_activate(GApplication* application) { GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - // Use a header bar when running in GNOME as this is the common style used - // by applications and is the setup most users will be using (e.g. Ubuntu - // desktop). - // If running on X and not using GNOME then just use a traditional title bar - // in case the window manager does more exotic layout, e.g. tiling. - // If running on Wayland assume the header bar will work (may need changing - // if future cases occur). - gboolean use_header_bar = TRUE; -#ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; - } - } -#endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "{{projectName}}"); - gtk_header_bar_set_show_close_button(header_bar, TRUE); - gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "{{projectName}}"); - } + // Use a header bar as the default style for GNOME/GTK4 applications. + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_header_bar_set_show_title_buttons(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + gtk_window_set_title(window, "{{projectName}}"); gtk_window_set_default_size(window, 1280, 720); @@ -65,7 +46,7 @@ static void my_application_activate(GApplication* application) { gdk_rgba_parse(&background_color, "#000000"); fl_view_set_background_color(view, &background_color); gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + gtk_window_set_child(window, GTK_WIDGET(view)); // Show the window when Flutter renders. // Requires the view to be realized so we can start rendering. From 4bc89b39f9f163629de8d0ac106f83d499bd4fbb Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 18:35:32 -0500 Subject: [PATCH 18/34] fix(engine/linux): handle GTK4 drawing area resize --- engine/src/flutter/shell/platform/linux/fl_view.cc | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index 7b886c3a787..2b84ec0aa97 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc @@ -776,6 +776,14 @@ static void size_allocate_cb(FlView* self) { handle_geometry_changed(self); } +#if FLUTTER_LINUX_GTK4 +static void resize_cb(FlView* self, int width, int height) { + (void)width; + (void)height; + handle_geometry_changed(self); +} +#endif + static void paint_background(FlView* self, cairo_t* cr) { // Don't bother drawing if fully transparent - the widget above this will // already be drawn by GTK. @@ -1084,8 +1092,13 @@ static void fl_view_init(FlView* self) { #endif g_signal_connect_swapped(self->render_area, "realize", G_CALLBACK(realize_cb), self); +#if FLUTTER_LINUX_GTK4 + g_signal_connect_swapped(self->render_area, "resize", G_CALLBACK(resize_cb), + self); +#else g_signal_connect_swapped(self->render_area, "size-allocate", G_CALLBACK(size_allocate_cb), self); +#endif #if FLUTTER_LINUX_GTK4 gtk_drawing_area_set_draw_func(self->render_area, draw_cb_gtk4, self, nullptr); From d012f1e1a9669c912a30d024f65b3d573998c601 Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 18:40:39 -0500 Subject: [PATCH 19/34] fix(engine/linux): guard size-allocate for GTK3 --- engine/src/flutter/shell/platform/linux/fl_view.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index 2b84ec0aa97..4a7954422c2 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc @@ -772,9 +772,11 @@ static void realize_cb(FlView* self) { handle_geometry_changed(self); } +#if !FLUTTER_LINUX_GTK4 static void size_allocate_cb(FlView* self) { handle_geometry_changed(self); } +#endif #if FLUTTER_LINUX_GTK4 static void resize_cb(FlView* self, int width, int height) { From 33235f42ae8701326742d102d4e699e862a954ed Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 19:03:35 -0500 Subject: [PATCH 20/34] fix(engine/linux): add GTK4 close-request and monitor updates --- .../platform/linux/fl_display_monitor.cc | 50 +++++++++++++++++++ .../flutter/shell/platform/linux/fl_view.cc | 27 +++++++--- .../shell/platform/linux/fl_window_monitor.cc | 28 +++++++++++ 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/fl_display_monitor.cc b/engine/src/flutter/shell/platform/linux/fl_display_monitor.cc index e6f12f69ace..75395d51b9e 100644 --- a/engine/src/flutter/shell/platform/linux/fl_display_monitor.cc +++ b/engine/src/flutter/shell/platform/linux/fl_display_monitor.cc @@ -84,6 +84,49 @@ static void monitor_removed_cb(FlDisplayMonitor* self, GdkMonitor* monitor) { notify_display_update(self); } +#if FLUTTER_LINUX_GTK4 +static void prune_display_ids_for_current_monitors(FlDisplayMonitor* self) { + GListModel* monitors = gdk_display_get_monitors(self->display); + guint n_monitors = g_list_model_get_n_items(monitors); + + GHashTable* current = g_hash_table_new(g_direct_hash, g_direct_equal); + for (guint i = 0; i < n_monitors; i++) { + GdkMonitor* monitor = GDK_MONITOR(g_list_model_get_item(monitors, i)); + g_hash_table_add(current, monitor); + } + + GHashTableIter iter; + gpointer key = nullptr; + g_hash_table_iter_init(&iter, self->display_ids_by_monitor); + while (g_hash_table_iter_next(&iter, &key, nullptr)) { + if (!g_hash_table_contains(current, key)) { + g_hash_table_iter_remove(&iter); + } + } + + GHashTableIter current_iter; + g_hash_table_iter_init(¤t_iter, current); + while (g_hash_table_iter_next(¤t_iter, &key, nullptr)) { + g_object_unref(G_OBJECT(key)); + } + g_hash_table_unref(current); +} + +static void monitors_changed_cb(GListModel* list, + guint position, + guint removed, + guint added, + gpointer user_data) { + (void)list; + (void)position; + (void)removed; + (void)added; + FlDisplayMonitor* self = FL_DISPLAY_MONITOR(user_data); + prune_display_ids_for_current_monitors(self); + notify_display_update(self); +} +#endif + static void fl_display_monitor_dispose(GObject* object) { FlDisplayMonitor* self = FL_DISPLAY_MONITOR(object); @@ -117,12 +160,19 @@ FlDisplayMonitor* fl_display_monitor_new(FlEngine* engine, void fl_display_monitor_start(FlDisplayMonitor* self) { g_return_if_fail(FL_IS_DISPLAY_MONITOR(self)); +#if FLUTTER_LINUX_GTK4 + GListModel* monitors = gdk_display_get_monitors(self->display); + g_signal_connect_object(monitors, "items-changed", + G_CALLBACK(monitors_changed_cb), self, + static_cast(0)); +#else g_signal_connect_object(self->display, "monitor-added", G_CALLBACK(monitor_added_cb), self, G_CONNECT_SWAPPED); g_signal_connect_object(self->display, "monitor-removed", G_CALLBACK(monitor_removed_cb), self, G_CONNECT_SWAPPED); +#endif notify_display_update(self); } diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index 4a7954422c2..ca32cbbff14 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc @@ -153,13 +153,23 @@ static FlGdkSurface* fl_view_get_toplevel_surface(FlView* self) { return toplevel != nullptr ? fl_gtk_widget_get_surface(toplevel) : nullptr; } -// Signal handler for GtkWidget::delete-event +// Signal handler for GtkWidget::delete-event (GTK3 only) static gboolean window_delete_event_cb(FlView* self) { fl_engine_request_app_exit(self->engine); // Stop the event from propagating. return TRUE; } +#if FLUTTER_LINUX_GTK4 +// Signal handler for GtkWindow::close-request. +static gboolean window_close_request_cb(GtkWindow* window, FlView* self) { + (void)window; + fl_engine_request_app_exit(self->engine); + // Stop the event from propagating. + return TRUE; +} +#endif + static void init_scrolling(FlView* self) { g_clear_object(&self->scrolling_manager); self->scrolling_manager = @@ -754,8 +764,13 @@ static void realize_cb(FlView* self) { GTK_WINDOW(toplevel_window)); // Handle requests by the user to close the application. +#if FLUTTER_LINUX_GTK4 + g_signal_connect(toplevel_window, "close-request", + G_CALLBACK(window_close_request_cb), self); +#else g_signal_connect_swapped(toplevel_window, "delete-event", G_CALLBACK(window_delete_event_cb), self); +#endif // Flutter engine will need to make the context current from raster thread // during initialization. @@ -767,6 +782,11 @@ static void realize_cb(FlView* self) { return; } +#if FLUTTER_LINUX_GTK4 + fl_text_input_handler_set_widget( + fl_engine_get_text_input_handler(self->engine), GTK_WIDGET(self)); +#endif + setup_cursor(self); handle_geometry_changed(self); @@ -1014,11 +1034,6 @@ static void setup_engine(FlView* self) { init_scrolling(self); init_touch(self); -#if FLUTTER_LINUX_GTK4 - fl_text_input_handler_set_widget( - fl_engine_get_text_input_handler(self->engine), - GTK_WIDGET(self)); -#endif self->on_pre_engine_restart_cb_id = g_signal_connect_swapped(self->engine, "on-pre-engine-restart", diff --git a/engine/src/flutter/shell/platform/linux/fl_window_monitor.cc b/engine/src/flutter/shell/platform/linux/fl_window_monitor.cc index b3c09bfe4c2..e6bf522977c 100644 --- a/engine/src/flutter/shell/platform/linux/fl_window_monitor.cc +++ b/engine/src/flutter/shell/platform/linux/fl_window_monitor.cc @@ -55,6 +55,13 @@ static void title_notify_cb(FlWindowMonitor* self) { self->on_title_notify(); } +#if FLUTTER_LINUX_GTK4 +static gboolean close_request_cb(FlWindowMonitor* self) { + flutter::IsolateScope scope(self->isolate); + self->on_close(); + return TRUE; +} +#else static gboolean delete_event_cb(FlWindowMonitor* self, GdkEvent* event) { flutter::IsolateScope scope(self->isolate); self->on_close(); @@ -62,6 +69,18 @@ static gboolean delete_event_cb(FlWindowMonitor* self, GdkEvent* event) { // Stop default behaviour of destroying the window. return TRUE; } +#endif + +#if FLUTTER_LINUX_GTK4 +static gboolean close_request_cb(GtkWindow* window, FlWindowMonitor* self) { + (void)window; + flutter::IsolateScope scope(self->isolate); + self->on_close(); + + // Stop default behaviour of destroying the window. + return TRUE; +} +#endif static void destroy_cb(FlWindowMonitor* self) { flutter::IsolateScope scope(self->isolate); @@ -114,8 +133,17 @@ G_MODULE_EXPORT FlWindowMonitor* fl_window_monitor_new( G_CALLBACK(is_active_notify_cb), self); g_signal_connect_swapped(window, "notify::title", G_CALLBACK(title_notify_cb), self); +#if FLUTTER_LINUX_GTK4 + g_signal_connect(window, "close-request", G_CALLBACK(close_request_cb), self); +#else +#if FLUTTER_LINUX_GTK4 + g_signal_connect_swapped(window, "close-request", + G_CALLBACK(close_request_cb), self); +#else g_signal_connect_swapped(window, "delete-event", G_CALLBACK(delete_event_cb), self); +#endif +#endif g_signal_connect_swapped(window, "destroy", G_CALLBACK(destroy_cb), self); return self; From 87ab3485e788a9710c6033a5d2f9dc1a085cea12 Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 19:04:39 -0500 Subject: [PATCH 21/34] fix(engine/linux): correct GTK4 close-request wiring --- .../shell/platform/linux/fl_window_monitor.cc | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/fl_window_monitor.cc b/engine/src/flutter/shell/platform/linux/fl_window_monitor.cc index e6bf522977c..7024392edb8 100644 --- a/engine/src/flutter/shell/platform/linux/fl_window_monitor.cc +++ b/engine/src/flutter/shell/platform/linux/fl_window_monitor.cc @@ -71,17 +71,6 @@ static gboolean delete_event_cb(FlWindowMonitor* self, GdkEvent* event) { } #endif -#if FLUTTER_LINUX_GTK4 -static gboolean close_request_cb(GtkWindow* window, FlWindowMonitor* self) { - (void)window; - flutter::IsolateScope scope(self->isolate); - self->on_close(); - - // Stop default behaviour of destroying the window. - return TRUE; -} -#endif - static void destroy_cb(FlWindowMonitor* self) { flutter::IsolateScope scope(self->isolate); self->on_destroy(); @@ -133,16 +122,12 @@ G_MODULE_EXPORT FlWindowMonitor* fl_window_monitor_new( G_CALLBACK(is_active_notify_cb), self); g_signal_connect_swapped(window, "notify::title", G_CALLBACK(title_notify_cb), self); -#if FLUTTER_LINUX_GTK4 - g_signal_connect(window, "close-request", G_CALLBACK(close_request_cb), self); -#else #if FLUTTER_LINUX_GTK4 g_signal_connect_swapped(window, "close-request", G_CALLBACK(close_request_cb), self); #else g_signal_connect_swapped(window, "delete-event", G_CALLBACK(delete_event_cb), self); -#endif #endif g_signal_connect_swapped(window, "destroy", G_CALLBACK(destroy_cb), self); From b6d4297880f0e9c52abc40411772d1221630ac72 Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 19:08:24 -0500 Subject: [PATCH 22/34] fix(engine/linux): guard GTK3 monitor callbacks --- engine/src/flutter/shell/platform/linux/fl_display_monitor.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/engine/src/flutter/shell/platform/linux/fl_display_monitor.cc b/engine/src/flutter/shell/platform/linux/fl_display_monitor.cc index 75395d51b9e..b6405e7473d 100644 --- a/engine/src/flutter/shell/platform/linux/fl_display_monitor.cc +++ b/engine/src/flutter/shell/platform/linux/fl_display_monitor.cc @@ -75,6 +75,7 @@ static void notify_display_update(FlDisplayMonitor* self) { fl_engine_notify_display_update(engine, displays, n_monitors); } +#if !FLUTTER_LINUX_GTK4 static void monitor_added_cb(FlDisplayMonitor* self, GdkMonitor* monitor) { notify_display_update(self); } @@ -83,6 +84,7 @@ static void monitor_removed_cb(FlDisplayMonitor* self, GdkMonitor* monitor) { g_hash_table_remove(self->display_ids_by_monitor, monitor); notify_display_update(self); } +#endif // !FLUTTER_LINUX_GTK4 #if FLUTTER_LINUX_GTK4 static void prune_display_ids_for_current_monitors(FlDisplayMonitor* self) { From 5c64528a0df4077b198f4ebc10e294289bc790ca Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 19:09:02 -0500 Subject: [PATCH 23/34] fix(engine/linux): guard GTK3 delete-event handler --- engine/src/flutter/shell/platform/linux/fl_view.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index ca32cbbff14..bb4f8197abc 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc @@ -154,11 +154,13 @@ static FlGdkSurface* fl_view_get_toplevel_surface(FlView* self) { } // Signal handler for GtkWidget::delete-event (GTK3 only) +#if !FLUTTER_LINUX_GTK4 static gboolean window_delete_event_cb(FlView* self) { fl_engine_request_app_exit(self->engine); // Stop the event from propagating. return TRUE; } +#endif #if FLUTTER_LINUX_GTK4 // Signal handler for GtkWindow::close-request. From 03480d5d4adb95bd98a2697c8d9e3848e4ecad8c Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 19:42:40 -0500 Subject: [PATCH 24/34] fix(engine/linux): adjust GTK4 geometry sizing and compositor dimensions --- .../platform/linux/fl_compositor_opengl.cc | 5 ++ .../platform/linux/fl_compositor_software.cc | 5 ++ .../flutter/shell/platform/linux/fl_view.cc | 59 +++++++++++++++---- .../shell/platform/linux/fl_window_monitor.cc | 2 +- 4 files changed, 59 insertions(+), 12 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc b/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc index d5b6f27b05c..a64dcd64d01 100644 --- a/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc +++ b/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc @@ -375,8 +375,13 @@ static gboolean fl_compositor_opengl_render(FlCompositor* compositor, // If frame not ready, then wait for it. gint scale_factor = fl_gtk_surface_get_scale_factor(surface); +#if FLUTTER_LINUX_GTK4 + size_t width = fl_gtk_surface_get_width(surface); + size_t height = fl_gtk_surface_get_height(surface); +#else size_t width = fl_gtk_surface_get_width(surface) * scale_factor; size_t height = fl_gtk_surface_get_height(surface) * scale_factor; +#endif while (fl_framebuffer_get_width(self->framebuffer) != width || fl_framebuffer_get_height(self->framebuffer) != height) { g_mutex_unlock(&self->frame_mutex); diff --git a/engine/src/flutter/shell/platform/linux/fl_compositor_software.cc b/engine/src/flutter/shell/platform/linux/fl_compositor_software.cc index f042753ddd5..3151d940328 100644 --- a/engine/src/flutter/shell/platform/linux/fl_compositor_software.cc +++ b/engine/src/flutter/shell/platform/linux/fl_compositor_software.cc @@ -83,8 +83,13 @@ static gboolean fl_compositor_software_render(FlCompositor* compositor, // If frame not ready, then wait for it. gint scale_factor = fl_gtk_surface_get_scale_factor(surface); +#if FLUTTER_LINUX_GTK4 + size_t width = fl_gtk_surface_get_width(surface); + size_t height = fl_gtk_surface_get_height(surface); +#else size_t width = fl_gtk_surface_get_width(surface) * scale_factor; size_t height = fl_gtk_surface_get_height(surface) * scale_factor; +#endif while (self->width != width || self->height != height) { g_mutex_unlock(&self->frame_mutex); fl_task_runner_wait(self->task_runner); diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index bb4f8197abc..2eb952a2a83 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc @@ -117,6 +117,14 @@ static void fl_renderable_iface_init(FlRenderableInterface* iface); static void fl_view_plugin_registry_iface_init( FlPluginRegistryInterface* iface); +static void log_once(bool* flag, const char* message) { + if (*flag) { + return; + } + *flag = true; + g_warning("%s", message); +} + G_DEFINE_TYPE_WITH_CODE( FlView, fl_view, @@ -167,8 +175,8 @@ static gboolean window_delete_event_cb(FlView* self) { static gboolean window_close_request_cb(GtkWindow* window, FlView* self) { (void)window; fl_engine_request_app_exit(self->engine); - // Stop the event from propagating. - return TRUE; + // Allow the default handler to destroy the window if the engine doesn't. + return FALSE; } #endif @@ -251,11 +259,18 @@ static void setup_cursor(FlView* self) { } // Updates the engine with the current window metrics. -static void handle_geometry_changed(FlView* self) { - GtkAllocation allocation; - gtk_widget_get_allocation(GTK_WIDGET(self), &allocation); +static void handle_geometry_changed_with_size(FlView* self, + int width, + int height) { gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); + if (width == 0 || height == 0) { + static bool logged_zero_allocation = false; + log_once(&logged_zero_allocation, + "handle_geometry_changed: zero-size allocation"); + return; + } + // Note we can't detect if a window is moved between monitors - this // information is provided by Wayland but GTK only notifies us if the scale // has changed, so moving between two monitors of the same scale doesn't @@ -276,9 +291,21 @@ static void handle_geometry_changed(FlView* self) { display_id = fl_display_monitor_get_display_id( fl_engine_get_display_monitor(self->engine), monitor); } - fl_engine_send_window_metrics_event( - self->engine, display_id, self->view_id, allocation.width * scale_factor, - allocation.height * scale_factor, scale_factor); + fl_engine_send_window_metrics_event(self->engine, display_id, self->view_id, + width * scale_factor, + height * scale_factor, scale_factor); +} + +static void handle_geometry_changed(FlView* self) { +#if FLUTTER_LINUX_GTK4 + int width = gtk_widget_get_width(GTK_WIDGET(self->render_area)); + int height = gtk_widget_get_height(GTK_WIDGET(self->render_area)); + handle_geometry_changed_with_size(self, width, height); +#else + GtkAllocation allocation; + gtk_widget_get_allocation(GTK_WIDGET(self), &allocation); + handle_geometry_changed_with_size(self, allocation.width, allocation.height); +#endif } static void view_added_cb(GObject* object, @@ -332,6 +359,12 @@ static void fl_view_present_layers(FlRenderable* renderable, size_t layers_count) { FlView* self = FL_VIEW(renderable); + if (layers_count > 0 && layers[0] != nullptr) { + static bool logged_first_layers = false; + log_once(&logged_first_layers, + "fl_view_present_layers: received first frame layers"); + } + fl_compositor_present_layers(self->compositor, layers, layers_count); // Perform the redraw in the GTK thead. @@ -802,9 +835,7 @@ static void size_allocate_cb(FlView* self) { #if FLUTTER_LINUX_GTK4 static void resize_cb(FlView* self, int width, int height) { - (void)width; - (void)height; - handle_geometry_changed(self); + handle_geometry_changed_with_size(self, width, height); } #endif @@ -843,6 +874,12 @@ static gboolean draw_cb(FlView* self, cairo_t* cr) { gdk_gl_context_clear_current(); } + if (!result) { + static bool logged_render_false = false; + log_once(&logged_render_false, + "draw_cb: compositor render returned false"); + } + return result; } diff --git a/engine/src/flutter/shell/platform/linux/fl_window_monitor.cc b/engine/src/flutter/shell/platform/linux/fl_window_monitor.cc index 7024392edb8..39c33ec5f91 100644 --- a/engine/src/flutter/shell/platform/linux/fl_window_monitor.cc +++ b/engine/src/flutter/shell/platform/linux/fl_window_monitor.cc @@ -59,7 +59,7 @@ static void title_notify_cb(FlWindowMonitor* self) { static gboolean close_request_cb(FlWindowMonitor* self) { flutter::IsolateScope scope(self->isolate); self->on_close(); - return TRUE; + return FALSE; } #else static gboolean delete_event_cb(FlWindowMonitor* self, GdkEvent* event) { From dedbfcf72503ae0c4dccf5ebf711d3b179d33130 Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 19:44:10 -0500 Subject: [PATCH 25/34] feat(engine/linux): add GTK4 lifecycle state monitoring --- .../platform/linux/fl_window_state_monitor.cc | 104 +++++++++++++++++- 1 file changed, 99 insertions(+), 5 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc b/engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc index 6318b9e6007..0e714cb4444 100644 --- a/engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc +++ b/engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc @@ -8,7 +8,6 @@ #include "flutter/shell/platform/linux/public/flutter_linux/fl_string_codec.h" -#if !FLUTTER_LINUX_GTK4 static constexpr const char* kFlutterLifecycleChannel = "flutter/lifecycle"; static constexpr const char* kAppLifecycleStateResumed = @@ -17,7 +16,6 @@ static constexpr const char* kAppLifecycleStateInactive = "AppLifecycleState.inactive"; static constexpr const char* kAppLifecycleStateHidden = "AppLifecycleState.hidden"; -#endif struct _FlWindowStateMonitor { GObject parent_instance; @@ -35,11 +33,16 @@ struct _FlWindowStateMonitor { // Signal connection ID for window-state-changed gulong window_state_event_cb_id; + +#if FLUTTER_LINUX_GTK4 + gulong toplevel_state_notify_cb_id; + gulong surface_mapped_notify_cb_id; + gulong realize_cb_id; +#endif }; G_DEFINE_TYPE(FlWindowStateMonitor, fl_window_state_monitor, G_TYPE_OBJECT); -#if !FLUTTER_LINUX_GTK4 static void send_lifecycle_state(FlWindowStateMonitor* self, const gchar* lifecycle_state) { g_autoptr(FlValue) value = fl_value_new_string(lifecycle_state); @@ -55,7 +58,6 @@ static void send_lifecycle_state(FlWindowStateMonitor* self, fl_binary_messenger_send_on_channel(self->messenger, kFlutterLifecycleChannel, message, nullptr, nullptr, nullptr); } -#endif #if !FLUTTER_LINUX_GTK4 static gboolean is_hidden(GdkWindowState state) { @@ -63,6 +65,11 @@ static gboolean is_hidden(GdkWindowState state) { (state & GDK_WINDOW_STATE_ICONIFIED); } #endif // !FLUTTER_LINUX_GTK4 +#if FLUTTER_LINUX_GTK4 +static gboolean is_hidden(GdkToplevelState state, gboolean mapped) { + return !mapped || (state & GDK_TOPLEVEL_STATE_MINIMIZED); +} +#endif #if !FLUTTER_LINUX_GTK4 // Signal handler for GtkWindow::window-state-event static gboolean window_state_event_cb(FlWindowStateMonitor* self, @@ -91,6 +98,67 @@ static gboolean window_state_event_cb(FlWindowStateMonitor* self, } #endif // !FLUTTER_LINUX_GTK4 +#if FLUTTER_LINUX_GTK4 +static void update_lifecycle_state_gtk4(FlWindowStateMonitor* self) { + GtkNative* native = gtk_widget_get_native(GTK_WIDGET(self->window)); + if (native == nullptr) { + return; + } + + GdkSurface* surface = gtk_native_get_surface(native); + if (surface == nullptr || !GDK_IS_TOPLEVEL(surface)) { + return; + } + + GdkToplevelState state = gdk_toplevel_get_state(GDK_TOPLEVEL(surface)); + gboolean mapped = gdk_surface_get_mapped(surface); + bool visible = !is_hidden(state, mapped); + bool focused = (state & GDK_TOPLEVEL_STATE_FOCUSED); + + const gchar* lifecycle_state; + if (visible) { + lifecycle_state = + focused ? kAppLifecycleStateResumed : kAppLifecycleStateInactive; + } else { + lifecycle_state = kAppLifecycleStateHidden; + } + + send_lifecycle_state(self, lifecycle_state); +} + +static void gtk4_surface_notify_cb(FlWindowStateMonitor* self) { + update_lifecycle_state_gtk4(self); +} + +static void gtk4_setup_surface(FlWindowStateMonitor* self) { + if (self->toplevel_state_notify_cb_id != 0 || + self->surface_mapped_notify_cb_id != 0) { + return; + } + + GtkNative* native = gtk_widget_get_native(GTK_WIDGET(self->window)); + if (native == nullptr) { + return; + } + + GdkSurface* surface = gtk_native_get_surface(native); + if (surface == nullptr || !GDK_IS_TOPLEVEL(surface)) { + return; + } + + self->toplevel_state_notify_cb_id = g_signal_connect_swapped( + surface, "notify::state", G_CALLBACK(gtk4_surface_notify_cb), self); + self->surface_mapped_notify_cb_id = g_signal_connect_swapped( + surface, "notify::mapped", G_CALLBACK(gtk4_surface_notify_cb), self); + + update_lifecycle_state_gtk4(self); +} + +static void gtk4_realize_cb(FlWindowStateMonitor* self) { + gtk4_setup_surface(self); +} +#endif // FLUTTER_LINUX_GTK4 + static void fl_window_state_monitor_dispose(GObject* object) { FlWindowStateMonitor* self = FL_WINDOW_STATE_MONITOR(object); @@ -99,6 +167,28 @@ static void fl_window_state_monitor_dispose(GObject* object) { g_signal_handler_disconnect(self->window, self->window_state_event_cb_id); self->window_state_event_cb_id = 0; } +#if FLUTTER_LINUX_GTK4 + GtkNative* native = gtk_widget_get_native(GTK_WIDGET(self->window)); + if (native != nullptr) { + GdkSurface* surface = gtk_native_get_surface(native); + if (surface != nullptr) { + if (self->toplevel_state_notify_cb_id != 0) { + g_signal_handler_disconnect(surface, + self->toplevel_state_notify_cb_id); + self->toplevel_state_notify_cb_id = 0; + } + if (self->surface_mapped_notify_cb_id != 0) { + g_signal_handler_disconnect(surface, + self->surface_mapped_notify_cb_id); + self->surface_mapped_notify_cb_id = 0; + } + } + } + if (self->realize_cb_id != 0) { + g_signal_handler_disconnect(self->window, self->realize_cb_id); + self->realize_cb_id = 0; + } +#endif G_OBJECT_CLASS(fl_window_state_monitor_parent_class)->dispose(object); } @@ -126,7 +216,11 @@ FlWindowStateMonitor* fl_window_state_monitor_new(FlBinaryMessenger* messenger, gdk_window_get_state(gtk_widget_get_window(GTK_WIDGET(self->window))); #else self->window_state_event_cb_id = 0; - // TODO(gtk4): Reconnect lifecycle updates using GdkToplevel state. + self->toplevel_state_notify_cb_id = 0; + self->surface_mapped_notify_cb_id = 0; + self->realize_cb_id = g_signal_connect_swapped( + self->window, "realize", G_CALLBACK(gtk4_realize_cb), self); + gtk4_setup_surface(self); #endif return self; From b4eaeeaf9f5eaa1bf019d1d5a7f142e7d7c82f05 Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 19:44:19 -0500 Subject: [PATCH 26/34] docs(gtk4): add porting TODOs --- docs/gtk4_porting_todos.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/gtk4_porting_todos.md diff --git a/docs/gtk4_porting_todos.md b/docs/gtk4_porting_todos.md new file mode 100644 index 00000000000..7fd8b490d74 --- /dev/null +++ b/docs/gtk4_porting_todos.md @@ -0,0 +1,38 @@ +# GTK4 Porting TODOs + +This checklist maps remaining work to GTK4 migration guide changes and to +specific files in this repo. + +## Window lifecycle/state (GdkToplevel) +- Replace GTK3 `window-state-event`/`GdkWindowState` with GTK4 `GdkToplevelState` + updates in `engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc`. +- Ensure lifecycle mapping: + - visible + focused => `AppLifecycleState.resumed` + - visible + not focused => `AppLifecycleState.inactive` + - not visible/minimized => `AppLifecycleState.hidden` + +## Window configure/state notifications (GTK4 signals) +- Replace GTK3 `configure-event` and `window-state-event` in + `engine/src/flutter/shell/platform/linux/fl_window_monitor.cc` with GTK4 + equivalents (toplevel state or surface size/notify signals). + +## Accessibility (ATK removal) +- Replace ATK-based implementation with GTK4 `GtkAccessible` API or + compatible bridge: + - `engine/src/flutter/shell/platform/linux/fl_view.cc` + - `engine/src/flutter/shell/platform/linux/fl_accessibility_handler.cc` + - `engine/src/flutter/shell/platform/linux/fl_view_accessible.*` and + related ATK types + +## GTK4 test coverage +- GTK3-specific tests are disabled under `use_gtk4`. Create GTK4 tests/mocks: + - `engine/src/flutter/shell/platform/linux/fl_window_state_monitor_test.cc` + - `engine/src/flutter/shell/platform/linux/testing/mock_gtk.*` + +## Runner templates (already updated) +- GTK4 runner templates now use GTK4 APIs and pkg-config: + - `packages/flutter_tools/templates/app/linux-gtk4.tmpl/*` + +## Plugin templates (GTK4) +- Plugin templates use GTK4 but may still need review for deprecated APIs: + - `packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/*` From f50b95f84151462246e370a9dc1ce31af73be254 Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 19:55:21 -0500 Subject: [PATCH 27/34] fix(engine/linux): correct layer size log format --- .../flutter/shell/platform/linux/fl_view.cc | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index 2eb952a2a83..becda5d7bee 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc @@ -265,10 +265,18 @@ static void handle_geometry_changed_with_size(FlView* self, gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); if (width == 0 || height == 0) { - static bool logged_zero_allocation = false; - log_once(&logged_zero_allocation, - "handle_geometry_changed: zero-size allocation"); - return; + // Try to fall back to the toplevel surface size if available. + FlGdkSurface* surface = fl_view_get_toplevel_surface(self); + if (surface != nullptr) { + width = fl_gtk_surface_get_width(surface); + height = fl_gtk_surface_get_height(surface); + } + if (width == 0 || height == 0) { + static bool logged_zero_allocation = false; + log_once(&logged_zero_allocation, + "handle_geometry_changed: zero-size allocation"); + return; + } } // Note we can't detect if a window is moved between monitors - this @@ -294,6 +302,15 @@ static void handle_geometry_changed_with_size(FlView* self, fl_engine_send_window_metrics_event(self->engine, display_id, self->view_id, width * scale_factor, height * scale_factor, scale_factor); + + { + static bool logged_metrics = false; + if (!logged_metrics) { + logged_metrics = true; + g_warning("handle_geometry_changed: metrics %d x %d (scale %d)", width, + height, scale_factor); + } + } } static void handle_geometry_changed(FlView* self) { @@ -363,6 +380,12 @@ static void fl_view_present_layers(FlRenderable* renderable, static bool logged_first_layers = false; log_once(&logged_first_layers, "fl_view_present_layers: received first frame layers"); + static bool logged_layer_size = false; + if (!logged_layer_size) { + logged_layer_size = true; + g_warning("fl_view_present_layers: first layer size %g x %g", + layers[0]->size.width, layers[0]->size.height); + } } fl_compositor_present_layers(self->compositor, layers, layers_count); @@ -835,6 +858,11 @@ static void size_allocate_cb(FlView* self) { #if FLUTTER_LINUX_GTK4 static void resize_cb(FlView* self, int width, int height) { + if (width > 0 && height > 0) { + static bool logged_resize = false; + log_once(&logged_resize, + "resize_cb: received non-zero size for render area"); + } handle_geometry_changed_with_size(self, width, height); } #endif @@ -861,12 +889,25 @@ static gboolean draw_cb(FlView* self, cairo_t* cr) { FlGdkSurface* surface = fl_gtk_widget_get_surface(GTK_WIDGET(self->render_area)); if (surface == nullptr) { + static bool logged_surface_null = false; + log_once(&logged_surface_null, "draw_cb: render area has no surface"); if (self->render_context) { gdk_gl_context_clear_current(); } return FALSE; } + { + static bool logged_surface_size = false; + if (!logged_surface_size) { + logged_surface_size = true; + g_warning("draw_cb: surface size %d x %d (scale %d)", + fl_gtk_surface_get_width(surface), + fl_gtk_surface_get_height(surface), + fl_gtk_surface_get_scale_factor(surface)); + } + } + gboolean result = fl_compositor_render( self->compositor, cr, surface); From 2d8edbb329dfcaf4e26b26eab849f98bd252b55a Mon Sep 17 00:00:00 2001 From: Rich Young Date: Thu, 5 Feb 2026 23:52:31 -0500 Subject: [PATCH 28/34] fix(engine/linux): correct GTK4 orientation and compositor flips Consolidates the GTK4 compositor sizing/orientation work with the flip adjustments. --- .../platform/linux/fl_compositor_opengl.cc | 34 +++++++++++++++++-- .../platform/linux/fl_compositor_software.cc | 12 +++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc b/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc index a64dcd64d01..fbcd8ef4d48 100644 --- a/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc +++ b/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc @@ -14,6 +14,7 @@ #include "flutter/shell/platform/linux/fl_gtk.h" // Vertex shader to draw Flutter window contents. +#if FLUTTER_LINUX_GTK4 static const char* vertex_shader_src = "attribute vec2 position;\n" "attribute vec2 in_texcoord;\n" @@ -25,6 +26,19 @@ static const char* vertex_shader_src = " gl_Position = vec4(offset + position * scale, 0, 1);\n" " texcoord = in_texcoord;\n" "}\n"; +#else +static const char* vertex_shader_src = + "attribute vec2 position;\n" + "attribute vec2 in_texcoord;\n" + "uniform vec2 offset;\n" + "uniform vec2 scale;\n" + "varying vec2 texcoord;\n" + "\n" + "void main() {\n" + " gl_Position = vec4(offset + position * scale, 0, 1);\n" + " texcoord = in_texcoord;\n" + "}\n"; +#endif // Fragment shader to draw Flutter window contents. static const char* fragment_shader_src = @@ -376,8 +390,16 @@ static gboolean fl_compositor_opengl_render(FlCompositor* compositor, // If frame not ready, then wait for it. gint scale_factor = fl_gtk_surface_get_scale_factor(surface); #if FLUTTER_LINUX_GTK4 - size_t width = fl_gtk_surface_get_width(surface); - size_t height = fl_gtk_surface_get_height(surface); + // In GTK4, the draw surface is the toplevel. Use the Cairo clip + // extents to get the drawing area size for this widget. + double x1 = 0.0, y1 = 0.0, x2 = 0.0, y2 = 0.0; + cairo_clip_extents(cr, &x1, &y1, &x2, &y2); + size_t width = static_cast(x2 - x1); + size_t height = static_cast(y2 - y1); + if (width == 0 || height == 0) { + width = fl_gtk_surface_get_width(surface); + height = fl_gtk_surface_get_height(surface); + } #else size_t width = fl_gtk_surface_get_width(surface) * scale_factor; size_t height = fl_gtk_surface_get_height(surface) * scale_factor; @@ -393,6 +415,9 @@ static gboolean fl_compositor_opengl_render(FlCompositor* compositor, g_autoptr(FlFramebuffer) sibling = fl_framebuffer_create_sibling(self->framebuffer); #if FLUTTER_LINUX_GTK4 + cairo_save(cr); + cairo_translate(cr, 0.0, static_cast(height)); + cairo_scale(cr, 1.0, -1.0); G_GNUC_BEGIN_IGNORE_DEPRECATIONS #endif gdk_cairo_draw_from_gl(cr, surface, @@ -400,6 +425,7 @@ static gboolean fl_compositor_opengl_render(FlCompositor* compositor, GL_TEXTURE, scale_factor, 0, 0, width, height); #if FLUTTER_LINUX_GTK4 G_GNUC_END_IGNORE_DEPRECATIONS + cairo_restore(cr); #endif } else { GLint saved_texture_binding; @@ -412,12 +438,16 @@ static gboolean fl_compositor_opengl_render(FlCompositor* compositor, GL_UNSIGNED_BYTE, self->pixels); #if FLUTTER_LINUX_GTK4 + cairo_save(cr); + cairo_translate(cr, 0.0, static_cast(height)); + cairo_scale(cr, 1.0, -1.0); G_GNUC_BEGIN_IGNORE_DEPRECATIONS #endif gdk_cairo_draw_from_gl(cr, surface, texture_id, GL_TEXTURE, scale_factor, 0, 0, width, height); #if FLUTTER_LINUX_GTK4 G_GNUC_END_IGNORE_DEPRECATIONS + cairo_restore(cr); #endif glDeleteTextures(1, &texture_id); diff --git a/engine/src/flutter/shell/platform/linux/fl_compositor_software.cc b/engine/src/flutter/shell/platform/linux/fl_compositor_software.cc index 3151d940328..222f94335f2 100644 --- a/engine/src/flutter/shell/platform/linux/fl_compositor_software.cc +++ b/engine/src/flutter/shell/platform/linux/fl_compositor_software.cc @@ -84,8 +84,16 @@ static gboolean fl_compositor_software_render(FlCompositor* compositor, // If frame not ready, then wait for it. gint scale_factor = fl_gtk_surface_get_scale_factor(surface); #if FLUTTER_LINUX_GTK4 - size_t width = fl_gtk_surface_get_width(surface); - size_t height = fl_gtk_surface_get_height(surface); + // In GTK4, the draw surface is the toplevel. Use the Cairo clip + // extents to get the drawing area size for this widget. + double x1 = 0.0, y1 = 0.0, x2 = 0.0, y2 = 0.0; + cairo_clip_extents(cr, &x1, &y1, &x2, &y2); + size_t width = static_cast(x2 - x1); + size_t height = static_cast(y2 - y1); + if (width == 0 || height == 0) { + width = fl_gtk_surface_get_width(surface); + height = fl_gtk_surface_get_height(surface); + } #else size_t width = fl_gtk_surface_get_width(surface) * scale_factor; size_t height = fl_gtk_surface_get_height(surface) * scale_factor; From 8f9ef6f4e7430a9e0ad080dea8eaae6bcda23742 Mon Sep 17 00:00:00 2001 From: Rich Young Date: Fri, 6 Feb 2026 02:03:33 -0500 Subject: [PATCH 29/34] fix(templates/linux): update GTK4 runner header and focus --- .../app/linux-gtk4.tmpl/runner/my_application.cc.tmpl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/my_application.cc.tmpl b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/my_application.cc.tmpl index 94d460068eb..d5cbd590953 100644 --- a/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/my_application.cc.tmpl +++ b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/my_application.cc.tmpl @@ -30,6 +30,8 @@ static void my_application_activate(GApplication* application) { // Use a header bar as the default style for GNOME/GTK4 applications. GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_header_bar_set_show_title_buttons(header_bar, TRUE); + GtkWidget* title_label = gtk_label_new("{{projectName}}"); + gtk_header_bar_set_title_widget(header_bar, title_label); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); gtk_window_set_title(window, "{{projectName}}"); @@ -45,7 +47,8 @@ static void my_application_activate(GApplication* application) { // for transparent. gdk_rgba_parse(&background_color, "#000000"); fl_view_set_background_color(view, &background_color); - gtk_widget_show(GTK_WIDGET(view)); + gtk_widget_set_focusable(GTK_WIDGET(view), TRUE); + gtk_widget_set_visible(GTK_WIDGET(view), TRUE); gtk_window_set_child(window, GTK_WIDGET(view)); // Show the window when Flutter renders. From de3019c86e77e21d3f4c76d78b476cdfefddb8f1 Mon Sep 17 00:00:00 2001 From: Rich Young Date: Fri, 6 Feb 2026 02:27:16 -0500 Subject: [PATCH 30/34] todo updates --- docs/gtk4_porting_todos.md | 66 ++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/docs/gtk4_porting_todos.md b/docs/gtk4_porting_todos.md index 7fd8b490d74..3fd543b1ce9 100644 --- a/docs/gtk4_porting_todos.md +++ b/docs/gtk4_porting_todos.md @@ -1,38 +1,48 @@ # GTK4 Porting TODOs -This checklist maps remaining work to GTK4 migration guide changes and to -specific files in this repo. +This checklist tracks remaining GTK4 work and notes what’s already landed in +this branch. Keep it short and focused on actionable items. -## Window lifecycle/state (GdkToplevel) -- Replace GTK3 `window-state-event`/`GdkWindowState` with GTK4 `GdkToplevelState` - updates in `engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc`. -- Ensure lifecycle mapping: - - visible + focused => `AppLifecycleState.resumed` - - visible + not focused => `AppLifecycleState.inactive` - - not visible/minimized => `AppLifecycleState.hidden` +## Done (GTK4 baseline in this branch) +- [x] GTK4 close-request wiring in `engine/src/flutter/shell/platform/linux/fl_window_monitor.cc`. +- [x] GTK4 lifecycle monitoring via `GdkToplevelState` + `GdkSurface::mapped` in + `engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc`. +- [x] GTK4 drawing area resize handling in `engine/src/flutter/shell/platform/linux/fl_view.cc`. +- [x] GTK4 input controllers/gestures wired in `engine/src/flutter/shell/platform/linux/fl_view.cc`. +- [x] GTK4 compositor sizing/orientation + output flip fixes in + `engine/src/flutter/shell/platform/linux/fl_compositor_*`. +- [x] GTK4 runner template updates in + `packages/flutter_tools/templates/app/linux-gtk4.tmpl/*`. ## Window configure/state notifications (GTK4 signals) -- Replace GTK3 `configure-event` and `window-state-event` in - `engine/src/flutter/shell/platform/linux/fl_window_monitor.cc` with GTK4 - equivalents (toplevel state or surface size/notify signals). +- [ ] Replace GTK3 `configure-event`/`window-state-event` in + `engine/src/flutter/shell/platform/linux/fl_window_monitor.cc` with GTK4 + equivalents: + - `GdkSurface::notify::width` / `notify::height` for size. + - `GdkSurface::notify::state` for minimize/maximize/fullscreen. +- [ ] Verify callbacks on Wayland and X11 (`on_configure`, `on_state_changed`). + +## Defaults, compatibility, and tooling +- [ ] Make GTK4 the default for new Linux apps (`flutter create --platforms=linux`). +- [ ] Keep GTK3 opt-in (flag/env) and document the selection mechanism. +- [ ] Decide how to keep/rename GTK3 runner templates (`linux-gtk3.tmpl`) so + they’re available but not default. ## Accessibility (ATK removal) -- Replace ATK-based implementation with GTK4 `GtkAccessible` API or - compatible bridge: - - `engine/src/flutter/shell/platform/linux/fl_view.cc` - - `engine/src/flutter/shell/platform/linux/fl_accessibility_handler.cc` - - `engine/src/flutter/shell/platform/linux/fl_view_accessible.*` and - related ATK types +- [ ] Replace ATK-based implementation with GTK4 `GtkAccessible` (or bridge): + - `engine/src/flutter/shell/platform/linux/fl_accessibility_handler.cc` + - `engine/src/flutter/shell/platform/linux/fl_view_accessible.*` + - `engine/src/flutter/shell/platform/linux/fl_accessible_*` +- [ ] Update ATK-based tests to GTK4 equivalents in + `engine/src/flutter/shell/platform/linux/*_test.cc`. ## GTK4 test coverage -- GTK3-specific tests are disabled under `use_gtk4`. Create GTK4 tests/mocks: - - `engine/src/flutter/shell/platform/linux/fl_window_state_monitor_test.cc` - - `engine/src/flutter/shell/platform/linux/testing/mock_gtk.*` +- [ ] Add GTK4 mocks in `engine/src/flutter/shell/platform/linux/testing/mock_gtk.*` + for `GdkSurface` and GTK4-only APIs. +- [ ] Add a GTK4 CI shard/build step in `dev/bots/test.dart` for + `flutter_linux_unittests` with `use_gtk4=true`. -## Runner templates (already updated) -- GTK4 runner templates now use GTK4 APIs and pkg-config: - - `packages/flutter_tools/templates/app/linux-gtk4.tmpl/*` - -## Plugin templates (GTK4) -- Plugin templates use GTK4 but may still need review for deprecated APIs: - - `packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/*` +## Plugins & registrant +- [ ] Audit `packages/flutter_tools/templates/plugin/linux-gtk4.tmpl/*` + for deprecated GTK3 APIs and verify example builds. +- [ ] Validate `flutter_linux` plugin registrar types against GTK4. From c12a5c8cc589b23b9338894777e3ae853f2a56ac Mon Sep 17 00:00:00 2001 From: Rich Young Date: Fri, 6 Feb 2026 02:32:10 -0500 Subject: [PATCH 31/34] test(engine/linux): add GTK4 window state monitor coverage --- .../linux/fl_window_state_monitor_test.cc | 91 +++++++++++++ .../shell/platform/linux/testing/mock_gtk.cc | 128 ++++++++++++++++++ .../shell/platform/linux/testing/mock_gtk.h | 6 + 3 files changed, 225 insertions(+) diff --git a/engine/src/flutter/shell/platform/linux/fl_window_state_monitor_test.cc b/engine/src/flutter/shell/platform/linux/fl_window_state_monitor_test.cc index 84de78122a7..b9017b5df9a 100644 --- a/engine/src/flutter/shell/platform/linux/fl_window_state_monitor_test.cc +++ b/engine/src/flutter/shell/platform/linux/fl_window_state_monitor_test.cc @@ -10,6 +10,11 @@ #include "gtest/gtest.h" +#if FLUTTER_LINUX_GTK4 +#include +#include +#endif + #if !FLUTTER_LINUX_GTK4 TEST(FlWindowStateMonitorTest, GainFocus) { g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); @@ -289,3 +294,89 @@ TEST(FlWindowStateMonitorTest, LeaveWithdrawnFocused) { } #endif // !FLUTTER_LINUX_GTK4 + +#if FLUTTER_LINUX_GTK4 +TEST(FlWindowStateMonitorTest, Gtk4FocusToInactive) { + g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); + ::testing::NiceMock mock_gtk; + + gtk_init(0, nullptr); + + EXPECT_CALL(mock_gtk, gdk_surface_get_mapped) + .WillRepeatedly(::testing::Return(TRUE)); + EXPECT_CALL(mock_gtk, gdk_toplevel_get_state) + .WillOnce(::testing::Return(GDK_TOPLEVEL_STATE_FOCUSED)) + .WillOnce(::testing::Return(static_cast(0))); + + std::vector lifecycle_states; + fl_mock_binary_messenger_set_string_message_channel( + messenger, "flutter/lifecycle", + [](FlMockBinaryMessenger* messenger, GTask* task, FlValue* message, + gpointer user_data) { + auto* states = static_cast*>(user_data); + states->emplace_back(fl_value_get_string(message)); + return fl_value_new_string(""); + }, + &lifecycle_states); + + GtkWindow* window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL)); + gtk_widget_show(GTK_WIDGET(window)); + g_autoptr(FlWindowStateMonitor) monitor = + fl_window_state_monitor_new(FL_BINARY_MESSENGER(messenger), window); + + GtkNative* native = gtk_widget_get_native(GTK_WIDGET(window)); + GdkSurface* surface = gtk_native_get_surface(native); + GParamSpec* state_pspec = + g_object_class_find_property(G_OBJECT_GET_CLASS(surface), "state"); + ASSERT_NE(state_pspec, nullptr); + g_object_notify_by_pspec(G_OBJECT(surface), state_pspec); + + ASSERT_EQ(lifecycle_states.size(), 2u); + EXPECT_EQ(lifecycle_states[0], "AppLifecycleState.resumed"); + EXPECT_EQ(lifecycle_states[1], "AppLifecycleState.inactive"); + + fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); +} + +TEST(FlWindowStateMonitorTest, Gtk4MappedToHidden) { + g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); + ::testing::NiceMock mock_gtk; + + gtk_init(0, nullptr); + + EXPECT_CALL(mock_gtk, gdk_surface_get_mapped) + .WillOnce(::testing::Return(TRUE)) + .WillOnce(::testing::Return(FALSE)); + EXPECT_CALL(mock_gtk, gdk_toplevel_get_state) + .WillRepeatedly(::testing::Return(static_cast(0))); + + std::vector lifecycle_states; + fl_mock_binary_messenger_set_string_message_channel( + messenger, "flutter/lifecycle", + [](FlMockBinaryMessenger* messenger, GTask* task, FlValue* message, + gpointer user_data) { + auto* states = static_cast*>(user_data); + states->emplace_back(fl_value_get_string(message)); + return fl_value_new_string(""); + }, + &lifecycle_states); + + GtkWindow* window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL)); + gtk_widget_show(GTK_WIDGET(window)); + g_autoptr(FlWindowStateMonitor) monitor = + fl_window_state_monitor_new(FL_BINARY_MESSENGER(messenger), window); + + GtkNative* native = gtk_widget_get_native(GTK_WIDGET(window)); + GdkSurface* surface = gtk_native_get_surface(native); + GParamSpec* mapped_pspec = + g_object_class_find_property(G_OBJECT_GET_CLASS(surface), "mapped"); + ASSERT_NE(mapped_pspec, nullptr); + g_object_notify_by_pspec(G_OBJECT(surface), mapped_pspec); + + ASSERT_EQ(lifecycle_states.size(), 2u); + EXPECT_EQ(lifecycle_states[0], "AppLifecycleState.inactive"); + EXPECT_EQ(lifecycle_states[1], "AppLifecycleState.hidden"); + + fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); +} +#endif // FLUTTER_LINUX_GTK4 diff --git a/engine/src/flutter/shell/platform/linux/testing/mock_gtk.cc b/engine/src/flutter/shell/platform/linux/testing/mock_gtk.cc index 61059dcf7d9..c184ad08541 100644 --- a/engine/src/flutter/shell/platform/linux/testing/mock_gtk.cc +++ b/engine/src/flutter/shell/platform/linux/testing/mock_gtk.cc @@ -25,6 +25,91 @@ static void fl_mock_keymap_class_init(FlMockKeymapClass* klass) { static void fl_mock_keymap_init(FlMockKeymap* self) {} +#if FLUTTER_LINUX_GTK4 +G_DECLARE_FINAL_TYPE(FlMockGtk4Surface, + fl_mock_gtk4_surface, + FL, + MOCK_GTK4_SURFACE, + GObject) + +struct _FlMockGtk4Surface { + GObject parent_instance; + GdkToplevelState state; + gboolean mapped; +}; + +G_DEFINE_TYPE_WITH_CODE( + FlMockGtk4Surface, + fl_mock_gtk4_surface, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(GDK_TYPE_TOPLEVEL, nullptr)) + +enum { + kPropState = 1, + kPropMapped, + kPropLast, +}; + +static GParamSpec* g_properties[kPropLast]; + +static void fl_mock_gtk4_surface_set_property(GObject* object, + guint prop_id, + const GValue* value, + GParamSpec* pspec) { + FlMockGtk4Surface* self = FL_MOCK_GTK4_SURFACE(object); + switch (prop_id) { + case kPropState: + self->state = + static_cast(g_value_get_flags(value)); + break; + case kPropMapped: + self->mapped = g_value_get_boolean(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void fl_mock_gtk4_surface_get_property(GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec) { + FlMockGtk4Surface* self = FL_MOCK_GTK4_SURFACE(object); + switch (prop_id) { + case kPropState: + g_value_set_flags(value, self->state); + break; + case kPropMapped: + g_value_set_boolean(value, self->mapped); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void fl_mock_gtk4_surface_class_init(FlMockGtk4SurfaceClass* klass) { + GObjectClass* object_class = G_OBJECT_CLASS(klass); + object_class->set_property = fl_mock_gtk4_surface_set_property; + object_class->get_property = fl_mock_gtk4_surface_get_property; + + g_properties[kPropState] = + g_param_spec_flags("state", "state", "state", GDK_TYPE_TOPLEVEL_STATE, + static_cast(0), + static_cast(G_PARAM_READWRITE)); + g_properties[kPropMapped] = + g_param_spec_boolean("mapped", "mapped", "mapped", FALSE, + static_cast(G_PARAM_READWRITE)); + g_object_class_install_properties(object_class, kPropLast, g_properties); +} + +static void fl_mock_gtk4_surface_init(FlMockGtk4Surface* self) { + self->state = static_cast(0); + self->mapped = FALSE; +} +#endif // FLUTTER_LINUX_GTK4 + // Override GdkKeymap GType gdk_keymap_get_type() { return fl_mock_keymap_get_type(); @@ -92,6 +177,17 @@ GdkWindowState gdk_window_get_state(GdkWindow* window) { return mock->gdk_window_get_state(window); } +#if FLUTTER_LINUX_GTK4 +GdkToplevelState gdk_toplevel_get_state(GdkToplevel* toplevel) { + check_thread(); + if (mock != nullptr) { + return mock->gdk_toplevel_get_state(toplevel); + } + FlMockGtk4Surface* surface = FL_MOCK_GTK4_SURFACE(toplevel); + return surface->state; +} +#endif + GdkDisplay* gdk_window_get_display(GdkWindow* window) { check_thread(); return GDK_DISPLAY(g_object_new(gdk_wayland_display_get_type(), nullptr)); @@ -141,6 +237,17 @@ GdkGLContext* gdk_window_create_gl_context(GdkWindow* window, GError** error) { return nullptr; } +#if FLUTTER_LINUX_GTK4 +gboolean gdk_surface_get_mapped(GdkSurface* surface) { + check_thread(); + if (mock != nullptr) { + return mock->gdk_surface_get_mapped(surface); + } + FlMockGtk4Surface* gtk4_surface = FL_MOCK_GTK4_SURFACE(surface); + return gtk4_surface->mapped; +} +#endif + void gdk_cairo_set_source_rgba(cairo_t* cr, const GdkRGBA* rgba) { check_thread(); } @@ -301,6 +408,27 @@ GdkWindow* gtk_widget_get_window(GtkWidget* widget) { return nullptr; } +#if FLUTTER_LINUX_GTK4 +GtkNative* gtk_widget_get_native(GtkWidget* widget) { + check_thread(); + static GObject* mock_native = nullptr; + if (mock_native == nullptr) { + mock_native = G_OBJECT(g_object_new(G_TYPE_OBJECT, nullptr)); + } + return reinterpret_cast(mock_native); +} + +GdkSurface* gtk_native_get_surface(GtkNative* native) { + check_thread(); + static FlMockGtk4Surface* mock_surface = nullptr; + if (mock_surface == nullptr) { + mock_surface = FL_MOCK_GTK4_SURFACE( + g_object_new(fl_mock_gtk4_surface_get_type(), nullptr)); + } + return reinterpret_cast(mock_surface); +} +#endif // FLUTTER_LINUX_GTK4 + void gtk_im_context_set_client_window(GtkIMContext* context, GdkWindow* window) { check_thread(); diff --git a/engine/src/flutter/shell/platform/linux/testing/mock_gtk.h b/engine/src/flutter/shell/platform/linux/testing/mock_gtk.h index 09cf144f6d6..2114adf7176 100644 --- a/engine/src/flutter/shell/platform/linux/testing/mock_gtk.h +++ b/engine/src/flutter/shell/platform/linux/testing/mock_gtk.h @@ -23,6 +23,12 @@ class MockGtk { gdk_keymap_lookup_key, (GdkKeymap * keymap, const GdkKeymapKey* key)); MOCK_METHOD(GdkWindowState, gdk_window_get_state, (GdkWindow * window)); +#if FLUTTER_LINUX_GTK4 + MOCK_METHOD(GdkToplevelState, + gdk_toplevel_get_state, + (GdkToplevel * toplevel)); + MOCK_METHOD(gboolean, gdk_surface_get_mapped, (GdkSurface * surface)); +#endif MOCK_METHOD(void, gtk_window_new, (GtkWindow * window, GtkWindowType type)); MOCK_METHOD(void, gtk_window_set_default_size, From 59e57067232470654b3bac9b5e6ca43645c7fa8e Mon Sep 17 00:00:00 2001 From: Rich Young Date: Fri, 6 Feb 2026 02:45:52 -0500 Subject: [PATCH 32/34] style(engine/linux): format BUILD.gn --- .../src/flutter/shell/platform/linux/BUILD.gn | 170 +++++++++--------- 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/BUILD.gn b/engine/src/flutter/shell/platform/linux/BUILD.gn index 62c6ce43e46..2920565750c 100644 --- a/engine/src/flutter/shell/platform/linux/BUILD.gn +++ b/engine/src/flutter/shell/platform/linux/BUILD.gn @@ -192,9 +192,9 @@ source_set("flutter_linux_sources") { # Set flag to stop headers being directly included (library users should not do this) defines = [ - "FLUTTER_LINUX_COMPILATION", - "FLUTTER_ENGINE_NO_PROTOTYPES", - ] + _gtk_defines + "FLUTTER_LINUX_COMPILATION", + "FLUTTER_ENGINE_NO_PROTOTYPES", + ] + _gtk_defines deps = [ "$dart_src/runtime:dart_api", @@ -234,94 +234,94 @@ copy("flutter_linux_gschemas") { if (!use_gtk4) { executable("flutter_linux_unittests") { - testonly = true + testonly = true - sources = [ - "fl_accessibility_handler_test.cc", - "fl_accessible_node_test.cc", - "fl_accessible_text_field_test.cc", - "fl_application_test.cc", - "fl_basic_message_channel_test.cc", - "fl_binary_codec_test.cc", - "fl_binary_messenger_test.cc", - "fl_compositor_opengl_test.cc", - "fl_compositor_software_test.cc", - "fl_dart_project_test.cc", - "fl_display_monitor_test.cc", - "fl_engine_test.cc", - "fl_event_channel_test.cc", - "fl_framebuffer_test.cc", - "fl_gnome_settings_test.cc", - "fl_json_message_codec_test.cc", - "fl_json_method_codec_test.cc", - "fl_key_channel_responder_test.cc", - "fl_key_embedder_responder_test.cc", - "fl_keyboard_handler_test.cc", - "fl_keyboard_layout_test.cc", - "fl_keyboard_manager_test.cc", - "fl_message_codec_test.cc", - "fl_method_channel_test.cc", - "fl_method_codec_test.cc", - "fl_method_response_test.cc", - "fl_pixel_buffer_texture_test.cc", - "fl_platform_channel_test.cc", - "fl_platform_handler_test.cc", - "fl_plugin_registrar_test.cc", - "fl_pointer_manager_test.cc", - "fl_scrolling_manager_test.cc", - "fl_settings_handler_test.cc", - "fl_settings_portal_test.cc", - "fl_standard_message_codec_test.cc", - "fl_standard_method_codec_test.cc", - "fl_string_codec_test.cc", - "fl_text_input_handler_test.cc", - "fl_texture_gl_test.cc", - "fl_texture_registrar_test.cc", - "fl_touch_manager_test.cc", - "fl_value_test.cc", - "fl_view_accessible_test.cc", - "fl_view_test.cc", - "fl_window_state_monitor_test.cc", - "key_mapping_test.cc", - "testing/fl_mock_binary_messenger.cc", - "testing/fl_test.cc", - "testing/fl_test_gtk_logs.cc", - "testing/fl_test_gtk_logs.h", - "testing/mock_engine.cc", - "testing/mock_epoxy.cc", - "testing/mock_gtk.cc", - "testing/mock_plugin_registrar.cc", - "testing/mock_renderable.cc", - "testing/mock_settings.cc", - "testing/mock_signal_handler.cc", - "testing/mock_texture_registrar.cc", - ] + sources = [ + "fl_accessibility_handler_test.cc", + "fl_accessible_node_test.cc", + "fl_accessible_text_field_test.cc", + "fl_application_test.cc", + "fl_basic_message_channel_test.cc", + "fl_binary_codec_test.cc", + "fl_binary_messenger_test.cc", + "fl_compositor_opengl_test.cc", + "fl_compositor_software_test.cc", + "fl_dart_project_test.cc", + "fl_display_monitor_test.cc", + "fl_engine_test.cc", + "fl_event_channel_test.cc", + "fl_framebuffer_test.cc", + "fl_gnome_settings_test.cc", + "fl_json_message_codec_test.cc", + "fl_json_method_codec_test.cc", + "fl_key_channel_responder_test.cc", + "fl_key_embedder_responder_test.cc", + "fl_keyboard_handler_test.cc", + "fl_keyboard_layout_test.cc", + "fl_keyboard_manager_test.cc", + "fl_message_codec_test.cc", + "fl_method_channel_test.cc", + "fl_method_codec_test.cc", + "fl_method_response_test.cc", + "fl_pixel_buffer_texture_test.cc", + "fl_platform_channel_test.cc", + "fl_platform_handler_test.cc", + "fl_plugin_registrar_test.cc", + "fl_pointer_manager_test.cc", + "fl_scrolling_manager_test.cc", + "fl_settings_handler_test.cc", + "fl_settings_portal_test.cc", + "fl_standard_message_codec_test.cc", + "fl_standard_method_codec_test.cc", + "fl_string_codec_test.cc", + "fl_text_input_handler_test.cc", + "fl_texture_gl_test.cc", + "fl_texture_registrar_test.cc", + "fl_touch_manager_test.cc", + "fl_value_test.cc", + "fl_view_accessible_test.cc", + "fl_view_test.cc", + "fl_window_state_monitor_test.cc", + "key_mapping_test.cc", + "testing/fl_mock_binary_messenger.cc", + "testing/fl_test.cc", + "testing/fl_test_gtk_logs.cc", + "testing/fl_test_gtk_logs.h", + "testing/mock_engine.cc", + "testing/mock_epoxy.cc", + "testing/mock_gtk.cc", + "testing/mock_plugin_registrar.cc", + "testing/mock_renderable.cc", + "testing/mock_settings.cc", + "testing/mock_signal_handler.cc", + "testing/mock_texture_registrar.cc", + ] - public_configs = [ "//flutter:config" ] + public_configs = [ "//flutter:config" ] - configs += [ - _gtk_config, - "//flutter/shell/platform/linux/config:epoxy", - ] + configs += [ + _gtk_config, + "//flutter/shell/platform/linux/config:epoxy", + ] - defines = [ - "FLUTTER_ENGINE_NO_PROTOTYPES", + defines = [ + "FLUTTER_ENGINE_NO_PROTOTYPES", - # Set flag to allow public headers to be directly included - # (library users should not do this) - "FLUTTER_LINUX_COMPILATION", - ] + # Set flag to allow public headers to be directly included + # (library users should not do this) + "FLUTTER_LINUX_COMPILATION", + ] - deps = [ - ":flutter_linux_fixtures", - ":flutter_linux_gschemas", - ":flutter_linux_sources", - "//flutter/runtime:libdart", - "//flutter/shell/platform/common:common_cpp_enums", - "//flutter/shell/platform/embedder:embedder_headers", - "//flutter/shell/platform/embedder:embedder_test_utils", - "//flutter/testing", - ] + deps = [ + ":flutter_linux_fixtures", + ":flutter_linux_gschemas", + ":flutter_linux_sources", + "//flutter/runtime:libdart", + "//flutter/shell/platform/common:common_cpp_enums", + "//flutter/shell/platform/embedder:embedder_headers", + "//flutter/shell/platform/embedder:embedder_test_utils", + "//flutter/testing", + ] } } From 2bef41145b67ff437f79eb689f6987fe529ed79a Mon Sep 17 00:00:00 2001 From: Rich Young Date: Fri, 6 Feb 2026 02:46:01 -0500 Subject: [PATCH 33/34] style(engine/linux): format embedder sources --- .../shell/platform/linux/fl_compositor.h | 2 +- .../platform/linux/fl_compositor_opengl.cc | 3 +- .../platform/linux/fl_display_monitor.cc | 3 +- .../platform/linux/fl_keyboard_manager.cc | 3 +- .../platform/linux/fl_text_input_handler.cc | 11 +++-- .../flutter/shell/platform/linux/fl_view.cc | 41 +++++++++---------- .../platform/linux/fl_window_state_monitor.cc | 6 +-- 7 files changed, 30 insertions(+), 39 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/fl_compositor.h b/engine/src/flutter/shell/platform/linux/fl_compositor.h index a633dd16ea9..f8385e1b8b5 100644 --- a/engine/src/flutter/shell/platform/linux/fl_compositor.h +++ b/engine/src/flutter/shell/platform/linux/fl_compositor.h @@ -8,8 +8,8 @@ #include #include -#include "flutter/shell/platform/linux/fl_gtk.h" #include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/linux/fl_gtk.h" G_BEGIN_DECLS diff --git a/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc b/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc index fbcd8ef4d48..47b3fe66f70 100644 --- a/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc +++ b/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc @@ -420,8 +420,7 @@ static gboolean fl_compositor_opengl_render(FlCompositor* compositor, cairo_scale(cr, 1.0, -1.0); G_GNUC_BEGIN_IGNORE_DEPRECATIONS #endif - gdk_cairo_draw_from_gl(cr, surface, - fl_framebuffer_get_texture_id(sibling), + gdk_cairo_draw_from_gl(cr, surface, fl_framebuffer_get_texture_id(sibling), GL_TEXTURE, scale_factor, 0, 0, width, height); #if FLUTTER_LINUX_GTK4 G_GNUC_END_IGNORE_DEPRECATIONS diff --git a/engine/src/flutter/shell/platform/linux/fl_display_monitor.cc b/engine/src/flutter/shell/platform/linux/fl_display_monitor.cc index b6405e7473d..9f01f3330bc 100644 --- a/engine/src/flutter/shell/platform/linux/fl_display_monitor.cc +++ b/engine/src/flutter/shell/platform/linux/fl_display_monitor.cc @@ -42,8 +42,7 @@ static void notify_display_update(FlDisplayMonitor* self) { FlutterEngineDisplay* display = &displays[i]; #if FLUTTER_LINUX_GTK4 - GdkMonitor* monitor = - GDK_MONITOR(g_list_model_get_item(monitors, i)); + GdkMonitor* monitor = GDK_MONITOR(g_list_model_get_item(monitors, i)); #else GdkMonitor* monitor = gdk_display_get_monitor(self->display, i); #endif diff --git a/engine/src/flutter/shell/platform/linux/fl_keyboard_manager.cc b/engine/src/flutter/shell/platform/linux/fl_keyboard_manager.cc index e8bad568e2f..84b03481c5f 100644 --- a/engine/src/flutter/shell/platform/linux/fl_keyboard_manager.cc +++ b/engine/src/flutter/shell/platform/linux/fl_keyboard_manager.cc @@ -370,8 +370,7 @@ static void fl_keyboard_manager_init(FlKeyboardManager* self) { } #if FLUTTER_LINUX_GTK4 - self->display = - GDK_DISPLAY(g_object_ref(gdk_display_get_default())); + self->display = GDK_DISPLAY(g_object_ref(gdk_display_get_default())); #else self->keymap = gdk_keymap_get_for_display(gdk_display_get_default()); self->keymap_keys_changed_cb_id = g_signal_connect_swapped( diff --git a/engine/src/flutter/shell/platform/linux/fl_text_input_handler.cc b/engine/src/flutter/shell/platform/linux/fl_text_input_handler.cc index af3d9ed2040..de4b9d237c9 100644 --- a/engine/src/flutter/shell/platform/linux/fl_text_input_handler.cc +++ b/engine/src/flutter/shell/platform/linux/fl_text_input_handler.cc @@ -367,8 +367,7 @@ static void update_im_cursor_position(FlTextInputHandler* self) { preedit_rect.x = static_cast(dest_x); preedit_rect.y = static_cast(dest_y); #else - gtk_widget_translate_coordinates(self->widget, toplevel, - static_cast(x), + gtk_widget_translate_coordinates(self->widget, toplevel, static_cast(x), static_cast(y), &preedit_rect.x, &preedit_rect.y); #endif @@ -523,12 +522,12 @@ gboolean fl_text_input_handler_filter_keypress(FlTextInputHandler* self, return FALSE; } - if (gtk_im_context_filter_keypress( - self->im_context, + if (gtk_im_context_filter_keypress(self->im_context, #if FLUTTER_LINUX_GTK4 - fl_key_event_get_origin(event))) { + fl_key_event_get_origin(event))) { #else - reinterpret_cast(fl_key_event_get_origin(event)))) { + reinterpret_cast( + fl_key_event_get_origin(event)))) { #endif return TRUE; } diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index becda5d7bee..5fd386e0897 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc @@ -568,8 +568,8 @@ static FlutterPointerDeviceKind get_device_kind_or_default(GdkEvent* event) { } static void motion_event_cb(FlView* self, gdouble x, gdouble y) { - GdkEvent* event = get_current_event(GTK_EVENT_CONTROLLER( - self->motion_controller)); + GdkEvent* event = + get_current_event(GTK_EVENT_CONTROLLER(self->motion_controller)); if (event == nullptr) { return; } @@ -585,13 +585,13 @@ static void motion_event_cb(FlView* self, gdouble x, gdouble y) { gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); fl_pointer_manager_handle_motion( - self->pointer_manager, gdk_event_get_time(event), - get_device_kind(event), x * scale_factor, y * scale_factor); + self->pointer_manager, gdk_event_get_time(event), get_device_kind(event), + x * scale_factor, y * scale_factor); } static void enter_event_cb(FlView* self, gdouble x, gdouble y) { - GdkEvent* event = get_current_event(GTK_EVENT_CONTROLLER( - self->motion_controller)); + GdkEvent* event = + get_current_event(GTK_EVENT_CONTROLLER(self->motion_controller)); gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); fl_pointer_manager_handle_enter( self->pointer_manager, get_event_time_or_now(event), @@ -599,23 +599,21 @@ static void enter_event_cb(FlView* self, gdouble x, gdouble y) { } static void leave_event_cb(FlView* self, gdouble x, gdouble y) { - GdkEvent* event = get_current_event(GTK_EVENT_CONTROLLER( - self->motion_controller)); + GdkEvent* event = + get_current_event(GTK_EVENT_CONTROLLER(self->motion_controller)); gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self)); fl_pointer_manager_handle_leave( self->pointer_manager, get_event_time_or_now(event), get_device_kind_or_default(event), x * scale_factor, y * scale_factor); } -static void click_pressed_cb(FlView* self, - gint n_press, - gdouble x, - gdouble y) { +static void click_pressed_cb(FlView* self, gint n_press, gdouble x, gdouble y) { if (n_press > 1) { return; } - GdkEvent* event = get_current_event(GTK_EVENT_CONTROLLER(self->click_gesture)); + GdkEvent* event = + get_current_event(GTK_EVENT_CONTROLLER(self->click_gesture)); if (event == nullptr) { return; } @@ -637,7 +635,8 @@ static void click_released_cb(FlView* self, gdouble x, gdouble y) { (void)n_press; - GdkEvent* event = get_current_event(GTK_EVENT_CONTROLLER(self->click_gesture)); + GdkEvent* event = + get_current_event(GTK_EVENT_CONTROLLER(self->click_gesture)); if (event == nullptr) { return; } @@ -908,8 +907,7 @@ static gboolean draw_cb(FlView* self, cairo_t* cr) { } } - gboolean result = fl_compositor_render( - self->compositor, cr, surface); + gboolean result = fl_compositor_render(self->compositor, cr, surface); if (self->render_context) { gdk_gl_context_clear_current(); @@ -917,8 +915,7 @@ static gboolean draw_cb(FlView* self, cairo_t* cr) { if (!result) { static bool logged_render_false = false; - log_once(&logged_render_false, - "draw_cb: compositor render returned false"); + log_once(&logged_render_false, "draw_cb: compositor render returned false"); } return result; @@ -1019,8 +1016,8 @@ static void fl_view_realize(GtkWidget* widget) { #if !FLUTTER_LINUX_GTK4 static gboolean handle_key_event(FlView* self, GdkEventKey* key_event) { - g_autoptr(FlKeyEvent) event = fl_key_event_new_from_gdk_event( - reinterpret_cast(key_event)); + g_autoptr(FlKeyEvent) event = + fl_key_event_new_from_gdk_event(reinterpret_cast(key_event)); fl_keyboard_manager_handle_event( fl_engine_get_keyboard_manager(self->engine), event, self->cancellable, @@ -1205,8 +1202,8 @@ static void fl_view_init(FlView* self) { #endif #if FLUTTER_LINUX_GTK4 - self->motion_controller = GTK_EVENT_CONTROLLER_MOTION( - gtk_event_controller_motion_new()); + self->motion_controller = + GTK_EVENT_CONTROLLER_MOTION(gtk_event_controller_motion_new()); g_signal_connect_swapped(self->motion_controller, "motion", G_CALLBACK(motion_event_cb), self); g_signal_connect_swapped(self->motion_controller, "enter", diff --git a/engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc b/engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc index 0e714cb4444..6a8b7b1a20d 100644 --- a/engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc +++ b/engine/src/flutter/shell/platform/linux/fl_window_state_monitor.cc @@ -173,13 +173,11 @@ static void fl_window_state_monitor_dispose(GObject* object) { GdkSurface* surface = gtk_native_get_surface(native); if (surface != nullptr) { if (self->toplevel_state_notify_cb_id != 0) { - g_signal_handler_disconnect(surface, - self->toplevel_state_notify_cb_id); + g_signal_handler_disconnect(surface, self->toplevel_state_notify_cb_id); self->toplevel_state_notify_cb_id = 0; } if (self->surface_mapped_notify_cb_id != 0) { - g_signal_handler_disconnect(surface, - self->surface_mapped_notify_cb_id); + g_signal_handler_disconnect(surface, self->surface_mapped_notify_cb_id); self->surface_mapped_notify_cb_id = 0; } } From ddf77cac057fab538b9de6f9fbb79db5c2af02dc Mon Sep 17 00:00:00 2001 From: Rich Young Date: Fri, 6 Feb 2026 02:46:07 -0500 Subject: [PATCH 34/34] style(engine/linux): format test helpers --- .../linux/fl_scrolling_manager_test.cc | 24 ++++++++++++------- .../shell/platform/linux/testing/mock_gtk.cc | 12 ++++------ 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/fl_scrolling_manager_test.cc b/engine/src/flutter/shell/platform/linux/fl_scrolling_manager_test.cc index 2c8897e0797..985f091e0a1 100644 --- a/engine/src/flutter/shell/platform/linux/fl_scrolling_manager_test.cc +++ b/engine/src/flutter/shell/platform/linux/fl_scrolling_manager_test.cc @@ -45,7 +45,8 @@ TEST(FlScrollingManagerTest, DiscreteDirectional) { event->y = 8.0; event->device = mouse; event->direction = GDK_SCROLL_UP; - fl_scrolling_manager_handle_scroll_event(manager, reinterpret_cast(event), 1.0); + fl_scrolling_manager_handle_scroll_event( + manager, reinterpret_cast(event), 1.0); EXPECT_EQ(pointer_events.size(), 1u); EXPECT_EQ(pointer_events[0].x, 4.0); EXPECT_EQ(pointer_events[0].y, 8.0); @@ -55,7 +56,8 @@ TEST(FlScrollingManagerTest, DiscreteDirectional) { EXPECT_EQ(pointer_events[0].scroll_delta_x, 0); EXPECT_EQ(pointer_events[0].scroll_delta_y, 53 * -1.0); event->direction = GDK_SCROLL_DOWN; - fl_scrolling_manager_handle_scroll_event(manager, reinterpret_cast(event), 1.0); + fl_scrolling_manager_handle_scroll_event( + manager, reinterpret_cast(event), 1.0); EXPECT_EQ(pointer_events.size(), 2u); EXPECT_EQ(pointer_events[1].x, 4.0); EXPECT_EQ(pointer_events[1].y, 8.0); @@ -65,7 +67,8 @@ TEST(FlScrollingManagerTest, DiscreteDirectional) { EXPECT_EQ(pointer_events[1].scroll_delta_x, 0); EXPECT_EQ(pointer_events[1].scroll_delta_y, 53 * 1.0); event->direction = GDK_SCROLL_LEFT; - fl_scrolling_manager_handle_scroll_event(manager, reinterpret_cast(event), 1.0); + fl_scrolling_manager_handle_scroll_event( + manager, reinterpret_cast(event), 1.0); EXPECT_EQ(pointer_events.size(), 3u); EXPECT_EQ(pointer_events[2].x, 4.0); EXPECT_EQ(pointer_events[2].y, 8.0); @@ -75,7 +78,8 @@ TEST(FlScrollingManagerTest, DiscreteDirectional) { EXPECT_EQ(pointer_events[2].scroll_delta_x, 53 * -1.0); EXPECT_EQ(pointer_events[2].scroll_delta_y, 0); event->direction = GDK_SCROLL_RIGHT; - fl_scrolling_manager_handle_scroll_event(manager, reinterpret_cast(event), 1.0); + fl_scrolling_manager_handle_scroll_event( + manager, reinterpret_cast(event), 1.0); EXPECT_EQ(pointer_events.size(), 4u); EXPECT_EQ(pointer_events[3].x, 4.0); EXPECT_EQ(pointer_events[3].y, 8.0); @@ -120,7 +124,8 @@ TEST(FlScrollingManagerTest, DiscreteScrolling) { event->delta_y = 2.0; event->device = mouse; event->direction = GDK_SCROLL_SMOOTH; - fl_scrolling_manager_handle_scroll_event(manager, reinterpret_cast(event), 1.0); + fl_scrolling_manager_handle_scroll_event( + manager, reinterpret_cast(event), 1.0); EXPECT_EQ(pointer_events.size(), 1u); EXPECT_EQ(pointer_events[0].x, 4.0); EXPECT_EQ(pointer_events[0].y, 8.0); @@ -165,7 +170,8 @@ TEST(FlScrollingManagerTest, Panning) { event->delta_y = 2.0; event->device = touchpad; event->direction = GDK_SCROLL_SMOOTH; - fl_scrolling_manager_handle_scroll_event(manager, reinterpret_cast(event), 1.0); + fl_scrolling_manager_handle_scroll_event( + manager, reinterpret_cast(event), 1.0); EXPECT_EQ(pointer_events.size(), 2u); EXPECT_EQ(pointer_events[0].x, 4.0); EXPECT_EQ(pointer_events[0].y, 8.0); @@ -181,7 +187,8 @@ TEST(FlScrollingManagerTest, Panning) { EXPECT_EQ(pointer_events[1].pan_y, 53 * -2.0); EXPECT_EQ(pointer_events[1].scale, 1.0); EXPECT_EQ(pointer_events[1].rotation, 0.0); - fl_scrolling_manager_handle_scroll_event(manager, reinterpret_cast(event), 1.0); + fl_scrolling_manager_handle_scroll_event( + manager, reinterpret_cast(event), 1.0); EXPECT_EQ(pointer_events.size(), 3u); EXPECT_EQ(pointer_events[2].x, 4.0); EXPECT_EQ(pointer_events[2].y, 8.0); @@ -193,7 +200,8 @@ TEST(FlScrollingManagerTest, Panning) { EXPECT_EQ(pointer_events[2].scale, 1.0); EXPECT_EQ(pointer_events[2].rotation, 0.0); event->is_stop = true; - fl_scrolling_manager_handle_scroll_event(manager, reinterpret_cast(event), 1.0); + fl_scrolling_manager_handle_scroll_event( + manager, reinterpret_cast(event), 1.0); EXPECT_EQ(pointer_events.size(), 4u); EXPECT_EQ(pointer_events[3].x, 4.0); EXPECT_EQ(pointer_events[3].y, 8.0); diff --git a/engine/src/flutter/shell/platform/linux/testing/mock_gtk.cc b/engine/src/flutter/shell/platform/linux/testing/mock_gtk.cc index c184ad08541..97c6a5a2d86 100644 --- a/engine/src/flutter/shell/platform/linux/testing/mock_gtk.cc +++ b/engine/src/flutter/shell/platform/linux/testing/mock_gtk.cc @@ -38,11 +38,10 @@ struct _FlMockGtk4Surface { gboolean mapped; }; -G_DEFINE_TYPE_WITH_CODE( - FlMockGtk4Surface, - fl_mock_gtk4_surface, - G_TYPE_OBJECT, - G_IMPLEMENT_INTERFACE(GDK_TYPE_TOPLEVEL, nullptr)) +G_DEFINE_TYPE_WITH_CODE(FlMockGtk4Surface, + fl_mock_gtk4_surface, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(GDK_TYPE_TOPLEVEL, nullptr)) enum { kPropState = 1, @@ -59,8 +58,7 @@ static void fl_mock_gtk4_surface_set_property(GObject* object, FlMockGtk4Surface* self = FL_MOCK_GTK4_SURFACE(object); switch (prop_id) { case kPropState: - self->state = - static_cast(g_value_get_flags(value)); + self->state = static_cast(g_value_get_flags(value)); break; case kPropMapped: self->mapped = g_value_get_boolean(value);