Merge ddf77cac057fab538b9de6f9fbb79db5c2af02dc into 06df71c51446e96939c6a615b7c34ce9123806ba

This commit is contained in:
Rich Young 2026-02-19 11:03:09 -05:00 committed by GitHub
commit 1ef8eceed9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 2414 additions and 199 deletions

37
AGENTS.md Normal file
View File

@ -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.

51
docs/gtk4-linux-plan.md Normal file
View File

@ -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.

View File

@ -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/`.

View File

@ -0,0 +1,48 @@
# GTK4 Porting TODOs
This checklist tracks remaining GTK4 work and notes whats 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
theyre 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.

View File

@ -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) {

View File

@ -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") {

View File

@ -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
}

View File

@ -14,6 +14,10 @@ _pkg_configs = [
name = "gtk"
package = "gtk+-3.0"
},
{
name = "gtk4"
package = "gtk4"
},
{
name = "egl"
package = "egl"

View File

@ -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<FlAccessibilityHandlerPrivate*>(
@ -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) {

View File

@ -5,9 +5,11 @@
#include "flutter/shell/platform/linux/public/flutter_linux/fl_application.h"
#include <gtk/gtk.h>
#if !FLUTTER_LINUX_GTK4
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#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);
}

View File

@ -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);
}

View File

@ -9,6 +9,7 @@
#include <gdk/gdk.h>
#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

View File

@ -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<size_t>(x2 - x1);
size_t height = static_cast<size_t>(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<double>(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<double>(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);

View File

@ -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<size_t>(x2 - x1);
size_t height = static_cast<size_t>(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);

View File

@ -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(&current_iter, current);
while (g_hash_table_iter_next(&current_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<GConnectFlags>(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);
}

View File

@ -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 <gtk/gtk.h>
#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_

View File

@ -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,

View File

@ -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<GdkModifierType>(0);
#if FLUTTER_LINUX_GTK4
keycode = static_cast<guint16>(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<guint8>(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);
}

View File

@ -111,9 +111,13 @@ struct _FlKeyboardManager {
std::unique_ptr<std::map<uint64_t, const LayoutGoal*>>
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<GdkModifierType>(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();
}

View File

@ -3,6 +3,10 @@
// found in the LICENSE file.
#include <epoxy/egl.h>
#if FLUTTER_LINUX_GTK4
#include <gdk/wayland/gdkwayland.h>
#include <gdk/x11/gdkx.h>
#else
#include <gdk/gdkwayland.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>

View File

@ -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;

View File

@ -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<GdkEvent*>(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,

View File

@ -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);
/**

View File

@ -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<GdkEvent*>(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<GdkEvent*>(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<GdkEvent*>(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<GdkEvent*>(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<GdkEvent*>(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<GdkEvent*>(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<GdkEvent*>(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<GdkEvent*>(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

View File

@ -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<int>(cursor_offset),
static_cast<int>(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<int>(dest_x);
preedit_rect.y = static_cast<int>(dest_y);
#else
gtk_widget_translate_coordinates(self->widget, toplevel, static_cast<gint>(x),
static_cast<gint>(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<GdkEventKey*>(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<GdkEventKey*>(
fl_key_event_get_origin(event)))) {
#endif
return TRUE;
}

View File

@ -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<GdkEvent*>(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<int32_t>(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;

View File

@ -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

View File

@ -4,25 +4,40 @@
#include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h"
#if !FLUTTER_LINUX_GTK4
#include <atk/atk.h>
#endif
#if FLUTTER_LINUX_GTK4
#include <gdk/wayland/gdkwayland.h>
#else
#include <gdk/gdkwayland.h>
#endif
#if !FLUTTER_LINUX_GTK4
#include <gtk/gtk-a11y.h>
#endif
#include <cstring>
#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<GdkModifierType>(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<GdkEvent*>(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<guint32>(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<GdkEvent*>(key_event)));
g_autoptr(FlKeyEvent) event =
fl_key_event_new_from_gdk_event(reinterpret_cast<GdkEvent*>(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

View File

@ -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

View File

@ -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;

View File

@ -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;
}

View File

@ -10,6 +10,12 @@
#include "gtest/gtest.h"
#if FLUTTER_LINUX_GTK4
#include <string>
#include <vector>
#endif
#if !FLUTTER_LINUX_GTK4
TEST(FlWindowStateMonitorTest, GainFocus) {
g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new();
::testing::NiceMock<flutter::testing::MockGtk> 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<flutter::testing::MockGtk> 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<flutter::testing::MockGtk> 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<GdkToplevelState>(0)));
std::vector<std::string> 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<std::vector<std::string>*>(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<flutter::testing::MockGtk> 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<GdkToplevelState>(0)));
std::vector<std::string> 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<std::vector<std::string>*>(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

View File

@ -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<LayoutGoal> layout_goals = {

View File

@ -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<GdkToplevelState>(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<GdkToplevelState>(0),
static_cast<GParamFlags>(G_PARAM_READWRITE));
g_properties[kPropMapped] =
g_param_spec_boolean("mapped", "mapped", "mapped", FALSE,
static_cast<GParamFlags>(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<GdkToplevelState>(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<GtkNative*>(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<GdkSurface*>(mock_surface);
}
#endif // FLUTTER_LINUX_GTK4
void gtk_im_context_set_client_window(GtkIMContext* context,
GdkWindow* window) {
check_thread();

View File

@ -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,

View File

@ -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: <String>['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',

View File

@ -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,

View File

@ -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;
}

View File

@ -0,0 +1 @@
flutter/ephemeral

View File

@ -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 "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>: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()

View File

@ -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}
)

View File

@ -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}")

View File

@ -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);
}

View File

@ -0,0 +1,132 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#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));
}

View File

@ -0,0 +1,21 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
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_

View File

@ -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

View File

@ -0,0 +1,26 @@
#ifndef FLUTTER_PLUGIN_{{pluginClassCapitalSnakeCase}}_H_
#define FLUTTER_PLUGIN_{{pluginClassCapitalSnakeCase}}_H_
#include <flutter_linux/flutter_linux.h>
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_

View File

@ -0,0 +1,76 @@
#include "include/{{projectName}}/{{pluginClassSnakeCase}}.h"
#include <flutter_linux/flutter_linux.h>
#include <gtk/gtk.h>
#include <sys/utsname.h>
#include <cstring>
#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);
}

View File

@ -0,0 +1,10 @@
#include <flutter_linux/flutter_linux.h>
#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();

View File

@ -0,0 +1,31 @@
#include <flutter_linux/flutter_linux.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#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}}

View File

@ -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.
$<TARGET_FILE:{{projectName}}>
PARENT_SCOPE
)

View File

@ -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",