diff --git a/engine/src/flutter/shell/platform/linux/fl_engine.cc b/engine/src/flutter/shell/platform/linux/fl_engine.cc index 089bd1b99a5..2bf4b321439 100644 --- a/engine/src/flutter/shell/platform/linux/fl_engine.cc +++ b/engine/src/flutter/shell/platform/linux/fl_engine.cc @@ -49,6 +49,9 @@ struct _FlEngine { FLUTTER_API_SYMBOL(FlutterEngine) engine; FlutterEngineProcTable embedder_api; + // Next ID to use for a view. + FlutterViewId next_view_id; + // Function to call when a platform message is received. FlEnginePlatformMessageHandler platform_message_handler; gpointer platform_message_handler_data; @@ -123,6 +126,29 @@ static void parse_locale(const gchar* locale, } } +static void view_added_cb(const FlutterAddViewResult* result) { + g_autoptr(GTask) task = G_TASK(result->user_data); + + FlutterViewId view_id = GPOINTER_TO_INT(g_task_get_task_data(task)); + if (result->added) { + g_task_return_pointer(task, GINT_TO_POINTER(view_id), nullptr); + } else { + g_task_return_new_error(task, fl_engine_error_quark(), + FL_ENGINE_ERROR_FAILED, "Failed to add view"); + } +} + +static void view_removed_cb(const FlutterRemoveViewResult* result) { + g_autoptr(GTask) task = G_TASK(result->user_data); + + if (result->removed) { + g_task_return_boolean(task, TRUE); + } else { + g_task_return_new_error(task, fl_engine_error_quark(), + FL_ENGINE_ERROR_FAILED, "Failed to remove view"); + } +} + // Passes locale information to the Flutter engine. static void setup_locales(FlEngine* self) { const gchar* const* languages = g_get_language_names(); @@ -177,12 +203,11 @@ static bool compositor_collect_backing_store_callback( } // Called when embedder should composite contents of each layer onto the screen. -static bool compositor_present_layers_callback(const FlutterLayer** layers, - size_t layers_count, - void* user_data) { - g_return_val_if_fail(FL_IS_RENDERER(user_data), false); - return fl_renderer_present_layers(FL_RENDERER(user_data), layers, - layers_count); +static bool compositor_present_view_callback( + const FlutterPresentViewInfo* info) { + g_return_val_if_fail(FL_IS_RENDERER(info->user_data), false); + return fl_renderer_present_layers(FL_RENDERER(info->user_data), info->layers, + info->layers_count); } // Flutter engine rendering callbacks. @@ -211,7 +236,7 @@ static uint32_t fl_engine_gl_get_fbo(void* user_data) { static bool fl_engine_gl_present(void* user_data) { // No action required, as this is handled in - // compositor_present_layers_callback. + // compositor_present_view_callback. return true; } @@ -429,6 +454,9 @@ static void fl_engine_init(FlEngine* self) { self->embedder_api.struct_size = sizeof(FlutterEngineProcTable); FlutterEngineGetProcAddresses(&self->embedder_api); + // Implicit view is 0, so start at 1. + self->next_view_id = 1; + self->texture_registrar = fl_texture_registrar_new(self); } @@ -511,7 +539,7 @@ gboolean fl_engine_start(FlEngine* self, GError** error) { compositor_create_backing_store_callback; compositor.collect_backing_store_callback = compositor_collect_backing_store_callback; - compositor.present_layers_callback = compositor_present_layers_callback; + compositor.present_view_callback = compositor_present_view_callback; args.compositor = &compositor; if (self->embedder_api.RunsAOTCompiledDartCode()) { @@ -580,6 +608,82 @@ FlutterEngineProcTable* fl_engine_get_embedder_api(FlEngine* self) { return &(self->embedder_api); } +void fl_engine_add_view(FlEngine* self, + size_t width, + size_t height, + double pixel_ratio, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer user_data) { + g_return_if_fail(FL_IS_ENGINE(self)); + + g_autoptr(GTask) task = g_task_new(self, cancellable, callback, user_data); + + FlutterViewId view_id = self->next_view_id; + self->next_view_id++; + g_task_set_task_data(task, GINT_TO_POINTER(view_id), nullptr); + + FlutterWindowMetricsEvent metrics; + metrics.struct_size = sizeof(FlutterWindowMetricsEvent); + metrics.width = width; + metrics.height = height; + metrics.pixel_ratio = pixel_ratio; + metrics.view_id = view_id; + FlutterAddViewInfo info; + info.struct_size = sizeof(FlutterAddViewInfo); + info.view_id = view_id; + info.view_metrics = &metrics; + info.user_data = g_object_ref(task); + info.add_view_callback = view_added_cb; + FlutterEngineResult result = self->embedder_api.AddView(self->engine, &info); + if (result != kSuccess) { + g_task_return_new_error(task, fl_engine_error_quark(), + FL_ENGINE_ERROR_FAILED, "AddView returned %d", + result); + // This would have been done in the callback, but that won't occur now. + g_object_unref(task); + } +} + +FlutterViewId fl_engine_add_view_finish(FlEngine* self, + GAsyncResult* result, + GError** error) { + g_return_val_if_fail(FL_IS_ENGINE(self), FALSE); + return GPOINTER_TO_INT(g_task_propagate_pointer(G_TASK(result), error)); +} + +void fl_engine_remove_view(FlEngine* self, + FlutterViewId view_id, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer user_data) { + g_return_if_fail(FL_IS_ENGINE(self)); + + g_autoptr(GTask) task = g_task_new(self, cancellable, callback, user_data); + + FlutterRemoveViewInfo info; + info.struct_size = sizeof(FlutterRemoveViewInfo); + info.view_id = view_id; + info.user_data = g_object_ref(task); + info.remove_view_callback = view_removed_cb; + FlutterEngineResult result = + self->embedder_api.RemoveView(self->engine, &info); + if (result != kSuccess) { + g_task_return_new_error(task, fl_engine_error_quark(), + FL_ENGINE_ERROR_FAILED, "RemoveView returned %d", + result); + // This would have been done in the callback, but that won't occur now. + g_object_unref(task); + } +} + +gboolean fl_engine_remove_view_finish(FlEngine* self, + GAsyncResult* result, + GError** error) { + g_return_val_if_fail(FL_IS_ENGINE(self), FALSE); + return g_task_propagate_boolean(G_TASK(result), error); +} + void fl_engine_set_platform_message_handler( FlEngine* self, FlEnginePlatformMessageHandler handler, 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 260f85d7fdb..6aaf0c70f6f 100644 --- a/engine/src/flutter/shell/platform/linux/fl_engine_private.h +++ b/engine/src/flutter/shell/platform/linux/fl_engine_private.h @@ -81,6 +81,18 @@ typedef void (*FlEngineOnPreEngineRestartHandler)(FlEngine* engine, */ FlEngine* fl_engine_new(FlDartProject* project, FlRenderer* renderer); +/** + * fl_engine_start: + * @engine: an #FlEngine. + * @error: (allow-none): #GError location to store the error occurring, or %NULL + * to ignore. + * + * Starts the Flutter engine. + * + * Returns: %TRUE on success. + */ +gboolean fl_engine_start(FlEngine* engine, GError** error); + /** * fl_engine_get_embedder_api: * @engine: an #FlEngine. @@ -91,6 +103,74 @@ FlEngine* fl_engine_new(FlDartProject* project, FlRenderer* renderer); */ FlutterEngineProcTable* fl_engine_get_embedder_api(FlEngine* engine); +/** + * fl_engine_add_view: + * @engine: an #FlEngine. + * @width: width of view in pixels. + * @height: height of view in pixels. + * @pixel_ratio: scale factor for view. + * @cancellable: (allow-none): a #GCancellable or %NULL. + * @callback: (scope async): a #GAsyncReadyCallback to call when the view is + * added. + * @user_data: (closure): user data to pass to @callback. + * + * Asynchronously add a new view. + */ +void fl_engine_add_view(FlEngine* engine, + size_t width, + size_t height, + double pixel_ratio, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +/** + * fl_engine_add_view_finish: + * @engine: an #FlEngine. + * @result: a #GAsyncResult. + * @error: (allow-none): #GError location to store the error occurring, or %NULL + * to ignore. + * + * Completes request started with fl_engine_add_view(). + * + * Returns: the newly added view ID or 0 on error. + */ +FlutterViewId fl_engine_add_view_finish(FlEngine* engine, + GAsyncResult* result, + GError** error); + +/** + * fl_engine_remove_view: + * @engine: an #FlEngine. + * @view_id: ID to remove. + * @cancellable: (allow-none): a #GCancellable or %NULL. + * @callback: (scope async): a #GAsyncReadyCallback to call when the view is + * added. + * @user_data: (closure): user data to pass to @callback. + * + * Removes a view previously added with fl_engine_add_view(). + */ +void fl_engine_remove_view(FlEngine* engine, + FlutterViewId view_id, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +/** + * fl_engine_remove_view_finish: + * @engine: an #FlEngine. + * @result: a #GAsyncResult. + * @error: (allow-none): #GError location to store the error occurring, or %NULL + * to ignore. + * + * Completes request started with fl_engine_remove_view(). + * + * Returns: TRUE on succcess. + */ +gboolean fl_engine_remove_view_finish(FlEngine* engine, + GAsyncResult* result, + GError** error); + /** * fl_engine_set_platform_message_handler: * @engine: an #FlEngine. @@ -143,18 +223,6 @@ void fl_engine_set_on_pre_engine_restart_handler( gpointer user_data, GDestroyNotify destroy_notify); -/** - * fl_engine_start: - * @engine: an #FlEngine. - * @error: (allow-none): #GError location to store the error occurring, or %NULL - * to ignore. - * - * Starts the Flutter engine. - * - * Returns: %TRUE on success. - */ -gboolean fl_engine_start(FlEngine* engine, GError** error); - /** * fl_engine_send_window_metrics_event: * @engine: an #FlEngine. diff --git a/engine/src/flutter/shell/platform/linux/fl_engine_test.cc b/engine/src/flutter/shell/platform/linux/fl_engine_test.cc index 38e56e39c12..4c3a2f1f563 100644 --- a/engine/src/flutter/shell/platform/linux/fl_engine_test.cc +++ b/engine/src/flutter/shell/platform/linux/fl_engine_test.cc @@ -425,6 +425,215 @@ TEST(FlEngineTest, SwitchesEmpty) { EXPECT_EQ(switches->len, 0U); } +static void add_view_cb(GObject* object, + GAsyncResult* result, + gpointer user_data) { + g_autoptr(GError) error = nullptr; + FlutterViewId view_id = + fl_engine_add_view_finish(FL_ENGINE(object), result, &error); + EXPECT_GT(view_id, 0); + EXPECT_EQ(error, nullptr); + + g_main_loop_quit(static_cast(user_data)); +} + +TEST(FlEngineTest, AddView) { + g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0); + + g_autoptr(FlEngine) engine = make_mock_engine(); + FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); + + bool called = false; + embedder_api->AddView = MOCK_ENGINE_PROC( + AddView, ([&called](auto engine, const FlutterAddViewInfo* info) { + called = true; + EXPECT_EQ(info->view_metrics->width, 123u); + EXPECT_EQ(info->view_metrics->height, 456u); + EXPECT_EQ(info->view_metrics->pixel_ratio, 2.0); + + FlutterAddViewResult result; + result.struct_size = sizeof(FlutterAddViewResult); + result.added = true; + result.user_data = info->user_data; + info->add_view_callback(&result); + + return kSuccess; + })); + + fl_engine_add_view(engine, 123, 456, 2.0, nullptr, add_view_cb, loop); + EXPECT_TRUE(called); + + // Blocks here until add_view_cb is called. + g_main_loop_run(loop); +} + +static void add_view_error_cb(GObject* object, + GAsyncResult* result, + gpointer user_data) { + g_autoptr(GError) error = nullptr; + FlutterViewId view_id = + fl_engine_add_view_finish(FL_ENGINE(object), result, &error); + EXPECT_EQ(view_id, 0); + EXPECT_NE(error, nullptr); + + g_main_loop_quit(static_cast(user_data)); +} + +TEST(FlEngineTest, AddViewError) { + g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0); + + g_autoptr(FlEngine) engine = make_mock_engine(); + FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); + + embedder_api->AddView = MOCK_ENGINE_PROC( + AddView, ([](auto engine, const FlutterAddViewInfo* info) { + FlutterAddViewResult result; + result.struct_size = sizeof(FlutterAddViewResult); + result.added = false; + result.user_data = info->user_data; + info->add_view_callback(&result); + + return kSuccess; + })); + + fl_engine_add_view(engine, 123, 456, 2.0, nullptr, add_view_error_cb, loop); + + // Blocks here until add_view_error_cb is called. + g_main_loop_run(loop); +} + +static void add_view_engine_error_cb(GObject* object, + GAsyncResult* result, + gpointer user_data) { + g_autoptr(GError) error = nullptr; + FlutterViewId view_id = + fl_engine_add_view_finish(FL_ENGINE(object), result, &error); + EXPECT_EQ(view_id, 0); + EXPECT_NE(error, nullptr); + + g_main_loop_quit(static_cast(user_data)); +} + +TEST(FlEngineTest, AddViewEngineError) { + g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0); + + g_autoptr(FlEngine) engine = make_mock_engine(); + FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); + + embedder_api->AddView = MOCK_ENGINE_PROC( + AddView, ([](auto engine, const FlutterAddViewInfo* info) { + return kInvalidArguments; + })); + + fl_engine_add_view(engine, 123, 456, 2.0, nullptr, add_view_engine_error_cb, + loop); + + // Blocks here until remove_view_engine_error_cb is called. + g_main_loop_run(loop); +} + +static void remove_view_cb(GObject* object, + GAsyncResult* result, + gpointer user_data) { + g_autoptr(GError) error = nullptr; + gboolean r = fl_engine_remove_view_finish(FL_ENGINE(object), result, &error); + EXPECT_TRUE(r); + EXPECT_EQ(error, nullptr); + + g_main_loop_quit(static_cast(user_data)); +} + +TEST(FlEngineTest, RemoveView) { + g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0); + + g_autoptr(FlEngine) engine = make_mock_engine(); + FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); + + bool called = false; + embedder_api->RemoveView = MOCK_ENGINE_PROC( + RemoveView, ([&called](auto engine, const FlutterRemoveViewInfo* info) { + called = true; + EXPECT_EQ(info->view_id, 123); + + FlutterRemoveViewResult result; + result.struct_size = sizeof(FlutterRemoveViewResult); + result.removed = true; + result.user_data = info->user_data; + info->remove_view_callback(&result); + + return kSuccess; + })); + + fl_engine_remove_view(engine, 123, nullptr, remove_view_cb, loop); + EXPECT_TRUE(called); + + // Blocks here until remove_view_cb is called. + g_main_loop_run(loop); +} + +static void remove_view_error_cb(GObject* object, + GAsyncResult* result, + gpointer user_data) { + g_autoptr(GError) error = nullptr; + gboolean r = fl_engine_remove_view_finish(FL_ENGINE(object), result, &error); + EXPECT_FALSE(r); + EXPECT_NE(error, nullptr); + + g_main_loop_quit(static_cast(user_data)); +} + +TEST(FlEngineTest, RemoveViewError) { + g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0); + + g_autoptr(FlEngine) engine = make_mock_engine(); + FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); + + embedder_api->RemoveView = MOCK_ENGINE_PROC( + RemoveView, ([](auto engine, const FlutterRemoveViewInfo* info) { + FlutterRemoveViewResult result; + result.struct_size = sizeof(FlutterRemoveViewResult); + result.removed = false; + result.user_data = info->user_data; + info->remove_view_callback(&result); + + return kSuccess; + })); + + fl_engine_remove_view(engine, 123, nullptr, remove_view_error_cb, loop); + + // Blocks here until remove_view_error_cb is called. + g_main_loop_run(loop); +} + +static void remove_view_engine_error_cb(GObject* object, + GAsyncResult* result, + gpointer user_data) { + g_autoptr(GError) error = nullptr; + gboolean r = fl_engine_remove_view_finish(FL_ENGINE(object), result, &error); + EXPECT_FALSE(r); + EXPECT_NE(error, nullptr); + + g_main_loop_quit(static_cast(user_data)); +} + +TEST(FlEngineTest, RemoveViewEngineError) { + g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0); + + g_autoptr(FlEngine) engine = make_mock_engine(); + FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); + + embedder_api->RemoveView = MOCK_ENGINE_PROC( + RemoveView, ([](auto engine, const FlutterRemoveViewInfo* info) { + return kInvalidArguments; + })); + + fl_engine_remove_view(engine, 123, nullptr, remove_view_engine_error_cb, + loop); + + // Blocks here until remove_view_engine_error_cb is called. + g_main_loop_run(loop); +} + #ifndef FLUTTER_RELEASE TEST(FlEngineTest, Switches) { g_autoptr(FlEngine) engine = make_mock_engine();