diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 85d0e5d66f..006d3e71d0 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -43869,6 +43869,11 @@ ORIGIN: ../../../flutter/shell/platform/linux/fl_view_test.cc + ../../../flutter ORIGIN: ../../../flutter/shell/platform/linux/fl_window_state_monitor.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/linux/fl_window_state_monitor.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/linux/fl_window_state_monitor_test.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/linux/fl_windowing_channel.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/linux/fl_windowing_channel.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/linux/fl_windowing_handler.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/linux/fl_windowing_handler.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/linux/fl_windowing_handler_test.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/linux/key_mapping.g.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/linux/key_mapping.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/linux/key_mapping_test.cc + ../../../flutter/LICENSE @@ -46813,6 +46818,11 @@ FILE: ../../../flutter/shell/platform/linux/fl_view_test.cc FILE: ../../../flutter/shell/platform/linux/fl_window_state_monitor.cc FILE: ../../../flutter/shell/platform/linux/fl_window_state_monitor.h FILE: ../../../flutter/shell/platform/linux/fl_window_state_monitor_test.cc +FILE: ../../../flutter/shell/platform/linux/fl_windowing_channel.cc +FILE: ../../../flutter/shell/platform/linux/fl_windowing_channel.h +FILE: ../../../flutter/shell/platform/linux/fl_windowing_handler.cc +FILE: ../../../flutter/shell/platform/linux/fl_windowing_handler.h +FILE: ../../../flutter/shell/platform/linux/fl_windowing_handler_test.cc FILE: ../../../flutter/shell/platform/linux/key_mapping.g.cc FILE: ../../../flutter/shell/platform/linux/key_mapping.h FILE: ../../../flutter/shell/platform/linux/key_mapping_test.cc diff --git a/engine/src/flutter/shell/platform/linux/.clang-tidy b/engine/src/flutter/shell/platform/linux/.clang-tidy index d31e07c442..af9191c97e 100644 --- a/engine/src/flutter/shell/platform/linux/.clang-tidy +++ b/engine/src/flutter/shell/platform/linux/.clang-tidy @@ -1,8 +1,10 @@ InheritParentConfig: true # EnumCastOutOfRange warns about some common usages of GTK macros +# Malloc generates false positives with g_autofree usage. Checks: >- - -clang-analyzer-optin.core.EnumCastOutOfRange + -clang-analyzer-optin.core.EnumCastOutOfRange, + -clang-analyzer-unix.Malloc CheckOptions: - key: readability-identifier-naming.EnumConstantCase diff --git a/engine/src/flutter/shell/platform/linux/BUILD.gn b/engine/src/flutter/shell/platform/linux/BUILD.gn index a73bd3282b..4394c37013 100644 --- a/engine/src/flutter/shell/platform/linux/BUILD.gn +++ b/engine/src/flutter/shell/platform/linux/BUILD.gn @@ -158,6 +158,8 @@ source_set("flutter_linux_sources") { "fl_view.cc", "fl_view_accessible.cc", "fl_window_state_monitor.cc", + "fl_windowing_channel.cc", + "fl_windowing_handler.cc", "key_mapping.g.cc", ] @@ -248,6 +250,7 @@ executable("flutter_linux_unittests") { "fl_view_accessible_test.cc", "fl_view_test.cc", "fl_window_state_monitor_test.cc", + "fl_windowing_handler_test.cc", "key_mapping_test.cc", "testing/fl_mock_binary_messenger.cc", "testing/fl_test.cc", diff --git a/engine/src/flutter/shell/platform/linux/fl_application.cc b/engine/src/flutter/shell/platform/linux/fl_application.cc index 721d53eac3..2ce5db4cd9 100644 --- a/engine/src/flutter/shell/platform/linux/fl_application.cc +++ b/engine/src/flutter/shell/platform/linux/fl_application.cc @@ -9,6 +9,7 @@ #include #endif +#include "flutter/shell/platform/linux/fl_engine_private.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_dart_project.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_plugin_registry.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h" @@ -31,6 +32,15 @@ G_DEFINE_TYPE_WITH_CODE(FlApplication, GTK_TYPE_APPLICATION, G_ADD_PRIVATE(FlApplication)) +// Called when the platform creates a window. +static GtkWindow* create_window_cb(FlApplication* self, FlView* view) { + GtkWindow* window; + g_signal_emit(self, fl_application_signals[SIGNAL_CREATE_WINDOW], 0, view, + &window); + + return window; +} + // Called when the first frame is received. static void first_frame_cb(FlApplication* self, FlView* view) { GtkWidget* window = gtk_widget_get_toplevel(GTK_WIDGET(view)); @@ -94,6 +104,11 @@ static void fl_application_activate(GApplication* application) { self); gtk_widget_show(GTK_WIDGET(view)); + FlWindowingHandler* windowing_handler = + fl_engine_get_windowing_handler(fl_view_get_engine(view)); + g_signal_connect_swapped(windowing_handler, "create_window", + G_CALLBACK(create_window_cb), self); + GtkWindow* window; g_signal_emit(self, fl_application_signals[SIGNAL_CREATE_WINDOW], 0, view, &window); diff --git a/engine/src/flutter/shell/platform/linux/fl_engine.cc b/engine/src/flutter/shell/platform/linux/fl_engine.cc index 74943adc2d..4c8f66228c 100644 --- a/engine/src/flutter/shell/platform/linux/fl_engine.cc +++ b/engine/src/flutter/shell/platform/linux/fl_engine.cc @@ -24,6 +24,7 @@ #include "flutter/shell/platform/linux/fl_settings_handler.h" #include "flutter/shell/platform/linux/fl_texture_gl_private.h" #include "flutter/shell/platform/linux/fl_texture_registrar_private.h" +#include "flutter/shell/platform/linux/fl_windowing_handler.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_plugin_registry.h" // Unique number associated with platform tasks. @@ -58,6 +59,9 @@ struct _FlEngine { // Implements the flutter/platform channel. FlPlatformHandler* platform_handler; + // Implements the flutter/windowing channel. + FlWindowingHandler* windowing_handler; + // Process keyboard events. FlKeyboardManager* keyboard_manager; @@ -487,6 +491,7 @@ static void fl_engine_dispose(GObject* object) { g_clear_object(&self->binary_messenger); g_clear_object(&self->settings_handler); g_clear_object(&self->platform_handler); + g_clear_object(&self->windowing_handler); g_clear_object(&self->keyboard_manager); g_clear_object(&self->text_input_handler); g_clear_object(&self->keyboard_handler); @@ -702,6 +707,7 @@ gboolean fl_engine_start(FlEngine* self, GError** error) { fl_settings_handler_start(self->settings_handler, settings); self->platform_handler = fl_platform_handler_new(self->binary_messenger); + self->windowing_handler = fl_windowing_handler_new(self); setup_keyboard(self); @@ -1295,6 +1301,11 @@ void fl_engine_request_app_exit(FlEngine* self) { fl_platform_handler_request_app_exit(self->platform_handler); } +FlWindowingHandler* fl_engine_get_windowing_handler(FlEngine* self) { + g_return_val_if_fail(FL_IS_ENGINE(self), nullptr); + return self->windowing_handler; +} + FlKeyboardManager* fl_engine_get_keyboard_manager(FlEngine* self) { g_return_val_if_fail(FL_IS_ENGINE(self), nullptr); return self->keyboard_manager; diff --git a/engine/src/flutter/shell/platform/linux/fl_engine_private.h b/engine/src/flutter/shell/platform/linux/fl_engine_private.h index d5c9399bf0..ff4f995c28 100644 --- a/engine/src/flutter/shell/platform/linux/fl_engine_private.h +++ b/engine/src/flutter/shell/platform/linux/fl_engine_private.h @@ -14,6 +14,7 @@ #include "flutter/shell/platform/linux/fl_renderer.h" #include "flutter/shell/platform/linux/fl_task_runner.h" #include "flutter/shell/platform/linux/fl_text_input_handler.h" +#include "flutter/shell/platform/linux/fl_windowing_handler.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_dart_project.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_engine.h" @@ -576,13 +577,23 @@ void fl_engine_update_accessibility_features(FlEngine* engine, int32_t flags); */ void fl_engine_request_app_exit(FlEngine* engine); +/** + * fl_engine_get_windowing_handler: + * @engine: an #FlEngine. + * + * Gets the windowing handler used by this engine. + * + * Returns: an #FlWindowingHandler. + */ +FlWindowingHandler* fl_engine_get_windowing_handler(FlEngine* engine); + /** * fl_engine_get_keyboard_manager: * @engine: an #FlEngine. * * Gets the keyboard manager used by this engine. * - * Returns: a #FlKeyboardManager. + * Returns: an #FlKeyboardManager. */ FlKeyboardManager* fl_engine_get_keyboard_manager(FlEngine* engine); @@ -592,7 +603,7 @@ FlKeyboardManager* fl_engine_get_keyboard_manager(FlEngine* engine); * * Gets the text input handler used by this engine. * - * Returns: a #FlTextInputHandler. + * Returns: an #FlTextInputHandler. */ FlTextInputHandler* fl_engine_get_text_input_handler(FlEngine* engine); @@ -602,7 +613,7 @@ FlTextInputHandler* fl_engine_get_text_input_handler(FlEngine* engine); * * Gets the mouse cursor handler used by this engine. * - * Returns: a #FlMouseCursorHandler. + * Returns: an #FlMouseCursorHandler. */ FlMouseCursorHandler* fl_engine_get_mouse_cursor_handler(FlEngine* engine); diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index da8730c178..e45131c53d 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc @@ -765,7 +765,7 @@ G_MODULE_EXPORT FlView* fl_view_new(FlDartProject* project) { fl_engine_set_update_semantics_handler(self->engine, update_semantics_cb, self, nullptr); self->on_pre_engine_restart_cb_id = - g_signal_connect_swapped(engine, "on-pre-engine-restart", + g_signal_connect_swapped(self->engine, "on-pre-engine-restart", G_CALLBACK(on_pre_engine_restart_cb), self); g_signal_connect_swapped(self->gl_area, "create-context", @@ -790,7 +790,7 @@ G_MODULE_EXPORT FlView* fl_view_new_for_engine(FlEngine* engine) { g_signal_connect_swapped(engine, "on-pre-engine-restart", G_CALLBACK(on_pre_engine_restart_cb), self); - self->view_id = fl_engine_add_view(self->engine, 1, 1, 1.0, self->cancellable, + self->view_id = fl_engine_add_view(engine, 1, 1, 1.0, self->cancellable, view_added_cb, self); fl_renderer_add_renderable(FL_RENDERER(self->renderer), self->view_id, FL_RENDERABLE(self)); diff --git a/engine/src/flutter/shell/platform/linux/fl_view_test.cc b/engine/src/flutter/shell/platform/linux/fl_view_test.cc index 61e87a323f..fcb0aec526 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view_test.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view_test.cc @@ -7,6 +7,7 @@ #include "flutter/shell/platform/linux/fl_engine_private.h" #include "flutter/shell/platform/linux/testing/fl_test.h" #include "flutter/shell/platform/linux/testing/fl_test_gtk_logs.h" +#include "flutter/shell/platform/linux/testing/mock_window.h" #include "gtest/gtest.h" @@ -136,8 +137,8 @@ TEST(FlViewTest, ViewDestroy) { int64_t implicit_view_id = fl_view_get_id(implicit_view); int64_t secondary_view_id = fl_view_get_id(secondary_view); - gtk_widget_destroy(GTK_WIDGET(secondary_view)); - gtk_widget_destroy(GTK_WIDGET(implicit_view)); + fl_gtk_widget_destroy(GTK_WIDGET(secondary_view)); + fl_gtk_widget_destroy(GTK_WIDGET(implicit_view)); EXPECT_EQ(removed_views->len, 2u); EXPECT_EQ(GPOINTER_TO_INT(g_ptr_array_index(removed_views, 0)), @@ -165,6 +166,6 @@ TEST(FlViewTest, ViewDestroyError) { FlView* secondary_view = fl_view_new_for_engine(engine); - gtk_widget_destroy(GTK_WIDGET(secondary_view)); - gtk_widget_destroy(GTK_WIDGET(implicit_view)); + fl_gtk_widget_destroy(GTK_WIDGET(secondary_view)); + fl_gtk_widget_destroy(GTK_WIDGET(implicit_view)); } diff --git a/engine/src/flutter/shell/platform/linux/fl_windowing_channel.cc b/engine/src/flutter/shell/platform/linux/fl_windowing_channel.cc new file mode 100644 index 0000000000..660eeaf47e --- /dev/null +++ b/engine/src/flutter/shell/platform/linux/fl_windowing_channel.cc @@ -0,0 +1,296 @@ +// 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. + +#include "flutter/shell/platform/linux/fl_windowing_channel.h" + +#include "flutter/shell/platform/linux/public/flutter_linux/fl_method_channel.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_standard_method_codec.h" + +static constexpr char kChannelName[] = "flutter/windowing"; +static constexpr char kBadArgumentsError[] = "Bad Arguments"; + +static constexpr char kCreateRegularMethod[] = "createRegular"; +static constexpr char kModifyRegularMethod[] = "modifyRegular"; +static constexpr char kDestroyWindowMethod[] = "destroyWindow"; + +static constexpr char kSizeKey[] = "size"; +static constexpr char kMinSizeKey[] = "minSize"; +static constexpr char kMaxSizeKey[] = "maxSize"; +static constexpr char kTitleKey[] = "title"; +static constexpr char kStateKey[] = "state"; +static constexpr char kViewIdKey[] = "viewId"; + +struct _FlWindowingChannel { + GObject parent_instance; + + FlMethodChannel* channel; + + // Handlers for incoming method calls. + FlWindowingChannelVTable* vtable; + + // User data to pass to method call handlers. + gpointer user_data; +}; + +G_DEFINE_TYPE(FlWindowingChannel, fl_windowing_channel, G_TYPE_OBJECT) + +// Returns TRUE if [args] is a valid size argument. +static gboolean is_valid_size_argument(FlValue* value) { + return fl_value_get_type(value) == FL_VALUE_TYPE_LIST && + fl_value_get_length(value) == 2 && + fl_value_get_type(fl_value_get_list_value(value, 0)) == + FL_VALUE_TYPE_FLOAT && + fl_value_get_type(fl_value_get_list_value(value, 1)) == + FL_VALUE_TYPE_FLOAT; +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(FlWindowingSize, g_free) + +static FlWindowingSize* parse_size_value(FlValue* value) { + FlWindowingSize* size = g_new0(FlWindowingSize, 1); + size->width = fl_value_get_float(fl_value_get_list_value(value, 0)); + size->height = fl_value_get_float(fl_value_get_list_value(value, 1)); + return size; +} + +static gboolean parse_window_state_value(FlValue* value, FlWindowState* state) { + if (fl_value_get_type(value) != FL_VALUE_TYPE_STRING) { + return FALSE; + } + + const gchar* text = fl_value_get_string(value); + if (strcmp(text, "WindowState.restored") == 0) { + *state = FL_WINDOW_STATE_RESTORED; + return TRUE; + } else if (strcmp(text, "WindowState.maximized") == 0) { + *state = FL_WINDOW_STATE_MAXIMIZED; + return TRUE; + } else if (strcmp(text, "WindowState.minimized") == 0) { + *state = FL_WINDOW_STATE_MINIMIZED; + return TRUE; + } + + return FALSE; +} + +static const gchar* window_state_to_string(FlWindowState state) { + switch (state) { + case FL_WINDOW_STATE_UNDEFINED: + return nullptr; + case FL_WINDOW_STATE_RESTORED: + return "WindowState.restored"; + case FL_WINDOW_STATE_MAXIMIZED: + return "WindowState.maximized"; + case FL_WINDOW_STATE_MINIMIZED: + return "WindowState.minimized"; + } + + return nullptr; +} + +// Called when a regular window should be created. +static FlMethodResponse* create_regular(FlWindowingChannel* self, + FlValue* args) { + if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + kBadArgumentsError, "Argument map missing or malformed", nullptr)); + } + + FlValue* size_value = fl_value_lookup_string(args, kSizeKey); + if (size_value == nullptr || !is_valid_size_argument(size_value)) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + kBadArgumentsError, "Missing/invalid size argument", nullptr)); + } + g_autoptr(FlWindowingSize) size = parse_size_value(size_value); + + FlValue* min_size_value = fl_value_lookup_string(args, kMinSizeKey); + g_autoptr(FlWindowingSize) min_size = nullptr; + if (min_size_value != nullptr) { + if (!is_valid_size_argument(min_size_value)) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + kBadArgumentsError, "Invalid minSize argument", nullptr)); + } + min_size = parse_size_value(min_size_value); + } + + FlValue* max_size_value = fl_value_lookup_string(args, kMaxSizeKey); + g_autoptr(FlWindowingSize) max_size = nullptr; + if (max_size_value != nullptr) { + if (!is_valid_size_argument(max_size_value)) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + kBadArgumentsError, "Invalid maxSize argument", nullptr)); + } + max_size = parse_size_value(max_size_value); + } + + FlValue* title_value = fl_value_lookup_string(args, kTitleKey); + const gchar* title = nullptr; + if (title_value != nullptr) { + if (fl_value_get_type(title_value) != FL_VALUE_TYPE_STRING) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + kBadArgumentsError, "Invalid title argument", nullptr)); + } + title = fl_value_get_string(title_value); + } + FlWindowState state = FL_WINDOW_STATE_UNDEFINED; + FlValue* state_value = fl_value_lookup_string(args, kStateKey); + if (state_value != nullptr) { + if (!parse_window_state_value(state_value, &state)) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + kBadArgumentsError, "Invalid state argument", nullptr)); + } + } + + return self->vtable->create_regular(size, min_size, max_size, title, state, + self->user_data); +} + +// Called when a regular window should be created. +static FlMethodResponse* modify_regular(FlWindowingChannel* self, + FlValue* args) { + if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + kBadArgumentsError, "Argument map missing or malformed", nullptr)); + } + + FlValue* view_id_value = fl_value_lookup_string(args, kViewIdKey); + if (view_id_value == nullptr || + fl_value_get_type(view_id_value) != FL_VALUE_TYPE_INT) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + kBadArgumentsError, "Missing/invalid viewId argument", nullptr)); + } + int64_t view_id = fl_value_get_int(view_id_value); + + g_autoptr(FlWindowingSize) size = nullptr; + FlValue* size_value = fl_value_lookup_string(args, kSizeKey); + if (size_value != nullptr) { + if (!is_valid_size_argument(size_value)) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + kBadArgumentsError, "Invalid size argument", nullptr)); + } + size = parse_size_value(size_value); + } + FlValue* title_value = fl_value_lookup_string(args, kTitleKey); + const gchar* title = nullptr; + if (title_value != nullptr) { + if (fl_value_get_type(title_value) != FL_VALUE_TYPE_STRING) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + kBadArgumentsError, "Invalid title argument", nullptr)); + } + title = fl_value_get_string(title_value); + } + FlWindowState state = FL_WINDOW_STATE_UNDEFINED; + FlValue* state_value = fl_value_lookup_string(args, kStateKey); + if (state_value != nullptr) { + if (!parse_window_state_value(state_value, &state)) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + kBadArgumentsError, "Invalid state argument", nullptr)); + } + } + + return self->vtable->modify_regular(view_id, size, title, state, + self->user_data); +} + +// Called when a window should be destroyed. +static FlMethodResponse* destroy_window(FlWindowingChannel* self, + FlValue* args) { + if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + kBadArgumentsError, "Argument map missing or malformed", nullptr)); + } + + FlValue* view_id_value = fl_value_lookup_string(args, kViewIdKey); + if (view_id_value == nullptr || + fl_value_get_type(view_id_value) != FL_VALUE_TYPE_INT) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + kBadArgumentsError, "Missing/invalid viewId argument", nullptr)); + } + int64_t view_id = fl_value_get_int(view_id_value); + + return self->vtable->destroy_window(view_id, self->user_data); +} + +// Called when a method call is received from Flutter. +static void method_call_cb(FlMethodChannel* channel, + FlMethodCall* method_call, + gpointer user_data) { + FlWindowingChannel* self = FL_WINDOWING_CHANNEL(user_data); + + const gchar* method = fl_method_call_get_name(method_call); + FlValue* args = fl_method_call_get_args(method_call); + g_autoptr(FlMethodResponse) response = nullptr; + + if (strcmp(method, kCreateRegularMethod) == 0) { + response = create_regular(self, args); + } else if (strcmp(method, kModifyRegularMethod) == 0) { + response = modify_regular(self, args); + } else if (strcmp(method, kDestroyWindowMethod) == 0) { + response = destroy_window(self, args); + } else { + response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); + } + + if (response != nullptr) { + g_autoptr(GError) error = nullptr; + if (!fl_method_call_respond(method_call, response, &error)) { + g_warning("Failed to send method call response: %s", error->message); + } + } +} + +static void fl_windowing_channel_dispose(GObject* object) { + FlWindowingChannel* self = FL_WINDOWING_CHANNEL(object); + + g_clear_object(&self->channel); + + G_OBJECT_CLASS(fl_windowing_channel_parent_class)->dispose(object); +} + +static void fl_windowing_channel_class_init(FlWindowingChannelClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_windowing_channel_dispose; +} + +static void fl_windowing_channel_init(FlWindowingChannel* self) {} + +FlWindowingChannel* fl_windowing_channel_new(FlBinaryMessenger* messenger, + FlWindowingChannelVTable* vtable, + gpointer user_data) { + FlWindowingChannel* self = FL_WINDOWING_CHANNEL( + g_object_new(fl_windowing_channel_get_type(), nullptr)); + + self->vtable = vtable; + self->user_data = user_data; + + g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); + self->channel = + fl_method_channel_new(messenger, kChannelName, FL_METHOD_CODEC(codec)); + fl_method_channel_set_method_call_handler(self->channel, method_call_cb, self, + nullptr); + + return self; +} + +FlMethodResponse* fl_windowing_channel_make_create_regular_response( + int64_t view_id, + FlWindowingSize* size, + FlWindowState state) { + g_autoptr(FlValue) result = fl_value_new_map(); + fl_value_set_string_take(result, kViewIdKey, fl_value_new_int(view_id)); + g_autoptr(FlValue) size_value = fl_value_new_list(); + fl_value_append_take(size_value, fl_value_new_float(size->width)); + fl_value_append_take(size_value, fl_value_new_float(size->height)); + fl_value_set_string(result, kSizeKey, size_value); + fl_value_set_string_take(result, kStateKey, + fl_value_new_string(window_state_to_string(state))); + return FL_METHOD_RESPONSE(fl_method_success_response_new(result)); +} + +FlMethodResponse* fl_windowing_channel_make_modify_regular_response() { + return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); +} + +FlMethodResponse* fl_windowing_channel_make_destroy_window_response() { + return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); +} diff --git a/engine/src/flutter/shell/platform/linux/fl_windowing_channel.h b/engine/src/flutter/shell/platform/linux/fl_windowing_channel.h new file mode 100644 index 0000000000..663b8653eb --- /dev/null +++ b/engine/src/flutter/shell/platform/linux/fl_windowing_channel.h @@ -0,0 +1,81 @@ +// 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_WINDOWING_CHANNEL_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_WINDOWING_CHANNEL_H_ + +#include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_method_response.h" + +G_BEGIN_DECLS + +G_DECLARE_FINAL_TYPE(FlWindowingChannel, + fl_windowing_channel, + FL, + WINDOWING_CHANNEL, + GObject); + +// States that a window can be in. +typedef enum { + FL_WINDOW_STATE_UNDEFINED, + FL_WINDOW_STATE_RESTORED, + FL_WINDOW_STATE_MAXIMIZED, + FL_WINDOW_STATE_MINIMIZED +} FlWindowState; + +// Size dimensions used in windowing channel. +typedef struct { + double width; + double height; +} FlWindowingSize; + +/** + * FlWindowingChannel: + * + * #FlWindowingChannel is a channel that implements the shell side + * of SystemChannels.windowing from the Flutter services library. + */ + +typedef struct { + FlMethodResponse* (*create_regular)(FlWindowingSize* size, + FlWindowingSize* min_size, + FlWindowingSize* max_size, + const gchar* title, + FlWindowState state, + gpointer user_data); + FlMethodResponse* (*modify_regular)(int64_t view_id, + FlWindowingSize* size, + const gchar* title, + FlWindowState state, + gpointer user_data); + FlMethodResponse* (*destroy_window)(int64_t view_id, gpointer user_data); +} FlWindowingChannelVTable; + +/** + * fl_windowing_channel_new: + * @messenger: an #FlBinaryMessenger + * @vtable: callbacks for incoming method calls. + * @user_data: data to pass in callbacks. + * + * Creates a new channel that sends handled windowing requests from the + * platform. + * + * Returns: a new #FlWindowingChannel + */ +FlWindowingChannel* fl_windowing_channel_new(FlBinaryMessenger* messenger, + FlWindowingChannelVTable* vtable, + gpointer user_data); + +FlMethodResponse* fl_windowing_channel_make_create_regular_response( + int64_t view_id, + FlWindowingSize* size, + FlWindowState state); + +FlMethodResponse* fl_windowing_channel_make_modify_regular_response(); + +FlMethodResponse* fl_windowing_channel_make_destroy_window_response(); + +G_END_DECLS + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_WINDOWING_CHANNEL_H_ diff --git a/engine/src/flutter/shell/platform/linux/fl_windowing_handler.cc b/engine/src/flutter/shell/platform/linux/fl_windowing_handler.cc new file mode 100644 index 0000000000..e56a14e264 --- /dev/null +++ b/engine/src/flutter/shell/platform/linux/fl_windowing_handler.cc @@ -0,0 +1,275 @@ +// 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. + +#include "flutter/shell/platform/linux/fl_windowing_handler.h" + +#include "flutter/shell/platform/linux/fl_windowing_channel.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h" + +typedef struct { + GWeakRef engine; + + FlWindowingChannel* channel; + + GHashTable* windows_by_view_id; +} FlWindowingHandlerPrivate; + +enum { SIGNAL_CREATE_WINDOW, LAST_SIGNAL }; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE_WITH_PRIVATE(FlWindowingHandler, + fl_windowing_handler, + G_TYPE_OBJECT) + +typedef struct { + GtkWindow* window; + FlView* view; + guint first_frame_cb_id; +} WindowData; + +static WindowData* window_data_new(GtkWindow* window, FlView* view) { + WindowData* data = g_new0(WindowData, 1); + data->window = GTK_WINDOW(g_object_ref(window)); + data->view = FL_VIEW(g_object_ref(view)); + return data; +} + +static void window_data_free(WindowData* data) { + g_signal_handler_disconnect(data->view, data->first_frame_cb_id); + g_object_unref(data->window); + g_object_unref(data->view); + g_free(data); +} + +// Called when the first frame is received. +static void first_frame_cb(FlView* view, WindowData* data) { + gtk_window_present(data->window); +} + +static WindowData* get_window_data(FlWindowingHandler* self, int64_t view_id) { + FlWindowingHandlerPrivate* priv = + reinterpret_cast( + fl_windowing_handler_get_instance_private(self)); + + return static_cast( + g_hash_table_lookup(priv->windows_by_view_id, GINT_TO_POINTER(view_id))); +} + +static GtkWindow* fl_windowing_handler_create_window( + FlWindowingHandler* handler, + FlView* view) { + GtkWidget* window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + return GTK_WINDOW(window); +} + +static FlMethodResponse* create_regular(FlWindowingSize* size, + FlWindowingSize* min_size, + FlWindowingSize* max_size, + const gchar* title, + FlWindowState state, + gpointer user_data) { + FlWindowingHandler* self = FL_WINDOWING_HANDLER(user_data); + FlWindowingHandlerPrivate* priv = + reinterpret_cast( + fl_windowing_handler_get_instance_private(self)); + + g_autoptr(FlEngine) engine = FL_ENGINE(g_weak_ref_get(&priv->engine)); + if (engine == nullptr) { + return FL_METHOD_RESPONSE( + fl_method_error_response_new("Internal error", "No engine", nullptr)); + } + + FlView* view = fl_view_new_for_engine(engine); + + GtkWindow* window; + g_signal_emit(self, signals[SIGNAL_CREATE_WINDOW], 0, view, &window); + if (window == nullptr) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "Internal error", "Failed to create window", nullptr)); + } + + gtk_window_set_default_size(GTK_WINDOW(window), size->width, size->height); + if (title != nullptr) { + gtk_window_set_title(GTK_WINDOW(window), title); + } + switch (state) { + case FL_WINDOW_STATE_MAXIMIZED: + gtk_window_maximize(GTK_WINDOW(window)); + break; + case FL_WINDOW_STATE_MINIMIZED: + gtk_window_iconify(GTK_WINDOW(window)); + break; + case FL_WINDOW_STATE_RESTORED: + case FL_WINDOW_STATE_UNDEFINED: + break; + } + + GdkGeometry geometry; + GdkWindowHints geometry_mask = static_cast(0); + if (min_size != nullptr) { + geometry.min_width = min_size->width; + geometry.min_height = min_size->height; + geometry_mask = + static_cast(geometry_mask | GDK_HINT_MIN_SIZE); + } + if (max_size != nullptr) { + geometry.max_width = max_size->width; + geometry.max_height = max_size->height; + geometry_mask = + static_cast(geometry_mask | GDK_HINT_MAX_SIZE); + } + if (geometry_mask != 0) { + gtk_window_set_geometry_hints(GTK_WINDOW(window), nullptr, &geometry, + geometry_mask); + } + + WindowData* data = window_data_new(GTK_WINDOW(window), view); + data->first_frame_cb_id = + g_signal_connect(view, "first-frame", G_CALLBACK(first_frame_cb), data); + + // Make the resources for the view so rendering can start. + // We'll show the view when we have the first frame. + gtk_widget_realize(GTK_WIDGET(view)); + + g_hash_table_insert(priv->windows_by_view_id, + GINT_TO_POINTER(fl_view_get_id(view)), data); + + // We don't know the current size and dimensions, so just reflect back what + // was requested. + FlWindowingSize* initial_size = size; + FlWindowState initial_state = state; + if (initial_state == FL_WINDOW_STATE_UNDEFINED) { + initial_state = FL_WINDOW_STATE_RESTORED; + } + + return fl_windowing_channel_make_create_regular_response( + fl_view_get_id(view), initial_size, initial_state); +} + +static FlMethodResponse* modify_regular(int64_t view_id, + FlWindowingSize* size, + const gchar* title, + FlWindowState state, + gpointer user_data) { + FlWindowingHandler* self = FL_WINDOWING_HANDLER(user_data); + + WindowData* data = get_window_data(self, view_id); + if (data == nullptr) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "Bad Arguments", "No window with given view ID", nullptr)); + } + + if (size != nullptr) { + gtk_window_resize(data->window, size->width, size->height); + } + + if (title != nullptr) { + gtk_window_set_title(data->window, title); + } + + GdkWindowState window_state; + switch (state) { + case FL_WINDOW_STATE_RESTORED: + if (gtk_window_is_maximized(data->window)) { + gtk_window_unmaximize(data->window); + } + window_state = + gdk_window_get_state(gtk_widget_get_window(GTK_WIDGET(data->window))); + if (window_state & GDK_WINDOW_STATE_ICONIFIED) { + gtk_window_deiconify(data->window); + } + break; + case FL_WINDOW_STATE_MAXIMIZED: + gtk_window_maximize(data->window); + break; + case FL_WINDOW_STATE_MINIMIZED: + gtk_window_iconify(data->window); + break; + case FL_WINDOW_STATE_UNDEFINED: + break; + } + + return fl_windowing_channel_make_modify_regular_response(); +} + +static FlMethodResponse* destroy_window(int64_t view_id, gpointer user_data) { + FlWindowingHandler* self = FL_WINDOWING_HANDLER(user_data); + FlWindowingHandlerPrivate* priv = + reinterpret_cast( + fl_windowing_handler_get_instance_private(self)); + + WindowData* data = get_window_data(self, view_id); + if (data == nullptr) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "Bad Arguments", "No window with given view ID", nullptr)); + } + + gtk_widget_destroy(GTK_WIDGET(data->window)); + + g_hash_table_remove(priv->windows_by_view_id, GINT_TO_POINTER(view_id)); + + return fl_windowing_channel_make_destroy_window_response(); +} + +static void fl_windowing_handler_dispose(GObject* object) { + FlWindowingHandler* self = FL_WINDOWING_HANDLER(object); + FlWindowingHandlerPrivate* priv = + reinterpret_cast( + fl_windowing_handler_get_instance_private(self)); + + g_weak_ref_clear(&priv->engine); + g_clear_object(&priv->channel); + g_clear_pointer(&priv->windows_by_view_id, g_hash_table_unref); + + G_OBJECT_CLASS(fl_windowing_handler_parent_class)->dispose(object); +} + +static void fl_windowing_handler_class_init(FlWindowingHandlerClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_windowing_handler_dispose; + + klass->create_window = fl_windowing_handler_create_window; + + signals[SIGNAL_CREATE_WINDOW] = g_signal_new( + "create-window", fl_windowing_handler_get_type(), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET(FlWindowingHandlerClass, create_window), + g_signal_accumulator_first_wins, nullptr, nullptr, GTK_TYPE_WINDOW, 1, + fl_view_get_type()); +} + +static void fl_windowing_handler_init(FlWindowingHandler* self) { + FlWindowingHandlerPrivate* priv = + reinterpret_cast( + fl_windowing_handler_get_instance_private(self)); + + priv->windows_by_view_id = + g_hash_table_new_full(g_direct_hash, g_direct_equal, nullptr, + reinterpret_cast(window_data_free)); +} + +static FlWindowingChannelVTable windowing_channel_vtable = { + .create_regular = create_regular, + .modify_regular = modify_regular, + .destroy_window = destroy_window, +}; + +FlWindowingHandler* fl_windowing_handler_new(FlEngine* engine) { + g_return_val_if_fail(FL_IS_ENGINE(engine), nullptr); + + FlWindowingHandler* self = FL_WINDOWING_HANDLER( + g_object_new(fl_windowing_handler_get_type(), nullptr)); + FlWindowingHandlerPrivate* priv = + reinterpret_cast( + fl_windowing_handler_get_instance_private(self)); + + g_weak_ref_init(&priv->engine, engine); + priv->channel = fl_windowing_channel_new( + fl_engine_get_binary_messenger(engine), &windowing_channel_vtable, self); + + return self; +} diff --git a/engine/src/flutter/shell/platform/linux/fl_windowing_handler.h b/engine/src/flutter/shell/platform/linux/fl_windowing_handler.h new file mode 100644 index 0000000000..07b8b52cf7 --- /dev/null +++ b/engine/src/flutter/shell/platform/linux/fl_windowing_handler.h @@ -0,0 +1,47 @@ +// 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_WINDOWING_HANDLER_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_WINDOWING_HANDLER_H_ + +#include + +#include "flutter/shell/platform/linux/public/flutter_linux/fl_engine.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h" + +G_BEGIN_DECLS + +G_DECLARE_DERIVABLE_TYPE(FlWindowingHandler, + fl_windowing_handler, + FL, + WINDOWING_HANDLER, + GObject); + +struct _FlWindowingHandlerClass { + GObjectClass parent_class; + + GtkWindow* (*create_window)(FlWindowingHandler* handler, FlView* view); +}; + +/** + * FlWindowingHandler: + * + * #FlWindowingHandler is a handler that implements the shell side + * of SystemChannels.windowing from the Flutter services library. + */ + +/** + * fl_windowing_handler_new: + * @engine: an #FlEngine. + * + * Creates a new handler that implements SystemChannels.windowing from the + * Flutter services library. + * + * Returns: a new #FlWindowingHandler + */ +FlWindowingHandler* fl_windowing_handler_new(FlEngine* engine); + +G_END_DECLS + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_WINDOWING_HANDLER_H_ diff --git a/engine/src/flutter/shell/platform/linux/fl_windowing_handler_test.cc b/engine/src/flutter/shell/platform/linux/fl_windowing_handler_test.cc new file mode 100644 index 0000000000..5660cc2452 --- /dev/null +++ b/engine/src/flutter/shell/platform/linux/fl_windowing_handler_test.cc @@ -0,0 +1,640 @@ +// 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. + +#include "flutter/shell/platform/linux/fl_windowing_handler.h" +#include "flutter/shell/platform/linux/fl_binary_messenger_private.h" +#include "flutter/shell/platform/linux/fl_engine_private.h" +#include "flutter/shell/platform/linux/testing/fl_mock_binary_messenger.h" +#include "flutter/shell/platform/linux/testing/fl_test_gtk_logs.h" +#include "flutter/shell/platform/linux/testing/mock_window.h" +#include "flutter/testing/testing.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +static void set_size_arg(FlValue* args, + const gchar* name, + double width, + double height) { + g_autoptr(FlValue) size_value = fl_value_new_list(); + fl_value_append_take(size_value, fl_value_new_float(width)); + fl_value_append_take(size_value, fl_value_new_float(height)); + fl_value_set_string(args, name, size_value); +} + +static FlValue* make_create_regular_args(double width, double height) { + FlValue* args = fl_value_new_map(); + set_size_arg(args, "size", width, height); + return args; +} + +static int64_t parse_create_regular_response(FlMethodResponse* response) { + EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + + FlValue* result = fl_method_success_response_get_result( + FL_METHOD_SUCCESS_RESPONSE(response)); + EXPECT_EQ(fl_value_get_type(result), FL_VALUE_TYPE_MAP); + + FlValue* view_id_value = fl_value_lookup_string(result, "viewId"); + EXPECT_NE(view_id_value, nullptr); + EXPECT_EQ(fl_value_get_type(view_id_value), FL_VALUE_TYPE_INT); + int64_t view_id = fl_value_get_int(view_id_value); + EXPECT_GT(view_id, 0); + + FlValue* size_value = fl_value_lookup_string(result, "size"); + EXPECT_NE(size_value, nullptr); + EXPECT_EQ(fl_value_get_type(size_value), FL_VALUE_TYPE_LIST); + EXPECT_EQ(fl_value_get_length(size_value), 2u); + EXPECT_EQ(fl_value_get_type(fl_value_get_list_value(size_value, 0)), + FL_VALUE_TYPE_FLOAT); + EXPECT_EQ(fl_value_get_type(fl_value_get_list_value(size_value, 1)), + FL_VALUE_TYPE_FLOAT); + + FlValue* state_value = fl_value_lookup_string(result, "state"); + EXPECT_NE(state_value, nullptr); + EXPECT_EQ(fl_value_get_type(state_value), FL_VALUE_TYPE_STRING); + + return view_id; +} + +static FlValue* make_modify_regular_args(int64_t view_id) { + FlValue* args = fl_value_new_map(); + fl_value_set_string_take(args, "viewId", fl_value_new_int(view_id)); + return args; +} + +static FlValue* make_destroy_window_args(int64_t view_id) { + FlValue* args = fl_value_new_map(); + fl_value_set_string_take(args, "viewId", fl_value_new_int(view_id)); + return args; +} + +TEST(FlWindowingHandlerTest, CreateRegular) { + flutter::testing::fl_ensure_gtk_init(); + ::testing::NiceMock mock_window; + + g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); + g_autoptr(FlEngine) engine = + fl_engine_new_with_binary_messenger(FL_BINARY_MESSENGER(messenger)); + g_autoptr(FlWindowingHandler) handler = fl_windowing_handler_new(engine); + + EXPECT_CALL(mock_window, gtk_window_new); + EXPECT_CALL(mock_window, gtk_window_set_default_size(::testing::_, 800, 600)); + + g_autoptr(FlValue) args = make_create_regular_args(800, 600); + + gboolean called = FALSE; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "createRegular", args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + parse_create_regular_response(response); + gboolean* called = static_cast(user_data); + *called = TRUE; + }, + &called); + EXPECT_TRUE(called); + + fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); +} + +TEST(FlWindowingHandlerTest, CreateRegularMinSize) { + flutter::testing::fl_ensure_gtk_init(); + ::testing::NiceMock mock_window; + + g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); + g_autoptr(FlEngine) engine = + fl_engine_new_with_binary_messenger(FL_BINARY_MESSENGER(messenger)); + g_autoptr(FlWindowingHandler) handler = fl_windowing_handler_new(engine); + + EXPECT_CALL(mock_window, gtk_window_new); + EXPECT_CALL(mock_window, + gtk_window_set_geometry_hints( + ::testing::_, nullptr, + ::testing::Pointee(::testing::AllOf( + ::testing::Field(&GdkGeometry::min_width, 100), + ::testing::Field(&GdkGeometry::min_height, 200))), + GDK_HINT_MIN_SIZE)); + + g_autoptr(FlValue) args = make_create_regular_args(800, 600); + set_size_arg(args, "minSize", 100, 200); + + gboolean called = FALSE; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "createRegular", args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + parse_create_regular_response(response); + gboolean* called = static_cast(user_data); + *called = TRUE; + }, + &called); + EXPECT_TRUE(called); + + fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); +} + +TEST(FlWindowingHandlerTest, CreateRegularMaxSize) { + flutter::testing::fl_ensure_gtk_init(); + ::testing::NiceMock mock_window; + + g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); + g_autoptr(FlEngine) engine = + fl_engine_new_with_binary_messenger(FL_BINARY_MESSENGER(messenger)); + g_autoptr(FlWindowingHandler) handler = fl_windowing_handler_new(engine); + + EXPECT_CALL(mock_window, gtk_window_new); + EXPECT_CALL(mock_window, + gtk_window_set_geometry_hints( + ::testing::_, nullptr, + ::testing::Pointee(::testing::AllOf( + ::testing::Field(&GdkGeometry::max_width, 1000), + ::testing::Field(&GdkGeometry::max_height, 2000))), + GDK_HINT_MAX_SIZE)); + + g_autoptr(FlValue) args = make_create_regular_args(800, 600); + set_size_arg(args, "maxSize", 1000, 2000); + + gboolean called = FALSE; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "createRegular", args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + parse_create_regular_response(response); + gboolean* called = static_cast(user_data); + *called = TRUE; + }, + &called); + EXPECT_TRUE(called); + + fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); +} + +TEST(FlWindowingHandlerTest, CreateRegularWithTitle) { + flutter::testing::fl_ensure_gtk_init(); + ::testing::NiceMock mock_window; + + g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); + g_autoptr(FlEngine) engine = + fl_engine_new_with_binary_messenger(FL_BINARY_MESSENGER(messenger)); + g_autoptr(FlWindowingHandler) handler = fl_windowing_handler_new(engine); + + EXPECT_CALL(mock_window, gtk_window_new); + EXPECT_CALL(mock_window, + gtk_window_set_title(::testing::_, ::testing::StrEq("TITLE"))); + + g_autoptr(FlValue) args = make_create_regular_args(800, 600); + fl_value_set_string_take(args, "title", fl_value_new_string("TITLE")); + + gboolean called = FALSE; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "createRegular", args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + parse_create_regular_response(response); + gboolean* called = static_cast(user_data); + *called = TRUE; + }, + &called); + EXPECT_TRUE(called); + + fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); +} + +TEST(FlWindowingHandlerTest, CreateRegularMaximized) { + flutter::testing::fl_ensure_gtk_init(); + ::testing::NiceMock mock_window; + + g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); + g_autoptr(FlEngine) engine = + fl_engine_new_with_binary_messenger(FL_BINARY_MESSENGER(messenger)); + g_autoptr(FlWindowingHandler) handler = fl_windowing_handler_new(engine); + + EXPECT_CALL(mock_window, gtk_window_new); + EXPECT_CALL(mock_window, gtk_window_maximize(::testing::_)); + + g_autoptr(FlValue) args = make_create_regular_args(800, 600); + fl_value_set_string_take(args, "state", + fl_value_new_string("WindowState.maximized")); + + gboolean called = FALSE; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "createRegular", args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + parse_create_regular_response(response); + gboolean* called = static_cast(user_data); + *called = TRUE; + }, + &called); + EXPECT_TRUE(called); + + fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); +} + +TEST(FlWindowingHandlerTest, CreateRegularMinimized) { + flutter::testing::fl_ensure_gtk_init(); + ::testing::NiceMock mock_window; + + g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); + g_autoptr(FlEngine) engine = + fl_engine_new_with_binary_messenger(FL_BINARY_MESSENGER(messenger)); + g_autoptr(FlWindowingHandler) handler = fl_windowing_handler_new(engine); + + EXPECT_CALL(mock_window, gtk_window_new); + EXPECT_CALL(mock_window, gtk_window_iconify(::testing::_)); + + g_autoptr(FlValue) args = make_create_regular_args(800, 600); + fl_value_set_string_take(args, "state", + fl_value_new_string("WindowState.minimized")); + + gboolean called = FALSE; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "createRegular", args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + parse_create_regular_response(response); + gboolean* called = static_cast(user_data); + *called = TRUE; + }, + &called); + EXPECT_TRUE(called); + + fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); +} + +TEST(FlWindowingHandlerTest, ModifyRegularSize) { + flutter::testing::fl_ensure_gtk_init(); + ::testing::NiceMock mock_window; + + g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); + g_autoptr(FlEngine) engine = + fl_engine_new_with_binary_messenger(FL_BINARY_MESSENGER(messenger)); + g_autoptr(FlWindowingHandler) handler = fl_windowing_handler_new(engine); + + EXPECT_CALL(mock_window, gtk_window_new); + EXPECT_CALL(mock_window, gtk_window_resize(::testing::_, 1024, 768)); + + g_autoptr(FlValue) create_args = make_create_regular_args(800, 600); + + int64_t view_id = -1; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "createRegular", create_args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + int64_t* view_id = static_cast(user_data); + *view_id = parse_create_regular_response(response); + }, + &view_id); + EXPECT_GT(view_id, 0); + + g_autoptr(FlValue) modify_args = make_modify_regular_args(view_id); + set_size_arg(modify_args, "size", 1024, 768); + + gboolean modify_called = FALSE; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "modifyRegular", modify_args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + gboolean* called = static_cast(user_data); + *called = TRUE; + }, + &modify_called); + EXPECT_TRUE(modify_called); + + fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); +} + +TEST(FlWindowingHandlerTest, ModifyRegularTitle) { + flutter::testing::fl_ensure_gtk_init(); + ::testing::NiceMock mock_window; + + g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); + g_autoptr(FlEngine) engine = + fl_engine_new_with_binary_messenger(FL_BINARY_MESSENGER(messenger)); + g_autoptr(FlWindowingHandler) handler = fl_windowing_handler_new(engine); + + EXPECT_CALL(mock_window, gtk_window_new); + EXPECT_CALL(mock_window, + gtk_window_set_title(::testing::_, ::testing::StrEq("TITLE"))); + + g_autoptr(FlValue) create_args = make_create_regular_args(800, 600); + + int64_t view_id = -1; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "createRegular", create_args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + int64_t* view_id = static_cast(user_data); + *view_id = parse_create_regular_response(response); + }, + &view_id); + EXPECT_GT(view_id, 0); + + g_autoptr(FlValue) modify_args = make_modify_regular_args(view_id); + fl_value_set_string_take(modify_args, "title", fl_value_new_string("TITLE")); + + gboolean modify_called = FALSE; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "modifyRegular", modify_args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + gboolean* called = static_cast(user_data); + *called = TRUE; + }, + &modify_called); + EXPECT_TRUE(modify_called); + + fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); +} + +TEST(FlWindowingHandlerTest, ModifyRegularMaximize) { + flutter::testing::fl_ensure_gtk_init(); + ::testing::NiceMock mock_window; + + g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); + g_autoptr(FlEngine) engine = + fl_engine_new_with_binary_messenger(FL_BINARY_MESSENGER(messenger)); + g_autoptr(FlWindowingHandler) handler = fl_windowing_handler_new(engine); + + EXPECT_CALL(mock_window, gtk_window_new); + + g_autoptr(FlValue) create_args = make_create_regular_args(800, 600); + + int64_t view_id = -1; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "createRegular", create_args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + int64_t* view_id = static_cast(user_data); + *view_id = parse_create_regular_response(response); + }, + &view_id); + EXPECT_GT(view_id, 0); + + EXPECT_CALL(mock_window, gtk_window_maximize(::testing::_)); + + g_autoptr(FlValue) modify_args = make_modify_regular_args(view_id); + fl_value_set_string_take(modify_args, "state", + fl_value_new_string("WindowState.maximized")); + + gboolean modify_called = FALSE; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "modifyRegular", modify_args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + gboolean* called = static_cast(user_data); + *called = TRUE; + }, + &modify_called); + EXPECT_TRUE(modify_called); + + fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); +} + +TEST(FlWindowingHandlerTest, ModifyRegularUnmaximize) { + flutter::testing::fl_ensure_gtk_init(); + ::testing::NiceMock mock_window; + + g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); + g_autoptr(FlEngine) engine = + fl_engine_new_with_binary_messenger(FL_BINARY_MESSENGER(messenger)); + g_autoptr(FlWindowingHandler) handler = fl_windowing_handler_new(engine); + + EXPECT_CALL(mock_window, gtk_window_new); + EXPECT_CALL(mock_window, gtk_window_maximize(::testing::_)); + + g_autoptr(FlValue) create_args = make_create_regular_args(800, 600); + fl_value_set_string_take(create_args, "state", + fl_value_new_string("WindowState.maximized")); + + int64_t view_id = -1; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "createRegular", create_args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + int64_t* view_id = static_cast(user_data); + *view_id = parse_create_regular_response(response); + }, + &view_id); + EXPECT_GT(view_id, 0); + + EXPECT_CALL(mock_window, gtk_window_is_maximized(::testing::_)) + .WillOnce(::testing::Return(TRUE)); + EXPECT_CALL(mock_window, gtk_window_unmaximize(::testing::_)); + + g_autoptr(FlValue) modify_args = make_modify_regular_args(view_id); + fl_value_set_string_take(modify_args, "state", + fl_value_new_string("WindowState.restored")); + + gboolean modify_called = FALSE; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "modifyRegular", modify_args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + gboolean* called = static_cast(user_data); + *called = TRUE; + }, + &modify_called); + EXPECT_TRUE(modify_called); + + fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); +} + +TEST(FlWindowingHandlerTest, ModifyRegularMinimize) { + flutter::testing::fl_ensure_gtk_init(); + ::testing::NiceMock mock_window; + + g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); + g_autoptr(FlEngine) engine = + fl_engine_new_with_binary_messenger(FL_BINARY_MESSENGER(messenger)); + g_autoptr(FlWindowingHandler) handler = fl_windowing_handler_new(engine); + + EXPECT_CALL(mock_window, gtk_window_new); + + g_autoptr(FlValue) create_args = make_create_regular_args(800, 600); + + int64_t view_id = -1; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "createRegular", create_args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + int64_t* view_id = static_cast(user_data); + *view_id = parse_create_regular_response(response); + }, + &view_id); + EXPECT_GT(view_id, 0); + + EXPECT_CALL(mock_window, gtk_window_iconify(::testing::_)); + + g_autoptr(FlValue) modify_args = make_modify_regular_args(view_id); + fl_value_set_string_take(modify_args, "state", + fl_value_new_string("WindowState.minimized")); + + gboolean modify_called = FALSE; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "modifyRegular", modify_args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + gboolean* called = static_cast(user_data); + *called = TRUE; + }, + &modify_called); + EXPECT_TRUE(modify_called); + + fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); +} + +TEST(FlWindowingHandlerTest, ModifyRegularUnminimize) { + flutter::testing::fl_ensure_gtk_init(); + ::testing::NiceMock mock_window; + + g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); + g_autoptr(FlEngine) engine = + fl_engine_new_with_binary_messenger(FL_BINARY_MESSENGER(messenger)); + g_autoptr(FlWindowingHandler) handler = fl_windowing_handler_new(engine); + + EXPECT_CALL(mock_window, gtk_window_new); + EXPECT_CALL(mock_window, gtk_window_iconify(::testing::_)); + + g_autoptr(FlValue) create_args = make_create_regular_args(800, 600); + fl_value_set_string_take(create_args, "state", + fl_value_new_string("WindowState.minimized")); + + int64_t view_id = -1; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "createRegular", create_args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + int64_t* view_id = static_cast(user_data); + *view_id = parse_create_regular_response(response); + }, + &view_id); + EXPECT_GT(view_id, 0); + + EXPECT_CALL(mock_window, gdk_window_get_state(::testing::_)) + .WillOnce(::testing::Return(GDK_WINDOW_STATE_ICONIFIED)); + EXPECT_CALL(mock_window, gtk_window_deiconify(::testing::_)); + + g_autoptr(FlValue) modify_args = make_modify_regular_args(view_id); + fl_value_set_string_take(modify_args, "state", + fl_value_new_string("WindowState.restored")); + + gboolean modify_called = FALSE; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "modifyRegular", modify_args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + gboolean* called = static_cast(user_data); + *called = TRUE; + }, + &modify_called); + EXPECT_TRUE(modify_called); + + fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); +} + +TEST(FlWindowingHandlerTest, ModifyUnknownWindow) { + g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); + g_autoptr(FlEngine) engine = + fl_engine_new_with_binary_messenger(FL_BINARY_MESSENGER(messenger)); + g_autoptr(FlWindowingHandler) handler = fl_windowing_handler_new(engine); + + g_autoptr(FlValue) args = make_modify_regular_args(99); + + gboolean called = FALSE; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "modifyRegular", args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + EXPECT_TRUE(FL_IS_METHOD_ERROR_RESPONSE(response)); + EXPECT_STREQ(fl_method_error_response_get_code( + FL_METHOD_ERROR_RESPONSE(response)), + "Bad Arguments"); + EXPECT_STREQ(fl_method_error_response_get_message( + FL_METHOD_ERROR_RESPONSE(response)), + "No window with given view ID"); + gboolean* called = static_cast(user_data); + *called = TRUE; + }, + &called); + EXPECT_TRUE(called); + + fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); +} + +TEST(FlWindowingHandlerTest, DestroyWindow) { + flutter::testing::fl_ensure_gtk_init(); + ::testing::NiceMock mock_window; + + g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); + g_autoptr(FlEngine) engine = + fl_engine_new_with_binary_messenger(FL_BINARY_MESSENGER(messenger)); + g_autoptr(FlWindowingHandler) handler = fl_windowing_handler_new(engine); + + EXPECT_CALL(mock_window, gtk_window_new); + EXPECT_CALL(mock_window, gtk_widget_destroy); + + g_autoptr(FlValue) create_args = make_create_regular_args(800, 600); + + int64_t view_id = -1; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "createRegular", create_args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + int64_t* view_id = static_cast(user_data); + *view_id = parse_create_regular_response(response); + }, + &view_id); + EXPECT_GT(view_id, 0); + + g_autoptr(FlValue) destroy_args = make_destroy_window_args(view_id); + gboolean destroy_called = FALSE; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "destroyWindow", destroy_args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + gboolean* called = static_cast(user_data); + *called = TRUE; + }, + &destroy_called); + EXPECT_TRUE(destroy_called); + + fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); +} + +TEST(FlWindowingHandlerTest, DestroyUnknownWindow) { + g_autoptr(FlMockBinaryMessenger) messenger = fl_mock_binary_messenger_new(); + g_autoptr(FlEngine) engine = + fl_engine_new_with_binary_messenger(FL_BINARY_MESSENGER(messenger)); + g_autoptr(FlWindowingHandler) handler = fl_windowing_handler_new(engine); + + g_autoptr(FlValue) args = make_destroy_window_args(99); + gboolean called = FALSE; + fl_mock_binary_messenger_invoke_standard_method( + messenger, "flutter/windowing", "destroyWindow", args, + [](FlMockBinaryMessenger* messenger, FlMethodResponse* response, + gpointer user_data) { + EXPECT_TRUE(FL_IS_METHOD_ERROR_RESPONSE(response)); + EXPECT_STREQ(fl_method_error_response_get_code( + FL_METHOD_ERROR_RESPONSE(response)), + "Bad Arguments"); + EXPECT_STREQ(fl_method_error_response_get_message( + FL_METHOD_ERROR_RESPONSE(response)), + "No window with given view ID"); + gboolean* called = static_cast(user_data); + *called = TRUE; + }, + &called); + EXPECT_TRUE(called); + + fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger)); +} diff --git a/engine/src/flutter/shell/platform/linux/testing/mock_window.cc b/engine/src/flutter/shell/platform/linux/testing/mock_window.cc index 415a14c521..69a3de2e49 100644 --- a/engine/src/flutter/shell/platform/linux/testing/mock_window.cc +++ b/engine/src/flutter/shell/platform/linux/testing/mock_window.cc @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include + #include "flutter/shell/platform/linux/testing/mock_window.h" using namespace flutter::testing; @@ -50,3 +52,60 @@ GdkCursor* gdk_cursor_new_from_name(GdkDisplay* display, const gchar* name) { } void gdk_window_set_cursor(GdkWindow* window, GdkCursor* cursor) {} + +GtkWidget* gtk_window_new(GtkWindowType type) { + GtkWindow* window = GTK_WINDOW(g_object_new(gtk_window_get_type(), nullptr)); + mock->gtk_window_new(window, type); + return GTK_WIDGET(window); +} + +void gtk_window_set_default_size(GtkWindow* window, gint width, gint height) { + mock->gtk_window_set_default_size(window, width, height); +} + +void gtk_window_set_title(GtkWindow* window, const gchar* title) { + mock->gtk_window_set_title(window, title); +} + +void gtk_window_set_geometry_hints(GtkWindow* window, + GtkWidget* widget, + GdkGeometry* geometry, + GdkWindowHints geometry_mask) { + mock->gtk_window_set_geometry_hints(window, widget, geometry, geometry_mask); +} + +void gtk_window_resize(GtkWindow* window, gint width, gint height) { + mock->gtk_window_resize(window, width, height); +} + +void gtk_window_maximize(GtkWindow* window) { + mock->gtk_window_maximize(window); +} + +void gtk_window_unmaximize(GtkWindow* window) { + mock->gtk_window_unmaximize(window); +} + +gboolean gtk_window_is_maximized(GtkWindow* window) { + return mock->gtk_window_is_maximized(window); +} + +void gtk_window_iconify(GtkWindow* window) { + mock->gtk_window_iconify(window); +} + +void gtk_window_deiconify(GtkWindow* window) { + mock->gtk_window_deiconify(window); +} + +void gtk_widget_show(GtkWidget* widget) {} + +void gtk_widget_destroy(GtkWidget* widget) { + mock->gtk_widget_destroy(widget); +} + +void fl_gtk_widget_destroy(GtkWidget* widget) { + void (*destroy)(GtkWidget*) = reinterpret_cast( + dlsym(RTLD_NEXT, "gtk_widget_destroy")); + destroy(widget); +} diff --git a/engine/src/flutter/shell/platform/linux/testing/mock_window.h b/engine/src/flutter/shell/platform/linux/testing/mock_window.h index bee5e0cd8e..61d7be1ac2 100644 --- a/engine/src/flutter/shell/platform/linux/testing/mock_window.h +++ b/engine/src/flutter/shell/platform/linux/testing/mock_window.h @@ -18,9 +18,34 @@ class MockWindow { ~MockWindow(); MOCK_METHOD(GdkWindowState, gdk_window_get_state, (GdkWindow * window)); + MOCK_METHOD(void, gtk_window_new, (GtkWindow * window, GtkWindowType type)); + MOCK_METHOD(void, + gtk_window_set_default_size, + (GtkWindow * window, gint width, gint height)); + MOCK_METHOD(void, + gtk_window_set_title, + (GtkWindow * window, const gchar* title)); + MOCK_METHOD(void, + gtk_window_set_geometry_hints, + (GtkWindow * window, + GtkWidget* widget, + GdkGeometry* geometry, + GdkWindowHints geometry_mask)); + MOCK_METHOD(void, + gtk_window_resize, + (GtkWindow * window, gint width, gint height)); + MOCK_METHOD(void, gtk_window_maximize, (GtkWindow * window)); + MOCK_METHOD(void, gtk_window_unmaximize, (GtkWindow * window)); + MOCK_METHOD(gboolean, gtk_window_is_maximized, (GtkWindow * window)); + MOCK_METHOD(void, gtk_window_iconify, (GtkWindow * window)); + MOCK_METHOD(void, gtk_window_deiconify, (GtkWindow * window)); + MOCK_METHOD(void, gtk_widget_destroy, (GtkWidget * widget)); }; } // namespace testing } // namespace flutter +// Calls original gtk_widget_destroy. +void fl_gtk_widget_destroy(GtkWidget* widget); + #endif // FLUTTER_SHELL_PLATFORM_LINUX_TESTING_MOCK_WINDOW_H_