| // 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/glfw/public/flutter_glfw.h" |
| |
| #include <GLFW/glfw3.h> |
| |
| #include <algorithm> |
| #include <cassert> |
| #include <chrono> |
| #include <cstdlib> |
| #include <filesystem> |
| #include <iostream> |
| |
| #include "flutter/shell/platform/common/client_wrapper/include/flutter/plugin_registrar.h" |
| #include "flutter/shell/platform/common/incoming_message_dispatcher.h" |
| #include "flutter/shell/platform/common/path_utils.h" |
| #include "flutter/shell/platform/embedder/embedder.h" |
| #include "flutter/shell/platform/glfw/glfw_event_loop.h" |
| #include "flutter/shell/platform/glfw/headless_event_loop.h" |
| #include "flutter/shell/platform/glfw/key_event_handler.h" |
| #include "flutter/shell/platform/glfw/keyboard_hook_handler.h" |
| #include "flutter/shell/platform/glfw/platform_handler.h" |
| #include "flutter/shell/platform/glfw/system_utils.h" |
| #include "flutter/shell/platform/glfw/text_input_plugin.h" |
| |
| // GLFW_TRUE & GLFW_FALSE are introduced since libglfw-3.3, |
| // add definitions here to compile under the old versions. |
| #ifndef GLFW_TRUE |
| #define GLFW_TRUE 1 |
| #endif |
| #ifndef GLFW_FALSE |
| #define GLFW_FALSE 0 |
| #endif |
| |
| using UniqueGLFWwindowPtr = std::unique_ptr<GLFWwindow, void (*)(GLFWwindow*)>; |
| |
| static_assert(FLUTTER_ENGINE_VERSION == 1, ""); |
| |
| const int kFlutterDesktopDontCare = GLFW_DONT_CARE; |
| |
| static constexpr double kDpPerInch = 160.0; |
| |
| // Struct for storing state within an instance of the GLFW Window. |
| struct FlutterDesktopWindowControllerState { |
| // The GLFW window that is bound to this state object. |
| UniqueGLFWwindowPtr window = UniqueGLFWwindowPtr(nullptr, glfwDestroyWindow); |
| |
| // The invisible GLFW window used to upload resources in the background. |
| UniqueGLFWwindowPtr resource_window = |
| UniqueGLFWwindowPtr(nullptr, glfwDestroyWindow); |
| |
| // The state associated with the engine. |
| std::unique_ptr<FlutterDesktopEngineState> engine; |
| |
| // The window handle given to API clients. |
| std::unique_ptr<FlutterDesktopWindow> window_wrapper; |
| |
| // Handlers for keyboard events from GLFW. |
| std::vector<std::unique_ptr<flutter::KeyboardHookHandler>> |
| keyboard_hook_handlers; |
| |
| // Whether or not the pointer has been added (or if tracking is enabled, |
| // has been added since it was last removed). |
| bool pointer_currently_added = false; |
| |
| // Whether or not the pointer is down. |
| bool pointer_currently_down = false; |
| |
| // The currently pressed buttons, as represented in FlutterPointerEvent. |
| int64_t buttons = 0; |
| |
| // The screen coordinates per inch on the primary monitor. Defaults to a sane |
| // value based on pixel_ratio 1.0. |
| double monitor_screen_coordinates_per_inch = kDpPerInch; |
| }; |
| |
| // Opaque reference for the GLFW window itself. This is separate from the |
| // controller so that it can be provided to plugins without giving them access |
| // to all of the controller-based functionality. |
| struct FlutterDesktopWindow { |
| // The GLFW window that (indirectly) owns this state object. |
| GLFWwindow* window; |
| |
| // Whether or not to track mouse movements to send kHover events. |
| bool hover_tracking_enabled = true; |
| |
| // The ratio of pixels per screen coordinate for the window. |
| double pixels_per_screen_coordinate = 1.0; |
| |
| // If non-zero, a forced pixel ratio to use instead of one computed based on |
| // screen information. |
| double pixel_ratio_override = 0.0; |
| |
| // Resizing triggers a window refresh, but the resize already updates Flutter. |
| // To avoid double messages, the refresh after each resize is skipped. |
| bool skip_next_window_refresh = false; |
| }; |
| |
| // Custom deleter for FlutterEngineAOTData. |
| struct AOTDataDeleter { |
| void operator()(FlutterEngineAOTData aot_data) { |
| FlutterEngineCollectAOTData(aot_data); |
| } |
| }; |
| |
| using UniqueAotDataPtr = std::unique_ptr<_FlutterEngineAOTData, AOTDataDeleter>; |
| /// Maintains one ref on the FlutterDesktopMessenger's internal reference count. |
| using FlutterDesktopMessengerReferenceOwner = |
| std::unique_ptr<FlutterDesktopMessenger, |
| decltype(&FlutterDesktopMessengerRelease)>; |
| |
| // Struct for storing state of a Flutter engine instance. |
| struct FlutterDesktopEngineState { |
| // The handle to the Flutter engine instance. |
| FLUTTER_API_SYMBOL(FlutterEngine) flutter_engine; |
| |
| // The event loop for the main thread that allows for delayed task execution. |
| std::unique_ptr<flutter::EventLoop> event_loop; |
| |
| // The plugin messenger handle given to API clients. |
| FlutterDesktopMessengerReferenceOwner messenger = { |
| nullptr, [](FlutterDesktopMessengerRef ref) {}}; |
| |
| // Message dispatch manager for messages from the Flutter engine. |
| std::unique_ptr<flutter::IncomingMessageDispatcher> message_dispatcher; |
| |
| // The plugin registrar handle given to API clients. |
| std::unique_ptr<FlutterDesktopPluginRegistrar> plugin_registrar; |
| |
| // The plugin registrar managing internal plugins. |
| std::unique_ptr<flutter::PluginRegistrar> internal_plugin_registrar; |
| |
| // Handler for the flutter/platform channel. |
| std::unique_ptr<flutter::PlatformHandler> platform_handler; |
| |
| // The controller associated with this engine instance, if any. |
| // This will always be null for a headless engine. |
| FlutterDesktopWindowControllerState* window_controller = nullptr; |
| |
| // AOT data for this engine instance, if applicable. |
| UniqueAotDataPtr aot_data = nullptr; |
| }; |
| |
| // State associated with the plugin registrar. |
| struct FlutterDesktopPluginRegistrar { |
| // The engine that backs this registrar. |
| FlutterDesktopEngineState* engine; |
| |
| // Callback to be called on registrar destruction. |
| FlutterDesktopOnPluginRegistrarDestroyed destruction_handler; |
| }; |
| |
| // State associated with the messenger used to communicate with the engine. |
| struct FlutterDesktopMessenger { |
| FlutterDesktopMessenger() = default; |
| |
| /// Increments the reference count. |
| /// |
| /// Thread-safe. |
| void AddRef() { ref_count_.fetch_add(1); } |
| |
| /// Decrements the reference count and deletes the object if the count has |
| /// gone to zero. |
| /// |
| /// Thread-safe. |
| void Release() { |
| int32_t old_count = ref_count_.fetch_sub(1); |
| if (old_count <= 1) { |
| delete this; |
| } |
| } |
| |
| /// Getter for the engine field. |
| FlutterDesktopEngineState* GetEngine() const { return engine_; } |
| |
| /// Setter for the engine field. |
| /// Thread-safe. |
| void SetEngine(FlutterDesktopEngineState* engine) { |
| std::scoped_lock lock(mutex_); |
| engine_ = engine; |
| } |
| |
| /// Returns the mutex associated with the |FlutterDesktopMessenger|. |
| /// |
| /// This mutex is used to synchronize reading or writing state inside the |
| /// |FlutterDesktopMessenger| (ie |engine_|). |
| std::mutex& GetMutex() { return mutex_; } |
| |
| FlutterDesktopMessenger(const FlutterDesktopMessenger& value) = delete; |
| FlutterDesktopMessenger& operator=(const FlutterDesktopMessenger& value) = |
| delete; |
| |
| private: |
| // The engine that backs this messenger. |
| FlutterDesktopEngineState* engine_; |
| std::atomic<int32_t> ref_count_ = 0; |
| std::mutex mutex_; |
| }; |
| |
| FlutterDesktopMessengerRef FlutterDesktopMessengerAddRef( |
| FlutterDesktopMessengerRef messenger) { |
| messenger->AddRef(); |
| return messenger; |
| } |
| |
| void FlutterDesktopMessengerRelease(FlutterDesktopMessengerRef messenger) { |
| messenger->Release(); |
| } |
| |
| bool FlutterDesktopMessengerIsAvailable(FlutterDesktopMessengerRef messenger) { |
| return messenger->GetEngine() != nullptr; |
| } |
| |
| FlutterDesktopMessengerRef FlutterDesktopMessengerLock( |
| FlutterDesktopMessengerRef messenger) { |
| messenger->GetMutex().lock(); |
| return messenger; |
| } |
| |
| void FlutterDesktopMessengerUnlock(FlutterDesktopMessengerRef messenger) { |
| messenger->GetMutex().unlock(); |
| } |
| |
| // Retrieves state bag for the window in question from the GLFWWindow. |
| static FlutterDesktopWindowControllerState* GetWindowController( |
| GLFWwindow* window) { |
| return reinterpret_cast<FlutterDesktopWindowControllerState*>( |
| glfwGetWindowUserPointer(window)); |
| } |
| |
| // Creates and returns an invisible GLFW window that shares |window|'s resource |
| // context. |
| static UniqueGLFWwindowPtr CreateShareWindowForWindow(GLFWwindow* window) { |
| glfwWindowHint(GLFW_DECORATED, GLFW_FALSE); |
| glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); |
| #if defined(__linux__) |
| glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API); |
| #endif |
| GLFWwindow* share_window = glfwCreateWindow(1, 1, "", NULL, window); |
| glfwDefaultWindowHints(); |
| return UniqueGLFWwindowPtr(share_window, glfwDestroyWindow); |
| } |
| |
| // Converts a FlutterPlatformMessage to an equivalent FlutterDesktopMessage. |
| static FlutterDesktopMessage ConvertToDesktopMessage( |
| const FlutterPlatformMessage& engine_message) { |
| FlutterDesktopMessage message = {}; |
| message.struct_size = sizeof(message); |
| message.channel = engine_message.channel; |
| message.message = engine_message.message; |
| message.message_size = engine_message.message_size; |
| message.response_handle = engine_message.response_handle; |
| return message; |
| } |
| |
| // Returns the number of screen coordinates per inch for the main monitor. |
| // If the information is unavailable, returns a default value that assumes |
| // that a screen coordinate is one dp. |
| static double GetScreenCoordinatesPerInch() { |
| auto* primary_monitor = glfwGetPrimaryMonitor(); |
| if (primary_monitor == nullptr) { |
| return kDpPerInch; |
| } |
| auto* primary_monitor_mode = glfwGetVideoMode(primary_monitor); |
| int primary_monitor_width_mm; |
| glfwGetMonitorPhysicalSize(primary_monitor, &primary_monitor_width_mm, |
| nullptr); |
| if (primary_monitor_width_mm == 0) { |
| return kDpPerInch; |
| } |
| return primary_monitor_mode->width / (primary_monitor_width_mm / 25.4); |
| } |
| |
| // Sends a window metrics update to the Flutter engine using the given |
| // framebuffer size and the current window information in |state|. |
| static void SendWindowMetrics(FlutterDesktopWindowControllerState* controller, |
| int width, |
| int height) { |
| double dpi = controller->window_wrapper->pixels_per_screen_coordinate * |
| controller->monitor_screen_coordinates_per_inch; |
| |
| FlutterWindowMetricsEvent event = {}; |
| event.struct_size = sizeof(event); |
| event.width = width; |
| event.height = height; |
| if (controller->window_wrapper->pixel_ratio_override == 0.0) { |
| // The Flutter pixel_ratio is defined as DPI/dp. Limit the ratio to a |
| // minimum of 1 to avoid rendering a smaller UI on standard resolution |
| // monitors. |
| event.pixel_ratio = std::max(dpi / kDpPerInch, 1.0); |
| } else { |
| event.pixel_ratio = controller->window_wrapper->pixel_ratio_override; |
| } |
| FlutterEngineSendWindowMetricsEvent(controller->engine->flutter_engine, |
| &event); |
| } |
| |
| // Populates |task_runner| with a description that uses |engine_state|'s event |
| // loop to run tasks. |
| static void ConfigurePlatformTaskRunner( |
| FlutterTaskRunnerDescription* task_runner, |
| FlutterDesktopEngineState* engine_state) { |
| task_runner->struct_size = sizeof(FlutterTaskRunnerDescription); |
| task_runner->user_data = engine_state; |
| task_runner->runs_task_on_current_thread_callback = [](void* state) -> bool { |
| return reinterpret_cast<FlutterDesktopEngineState*>(state) |
| ->event_loop->RunsTasksOnCurrentThread(); |
| }; |
| task_runner->post_task_callback = |
| [](FlutterTask task, uint64_t target_time_nanos, void* state) -> void { |
| reinterpret_cast<FlutterDesktopEngineState*>(state)->event_loop->PostTask( |
| task, target_time_nanos); |
| }; |
| } |
| |
| // When GLFW calls back to the window with a framebuffer size change, notify |
| // FlutterEngine about the new window metrics. |
| static void GLFWFramebufferSizeCallback(GLFWwindow* window, |
| int width_px, |
| int height_px) { |
| int width; |
| glfwGetWindowSize(window, &width, nullptr); |
| auto* controller = GetWindowController(window); |
| controller->window_wrapper->pixels_per_screen_coordinate = |
| width > 0 ? width_px / width : 1; |
| |
| SendWindowMetrics(controller, width_px, height_px); |
| controller->window_wrapper->skip_next_window_refresh = true; |
| } |
| |
| // Indicates that the window needs to be redrawn. |
| void GLFWWindowRefreshCallback(GLFWwindow* window) { |
| auto* controller = GetWindowController(window); |
| if (controller->window_wrapper->skip_next_window_refresh) { |
| controller->window_wrapper->skip_next_window_refresh = false; |
| return; |
| } |
| // There's no engine API to request a redraw explicitly, so instead send a |
| // window metrics event with the current size to trigger it. |
| int width_px, height_px; |
| glfwGetFramebufferSize(window, &width_px, &height_px); |
| if (width_px > 0 && height_px > 0) { |
| SendWindowMetrics(controller, width_px, height_px); |
| } |
| } |
| |
| // Sends a pointer event to the Flutter engine based on the given data. |
| // |
| // Any coordinate/distance values in |event_data| should be in screen |
| // coordinates; they will be adjusted to pixel values before being sent. |
| static void SendPointerEventWithData(GLFWwindow* window, |
| const FlutterPointerEvent& event_data) { |
| auto* controller = GetWindowController(window); |
| // If sending anything other than an add, and the pointer isn't already added, |
| // synthesize an add to satisfy Flutter's expectations about events. |
| if (!controller->pointer_currently_added && |
| event_data.phase != FlutterPointerPhase::kAdd) { |
| FlutterPointerEvent event = {}; |
| event.phase = FlutterPointerPhase::kAdd; |
| event.x = event_data.x; |
| event.y = event_data.y; |
| SendPointerEventWithData(window, event); |
| } |
| // Don't double-add (e.g., if events are delivered out of order, so an add has |
| // already been synthesized). |
| if (controller->pointer_currently_added && |
| event_data.phase == FlutterPointerPhase::kAdd) { |
| return; |
| } |
| |
| FlutterPointerEvent event = event_data; |
| // Set metadata that's always the same regardless of the event. |
| event.struct_size = sizeof(event); |
| event.timestamp = |
| std::chrono::duration_cast<std::chrono::microseconds>( |
| std::chrono::high_resolution_clock::now().time_since_epoch()) |
| .count(); |
| event.device_kind = FlutterPointerDeviceKind::kFlutterPointerDeviceKindMouse; |
| event.buttons = |
| (event.phase == FlutterPointerPhase::kAdd) ? 0 : controller->buttons; |
| |
| // Convert all screen coordinates to pixel coordinates. |
| double pixels_per_coordinate = |
| controller->window_wrapper->pixels_per_screen_coordinate; |
| event.x *= pixels_per_coordinate; |
| event.y *= pixels_per_coordinate; |
| event.scroll_delta_x *= pixels_per_coordinate; |
| event.scroll_delta_y *= pixels_per_coordinate; |
| |
| FlutterEngineSendPointerEvent(controller->engine->flutter_engine, &event, 1); |
| |
| if (event_data.phase == FlutterPointerPhase::kAdd) { |
| controller->pointer_currently_added = true; |
| } else if (event_data.phase == FlutterPointerPhase::kRemove) { |
| controller->pointer_currently_added = false; |
| } else if (event_data.phase == FlutterPointerPhase::kDown) { |
| controller->pointer_currently_down = true; |
| } else if (event_data.phase == FlutterPointerPhase::kUp) { |
| controller->pointer_currently_down = false; |
| } |
| } |
| |
| // Updates |event_data| with the current location of the mouse cursor. |
| static void SetEventLocationFromCursorPosition( |
| GLFWwindow* window, |
| FlutterPointerEvent* event_data) { |
| glfwGetCursorPos(window, &event_data->x, &event_data->y); |
| } |
| |
| // Set's |event_data|'s phase depending on the current mouse state. |
| // If a kUp or kDown event is triggered while the current state is already |
| // up/down, a hover/move will be called instead to avoid a crash in the Flutter |
| // engine. |
| static void SetEventPhaseFromCursorButtonState(GLFWwindow* window, |
| FlutterPointerEvent* event_data, |
| int64_t buttons) { |
| auto* controller = GetWindowController(window); |
| event_data->phase = |
| (buttons == 0) |
| ? (controller->pointer_currently_down ? FlutterPointerPhase::kUp |
| : FlutterPointerPhase::kHover) |
| : (controller->pointer_currently_down ? FlutterPointerPhase::kMove |
| : FlutterPointerPhase::kDown); |
| } |
| |
| // Reports the mouse entering or leaving the Flutter view. |
| static void GLFWCursorEnterCallback(GLFWwindow* window, int entered) { |
| FlutterPointerEvent event = {}; |
| event.phase = |
| entered ? FlutterPointerPhase::kAdd : FlutterPointerPhase::kRemove; |
| SetEventLocationFromCursorPosition(window, &event); |
| SendPointerEventWithData(window, event); |
| } |
| |
| // Reports mouse movement to the Flutter engine. |
| static void GLFWCursorPositionCallback(GLFWwindow* window, double x, double y) { |
| FlutterPointerEvent event = {}; |
| event.x = x; |
| event.y = y; |
| auto* controller = GetWindowController(window); |
| SetEventPhaseFromCursorButtonState(window, &event, controller->buttons); |
| SendPointerEventWithData(window, event); |
| } |
| |
| // Reports mouse button press to the Flutter engine. |
| static void GLFWMouseButtonCallback(GLFWwindow* window, |
| int key, |
| int action, |
| int mods) { |
| int64_t button; |
| if (key == GLFW_MOUSE_BUTTON_LEFT) { |
| button = FlutterPointerMouseButtons::kFlutterPointerButtonMousePrimary; |
| } else if (key == GLFW_MOUSE_BUTTON_RIGHT) { |
| button = FlutterPointerMouseButtons::kFlutterPointerButtonMouseSecondary; |
| } else { |
| return; |
| } |
| |
| auto* controller = GetWindowController(window); |
| controller->buttons = (action == GLFW_PRESS) ? controller->buttons | button |
| : controller->buttons & ~button; |
| |
| FlutterPointerEvent event = {}; |
| SetEventPhaseFromCursorButtonState(window, &event, controller->buttons); |
| SetEventLocationFromCursorPosition(window, &event); |
| SendPointerEventWithData(window, event); |
| |
| // If mouse tracking isn't already enabled, turn it on for the duration of |
| // the drag to generate kMove events. |
| bool hover_enabled = |
| GetWindowController(window)->window_wrapper->hover_tracking_enabled; |
| if (!hover_enabled) { |
| glfwSetCursorPosCallback(window, (controller->buttons != 0) |
| ? GLFWCursorPositionCallback |
| : nullptr); |
| } |
| // Disable enter/exit events while the mouse button is down; GLFW will send |
| // an exit event when the mouse button is released, and the pointer should |
| // stay valid until then. |
| if (hover_enabled) { |
| glfwSetCursorEnterCallback( |
| window, (controller->buttons != 0) ? nullptr : GLFWCursorEnterCallback); |
| } |
| } |
| |
| // Reports scroll wheel events to the Flutter engine. |
| static void GLFWScrollCallback(GLFWwindow* window, |
| double delta_x, |
| double delta_y) { |
| FlutterPointerEvent event = {}; |
| SetEventLocationFromCursorPosition(window, &event); |
| auto* controller = GetWindowController(window); |
| SetEventPhaseFromCursorButtonState(window, &event, controller->buttons); |
| event.signal_kind = FlutterPointerSignalKind::kFlutterPointerSignalKindScroll; |
| // TODO(chrome-bot): See if this can be queried from the OS; this value is |
| // chosen arbitrarily to get something that feels reasonable. |
| const int kScrollOffsetMultiplier = 20; |
| event.scroll_delta_x = delta_x * kScrollOffsetMultiplier; |
| event.scroll_delta_y = -delta_y * kScrollOffsetMultiplier; |
| SendPointerEventWithData(window, event); |
| } |
| |
| // Passes character input events to registered handlers. |
| static void GLFWCharCallback(GLFWwindow* window, unsigned int code_point) { |
| for (const auto& handler : |
| GetWindowController(window)->keyboard_hook_handlers) { |
| handler->CharHook(window, code_point); |
| } |
| } |
| |
| // Passes raw key events to registered handlers. |
| static void GLFWKeyCallback(GLFWwindow* window, |
| int key, |
| int scancode, |
| int action, |
| int mods) { |
| for (const auto& handler : |
| GetWindowController(window)->keyboard_hook_handlers) { |
| handler->KeyboardHook(window, key, scancode, action, mods); |
| } |
| } |
| |
| // Enables/disables the callbacks related to mouse tracking. |
| static void SetHoverCallbacksEnabled(GLFWwindow* window, bool enabled) { |
| glfwSetCursorEnterCallback(window, |
| enabled ? GLFWCursorEnterCallback : nullptr); |
| glfwSetCursorPosCallback(window, |
| enabled ? GLFWCursorPositionCallback : nullptr); |
| } |
| |
| // Flushes event queue and then assigns default window callbacks. |
| static void GLFWAssignEventCallbacks(GLFWwindow* window) { |
| glfwPollEvents(); |
| glfwSetKeyCallback(window, GLFWKeyCallback); |
| glfwSetCharCallback(window, GLFWCharCallback); |
| glfwSetMouseButtonCallback(window, GLFWMouseButtonCallback); |
| glfwSetScrollCallback(window, GLFWScrollCallback); |
| if (GetWindowController(window)->window_wrapper->hover_tracking_enabled) { |
| SetHoverCallbacksEnabled(window, true); |
| } |
| } |
| |
| // Clears default window events. |
| static void GLFWClearEventCallbacks(GLFWwindow* window) { |
| glfwSetKeyCallback(window, nullptr); |
| glfwSetCharCallback(window, nullptr); |
| glfwSetMouseButtonCallback(window, nullptr); |
| glfwSetScrollCallback(window, nullptr); |
| SetHoverCallbacksEnabled(window, false); |
| } |
| |
| // The Flutter Engine calls out to this function when new platform messages are |
| // available |
| static void EngineOnFlutterPlatformMessage( |
| const FlutterPlatformMessage* engine_message, |
| void* user_data) { |
| if (engine_message->struct_size != sizeof(FlutterPlatformMessage)) { |
| std::cerr << "Invalid message size received. Expected: " |
| << sizeof(FlutterPlatformMessage) << " but received " |
| << engine_message->struct_size << std::endl; |
| return; |
| } |
| |
| FlutterDesktopEngineState* engine_state = |
| static_cast<FlutterDesktopEngineState*>(user_data); |
| GLFWwindow* window = engine_state->window_controller == nullptr |
| ? nullptr |
| : engine_state->window_controller->window.get(); |
| |
| auto message = ConvertToDesktopMessage(*engine_message); |
| engine_state->message_dispatcher->HandleMessage( |
| message, |
| [window] { |
| if (window) { |
| GLFWClearEventCallbacks(window); |
| } |
| }, |
| [window] { |
| if (window) { |
| GLFWAssignEventCallbacks(window); |
| } |
| }); |
| } |
| |
| static bool EngineMakeContextCurrent(void* user_data) { |
| FlutterDesktopEngineState* engine_state = |
| static_cast<FlutterDesktopEngineState*>(user_data); |
| FlutterDesktopWindowControllerState* window_controller = |
| engine_state->window_controller; |
| if (!window_controller) { |
| return false; |
| } |
| glfwMakeContextCurrent(window_controller->window.get()); |
| return true; |
| } |
| |
| static bool EngineMakeResourceContextCurrent(void* user_data) { |
| FlutterDesktopEngineState* engine_state = |
| static_cast<FlutterDesktopEngineState*>(user_data); |
| FlutterDesktopWindowControllerState* window_controller = |
| engine_state->window_controller; |
| if (!window_controller) { |
| return false; |
| } |
| glfwMakeContextCurrent(window_controller->resource_window.get()); |
| return true; |
| } |
| |
| static bool EngineClearContext(void* user_data) { |
| FlutterDesktopEngineState* engine_state = |
| static_cast<FlutterDesktopEngineState*>(user_data); |
| FlutterDesktopWindowControllerState* window_controller = |
| engine_state->window_controller; |
| if (!window_controller) { |
| return false; |
| } |
| glfwMakeContextCurrent(nullptr); |
| return true; |
| } |
| |
| static bool EnginePresent(void* user_data) { |
| FlutterDesktopEngineState* engine_state = |
| static_cast<FlutterDesktopEngineState*>(user_data); |
| FlutterDesktopWindowControllerState* window_controller = |
| engine_state->window_controller; |
| if (!window_controller) { |
| return false; |
| } |
| glfwSwapBuffers(window_controller->window.get()); |
| return true; |
| } |
| |
| static uint32_t EngineGetActiveFbo(void* user_data) { |
| return 0; |
| } |
| |
| // Resolves the address of the specified OpenGL or OpenGL ES |
| // core or extension function, if it is supported by the current context. |
| static void* EngineProcResolver(void* user_data, const char* name) { |
| return reinterpret_cast<void*>(glfwGetProcAddress(name)); |
| } |
| |
| // Clears the GLFW window to Material Blue-Grey. |
| // |
| // This function is primarily to fix an issue when the Flutter Engine is |
| // spinning up, wherein artifacts of existing windows are rendered onto the |
| // canvas for a few moments. |
| // |
| // This function isn't necessary, but makes starting the window much easier on |
| // the eyes. |
| static void GLFWClearCanvas(GLFWwindow* window) { |
| glfwMakeContextCurrent(window); |
| // This color is Material Blue Grey. |
| glClearColor(236.0f / 255.0f, 239.0f / 255.0f, 241.0f / 255.0f, 0.0f); |
| glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); |
| glFlush(); |
| glfwSwapBuffers(window); |
| glfwMakeContextCurrent(nullptr); |
| } |
| |
| static void GLFWErrorCallback(int error_code, const char* description) { |
| std::cerr << "GLFW error " << error_code << ": " << description << std::endl; |
| } |
| |
| // Attempts to load AOT data from the given path, which must be absolute and |
| // non-empty. Logs and returns nullptr on failure. |
| UniqueAotDataPtr LoadAotData(const std::filesystem::path& aot_data_path) { |
| if (aot_data_path.empty()) { |
| std::cerr |
| << "Attempted to load AOT data, but no aot_data_path was provided." |
| << std::endl; |
| return nullptr; |
| } |
| std::string path_string = aot_data_path.string(); |
| if (!std::filesystem::exists(aot_data_path)) { |
| std::cerr << "Can't load AOT data from " << path_string << "; no such file." |
| << std::endl; |
| return nullptr; |
| } |
| FlutterEngineAOTDataSource source = {}; |
| source.type = kFlutterEngineAOTDataSourceTypeElfPath; |
| source.elf_path = path_string.c_str(); |
| FlutterEngineAOTData data = nullptr; |
| auto result = FlutterEngineCreateAOTData(&source, &data); |
| if (result != kSuccess) { |
| std::cerr << "Failed to load AOT data from: " << path_string << std::endl; |
| return nullptr; |
| } |
| return UniqueAotDataPtr(data); |
| } |
| |
| // Starts an instance of the Flutter Engine. |
| // |
| // Configures the engine according to |engine_propreties| and using |event_loop| |
| // to schedule engine tasks. |
| // |
| // Returns true on success, in which case |engine_state|'s 'engine' field will |
| // be updated to point to the started engine. |
| static bool RunFlutterEngine( |
| FlutterDesktopEngineState* engine_state, |
| const FlutterDesktopEngineProperties& engine_properties, |
| std::unique_ptr<flutter::EventLoop> event_loop) { |
| // FlutterProjectArgs is expecting a full argv, so when processing it for |
| // flags the first item is treated as the executable and ignored. Add a dummy |
| // value so that all provided arguments are used. |
| std::vector<const char*> argv = {"placeholder"}; |
| if (engine_properties.switches_count > 0) { |
| argv.insert(argv.end(), &engine_properties.switches[0], |
| &engine_properties.switches[engine_properties.switches_count]); |
| } |
| |
| std::filesystem::path assets_path = |
| std::filesystem::u8path(engine_properties.assets_path); |
| std::filesystem::path icu_path = |
| std::filesystem::u8path(engine_properties.icu_data_path); |
| std::filesystem::path aot_library_path = |
| std::filesystem::u8path(engine_properties.aot_library_path); |
| if (assets_path.is_relative() || icu_path.is_relative() || |
| (!aot_library_path.empty() && aot_library_path.is_relative())) { |
| // Treat relative paths as relative to the directory of this executable. |
| std::filesystem::path executable_location = |
| flutter::GetExecutableDirectory(); |
| if (executable_location.empty()) { |
| std::cerr << "Unable to find executable location to resolve paths." |
| << std::endl; |
| return false; |
| } |
| assets_path = std::filesystem::path(executable_location) / assets_path; |
| icu_path = std::filesystem::path(executable_location) / icu_path; |
| if (!aot_library_path.empty()) { |
| aot_library_path = |
| std::filesystem::path(executable_location) / aot_library_path; |
| } |
| } |
| // Configure a task runner using the event loop. |
| engine_state->event_loop = std::move(event_loop); |
| FlutterTaskRunnerDescription platform_task_runner = {}; |
| ConfigurePlatformTaskRunner(&platform_task_runner, engine_state); |
| FlutterCustomTaskRunners task_runners = {}; |
| task_runners.struct_size = sizeof(FlutterCustomTaskRunners); |
| task_runners.platform_task_runner = &platform_task_runner; |
| |
| FlutterRendererConfig config = {}; |
| config.type = kOpenGL; |
| config.open_gl.struct_size = sizeof(config.open_gl); |
| config.open_gl.make_current = EngineMakeContextCurrent; |
| config.open_gl.clear_current = EngineClearContext; |
| config.open_gl.present = EnginePresent; |
| config.open_gl.fbo_callback = EngineGetActiveFbo; |
| config.open_gl.make_resource_current = EngineMakeResourceContextCurrent; |
| // Don't provide a resolver in headless mode, since headless mode should |
| // work even if GLFW initialization failed. |
| if (engine_state->window_controller != nullptr) { |
| config.open_gl.gl_proc_resolver = EngineProcResolver; |
| } |
| FlutterProjectArgs args = {}; |
| args.struct_size = sizeof(FlutterProjectArgs); |
| args.assets_path = assets_path.string().c_str(); |
| args.icu_data_path = icu_path.string().c_str(); |
| args.command_line_argc = static_cast<int>(argv.size()); |
| args.command_line_argv = &argv[0]; |
| args.platform_message_callback = EngineOnFlutterPlatformMessage; |
| args.custom_task_runners = &task_runners; |
| |
| if (FlutterEngineRunsAOTCompiledDartCode()) { |
| engine_state->aot_data = LoadAotData(aot_library_path); |
| if (!engine_state->aot_data) { |
| std::cerr << "Unable to start engine without AOT data." << std::endl; |
| return false; |
| } |
| args.aot_data = engine_state->aot_data.get(); |
| } |
| |
| FLUTTER_API_SYMBOL(FlutterEngine) engine = nullptr; |
| auto result = FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, &args, |
| engine_state, &engine); |
| if (result != kSuccess || engine == nullptr) { |
| std::cerr << "Failed to start Flutter engine: error " << result |
| << std::endl; |
| return false; |
| } |
| engine_state->flutter_engine = engine; |
| return true; |
| } |
| |
| // Passes locale information to the Flutter engine. |
| static void SetUpLocales(FlutterDesktopEngineState* state) { |
| std::vector<flutter::LanguageInfo> languages = |
| flutter::GetPreferredLanguageInfo(); |
| std::vector<FlutterLocale> flutter_locales = |
| flutter::ConvertToFlutterLocale(languages); |
| // Convert the locale list to the locale pointer list that must be provided. |
| std::vector<const FlutterLocale*> flutter_locale_list; |
| flutter_locale_list.reserve(flutter_locales.size()); |
| std::transform(flutter_locales.begin(), flutter_locales.end(), |
| std::back_inserter(flutter_locale_list), |
| [](const auto& arg) -> const auto* { return &arg; }); |
| FlutterEngineResult result = FlutterEngineUpdateLocales( |
| state->flutter_engine, flutter_locale_list.data(), |
| flutter_locale_list.size()); |
| if (result != kSuccess) { |
| std::cerr << "Failed to set up Flutter locales." << std::endl; |
| } |
| } |
| |
| // Populates |state|'s helper object fields that are common to normal and |
| // headless mode. |
| // |
| // Window is optional; if present it will be provided to the created |
| // PlatformHandler. |
| static void SetUpCommonEngineState(FlutterDesktopEngineState* state, |
| GLFWwindow* window) { |
| // Messaging. |
| state->messenger = FlutterDesktopMessengerReferenceOwner( |
| FlutterDesktopMessengerAddRef(new FlutterDesktopMessenger()), |
| &FlutterDesktopMessengerRelease); |
| state->messenger->SetEngine(state); |
| state->message_dispatcher = |
| std::make_unique<flutter::IncomingMessageDispatcher>( |
| state->messenger.get()); |
| |
| // Plugins. |
| state->plugin_registrar = std::make_unique<FlutterDesktopPluginRegistrar>(); |
| state->plugin_registrar->engine = state; |
| state->internal_plugin_registrar = |
| std::make_unique<flutter::PluginRegistrar>(state->plugin_registrar.get()); |
| |
| // System channel handler. |
| state->platform_handler = std::make_unique<flutter::PlatformHandler>( |
| state->internal_plugin_registrar->messenger(), window); |
| |
| SetUpLocales(state); |
| } |
| |
| bool FlutterDesktopInit() { |
| // Before making any GLFW calls, set up a logging error handler. |
| glfwSetErrorCallback(GLFWErrorCallback); |
| return glfwInit(); |
| } |
| |
| void FlutterDesktopTerminate() { |
| glfwTerminate(); |
| } |
| |
| FlutterDesktopWindowControllerRef FlutterDesktopCreateWindow( |
| const FlutterDesktopWindowProperties& window_properties, |
| const FlutterDesktopEngineProperties& engine_properties) { |
| auto state = std::make_unique<FlutterDesktopWindowControllerState>(); |
| |
| // Create the window, and set the state as its user data. |
| if (window_properties.prevent_resize) { |
| glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); |
| } |
| #if defined(__linux__) |
| glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API); |
| #endif |
| state->window = UniqueGLFWwindowPtr( |
| glfwCreateWindow(window_properties.width, window_properties.height, |
| window_properties.title, NULL, NULL), |
| glfwDestroyWindow); |
| glfwDefaultWindowHints(); |
| GLFWwindow* window = state->window.get(); |
| if (window == nullptr) { |
| return nullptr; |
| } |
| GLFWClearCanvas(window); |
| glfwSetWindowUserPointer(window, state.get()); |
| |
| // Create the share window before starting the engine, since it may call |
| // EngineMakeResourceContextCurrent immediately. |
| state->resource_window = CreateShareWindowForWindow(window); |
| |
| state->engine = std::make_unique<FlutterDesktopEngineState>(); |
| state->engine->window_controller = state.get(); |
| |
| // Create an event loop for the window. It is not running yet. |
| auto event_loop = std::make_unique<flutter::GLFWEventLoop>( |
| std::this_thread::get_id(), // main GLFW thread |
| [engine_state = state->engine.get()](const auto* task) { |
| if (FlutterEngineRunTask(engine_state->flutter_engine, task) != |
| kSuccess) { |
| std::cerr << "Could not post an engine task." << std::endl; |
| } |
| }); |
| |
| // Start the engine. |
| if (!RunFlutterEngine(state->engine.get(), engine_properties, |
| std::move(event_loop))) { |
| return nullptr; |
| } |
| SetUpCommonEngineState(state->engine.get(), window); |
| |
| state->window_wrapper = std::make_unique<FlutterDesktopWindow>(); |
| state->window_wrapper->window = window; |
| |
| // Set up the keyboard handlers |
| auto internal_plugin_messenger = |
| state->engine->internal_plugin_registrar->messenger(); |
| state->keyboard_hook_handlers.push_back( |
| std::make_unique<flutter::KeyEventHandler>(internal_plugin_messenger)); |
| state->keyboard_hook_handlers.push_back( |
| std::make_unique<flutter::TextInputPlugin>(internal_plugin_messenger)); |
| |
| // Trigger an initial size callback to send size information to Flutter. |
| state->monitor_screen_coordinates_per_inch = GetScreenCoordinatesPerInch(); |
| int width_px, height_px; |
| glfwGetFramebufferSize(window, &width_px, &height_px); |
| GLFWFramebufferSizeCallback(window, width_px, height_px); |
| |
| // Set up GLFW callbacks for the window. |
| glfwSetFramebufferSizeCallback(window, GLFWFramebufferSizeCallback); |
| glfwSetWindowRefreshCallback(window, GLFWWindowRefreshCallback); |
| GLFWAssignEventCallbacks(window); |
| |
| return state.release(); |
| } |
| |
| void FlutterDesktopDestroyWindow(FlutterDesktopWindowControllerRef controller) { |
| controller->engine->messenger->SetEngine(nullptr); |
| FlutterDesktopPluginRegistrarRef registrar = |
| controller->engine->plugin_registrar.get(); |
| if (registrar->destruction_handler) { |
| registrar->destruction_handler(registrar); |
| } |
| FlutterEngineShutdown(controller->engine->flutter_engine); |
| delete controller; |
| } |
| |
| void FlutterDesktopWindowSetHoverEnabled(FlutterDesktopWindowRef flutter_window, |
| bool enabled) { |
| flutter_window->hover_tracking_enabled = enabled; |
| SetHoverCallbacksEnabled(flutter_window->window, enabled); |
| } |
| |
| void FlutterDesktopWindowSetTitle(FlutterDesktopWindowRef flutter_window, |
| const char* title) { |
| GLFWwindow* window = flutter_window->window; |
| glfwSetWindowTitle(window, title); |
| } |
| |
| void FlutterDesktopWindowSetIcon(FlutterDesktopWindowRef flutter_window, |
| uint8_t* pixel_data, |
| int width, |
| int height) { |
| GLFWimage image = {width, height, static_cast<unsigned char*>(pixel_data)}; |
| glfwSetWindowIcon(flutter_window->window, pixel_data ? 1 : 0, &image); |
| } |
| |
| void FlutterDesktopWindowGetFrame(FlutterDesktopWindowRef flutter_window, |
| int* x, |
| int* y, |
| int* width, |
| int* height) { |
| glfwGetWindowPos(flutter_window->window, x, y); |
| glfwGetWindowSize(flutter_window->window, width, height); |
| // The above gives content area size and position; adjust for the window |
| // decoration to give actual window frame. |
| int frame_left, frame_top, frame_right, frame_bottom; |
| glfwGetWindowFrameSize(flutter_window->window, &frame_left, &frame_top, |
| &frame_right, &frame_bottom); |
| if (x) { |
| *x -= frame_left; |
| } |
| if (y) { |
| *y -= frame_top; |
| } |
| if (width) { |
| *width += frame_left + frame_right; |
| } |
| if (height) { |
| *height += frame_top + frame_bottom; |
| } |
| } |
| |
| void FlutterDesktopWindowSetFrame(FlutterDesktopWindowRef flutter_window, |
| int x, |
| int y, |
| int width, |
| int height) { |
| // Get the window decoration sizes to adjust, since the GLFW setters take |
| // content position and size. |
| int frame_left, frame_top, frame_right, frame_bottom; |
| glfwGetWindowFrameSize(flutter_window->window, &frame_left, &frame_top, |
| &frame_right, &frame_bottom); |
| glfwSetWindowPos(flutter_window->window, x + frame_left, y + frame_top); |
| glfwSetWindowSize(flutter_window->window, width - frame_left - frame_right, |
| height - frame_top - frame_bottom); |
| } |
| |
| double FlutterDesktopWindowGetScaleFactor( |
| FlutterDesktopWindowRef flutter_window) { |
| return flutter_window->pixels_per_screen_coordinate; |
| } |
| |
| void FlutterDesktopWindowSetPixelRatioOverride( |
| FlutterDesktopWindowRef flutter_window, |
| double pixel_ratio) { |
| flutter_window->pixel_ratio_override = pixel_ratio; |
| // Send a metrics update using the new pixel ratio. |
| int width_px, height_px; |
| glfwGetFramebufferSize(flutter_window->window, &width_px, &height_px); |
| if (width_px > 0 && height_px > 0) { |
| auto* controller = GetWindowController(flutter_window->window); |
| SendWindowMetrics(controller, width_px, height_px); |
| } |
| } |
| |
| void FlutterDesktopWindowSetSizeLimits(FlutterDesktopWindowRef flutter_window, |
| FlutterDesktopSize minimum_size, |
| FlutterDesktopSize maximum_size) { |
| glfwSetWindowSizeLimits(flutter_window->window, minimum_size.width, |
| minimum_size.height, maximum_size.width, |
| maximum_size.height); |
| } |
| |
| bool FlutterDesktopRunWindowEventLoopWithTimeout( |
| FlutterDesktopWindowControllerRef controller, |
| uint32_t timeout_milliseconds) { |
| FlutterDesktopRunEngineEventLoopWithTimeout(controller->engine.get(), |
| timeout_milliseconds); |
| return !glfwWindowShouldClose(controller->window.get()); |
| } |
| |
| FlutterDesktopWindowRef FlutterDesktopGetWindow( |
| FlutterDesktopWindowControllerRef controller) { |
| // Currently, one registrar acts as the registrar for all plugins, so the |
| // name is ignored. It is part of the API to reduce churn in the future when |
| // aligning more closely with the Flutter registrar system. |
| return controller->window_wrapper.get(); |
| } |
| |
| FlutterDesktopEngineRef FlutterDesktopGetEngine( |
| FlutterDesktopWindowControllerRef controller) { |
| return controller->engine.get(); |
| } |
| |
| FlutterDesktopPluginRegistrarRef FlutterDesktopGetPluginRegistrar( |
| FlutterDesktopEngineRef engine, |
| const char* plugin_name) { |
| // Currently, one registrar acts as the registrar for all plugins, so the |
| // name is ignored. It is part of the API to reduce churn in the future when |
| // aligning more closely with the Flutter registrar system. |
| return engine->plugin_registrar.get(); |
| } |
| |
| FlutterDesktopEngineRef FlutterDesktopRunEngine( |
| const FlutterDesktopEngineProperties& properties) { |
| auto engine_state = std::make_unique<FlutterDesktopEngineState>(); |
| |
| auto event_loop = std::make_unique<flutter::HeadlessEventLoop>( |
| std::this_thread::get_id(), |
| [state = engine_state.get()](const auto* task) { |
| if (FlutterEngineRunTask(state->flutter_engine, task) != kSuccess) { |
| std::cerr << "Could not post an engine task." << std::endl; |
| } |
| }); |
| |
| if (!RunFlutterEngine(engine_state.get(), properties, |
| std::move(event_loop))) { |
| return nullptr; |
| } |
| SetUpCommonEngineState(engine_state.get(), nullptr); |
| |
| return engine_state.release(); |
| } |
| |
| void FlutterDesktopRunEngineEventLoopWithTimeout( |
| FlutterDesktopEngineRef engine, |
| uint32_t timeout_milliseconds) { |
| std::chrono::nanoseconds wait_duration = |
| timeout_milliseconds == 0 |
| ? std::chrono::nanoseconds::max() |
| : std::chrono::milliseconds(timeout_milliseconds); |
| engine->event_loop->WaitForEvents(wait_duration); |
| } |
| |
| bool FlutterDesktopShutDownEngine(FlutterDesktopEngineRef engine) { |
| auto result = FlutterEngineShutdown(engine->flutter_engine); |
| delete engine; |
| return (result == kSuccess); |
| } |
| |
| void FlutterDesktopPluginRegistrarEnableInputBlocking( |
| FlutterDesktopPluginRegistrarRef registrar, |
| const char* channel) { |
| registrar->engine->message_dispatcher->EnableInputBlockingForChannel(channel); |
| } |
| |
| FlutterDesktopMessengerRef FlutterDesktopPluginRegistrarGetMessenger( |
| FlutterDesktopPluginRegistrarRef registrar) { |
| return registrar->engine->messenger.get(); |
| } |
| |
| void FlutterDesktopPluginRegistrarSetDestructionHandler( |
| FlutterDesktopPluginRegistrarRef registrar, |
| FlutterDesktopOnPluginRegistrarDestroyed callback) { |
| registrar->destruction_handler = callback; |
| } |
| |
| FlutterDesktopWindowRef FlutterDesktopPluginRegistrarGetWindow( |
| FlutterDesktopPluginRegistrarRef registrar) { |
| FlutterDesktopWindowControllerState* controller = |
| registrar->engine->window_controller; |
| if (!controller) { |
| return nullptr; |
| } |
| return controller->window_wrapper.get(); |
| } |
| |
| bool FlutterDesktopMessengerSendWithReply(FlutterDesktopMessengerRef messenger, |
| const char* channel, |
| const uint8_t* message, |
| const size_t message_size, |
| const FlutterDesktopBinaryReply reply, |
| void* user_data) { |
| FlutterPlatformMessageResponseHandle* response_handle = nullptr; |
| if (reply != nullptr && user_data != nullptr) { |
| FlutterEngineResult result = FlutterPlatformMessageCreateResponseHandle( |
| messenger->GetEngine()->flutter_engine, reply, user_data, |
| &response_handle); |
| if (result != kSuccess) { |
| std::cout << "Failed to create response handle\n"; |
| return false; |
| } |
| } |
| |
| FlutterPlatformMessage platform_message = { |
| sizeof(FlutterPlatformMessage), |
| channel, |
| message, |
| message_size, |
| response_handle, |
| }; |
| |
| FlutterEngineResult message_result = FlutterEngineSendPlatformMessage( |
| messenger->GetEngine()->flutter_engine, &platform_message); |
| |
| if (response_handle != nullptr) { |
| FlutterPlatformMessageReleaseResponseHandle( |
| messenger->GetEngine()->flutter_engine, response_handle); |
| } |
| |
| return message_result == kSuccess; |
| } |
| |
| bool FlutterDesktopMessengerSend(FlutterDesktopMessengerRef messenger, |
| const char* channel, |
| const uint8_t* message, |
| const size_t message_size) { |
| return FlutterDesktopMessengerSendWithReply(messenger, channel, message, |
| message_size, nullptr, nullptr); |
| } |
| |
| void FlutterDesktopMessengerSendResponse( |
| FlutterDesktopMessengerRef messenger, |
| const FlutterDesktopMessageResponseHandle* handle, |
| const uint8_t* data, |
| size_t data_length) { |
| FlutterEngineSendPlatformMessageResponse( |
| messenger->GetEngine()->flutter_engine, handle, data, data_length); |
| } |
| |
| void FlutterDesktopMessengerSetCallback(FlutterDesktopMessengerRef messenger, |
| const char* channel, |
| FlutterDesktopMessageCallback callback, |
| void* user_data) { |
| messenger->GetEngine()->message_dispatcher->SetMessageCallback( |
| channel, callback, user_data); |
| } |
| |
| FlutterDesktopTextureRegistrarRef FlutterDesktopRegistrarGetTextureRegistrar( |
| FlutterDesktopPluginRegistrarRef registrar) { |
| std::cerr << "GLFW Texture support is not implemented yet." << std::endl; |
| return nullptr; |
| } |
| |
| int64_t FlutterDesktopTextureRegistrarRegisterExternalTexture( |
| FlutterDesktopTextureRegistrarRef texture_registrar, |
| const FlutterDesktopTextureInfo* texture_info) { |
| std::cerr << "GLFW Texture support is not implemented yet." << std::endl; |
| return -1; |
| } |
| |
| void FlutterDesktopTextureRegistrarUnregisterExternalTexture( |
| FlutterDesktopTextureRegistrarRef texture_registrar, |
| int64_t texture_id, |
| void (*callback)(void* user_data), |
| void* user_data) { |
| std::cerr << "GLFW Texture support is not implemented yet." << std::endl; |
| } |
| |
| bool FlutterDesktopTextureRegistrarMarkExternalTextureFrameAvailable( |
| FlutterDesktopTextureRegistrarRef texture_registrar, |
| int64_t texture_id) { |
| std::cerr << "GLFW Texture support is not implemented yet." << std::endl; |
| return false; |
| } |