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. diff --git a/docs/gtk4-linux-plan.md b/docs/gtk4-linux-plan.md new file mode 100644 index 00000000000..6585ff78c58 --- /dev/null +++ b/docs/gtk4-linux-plan.md @@ -0,0 +1,51 @@ +# 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. + +## 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. +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/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/`. diff --git a/docs/gtk4_porting_todos.md b/docs/gtk4_porting_todos.md new file mode 100644 index 00000000000..3fd543b1ce9 --- /dev/null +++ b/docs/gtk4_porting_todos.md @@ -0,0 +1,48 @@ +# GTK4 Porting TODOs + +This checklist tracks remaining GTK4 work and notes what’s already landed in +this branch. Keep it short and focused on actionable items. + +## 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`/`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` (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 +- [ ] 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`. + +## 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. diff --git a/engine/src/flutter/BUILD.gn b/engine/src/flutter/BUILD.gn index 7a4d1eaab38..b8c10213b20 100644 --- a/engine/src/flutter/BUILD.gn +++ b/engine/src/flutter/BUILD.gn @@ -8,6 +8,7 @@ import("//flutter/examples/examples.gni") import("//flutter/shell/config.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") @@ -280,7 +281,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 b9b1fb6657b..2920565750c 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,11 +82,20 @@ config("disable_warnings") { cflags_cc = warning_flags } +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 + [ "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", @@ -104,15 +114,13 @@ source_set("flutter_linux_sources") { "key_mapping.h", ] - configs += [ "//flutter/shell/platform/linux/config:gtk" ] + configs += [ _gtk_config ] public_configs = [ ":disable_warnings" ] 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", @@ -156,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", @@ -169,17 +176,25 @@ 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", - "FLUTTER_ENGINE_NO_PROTOTYPES", - ] + "FLUTTER_LINUX_COMPILATION", + "FLUTTER_ENGINE_NO_PROTOTYPES", + ] + _gtk_defines deps = [ "$dart_src/runtime:dart_api", @@ -195,7 +210,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", ] @@ -217,95 +232,97 @@ copy("flutter_linux_gschemas") { outputs = [ "$target_gen_dir/assets/{{source_name_part}}/gschemas.compiled" ] } -executable("flutter_linux_unittests") { - testonly = true +if (!use_gtk4) { + executable("flutter_linux_unittests") { + 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 += [ - "//flutter/shell/platform/linux/config:gtk", - "//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", + ] + } } shared_library("flutter_linux_gtk") { 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" 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 42ade0134d3..71e95cbb97b 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,12 @@ 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 + 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 // Show the main window. if (window != nullptr && GTK_IS_WINDOW(window)) { @@ -61,6 +68,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,15 +76,24 @@ 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()); 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)); } +#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); } 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..f8385e1b8b5 100644 --- a/engine/src/flutter/shell/platform/linux/fl_compositor.h +++ b/engine/src/flutter/shell/platform/linux/fl_compositor.h @@ -9,6 +9,7 @@ #include #include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/linux/fl_gtk.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..47b3fe66f70 100644 --- a/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc +++ b/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc @@ -11,8 +11,10 @@ #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. +#if FLUTTER_LINUX_GTK4 static const char* vertex_shader_src = "attribute vec2 position;\n" "attribute vec2 in_texcoord;\n" @@ -24,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 = @@ -363,7 +378,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 +388,22 @@ 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); +#if FLUTTER_LINUX_GTK4 + // 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; +#endif while (fl_framebuffer_get_width(self->framebuffer) != width || fl_framebuffer_get_height(self->framebuffer) != height) { g_mutex_unlock(&self->frame_mutex); @@ -386,8 +414,18 @@ 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), +#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, fl_framebuffer_get_texture_id(sibling), 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; glGetIntegerv(GL_TEXTURE_BINDING_2D, &saved_texture_binding); @@ -398,8 +436,18 @@ 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, +#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 0795851a527..222f94335f2 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,22 @@ 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); +#if FLUTTER_LINUX_GTK4 + // 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; +#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_display_monitor.cc b/engine/src/flutter/shell/platform/linux/fl_display_monitor.cc index bea40b2cbd1..9f01f3330bc 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,22 @@ 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,11 +65,16 @@ 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); } +#if !FLUTTER_LINUX_GTK4 static void monitor_added_cb(FlDisplayMonitor* self, GdkMonitor* monitor) { notify_display_update(self); } @@ -69,6 +83,50 @@ 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) { + 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); @@ -103,12 +161,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_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_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 045f6859859..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,19 +59,30 @@ 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; self->keycode = keycode; self->keyval = keyval; self->state = state; +#if FLUTTER_LINUX_GTK4 + self->group = static_cast(gdk_key_event_get_layout(event)); + 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 +125,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_keyboard_manager.cc b/engine/src/flutter/shell/platform/linux/fl_keyboard_manager.cc index 7a81da289ec..84b03481c5f 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,13 @@ 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_opengl_manager.cc b/engine/src/flutter/shell/platform/linux/fl_opengl_manager.cc index 25a9911c305..c7fd4d1e539 100644 --- a/engine/src/flutter/shell/platform/linux/fl_opengl_manager.cc +++ b/engine/src/flutter/shell/platform/linux/fl_opengl_manager.cc @@ -3,6 +3,10 @@ // found in the LICENSE file. #include +#if FLUTTER_LINUX_GTK4 +#include +#include +#else #include #ifdef GDK_WINDOWING_X11 #include 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_scrolling_manager.cc b/engine/src/flutter/shell/platform/linux/fl_scrolling_manager.cc index e49924a2cae..628f3c3c0a3 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,13 +78,33 @@ 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; +#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; @@ -102,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). @@ -109,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_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..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 @@ -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,8 @@ 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 +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, 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 +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, 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 +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, 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 +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, 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 +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, 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 +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, 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 +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, 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 +438,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_text_input_handler.cc b/engine/src/flutter/shell/platform/linux/fl_text_input_handler.cc index 18b9030b592..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 @@ -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,18 +345,32 @@ 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 = {}; - gtk_widget_translate_coordinates(self->widget, - gtk_widget_get_toplevel(self->widget), x, y, - &preedit_rect.x, &preedit_rect.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 +#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. @@ -480,8 +501,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) { @@ -497,9 +522,13 @@ gboolean fl_text_input_handler_filter_keypress(FlTextInputHandler* self, return FALSE; } - if (gtk_im_context_filter_keypress( - self->im_context, - reinterpret_cast(fl_key_event_get_origin(event)))) { + 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/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index a9be94291bc..5fd386e0897 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc @@ -4,25 +4,40 @@ #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" +#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" #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" @@ -73,13 +88,24 @@ 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; 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 }; @@ -91,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, @@ -113,12 +147,38 @@ static gboolean redraw_cb(gpointer user_data) { return FALSE; } -// Signal handler for GtkWidget::delete-event +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 (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. +static gboolean window_close_request_cb(GtkWindow* window, FlView* self) { + (void)window; + fl_engine_request_app_exit(self->engine); + // Allow the default handler to destroy the window if the engine doesn't. + return FALSE; +} +#endif static void init_scrolling(FlView* self) { g_clear_object(&self->scrolling_manager); @@ -132,8 +192,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: @@ -148,6 +226,8 @@ static FlutterPointerDeviceKind get_device_kind(GdkEvent* event) { case GDK_SOURCE_MOUSE: return kFlutterPointerDeviceKindMouse; } +#endif + return kFlutterPointerDeviceKindMouse; } // Called when the mouse cursor changes. @@ -155,11 +235,17 @@ 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; + } +#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); } // Set the mouse cursor. @@ -173,18 +259,32 @@ 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) { + // 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 // 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 // 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,15 +293,36 @@ 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); } - 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 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) { +#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, @@ -231,7 +352,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. @@ -250,6 +376,18 @@ 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"); + 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); // Perform the redraw in the GTK thead. @@ -278,8 +416,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); } @@ -290,6 +432,7 @@ static void set_scrolling_position(FlView* self, gdouble x, gdouble y) { 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) { @@ -343,7 +486,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; } @@ -403,6 +546,193 @@ 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); @@ -434,8 +764,14 @@ static void gesture_zoom_end_cb(FlView* self) { 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 +786,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,15 +814,20 @@ 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), 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. @@ -498,14 +839,32 @@ 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); } +#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) { + 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 static void paint_background(FlView* self, cairo_t* cr) { // Don't bother drawing if fully transparent - the widget above this will @@ -526,17 +885,56 @@ static gboolean draw_cb(FlView* self, cairo_t* cr) { gdk_gl_context_make_current(self->render_context); } - gboolean result = fl_compositor_render( - self->compositor, cr, - gtk_widget_get_window(GTK_WIDGET(self->render_area))); + 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); if (self->render_context) { 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; } +#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); @@ -586,7 +984,17 @@ 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 +#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); @@ -594,17 +1002,22 @@ 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); // 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))); + 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, @@ -660,6 +1073,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,24 +1082,30 @@ 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, 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); @@ -702,7 +1122,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; @@ -710,6 +1134,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 +1172,91 @@ static void fl_view_init(FlView* self) { self); g_signal_connect_swapped(self->event_box, "touch-event", G_CALLBACK(touch_event_cb), self); +#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), GTK_WIDGET(self->render_area)); +#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); +#else 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) { @@ -805,7 +1304,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..39c33ec5f91 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); @@ -53,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 FALSE; +} +#else static gboolean delete_event_cb(FlWindowMonitor* self, GdkEvent* event) { flutter::IsolateScope scope(self->isolate); self->on_close(); @@ -60,6 +69,7 @@ static gboolean delete_event_cb(FlWindowMonitor* self, GdkEvent* event) { // Stop default behaviour of destroying the window. return TRUE; } +#endif static void destroy_cb(FlWindowMonitor* self) { flutter::IsolateScope scope(self->isolate); @@ -102,16 +112,23 @@ 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), self); +#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 g_signal_connect_swapped(window, "destroy", G_CALLBACK(destroy_cb), self); return 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..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 @@ -27,10 +27,18 @@ 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; + +#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); @@ -51,11 +59,18 @@ 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 +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, GdkEvent* event) { @@ -81,6 +96,68 @@ static gboolean window_state_event_cb(FlWindowStateMonitor* self, return FALSE; } +#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); @@ -90,6 +167,26 @@ 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); } @@ -109,11 +206,20 @@ 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; + 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; } 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..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,12 @@ #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(); ::testing::NiceMock mock_gtk; @@ -44,7 +50,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 +292,91 @@ TEST(FlWindowStateMonitorTest, LeaveWithdrawnFocused) { fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); } + +#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/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 = { 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..97c6a5a2d86 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,89 @@ 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 +175,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 +235,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 +406,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, diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart index 4a9ebd79843..89f2219f241 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: @@ -461,6 +467,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 6f9caf3080c..b37ed6b1ad6 100644 --- a/packages/flutter_tools/lib/src/commands/create_base.dart +++ b/packages/flutter_tools/lib/src/commands/create_base.dart @@ -327,6 +327,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, @@ -383,6 +384,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; } 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..c1617eb9271 --- /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 gtk4) + +# 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..493a3fb92b5 --- /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 gtk4) +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..d5cbd590953 --- /dev/null +++ b/packages/flutter_tools/templates/app/linux-gtk4.tmpl/runner/my_application.cc.tmpl @@ -0,0 +1,132 @@ +#include "my_application.h" + +#include + +#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) { + (void)self; + GtkRoot* root = gtk_widget_get_root(GTK_WIDGET(view)); + if (root == nullptr) { + return; + } + gtk_window_present(GTK_WINDOW(root)); +} + +// 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 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}}"); + + 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_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. + // 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",