From 64daebe53d819d5dc4578a4555534ee23b0f7d3e Mon Sep 17 00:00:00 2001 From: JunsuChoi Date: Thu, 6 Aug 2026 21:01:45 +0900 Subject: [PATCH 1/2] Replace ecore_pipe to glib api in TizenEventLoop Replace Ecore pipes and per-task timers with a single GLib source that tracks the earliest Flutter task on the Tizen main context. Preserve delayed execution, posting order, worker-thread scheduling, re-entrant callbacks, and source cleanup. Remove the remaining Ecore dependency from display refresh reporting by using the existing 60 Hz constant. --- flutter/shell/platform/tizen/BUILD.gn | 2 + .../platform/tizen/channels/window_channel.cc | 1 - .../tizen/flutter_tizen_display_monitor.cc | 8 +- .../tizen/flutter_tizen_engine_unittest.cc | 5 - ...utter_tizen_texture_registrar_unittests.cc | 5 - .../tizen/flutter_tizen_view_unittests.cc | 4 - .../shell/platform/tizen/tizen_event_loop.cc | 136 +++++++++--------- .../shell/platform/tizen/tizen_event_loop.h | 59 +++----- .../tizen/tizen_event_loop_unittests.cc | 126 ++++++++++++++++ 9 files changed, 220 insertions(+), 126 deletions(-) create mode 100644 flutter/shell/platform/tizen/tizen_event_loop_unittests.cc diff --git a/flutter/shell/platform/tizen/BUILD.gn b/flutter/shell/platform/tizen/BUILD.gn index 3c21911a..899babcc 100644 --- a/flutter/shell/platform/tizen/BUILD.gn +++ b/flutter/shell/platform/tizen/BUILD.gn @@ -115,6 +115,7 @@ template("embedder") { "ecore_input", "ecore_wl2", "eina", + "glib-2.0", "gio-2.0", "feedback", "flutter_engine", @@ -236,6 +237,7 @@ executable("flutter_tizen_unittests") { "flutter_tizen_engine_unittest.cc", "flutter_tizen_texture_registrar_unittests.cc", "flutter_tizen_view_unittests.cc", + "tizen_event_loop_unittests.cc", ] ldflags = [ "-Wl,--unresolved-symbols=ignore-in-shared-libs" ] diff --git a/flutter/shell/platform/tizen/channels/window_channel.cc b/flutter/shell/platform/tizen/channels/window_channel.cc index 5e618e55..242f4bfe 100644 --- a/flutter/shell/platform/tizen/channels/window_channel.cc +++ b/flutter/shell/platform/tizen/channels/window_channel.cc @@ -8,7 +8,6 @@ #include "flutter/shell/platform/tizen/channels/encodable_value_holder.h" #include "flutter/shell/platform/tizen/logger.h" #include "flutter/shell/platform/tizen/tizen_window.h" -#include "flutter/shell/platform/tizen/tizen_window_ecore_wl2.h" namespace flutter { diff --git a/flutter/shell/platform/tizen/flutter_tizen_display_monitor.cc b/flutter/shell/platform/tizen/flutter_tizen_display_monitor.cc index 0a5bdf61..740260a1 100644 --- a/flutter/shell/platform/tizen/flutter_tizen_display_monitor.cc +++ b/flutter/shell/platform/tizen/flutter_tizen_display_monitor.cc @@ -3,7 +3,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include #include #include "flutter/shell/platform/tizen/flutter_tizen_display_monitor.h" @@ -29,12 +28,7 @@ void FlutterTizenDisplayMonitor::UpdateDisplays() { display.display_id = 0; display.single_display = true; - double fps = ecore_animator_frametime_get(); - if (fps <= 0.0) { - display.refresh_rate = 0.0; - } else { - display.refresh_rate = 1 / fps; - } + display.refresh_rate = 60.0; int32_t width = 0, height = 0, dpi = 0; FlutterTizenView* view = engine_->view(); diff --git a/flutter/shell/platform/tizen/flutter_tizen_engine_unittest.cc b/flutter/shell/platform/tizen/flutter_tizen_engine_unittest.cc index 15665584..a5665550 100644 --- a/flutter/shell/platform/tizen/flutter_tizen_engine_unittest.cc +++ b/flutter/shell/platform/tizen/flutter_tizen_engine_unittest.cc @@ -4,8 +4,6 @@ #include "flutter/shell/platform/tizen/flutter_tizen_engine.h" -#include - #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" #include "flutter/shell/platform/tizen/testing/engine_modifier.h" #include "gtest/gtest.h" @@ -14,9 +12,6 @@ namespace flutter { namespace testing { class FlutterTizenEngineTest : public ::testing::Test { - public: - FlutterTizenEngineTest() { ecore_init(); } - protected: void SetUp() { FlutterDesktopEngineProperties engine_prop = {}; diff --git a/flutter/shell/platform/tizen/flutter_tizen_texture_registrar_unittests.cc b/flutter/shell/platform/tizen/flutter_tizen_texture_registrar_unittests.cc index b76074e0..3d7e7e5d 100644 --- a/flutter/shell/platform/tizen/flutter_tizen_texture_registrar_unittests.cc +++ b/flutter/shell/platform/tizen/flutter_tizen_texture_registrar_unittests.cc @@ -4,8 +4,6 @@ #include "flutter/shell/platform/tizen/flutter_tizen_texture_registrar.h" -#include - #include #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" @@ -17,9 +15,6 @@ namespace flutter { namespace testing { class FlutterTizenTextureRegistrarTest : public ::testing::Test { - public: - FlutterTizenTextureRegistrarTest() { ecore_init(); } - protected: void SetUp() { FlutterDesktopEngineProperties engine_prop = {}; diff --git a/flutter/shell/platform/tizen/flutter_tizen_view_unittests.cc b/flutter/shell/platform/tizen/flutter_tizen_view_unittests.cc index 0ac71df2..35fc9b7b 100644 --- a/flutter/shell/platform/tizen/flutter_tizen_view_unittests.cc +++ b/flutter/shell/platform/tizen/flutter_tizen_view_unittests.cc @@ -4,8 +4,6 @@ #include "flutter/shell/platform/tizen/flutter_tizen_view.h" -#include - #include #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" @@ -47,7 +45,6 @@ class TestTizenView : public TizenWindow { }; TEST(FlutterTizenViewTest, SendsAndRequestsViewFocus) { - ecore_init(); { FlutterDesktopEngineProperties properties = {}; properties.assets_path = "/foo/flutter_assets"; @@ -110,7 +107,6 @@ TEST(FlutterTizenViewTest, SendsAndRequestsViewFocus) { view.OnFocusChangeRequest(request); EXPECT_FALSE(tizen_view_ptr->activated); } - ecore_shutdown(); } } // namespace diff --git a/flutter/shell/platform/tizen/tizen_event_loop.cc b/flutter/shell/platform/tizen/tizen_event_loop.cc index 0724b864..c5b3ac8b 100644 --- a/flutter/shell/platform/tizen/tizen_event_loop.cc +++ b/flutter/shell/platform/tizen/tizen_event_loop.cc @@ -6,109 +6,111 @@ #include "tizen_event_loop.h" #include +#include namespace flutter { +namespace { + +GSourceFuncs kTaskSourceFuncs = { + nullptr, + nullptr, + [](GSource*, GSourceFunc callback, gpointer user_data) -> gboolean { + return callback(user_data); + }, + nullptr, + nullptr, + nullptr, +}; + +} // namespace + TizenEventLoop::TizenEventLoop(std::thread::id main_thread_id, CurrentTimeProc get_current_time, TaskExpiredCallback on_task_expired) : main_thread_id_(main_thread_id), get_current_time_(get_current_time), - on_task_expired_(std::move(on_task_expired)) { - ecore_pipe_ = ecore_pipe_add( - [](void* data, void* buffer, unsigned int nbyte) -> void { - auto* self = static_cast(data); - self->ExecuteTaskEvents(); + on_task_expired_(std::move(on_task_expired)), + task_source_(g_source_new(&kTaskSourceFuncs, sizeof(GSource))) { + g_source_set_callback( + task_source_, + [](gpointer data) -> gboolean { + static_cast(data)->ExecuteTaskEvents(); + return G_SOURCE_CONTINUE; }, - this); + this, nullptr); + + GMainContext* main_context = g_main_context_ref_thread_default(); + g_source_attach(task_source_, main_context); + g_main_context_unref(main_context); } TizenEventLoop::~TizenEventLoop() { - if (ecore_pipe_) { - ecore_pipe_del(ecore_pipe_); - } + g_source_destroy(task_source_); + g_source_unref(task_source_); } bool TizenEventLoop::RunsTasksOnCurrentThread() const { return std::this_thread::get_id() == main_thread_id_; } +void TizenEventLoop::PostTask(FlutterTask flutter_task, + uint64_t flutter_target_time_nanos) { + std::lock_guard lock(task_queue_mutex_); + const bool should_reschedule = + task_queue_.empty() || + flutter_target_time_nanos < task_queue_.top().target_time_nanos; + task_queue_.push({flutter_target_time_nanos, task_order_++, flutter_task}); + if (should_reschedule) { + UpdateSourceReadyTimeLocked(get_current_time_()); + } +} + void TizenEventLoop::ExecuteTaskEvents() { - const TaskTimePoint now = TaskTimePoint::clock::now(); + std::vector expired_tasks; { - std::lock_guard lock1(task_queue_mutex_); - std::lock_guard lock2(expired_tasks_mutex_); - while (!task_queue_.empty()) { - const Task& top = task_queue_.top(); - - if (top.fire_time > now) { - break; - } - - expired_tasks_.push_back(task_queue_.top()); + std::lock_guard lock(task_queue_mutex_); + const uint64_t now = get_current_time_(); + while (!task_queue_.empty() && task_queue_.top().target_time_nanos <= now) { + expired_tasks.push_back(task_queue_.top().task); task_queue_.pop(); } + UpdateSourceReadyTimeLocked(now); } - OnTaskExpired(); -} -TizenEventLoop::TaskTimePoint TizenEventLoop::TimePointFromFlutterTime( - uint64_t flutter_target_time_nanos) { - const TaskTimePoint now = TaskTimePoint::clock::now(); - const uint64_t flutter_duration = - flutter_target_time_nanos - get_current_time_(); - return now + std::chrono::nanoseconds(flutter_duration); + for (const FlutterTask& task : expired_tasks) { + OnTaskExpired(&task); + } } -void TizenEventLoop::PostTask(FlutterTask flutter_task, - uint64_t flutter_target_time_nanos) { - Task task; - task.order = ++task_order_; - task.fire_time = TimePointFromFlutterTime(flutter_target_time_nanos); - task.task = flutter_task; - { - std::lock_guard lock(task_queue_mutex_); - task_queue_.push(task); +void TizenEventLoop::UpdateSourceReadyTimeLocked(uint64_t now) { + if (task_queue_.empty()) { + g_source_set_ready_time(task_source_, -1); + return; } - const double flutter_duration = - static_cast(flutter_target_time_nanos) - get_current_time_(); - if (flutter_duration > 0) { - ecore_timer_add( - flutter_duration / 1000000000.0, - [](void* data) -> Eina_Bool { - auto* self = static_cast(data); - if (self->ecore_pipe_) { - ecore_pipe_write(self->ecore_pipe_, nullptr, 0); - } - return ECORE_CALLBACK_CANCEL; - }, - this); - } else { - if (ecore_pipe_) { - ecore_pipe_write(ecore_pipe_, nullptr, 0); - } + const uint64_t target_time_nanos = task_queue_.top().target_time_nanos; + if (target_time_nanos <= now) { + g_source_set_ready_time(task_source_, 0); + return; } + + const uint64_t delay_nanos = target_time_nanos - now; + const uint64_t delay_micros = delay_nanos / 1000 + (delay_nanos % 1000 != 0); + g_source_set_ready_time( + task_source_, g_get_monotonic_time() + static_cast(delay_micros)); } TizenPlatformEventLoop::TizenPlatformEventLoop( std::thread::id main_thread_id, CurrentTimeProc get_current_time, TaskExpiredCallback on_task_expired) - : TizenEventLoop(main_thread_id, get_current_time, on_task_expired) {} - -TizenPlatformEventLoop::~TizenPlatformEventLoop() {} - -void TizenPlatformEventLoop::OnTaskExpired() { - std::vector local_expired_tasks; - { - std::lock_guard lock(expired_tasks_mutex_); - local_expired_tasks = std::move(expired_tasks_); - } + : TizenEventLoop(main_thread_id, + get_current_time, + std::move(on_task_expired)) {} - for (const Task& task : local_expired_tasks) { - on_task_expired_(&task.task); - } +void TizenPlatformEventLoop::OnTaskExpired(const FlutterTask* task) { + on_task_expired_(task); } } // namespace flutter diff --git a/flutter/shell/platform/tizen/tizen_event_loop.h b/flutter/shell/platform/tizen/tizen_event_loop.h index bf7817e6..e428d171 100644 --- a/flutter/shell/platform/tizen/tizen_event_loop.h +++ b/flutter/shell/platform/tizen/tizen_event_loop.h @@ -6,22 +6,18 @@ #ifndef EMBEDDER_TIZEN_EVENT_LOOP_H_ #define EMBEDDER_TIZEN_EVENT_LOOP_H_ -#include +#include -#include -#include -#include #include #include #include #include #include "flutter/shell/platform/embedder/embedder.h" -#include "flutter/shell/platform/tizen/tizen_renderer.h" namespace flutter { -typedef uint64_t (*CurrentTimeProc)(); +using CurrentTimeProc = uint64_t (*)(); class TizenEventLoop { public: @@ -32,61 +28,50 @@ class TizenEventLoop { TaskExpiredCallback on_task_expired); virtual ~TizenEventLoop(); - // Prevent copying. TizenEventLoop(const TizenEventLoop&) = delete; TizenEventLoop& operator=(const TizenEventLoop&) = delete; bool RunsTasksOnCurrentThread() const; - - void ExecuteTaskEvents(); - - // Post a Flutter engine tasks to the event loop for delayed execution. void PostTask(FlutterTask flutter_task, uint64_t flutter_target_time_nanos); - virtual void OnTaskExpired() = 0; + virtual void OnTaskExpired(const FlutterTask* task) = 0; protected: - using TaskTimePoint = std::chrono::steady_clock::time_point; + std::thread::id main_thread_id_; + CurrentTimeProc get_current_time_; + TaskExpiredCallback on_task_expired_; + private: struct Task { + uint64_t target_time_nanos; uint64_t order; - TaskTimePoint fire_time; FlutterTask task; - struct Comparer { - bool operator()(const Task& a, const Task& b) { - if (a.fire_time == b.fire_time) { - return a.order > b.order; - } - return a.fire_time > b.fire_time; + bool operator<(const Task& other) const { + if (target_time_nanos == other.target_time_nanos) { + return order > other.order; } - }; + return target_time_nanos > other.target_time_nanos; + } }; - std::thread::id main_thread_id_; - CurrentTimeProc get_current_time_; - TaskExpiredCallback on_task_expired_; - std::mutex task_queue_mutex_; - std::priority_queue, Task::Comparer> task_queue_; - std::vector expired_tasks_; - std::mutex expired_tasks_mutex_; - std::atomic task_order_ = 0; - - private: - Ecore_Pipe* ecore_pipe_ = nullptr; + void ExecuteTaskEvents(); + void UpdateSourceReadyTimeLocked(uint64_t now); - // Returns a TaskTimePoint computed from the given target time from Flutter. - TaskTimePoint TimePointFromFlutterTime(uint64_t flutter_target_time_nanos); + std::mutex task_queue_mutex_; + std::priority_queue task_queue_; + uint64_t task_order_ = 0; + GSource* task_source_ = nullptr; }; -class TizenPlatformEventLoop : public TizenEventLoop { +class TizenPlatformEventLoop final : public TizenEventLoop { public: TizenPlatformEventLoop(std::thread::id main_thread_id, CurrentTimeProc get_current_time, TaskExpiredCallback on_task_expired); - virtual ~TizenPlatformEventLoop(); + ~TizenPlatformEventLoop() override = default; - virtual void OnTaskExpired() override; + void OnTaskExpired(const FlutterTask* task) override; }; } // namespace flutter diff --git a/flutter/shell/platform/tizen/tizen_event_loop_unittests.cc b/flutter/shell/platform/tizen/tizen_event_loop_unittests.cc new file mode 100644 index 00000000..6f2d748d --- /dev/null +++ b/flutter/shell/platform/tizen/tizen_event_loop_unittests.cc @@ -0,0 +1,126 @@ +// Copyright 2026 Samsung Electronics Co., Ltd. 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/tizen/tizen_event_loop.h" + +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { + +namespace { + +using namespace std::chrono_literals; + +uint64_t GetCurrentTimeNanos() { + return static_cast(g_get_monotonic_time()) * 1000; +} + +FlutterTask MakeTask(uint64_t id) { + FlutterTask task = {}; + task.task = id; + return task; +} + +} // namespace + +class TizenEventLoopTest : public ::testing::Test { + protected: + void SetUp() override { + main_context_ = g_main_context_new(); + g_main_context_push_thread_default(main_context_); + event_loop_ = std::make_unique( + std::this_thread::get_id(), GetCurrentTimeNanos, + [this](const FlutterTask* task) { + executed_tasks_.push_back(task->task); + execution_time_ = GetCurrentTimeNanos(); + execution_thread_ = std::this_thread::get_id(); + if (on_task_) { + on_task_(*task); + } + }); + } + + void TearDown() override { + event_loop_.reset(); + g_main_context_pop_thread_default(main_context_); + g_main_context_unref(main_context_); + } + + bool RunUntil(const std::function& condition, + std::chrono::milliseconds timeout = 1s) { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (!condition() && std::chrono::steady_clock::now() < deadline) { + g_main_context_iteration(main_context_, FALSE); + std::this_thread::sleep_for(1ms); + } + return condition(); + } + + GMainContext* main_context_ = nullptr; + std::unique_ptr event_loop_; + std::vector executed_tasks_; + uint64_t execution_time_ = 0; + std::thread::id execution_thread_; + std::function on_task_; +}; + +TEST_F(TizenEventLoopTest, ExecutesPastAndImmediateTasksInPostingOrder) { + const uint64_t now = GetCurrentTimeNanos(); + event_loop_->PostTask(MakeTask(1), now - 1); + event_loop_->PostTask(MakeTask(2), now); + event_loop_->PostTask(MakeTask(3), now); + + ASSERT_TRUE(RunUntil([this] { return executed_tasks_.size() == 3; })); + EXPECT_EQ(executed_tasks_, (std::vector{1, 2, 3})); +} + +TEST_F(TizenEventLoopTest, ReschedulesEarlierTaskWithoutRunningItEarly) { + const uint64_t now = GetCurrentTimeNanos(); + const uint64_t earlier_target = now + 20'000'000; + event_loop_->PostTask(MakeTask(1), now + 500'000'000); + event_loop_->PostTask(MakeTask(2), earlier_target); + + ASSERT_TRUE(RunUntil([this] { return !executed_tasks_.empty(); }, 250ms)); + EXPECT_EQ(executed_tasks_, (std::vector{2})); + EXPECT_GE(execution_time_, earlier_target); +} + +TEST_F(TizenEventLoopTest, RunsWorkerThreadTaskOnMainThread) { + std::thread worker( + [this] { event_loop_->PostTask(MakeTask(1), GetCurrentTimeNanos()); }); + worker.join(); + + ASSERT_TRUE(RunUntil([this] { return !executed_tasks_.empty(); })); + EXPECT_EQ(execution_thread_, std::this_thread::get_id()); +} + +TEST_F(TizenEventLoopTest, SupportsReentrantPostTask) { + on_task_ = [this](const FlutterTask& task) { + if (task.task == 1) { + event_loop_->PostTask(MakeTask(2), GetCurrentTimeNanos()); + } + }; + event_loop_->PostTask(MakeTask(1), GetCurrentTimeNanos()); + + ASSERT_TRUE(RunUntil([this] { return executed_tasks_.size() == 2; })); + EXPECT_EQ(executed_tasks_, (std::vector{1, 2})); +} + +TEST_F(TizenEventLoopTest, RemovesPendingSourceOnDestruction) { + event_loop_->PostTask(MakeTask(1), GetCurrentTimeNanos()); + event_loop_.reset(); + + g_main_context_iteration(main_context_, FALSE); + EXPECT_TRUE(executed_tasks_.empty()); +} + +} // namespace testing +} // namespace flutter From 09bb0625eba98f54b32e0befdf90b0c243866222 Mon Sep 17 00:00:00 2001 From: JunsuChoi Date: Mon, 3 Aug 2026 13:47:07 +0900 Subject: [PATCH 2/2] Add --use-tcore option for tizen-core-wl backend Introduce a use_tcore GN flag (surfaced as the --use-tcore build option) that switches the window and input-method backends from Ecore/ecore_wl2 to the tizen-core-wl / tizen-core-imf APIs. - Add tizen_window_tcore_wl and tizen_input_method_context_tcore, selected in BUILD.gn when use_tcore is set. - Reimplement the clipboard on tizen-core-wl for tcore builds. - Expose the native window handle via FlutterDesktopViewGetNativeHandle, and document that --use-tcore builds return a tizen_core_wl_window_h while default builds return an Ecore_Wl2_Window*. - Install the tizen-core-* sysroot packages for Tizen 10.0 and above. - Reject --use-tcore on Tizen versions below 10. Extract the libvd-win-util.so access shared by both window backends into TizenWindowUtil (tizen_window_util.{h,cc}). The TV profile cursor, mouse-pointer and unsupported-toast helpers each used to dlopen the library, resolve its symbols and dlclose it inline, so adding the tcore backend would have duplicated that block eight times. The new class resolves the six symbols once per instance and keeps the existing behaviour, including bailing out with the feature disabled when the library or a symbol is unavailable. Squashed from #170 and rebased onto master. The DALI/NUI sysroot packages removed upstream are not reintroduced, and the work-in-progress key-event trace logging is dropped. --- build/config/BUILDCONFIG.gn | 3 + flutter/shell/platform/tizen/BUILD.gn | 78 +- .../tizen/channels/input_panel_channel.cc | 4 + .../platform/tizen/channels/key_mapping.cc | 2 +- .../platform/tizen/channels/key_mapping.h | 10 +- .../tizen/channels/keyboard_channel.cc | 2 +- .../tizen/channels/text_input_channel.h | 4 + flutter/shell/platform/tizen/flutter_tizen.cc | 13 + .../platform/tizen/flutter_tizen_engine.cc | 4 + .../platform/tizen/public/flutter_tizen.h | 4 +- .../shell/platform/tizen/tizen_clipboard.cc | 278 ++++++ .../shell/platform/tizen/tizen_clipboard.h | 13 + .../tizen/tizen_input_method_context_tcore.cc | 453 +++++++++ .../tizen/tizen_input_method_context_tcore.h | 107 +++ .../platform/tizen/tizen_renderer_egl.cc | 9 + .../shell/platform/tizen/tizen_view_base.h | 4 + .../platform/tizen/tizen_window_ecore_wl2.cc | 117 +-- .../platform/tizen/tizen_window_tcore_wl.cc | 874 ++++++++++++++++++ .../platform/tizen/tizen_window_tcore_wl.h | 114 +++ .../shell/platform/tizen/tizen_window_util.cc | 117 +++ .../shell/platform/tizen/tizen_window_util.h | 96 ++ tools/generate_sysroot.py | 14 + tools/gn | 16 + 23 files changed, 2210 insertions(+), 126 deletions(-) create mode 100644 flutter/shell/platform/tizen/tizen_input_method_context_tcore.cc create mode 100644 flutter/shell/platform/tizen/tizen_input_method_context_tcore.h create mode 100644 flutter/shell/platform/tizen/tizen_window_tcore_wl.cc create mode 100644 flutter/shell/platform/tizen/tizen_window_tcore_wl.h create mode 100644 flutter/shell/platform/tizen/tizen_window_util.cc create mode 100644 flutter/shell/platform/tizen/tizen_window_util.h diff --git a/build/config/BUILDCONFIG.gn b/build/config/BUILDCONFIG.gn index 65876b36..baea25a7 100644 --- a/build/config/BUILDCONFIG.gn +++ b/build/config/BUILDCONFIG.gn @@ -31,6 +31,9 @@ declare_args() { # Use the system libstdc++ without building third_party/libcxx from source. use_system_cxx = false + + # Use tizen-core-wl API instead of ecore. + use_tcore = false } _default_configs = [ diff --git a/flutter/shell/platform/tizen/BUILD.gn b/flutter/shell/platform/tizen/BUILD.gn index 899babcc..6578a72c 100644 --- a/flutter/shell/platform/tizen/BUILD.gn +++ b/flutter/shell/platform/tizen/BUILD.gn @@ -14,15 +14,6 @@ config("flutter_tizen_config") { "${sysroot_path}/usr/include/appfw", "${sysroot_path}/usr/include/base", "${sysroot_path}/usr/include/dlog", - "${sysroot_path}/usr/include/ecore-1", - "${sysroot_path}/usr/include/ecore-imf-1", - "${sysroot_path}/usr/include/ecore-input-1", - "${sysroot_path}/usr/include/ecore-wayland-1", - "${sysroot_path}/usr/include/ecore-wl2-1", - "${sysroot_path}/usr/include/efl-1", - "${sysroot_path}/usr/include/eina-1", - "${sysroot_path}/usr/include/eina-1/eina", - "${sysroot_path}/usr/include/eo-1", "${sysroot_path}/usr/include/feedback", "${sysroot_path}/usr/include/system", "${sysroot_path}/usr/include/tzsh", @@ -32,6 +23,28 @@ config("flutter_tizen_config") { "${sysroot_path}/usr/lib/glib-2.0/include", ] + # tcore-specific includes + if (use_tcore) { + include_dirs += [ + "${sysroot_path}/usr/include/tizen-core", + "${sysroot_path}/usr/include/tizen-core-imf", + "${sysroot_path}/usr/include/tizen-core-wl", + ] + } else { + # ecore-specific includes + include_dirs += [ + "${sysroot_path}/usr/include/ecore-1", + "${sysroot_path}/usr/include/ecore-imf-1", + "${sysroot_path}/usr/include/ecore-input-1", + "${sysroot_path}/usr/include/ecore-wayland-1", + "${sysroot_path}/usr/include/ecore-wl2-1", + "${sysroot_path}/usr/include/efl-1", + "${sysroot_path}/usr/include/eina-1", + "${sysroot_path}/usr/include/eina-1/eina", + "${sysroot_path}/usr/include/eo-1", + ] + } + cflags_cc = [ "-Wno-newline-eof", "-Wno-macro-redefined", @@ -90,14 +103,26 @@ template("embedder") { "logger.cc", "system_utils.cc", "tizen_event_loop.cc", - "tizen_input_method_context.cc", "tizen_renderer.cc", "tizen_renderer_egl.cc", "tizen_renderer_gl.cc", "tizen_vsync_waiter.cc", - "tizen_window_ecore_wl2.cc", + "tizen_window_util.cc", ] + # Select window implementation based on use_tcore + if (use_tcore) { + sources += [ + "tizen_input_method_context_tcore.cc", + "tizen_window_tcore_wl.cc", + ] + } else { + sources += [ + "tizen_input_method_context.cc", + "tizen_window_ecore_wl2.cc", + ] + } + lib_dirs = [ "//engine/${target_cpu}" ] libs = [ @@ -110,11 +135,6 @@ template("embedder") { "capi-system-info", "capi-system-system-settings", "dlog", - "ecore", - "ecore_imf", - "ecore_input", - "ecore_wl2", - "eina", "glib-2.0", "gio-2.0", "feedback", @@ -128,6 +148,23 @@ template("embedder") { "GLESv2", ] + # tcore-specific or ecore-specific libraries + if (use_tcore) { + libs += [ + "tizen-core", + "tizen-core-imf", + "tizen-core-wl", + ] + } else { + libs += [ + "ecore", + "ecore_imf", + "ecore_input", + "ecore_wl2", + "eina", + ] + } + if (target_name == "flutter_tizen_common" || target_name == "flutter_tizen_common_experimental") { sources += [ "channels/tizen_shell.cc" ] @@ -141,6 +178,11 @@ template("embedder") { defines += invoker.defines defines += [ "FLUTTER_ENGINE_NO_PROTOTYPES" ] + # Add USE_TCORE_WL define when using tcore + if (use_tcore) { + defines += [ "USE_TCORE_WL" ] + } + if (target_name == "flutter_tizen_mobile_experimental" || target_name == "flutter_tizen_tv_experimental" || target_name == "flutter_tizen_common_experimental") { @@ -242,6 +284,10 @@ executable("flutter_tizen_unittests") { ldflags = [ "-Wl,--unresolved-symbols=ignore-in-shared-libs" ] + if (use_tcore) { + defines = [ "USE_TCORE_WL" ] + } + configs += [ ":flutter_tizen_config" ] deps += [ diff --git a/flutter/shell/platform/tizen/channels/input_panel_channel.cc b/flutter/shell/platform/tizen/channels/input_panel_channel.cc index d3e48236..e1467a7d 100644 --- a/flutter/shell/platform/tizen/channels/input_panel_channel.cc +++ b/flutter/shell/platform/tizen/channels/input_panel_channel.cc @@ -7,7 +7,11 @@ #include "flutter/shell/platform/common/client_wrapper/include/flutter/event_stream_handler_functions.h" #include "flutter/shell/platform/common/client_wrapper/include/flutter/standard_method_codec.h" #include "flutter/shell/platform/tizen/logger.h" +#ifdef USE_TCORE_WL +#include "flutter/shell/platform/tizen/tizen_input_method_context_tcore.h" +#else #include "flutter/shell/platform/tizen/tizen_input_method_context.h" +#endif namespace { diff --git a/flutter/shell/platform/tizen/channels/key_mapping.cc b/flutter/shell/platform/tizen/channels/key_mapping.cc index 35c0f368..54ea04de 100644 --- a/flutter/shell/platform/tizen/channels/key_mapping.cc +++ b/flutter/shell/platform/tizen/channels/key_mapping.cc @@ -177,7 +177,7 @@ std::map kScanCodeToGtkKeyCode = { {0x0000024d, 269025069}, // launchScreenSaver }; -std::map kEcoreModifierToGtkModifier = { +std::map kModifierToGtkModifier = { {0x0001, 1 << 0}, // SHIFT -> modifierShift {0x0002, 1 << 2}, // CTRL -> modifierControl {0x0004, 1 << 3}, // ALT -> modifierMod1 diff --git a/flutter/shell/platform/tizen/channels/key_mapping.h b/flutter/shell/platform/tizen/channels/key_mapping.h index d2ec300b..e07e1fb6 100644 --- a/flutter/shell/platform/tizen/channels/key_mapping.h +++ b/flutter/shell/platform/tizen/channels/key_mapping.h @@ -19,15 +19,15 @@ // legacy RawKeyboard API is removed from the framework in the future. extern std::map kScanCodeToGtkKeyCode; -// Maps Ecore modifiers to GTK modifiers. +// Maps Tizen modifier bitmask values to GTK modifiers. // -// The values are originally defined in: -// - efl/Ecore_Input.h -// - flutter/raw_keyboard_linux.dart (GtkKeyHelper) +// The modifier bitmask values match tizen_core_wl_modifier_e. +// The GTK modifier values are from flutter/raw_keyboard_linux.dart +// (GtkKeyHelper). // // Provided only for backward compatibility. This will be removed after the // legacy RawKeyboard API is removed from the framework in the future. -extern std::map kEcoreModifierToGtkModifier; +extern std::map kModifierToGtkModifier; // Maps XKB scan codes to Flutter's physical key codes. // diff --git a/flutter/shell/platform/tizen/channels/keyboard_channel.cc b/flutter/shell/platform/tizen/channels/keyboard_channel.cc index d48caecb..8b6e32cb 100644 --- a/flutter/shell/platform/tizen/channels/keyboard_channel.cc +++ b/flutter/shell/platform/tizen/channels/keyboard_channel.cc @@ -164,7 +164,7 @@ void KeyboardChannel::SendChannelEvent(const char* key, } int gtk_modifiers = 0; - for (auto element : kEcoreModifierToGtkModifier) { + for (auto element : kModifierToGtkModifier) { if (element.first & modifiers) { gtk_modifiers |= element.second; } diff --git a/flutter/shell/platform/tizen/channels/text_input_channel.h b/flutter/shell/platform/tizen/channels/text_input_channel.h index 11d3aa35..91c2a68a 100644 --- a/flutter/shell/platform/tizen/channels/text_input_channel.h +++ b/flutter/shell/platform/tizen/channels/text_input_channel.h @@ -11,7 +11,11 @@ #include "flutter/shell/platform/common/client_wrapper/include/flutter/binary_messenger.h" #include "flutter/shell/platform/common/client_wrapper/include/flutter/method_channel.h" #include "flutter/shell/platform/common/text_input_model.h" +#ifdef USE_TCORE_WL +#include "flutter/shell/platform/tizen/tizen_input_method_context_tcore.h" +#else #include "flutter/shell/platform/tizen/tizen_input_method_context.h" +#endif #include "rapidjson/document.h" namespace flutter { diff --git a/flutter/shell/platform/tizen/flutter_tizen.cc b/flutter/shell/platform/tizen/flutter_tizen.cc index 5105c0e1..1fde694e 100644 --- a/flutter/shell/platform/tizen/flutter_tizen.cc +++ b/flutter/shell/platform/tizen/flutter_tizen.cc @@ -15,7 +15,11 @@ #include "flutter/shell/platform/tizen/logger.h" #include "flutter/shell/platform/tizen/public/flutter_platform_view.h" #include "flutter/shell/platform/tizen/tizen_window.h" +#ifdef USE_TCORE_WL +#include "flutter/shell/platform/tizen/tizen_window_tcore_wl.h" +#else #include "flutter/shell/platform/tizen/tizen_window_ecore_wl2.h" +#endif namespace { @@ -208,12 +212,21 @@ FlutterDesktopViewRef FlutterDesktopViewCreateFromNewWindow( std::unique_ptr window; +#ifdef USE_TCORE_WL + window = std::make_unique( + window_geometry, window_properties.transparent, + window_properties.focusable, window_properties.top_level, + window_properties.pointing_device_support, + window_properties.floating_menu_support, window_properties.window_handle, + window_properties.renderer_type == kEVulkan); +#else window = std::make_unique( window_geometry, window_properties.transparent, window_properties.focusable, window_properties.top_level, window_properties.pointing_device_support, window_properties.floating_menu_support, window_properties.window_handle, window_properties.renderer_type == kEVulkan); +#endif auto view = std::make_unique( flutter::kImplicitViewId, std::move(window), diff --git a/flutter/shell/platform/tizen/flutter_tizen_engine.cc b/flutter/shell/platform/tizen/flutter_tizen_engine.cc index 2de1a11a..c88d9d59 100644 --- a/flutter/shell/platform/tizen/flutter_tizen_engine.cc +++ b/flutter/shell/platform/tizen/flutter_tizen_engine.cc @@ -15,7 +15,11 @@ #include "flutter/shell/platform/tizen/flutter_tizen_view.h" #include "flutter/shell/platform/tizen/logger.h" #include "flutter/shell/platform/tizen/system_utils.h" +#ifdef USE_TCORE_WL +#include "flutter/shell/platform/tizen/tizen_input_method_context_tcore.h" +#else #include "flutter/shell/platform/tizen/tizen_input_method_context.h" +#endif #include "flutter/shell/platform/tizen/tizen_renderer_egl.h" #ifdef FLUTTER_TIZEN_EXPERIMENTAL diff --git a/flutter/shell/platform/tizen/public/flutter_tizen.h b/flutter/shell/platform/tizen/public/flutter_tizen.h index e272a7ef..4a7b0875 100644 --- a/flutter/shell/platform/tizen/public/flutter_tizen.h +++ b/flutter/shell/platform/tizen/public/flutter_tizen.h @@ -179,7 +179,9 @@ FLUTTER_EXPORT void FlutterDesktopViewDestroy(FlutterDesktopViewRef view); // Returns a native UI toolkit handle for manipulation in host application. // -// Cast the returned void* to Ecore_Wl2_Window*. +// Cast the returned void*: +// - default (ecore_wl2) : to Ecore_Wl2_Window* +// - --use-tcore build : to tizen_core_wl_window_h // @warning This API is a work-in-progress and may change. FLUTTER_EXPORT void* FlutterDesktopViewGetNativeHandle( FlutterDesktopViewRef view); diff --git a/flutter/shell/platform/tizen/tizen_clipboard.cc b/flutter/shell/platform/tizen/tizen_clipboard.cc index c8393aae..8b85c067 100644 --- a/flutter/shell/platform/tizen/tizen_clipboard.cc +++ b/flutter/shell/platform/tizen/tizen_clipboard.cc @@ -4,9 +4,19 @@ #include "tizen_clipboard.h" +#include + +#include +#include + #include "flutter/shell/platform/tizen/logger.h" #include "flutter/shell/platform/tizen/tizen_window.h" + +#ifdef USE_TCORE_WL +#include "flutter/shell/platform/tizen/tizen_window_tcore_wl.h" +#else #include "flutter/shell/platform/tizen/tizen_window_ecore_wl2.h" +#endif namespace flutter { @@ -16,6 +26,272 @@ constexpr char kMimeTypeTextPlain[] = "text/plain;charset=utf-8"; } // namespace +#ifdef USE_TCORE_WL + +TizenClipboard::TizenClipboard(TizenViewBase* view) { + if (auto* window = dynamic_cast(view)) { + tizen_core_wl_window_h tcore_window = + static_cast(window->GetNativeHandle()); + tizen_core_wl_window_get_display(tcore_window, &display_); + } else { + if (tizen_core_wl_init() != TIZEN_CORE_WL_ERROR_NONE || + tizen_core_wl_display_create(&display_) != TIZEN_CORE_WL_ERROR_NONE || + tizen_core_wl_display_connect(display_, nullptr) != + TIZEN_CORE_WL_ERROR_NONE) { + FT_LOG(Error) << "Failed to connect display for clipboard."; + display_ = nullptr; + return; + } + owns_display_ = true; + } + + if (tizen_core_wl_display_get_event(display_, &tcore_wl_event_) != + TIZEN_CORE_WL_ERROR_NONE) { + FT_LOG(Error) << "Failed to get event handle for clipboard."; + return; + } + + tizen_core_wl_event_add_listener( + tcore_wl_event_, TIZEN_CORE_WL_EVENT_DATA_SOURCE_SEND, + [](void* event, tizen_core_wl_event_type_e type, void* data) { + auto* self = static_cast(data); + self->SendData(event); + }, + this, &send_listener_); + + tizen_core_wl_event_add_listener( + tcore_wl_event_, TIZEN_CORE_WL_EVENT_DATA_READY, + [](void* event, tizen_core_wl_event_type_e type, void* data) { + auto* self = static_cast(data); + self->ReceiveData(event); + }, + this, &receive_listener_); +} + +TizenClipboard::~TizenClipboard() { + on_data_callback_ = nullptr; + + if (send_listener_) { + tizen_core_wl_event_remove_listener(tcore_wl_event_, send_listener_); + } + if (receive_listener_) { + tizen_core_wl_event_remove_listener(tcore_wl_event_, receive_listener_); + } + + if (owns_display_ && display_) { + tizen_core_wl_display_disconnect(display_); + tizen_core_wl_display_destroy(display_); + tizen_core_wl_shutdown(); + } +} + +void TizenClipboard::SendData(void* event) { + if (!event) { + return; + } + tizen_core_wl_event_data_source_send_h send_event = + static_cast(event); + + char* mime_type = nullptr; + tizen_core_wl_event_data_source_send_get_type(send_event, &mime_type); + int fd = -1; + tizen_core_wl_event_data_source_send_get_fd(send_event, &fd); + + // Get serial from base event. + tizen_core_wl_event_data_source_base_h base_event = + static_cast(event); + unsigned int serial = 0; + tizen_core_wl_event_data_source_base_get_serial(base_event, &serial); + + if (!mime_type || + (strlen(mime_type) != 0 && strcmp(mime_type, kMimeTypeTextPlain))) { + FT_LOG(Error) << "Invaild mime type(" << (mime_type ? mime_type : "null") + << ")."; + if (fd >= 0) { + close(fd); + } + return; + } + + if (serial != selection_serial_) { + FT_LOG(Error) << "The serial doesn't match."; + if (fd >= 0) { + close(fd); + } + return; + } + + const char* buffer = data_.c_str(); + size_t remaining = data_.length(); + while (remaining > 0) { + ssize_t written = write(fd, buffer, remaining); + if (written < 0) { + if (errno == EINTR) { + continue; + } + FT_LOG(Error) << "Failed to write clipboard data: " << strerror(errno); + break; + } + buffer += written; + remaining -= written; + } + close(fd); +} + +void TizenClipboard::ReceiveData(void* event) { + if (!event) { + return; + } + tizen_core_wl_event_data_ready_h ready_event = + static_cast(event); + + void* raw_data = nullptr; + tizen_core_wl_event_data_ready_get_data(ready_event, &raw_data); + const char* data = static_cast(raw_data); + int len = 0; + tizen_core_wl_event_data_ready_get_len(ready_event, &len); + + if (data == nullptr || len < 1) { + FT_LOG(Info) << "No data available."; + if (on_data_callback_) { + on_data_callback_(""); + on_data_callback_ = nullptr; + } + return; + } + + tizen_core_wl_data_offer_h offer = nullptr; + tizen_core_wl_event_data_ready_get_offer(ready_event, &offer); + + if (offer != selection_offer_) { + FT_LOG(Error) << "The offer doesn't match."; + if (on_data_callback_) { + on_data_callback_(std::nullopt); + on_data_callback_ = nullptr; + } + return; + } + + size_t data_length = strlen(data); + size_t buffer_size = len; + std::string content; + + if (data_length < buffer_size) { + content.append(data, data_length); + } else { + content.append(data, buffer_size); + } + + if (on_data_callback_) { + on_data_callback_(content); + on_data_callback_ = nullptr; + } +} + +void TizenClipboard::SetData(const std::string& data) { + data_ = data; + + const char* mime_types[3]; + mime_types[0] = kMimeTypeTextPlain; + // TODO(jsuya): There is an issue where ECORE_WL2_EVENT_DATA_SOURCE_SEND event + // does not work properly even if ecore_wl2_dnd_selection_set() is called in + // Tizen 6.5 or lower. Therefore, add empty mimetype for event call from the + // cbhm module. Since it works normally from Tizen 8.0, this part may be + // modified in the future. + mime_types[1] = ""; + mime_types[2] = nullptr; + + tizen_core_wl_seat_h default_seat = nullptr; + tizen_core_wl_display_get_default_seat(display_, &default_seat); + if (default_seat) { + tizen_core_wl_data_h data_handle = nullptr; + tizen_core_wl_seat_get_data(default_seat, &data_handle); + if (data_handle) { + tizen_core_wl_data_set_selection(data_handle, mime_types, + &selection_serial_); + } + } + tizen_core_wl_display_flush(display_); +} + +bool TizenClipboard::GetData(ClipboardCallback on_data_callback) { + on_data_callback_ = std::move(on_data_callback); + + tizen_core_wl_seat_h default_seat = nullptr; + tizen_core_wl_display_get_default_seat(display_, &default_seat); + if (!default_seat) { + if (on_data_callback_) { + on_data_callback_ = nullptr; + } + return false; + } + + tizen_core_wl_data_h data_handle = nullptr; + tizen_core_wl_seat_get_data(default_seat, &data_handle); + if (!data_handle) { + if (on_data_callback_) { + on_data_callback_ = nullptr; + } + return false; + } + + tizen_core_wl_data_offer_h offer = nullptr; + tizen_core_wl_data_get_selection(data_handle, &offer); + selection_offer_ = offer; + if (!selection_offer_) { + FT_LOG(Error) << "tizen_core_wl_data_get_selection() failed."; + if (on_data_callback_) { + on_data_callback_ = nullptr; + } + return false; + } + + tizen_core_wl_data_receive(selection_offer_, + const_cast(kMimeTypeTextPlain)); + return true; +} + +bool TizenClipboard::HasStrings() { + tizen_core_wl_seat_h default_seat = nullptr; + tizen_core_wl_display_get_default_seat(display_, &default_seat); + if (!default_seat) { + return false; + } + + tizen_core_wl_data_h data_handle = nullptr; + tizen_core_wl_seat_get_data(default_seat, &data_handle); + if (!data_handle) { + return false; + } + + tizen_core_wl_data_offer_h offer = nullptr; + tizen_core_wl_data_get_selection(data_handle, &offer); + selection_offer_ = offer; + if (!selection_offer_) { + return false; + } + + char** mimes = nullptr; + int mime_count = 0; + tizen_core_wl_data_get_mimes(selection_offer_, &mimes, &mime_count); + + if (!mimes) { + return false; + } + + bool found = false; + for (int i = 0; i < mime_count; ++i) { + if (mimes[i] && !strcmp(kMimeTypeTextPlain, mimes[i])) { + found = true; + break; + } + } + free(mimes); + return found; +} + +#else // USE_TCORE_WL + TizenClipboard::TizenClipboard(TizenViewBase* view) { if (auto* window = dynamic_cast(view)) { auto* ecore_wl2_window = @@ -179,4 +455,6 @@ bool TizenClipboard::HasStrings() { return false; } +#endif // USE_TCORE_WL + } // namespace flutter diff --git a/flutter/shell/platform/tizen/tizen_clipboard.h b/flutter/shell/platform/tizen/tizen_clipboard.h index 244eafe8..df8d70c9 100644 --- a/flutter/shell/platform/tizen/tizen_clipboard.h +++ b/flutter/shell/platform/tizen/tizen_clipboard.h @@ -5,8 +5,12 @@ #ifndef EMBEDDER_TIZEN_CLIPBOARD_H_ #define EMBEDDER_TIZEN_CLIPBOARD_H_ +#ifdef USE_TCORE_WL +#include +#else #define EFL_BETA_API_SUPPORT #include +#endif #include #include @@ -35,10 +39,19 @@ class TizenClipboard { std::string data_; ClipboardCallback on_data_callback_; uint32_t selection_serial_ = 0; +#ifdef USE_TCORE_WL + tizen_core_wl_data_offer_h selection_offer_ = nullptr; + tizen_core_wl_display_h display_ = nullptr; + bool owns_display_ = false; + tizen_core_event_h tcore_wl_event_ = nullptr; + tizen_core_wl_event_listener_h send_listener_ = nullptr; + tizen_core_wl_event_listener_h receive_listener_ = nullptr; +#else Ecore_Wl2_Offer* selection_offer_ = nullptr; Ecore_Wl2_Display* display_ = nullptr; Ecore_Event_Handler* send_handler = nullptr; Ecore_Event_Handler* receive_handler = nullptr; +#endif }; } // namespace flutter diff --git a/flutter/shell/platform/tizen/tizen_input_method_context_tcore.cc b/flutter/shell/platform/tizen/tizen_input_method_context_tcore.cc new file mode 100644 index 00000000..5f07bfe4 --- /dev/null +++ b/flutter/shell/platform/tizen/tizen_input_method_context_tcore.cc @@ -0,0 +1,453 @@ +// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "tizen_input_method_context_tcore.h" + +#include + +#include "flutter/shell/platform/tizen/logger.h" + +namespace { + +tizen_core_imf_input_panel_layout_e TextInputTypeToImfInputPanelLayout( + const std::string& text_input_type) { + if (text_input_type == "TextInputType.text" || + text_input_type == "TextInputType.multiline") { + return TIZEN_CORE_IMF_INPUT_PANEL_LAYOUT_NORMAL; + } else if (text_input_type == "TextInputType.number") { + return TIZEN_CORE_IMF_INPUT_PANEL_LAYOUT_NUMBERONLY; + } else if (text_input_type == "TextInputType.phone") { + return TIZEN_CORE_IMF_INPUT_PANEL_LAYOUT_PHONENUMBER; + } else if (text_input_type == "TextInputType.datetime") { + return TIZEN_CORE_IMF_INPUT_PANEL_LAYOUT_DATETIME; + } else if (text_input_type == "TextInputType.emailAddress" || + text_input_type == "TextInputType.twitter") { + return TIZEN_CORE_IMF_INPUT_PANEL_LAYOUT_EMAIL; + } else if (text_input_type == "TextInputType.url" || + text_input_type == "TextInputType.webSearch") { + return TIZEN_CORE_IMF_INPUT_PANEL_LAYOUT_URL; + } else if (text_input_type == "TextInputType.visiblePassword") { + return TIZEN_CORE_IMF_INPUT_PANEL_LAYOUT_PASSWORD; + } else { + FT_LOG(Warn) << "The requested input type " << text_input_type + << " is not supported."; + return TIZEN_CORE_IMF_INPUT_PANEL_LAYOUT_NORMAL; + } +} + +tizen_core_imf_keyboard_modifiers_e ModifiersToImfModifiers( + unsigned int modifiers) { + unsigned int imf_modifiers = TIZEN_CORE_IMF_KEYBOARD_MODIFIERS_NONE; + if (modifiers & TIZEN_CORE_WL_MODIFIER_SHIFT) { + imf_modifiers |= TIZEN_CORE_IMF_KEYBOARD_MODIFIERS_SHIFT; + } + if (modifiers & TIZEN_CORE_WL_MODIFIER_ALT) { + imf_modifiers |= TIZEN_CORE_IMF_KEYBOARD_MODIFIERS_ALT; + } + if (modifiers & TIZEN_CORE_WL_MODIFIER_CTRL) { + imf_modifiers |= TIZEN_CORE_IMF_KEYBOARD_MODIFIERS_CTRL; + } + if (modifiers & TIZEN_CORE_WL_MODIFIER_WIN) { + imf_modifiers |= TIZEN_CORE_IMF_KEYBOARD_MODIFIERS_WIN; + } + if (modifiers & TIZEN_CORE_WL_MODIFIER_ALTGR) { + imf_modifiers |= TIZEN_CORE_IMF_KEYBOARD_MODIFIERS_ALTGR; + } + return static_cast(imf_modifiers); +} + +tizen_core_imf_keyboard_locks_e ModifiersToImfLocks(unsigned int modifiers) { + unsigned int locks = TIZEN_CORE_IMF_KEYBOARD_LOCKS_NONE; + if (modifiers & TIZEN_CORE_WL_MODIFIER_NUM) { + locks |= TIZEN_CORE_IMF_KEYBOARD_LOCKS_NUM; + } + if (modifiers & TIZEN_CORE_WL_MODIFIER_CAPS) { + locks |= TIZEN_CORE_IMF_KEYBOARD_LOCKS_CAPS; + } + if (modifiers & TIZEN_CORE_WL_MODIFIER_SCROLL) { + locks |= TIZEN_CORE_IMF_KEYBOARD_LOCKS_SCROLL; + } + return static_cast(locks); +} + +tizen_core_imf_event_key_h CreateImfKeyEventFromTcoreWlEvent(void* event) { + auto* ev = static_cast(event); + + tizen_core_imf_event_key_h imf_key = nullptr; + tizen_core_imf_event_key_create(&imf_key); + if (!imf_key) { + return nullptr; + } + + char* keyname = nullptr; + tizen_core_wl_event_key_get_keyname(ev, &keyname); + if (keyname) { + tizen_core_imf_event_key_set_keyname(imf_key, keyname); + tizen_core_imf_event_key_set_key(imf_key, keyname); + free(keyname); + } + + char* keysymbol = nullptr; + tizen_core_wl_event_key_get_keysymbol(ev, &keysymbol); + if (keysymbol) { + tizen_core_imf_event_key_set_string(imf_key, keysymbol); + free(keysymbol); + } + + char* compose = nullptr; + tizen_core_wl_event_key_get_compose(ev, &compose); + if (compose) { + tizen_core_imf_event_key_set_compose(imf_key, compose); + free(compose); + } + + unsigned int keycode = 0; + tizen_core_wl_event_key_get_keycode(ev, &keycode); + tizen_core_imf_event_key_set_keycode(imf_key, keycode); + + unsigned int modifiers = 0; + tizen_core_wl_event_key_get_modifiers(ev, &modifiers); + tizen_core_imf_event_key_set_modifiers(imf_key, + ModifiersToImfModifiers(modifiers)); + tizen_core_imf_event_key_set_locks(imf_key, ModifiersToImfLocks(modifiers)); + + uint32_t timestamp = 0; + tizen_core_wl_event_input_base_get_timestamp(ev, ×tamp); + tizen_core_imf_event_key_set_timestamp(imf_key, timestamp); + + char* dev_identifier = nullptr; + tizen_core_wl_event_input_base_get_device_identifier(ev, &dev_identifier); + if (dev_identifier) { + tizen_core_imf_event_key_set_device_name(imf_key, dev_identifier); + free(dev_identifier); + } + + return imf_key; +} + +} // namespace + +namespace flutter { + +TizenInputMethodContext::TizenInputMethodContext(uintptr_t window_id) { + tizen_core_imf_init(); + + if (tizen_core_imf_context_create(&imf_context_) != + TIZEN_CORE_IMF_ERROR_NONE) { + FT_LOG(Error) << "Failed to create tizen_core_imf_context."; + return; + } + + tizen_core_imf_context_set_client_window(imf_context_, + reinterpret_cast(window_id)); + SetContextOptions(); + SetInputPanelOptions(); + RegisterEventCallbacks(); + RegisterInputPanelEventCallback(); +} + +TizenInputMethodContext::~TizenInputMethodContext() { + UnregisterInputPanelEventCallback(); + UnregisterEventCallbacks(); + + if (imf_context_) { + tizen_core_imf_context_destroy(imf_context_); + } + + tizen_core_imf_shutdown(); +} + +bool TizenInputMethodContext::HandleTcoreWlEventKey(void* event, bool is_down) { + FT_ASSERT(imf_context_); + FT_ASSERT(event); + + tizen_core_imf_event_key_h imf_key = CreateImfKeyEventFromTcoreWlEvent(event); + if (!imf_key) { + return false; + } + + bool filter_result = false; + tizen_core_imf_context_filter_event(imf_context_, + is_down + ? TIZEN_CORE_IMF_EVENT_TYPE_KEY_DOWN + : TIZEN_CORE_IMF_EVENT_TYPE_KEY_UP, + imf_key, &filter_result); + tizen_core_imf_event_key_destroy(imf_key); + return filter_result; +} + +#ifdef NUI_SUPPORT +bool TizenInputMethodContext::HandleNuiKeyEvent(const char* device_name, + uint32_t device_class, + uint32_t device_subclass, + const char* key, + const char* string, + uint32_t modifiers, + uint32_t scan_code, + size_t timestamp, + bool is_down) { + tizen_core_imf_event_key_h imf_key = nullptr; + tizen_core_imf_event_key_create(&imf_key); + if (!imf_key) { + return false; + } + + if (key) { + tizen_core_imf_event_key_set_keyname(imf_key, key); + tizen_core_imf_event_key_set_key(imf_key, key); + } + if (string) { + tizen_core_imf_event_key_set_string(imf_key, string); + } + + tizen_core_imf_event_key_set_modifiers(imf_key, + ModifiersToImfModifiers(modifiers)); + tizen_core_imf_event_key_set_locks(imf_key, ModifiersToImfLocks(modifiers)); + tizen_core_imf_event_key_set_keycode(imf_key, scan_code); + tizen_core_imf_event_key_set_timestamp(imf_key, timestamp); + + if (device_name) { + tizen_core_imf_event_key_set_device_name(imf_key, device_name); + } + tizen_core_imf_event_key_set_device_class( + imf_key, static_cast(device_class)); + tizen_core_imf_event_key_set_device_subclass( + imf_key, static_cast(device_subclass)); + + bool filter_result = false; + tizen_core_imf_context_filter_event(imf_context_, + is_down + ? TIZEN_CORE_IMF_EVENT_TYPE_KEY_DOWN + : TIZEN_CORE_IMF_EVENT_TYPE_KEY_UP, + imf_key, &filter_result); + tizen_core_imf_event_key_destroy(imf_key); + return filter_result; +} +#endif + +InputPanelGeometry TizenInputMethodContext::GetInputPanelGeometry() { + FT_ASSERT(imf_context_); + InputPanelGeometry geometry; + tizen_core_imf_context_get_input_panel_geometry( + imf_context_, &geometry.x, &geometry.y, &geometry.w, &geometry.h); + return geometry; +} + +void TizenInputMethodContext::ResetInputMethodContext() { + FT_ASSERT(imf_context_); + tizen_core_imf_context_reset(imf_context_); +} + +void TizenInputMethodContext::ShowInputPanel() { + FT_ASSERT(imf_context_); + tizen_core_imf_context_input_panel_show(imf_context_); + tizen_core_imf_context_focus_in(imf_context_); +} + +void TizenInputMethodContext::HideInputPanel() { + FT_ASSERT(imf_context_); + tizen_core_imf_context_focus_out(imf_context_); + tizen_core_imf_context_input_panel_hide(imf_context_); +} + +bool TizenInputMethodContext::IsInputPanelShown() { + tizen_core_imf_input_panel_state_e state; + tizen_core_imf_context_get_input_panel_state(imf_context_, &state); + return state == TIZEN_CORE_IMF_INPUT_PANEL_STATE_SHOW; +} + +void TizenInputMethodContext::SetInputPanelLayout( + const std::string& input_type) { + FT_ASSERT(imf_context_); + tizen_core_imf_input_panel_layout_e panel_layout = + TextInputTypeToImfInputPanelLayout(input_type); + tizen_core_imf_context_set_input_panel_layout(imf_context_, panel_layout); +} + +void TizenInputMethodContext::SetInputPanelLayoutVariation(bool is_signed, + bool is_decimal) { + tizen_core_imf_layout_numberonly_variation_e variation; + if (is_signed && is_decimal) { + variation = TIZEN_CORE_IMF_LAYOUT_NUMBERONLY_VARIATION_SIGNED_AND_DECIMAL; + } else if (is_signed) { + variation = TIZEN_CORE_IMF_LAYOUT_NUMBERONLY_VARIATION_SIGNED; + } else if (is_decimal) { + variation = TIZEN_CORE_IMF_LAYOUT_NUMBERONLY_VARIATION_DECIMAL; + } else { + variation = TIZEN_CORE_IMF_LAYOUT_NUMBERONLY_VARIATION_NORMAL; + } + tizen_core_imf_context_set_input_panel_layout_variation(imf_context_, + variation); +} + +void TizenInputMethodContext::SetAutocapitalType(const std::string& type) { + tizen_core_imf_autocapital_type_e autocapital_type = + TIZEN_CORE_IMF_AUTOCAPITAL_TYPE_NONE; + + if (type == "TextCapitalization.characters") { + autocapital_type = TIZEN_CORE_IMF_AUTOCAPITAL_TYPE_ALLCHARACTER; + } else if (type == "TextCapitalization.words") { + autocapital_type = TIZEN_CORE_IMF_AUTOCAPITAL_TYPE_WORD; + } else if (type == "TextCapitalization.sentences") { + autocapital_type = TIZEN_CORE_IMF_AUTOCAPITAL_TYPE_SENTENCE; + } else if (type == "TextCapitalization.none") { + autocapital_type = TIZEN_CORE_IMF_AUTOCAPITAL_TYPE_NONE; + } + tizen_core_imf_context_set_autocapital_type(imf_context_, autocapital_type); +} + +void TizenInputMethodContext::SetEditingActive(bool active) { + if (!imf_context_) { + return; + } + if (active) { + tizen_core_imf_context_focus_in(imf_context_); + } else { + tizen_core_imf_context_focus_out(imf_context_); + } +} + +void TizenInputMethodContext::SetInputPanelEnabled(bool enabled) { + if (!imf_context_) { + return; + } + tizen_core_imf_context_set_input_panel_enabled(imf_context_, enabled); +} + +void TizenInputMethodContext::RegisterEventCallbacks() { + FT_ASSERT(imf_context_); + + // commit callback + event_callbacks_[TIZEN_CORE_IMF_CALLBACK_COMMIT] = + [](tizen_core_imf_context_h ctx, void* event_info, void* data) { + auto* self = static_cast(data); + char* str = static_cast(event_info); + if (self->on_commit_) { + self->on_commit_(str); + } + }; + tizen_core_imf_context_add_event_callback( + imf_context_, TIZEN_CORE_IMF_CALLBACK_COMMIT, + event_callbacks_[TIZEN_CORE_IMF_CALLBACK_COMMIT], this); + + // pre-edit start callback + event_callbacks_[TIZEN_CORE_IMF_CALLBACK_PREEDIT_START] = + [](tizen_core_imf_context_h ctx, void* event_info, void* data) { + auto* self = static_cast(data); + if (self->on_preedit_start_) { + self->on_preedit_start_(); + } + }; + tizen_core_imf_context_add_event_callback( + imf_context_, TIZEN_CORE_IMF_CALLBACK_PREEDIT_START, + event_callbacks_[TIZEN_CORE_IMF_CALLBACK_PREEDIT_START], this); + + // pre-edit end callback + event_callbacks_[TIZEN_CORE_IMF_CALLBACK_PREEDIT_END] = + [](tizen_core_imf_context_h ctx, void* event_info, void* data) { + auto* self = static_cast(data); + if (self->on_preedit_end_) { + self->on_preedit_end_(); + } + }; + tizen_core_imf_context_add_event_callback( + imf_context_, TIZEN_CORE_IMF_CALLBACK_PREEDIT_END, + event_callbacks_[TIZEN_CORE_IMF_CALLBACK_PREEDIT_END], this); + + // pre-edit changed callback + event_callbacks_[TIZEN_CORE_IMF_CALLBACK_PREEDIT_CHANGED] = + [](tizen_core_imf_context_h ctx, void* event_info, void* data) { + auto* self = static_cast(data); + if (self->on_preedit_changed_) { + char* str = nullptr; + int cursor_pos = 0; + tizen_core_imf_context_get_preedit_string(ctx, &str, nullptr, nullptr, + &cursor_pos); + if (str) { + self->on_preedit_changed_(str, cursor_pos); + free(str); + } + } + }; + tizen_core_imf_context_add_event_callback( + imf_context_, TIZEN_CORE_IMF_CALLBACK_PREEDIT_CHANGED, + event_callbacks_[TIZEN_CORE_IMF_CALLBACK_PREEDIT_CHANGED], this); +} + +void TizenInputMethodContext::UnregisterEventCallbacks() { + FT_ASSERT(imf_context_); + tizen_core_imf_context_del_event_callback( + imf_context_, TIZEN_CORE_IMF_CALLBACK_COMMIT, + event_callbacks_[TIZEN_CORE_IMF_CALLBACK_COMMIT]); + tizen_core_imf_context_del_event_callback( + imf_context_, TIZEN_CORE_IMF_CALLBACK_PREEDIT_CHANGED, + event_callbacks_[TIZEN_CORE_IMF_CALLBACK_PREEDIT_CHANGED]); + tizen_core_imf_context_del_event_callback( + imf_context_, TIZEN_CORE_IMF_CALLBACK_PREEDIT_START, + event_callbacks_[TIZEN_CORE_IMF_CALLBACK_PREEDIT_START]); + tizen_core_imf_context_del_event_callback( + imf_context_, TIZEN_CORE_IMF_CALLBACK_PREEDIT_END, + event_callbacks_[TIZEN_CORE_IMF_CALLBACK_PREEDIT_END]); +} + +void TizenInputMethodContext::SetContextOptions() { + FT_ASSERT(imf_context_); + tizen_core_imf_context_set_autocapital_type( + imf_context_, TIZEN_CORE_IMF_AUTOCAPITAL_TYPE_NONE); +} + +void TizenInputMethodContext::SetInputPanelOptions() { + FT_ASSERT(imf_context_); + tizen_core_imf_context_set_input_panel_layout( + imf_context_, TIZEN_CORE_IMF_INPUT_PANEL_LAYOUT_NORMAL); + tizen_core_imf_context_set_input_panel_return_key_type( + imf_context_, TIZEN_CORE_IMF_INPUT_PANEL_RETURN_KEY_TYPE_DEFAULT); +} + +void TizenInputMethodContext::InputPanelStateChangedCallback( + tizen_core_imf_context_h ctx, + int value, + void* data) { + auto* self = static_cast(data); + tizen_core_imf_input_panel_state_e state = + static_cast(value); + + std::string state_str; + switch (state) { + case TIZEN_CORE_IMF_INPUT_PANEL_STATE_SHOW: + state_str = "show"; + break; + case TIZEN_CORE_IMF_INPUT_PANEL_STATE_HIDE: + state_str = "hide"; + break; + case TIZEN_CORE_IMF_INPUT_PANEL_STATE_WILL_SHOW: + state_str = "will_show"; + break; + default: + state_str = "unknown"; + break; + } + + if (self->on_input_panel_state_changed_) { + self->on_input_panel_state_changed_(state_str); + } +} + +void TizenInputMethodContext::RegisterInputPanelEventCallback() { + FT_ASSERT(imf_context_); + + tizen_core_imf_context_add_input_panel_event_callback( + imf_context_, TIZEN_CORE_IMF_INPUT_PANEL_EVENT_STATE, + InputPanelStateChangedCallback, this); +} + +void TizenInputMethodContext::UnregisterInputPanelEventCallback() { + FT_ASSERT(imf_context_); + + tizen_core_imf_context_del_input_panel_event_callback( + imf_context_, TIZEN_CORE_IMF_INPUT_PANEL_EVENT_STATE, + InputPanelStateChangedCallback); +} + +} // namespace flutter diff --git a/flutter/shell/platform/tizen/tizen_input_method_context_tcore.h b/flutter/shell/platform/tizen/tizen_input_method_context_tcore.h new file mode 100644 index 00000000..9c4d79d5 --- /dev/null +++ b/flutter/shell/platform/tizen/tizen_input_method_context_tcore.h @@ -0,0 +1,107 @@ +// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef EMBEDDER_TIZEN_INPUT_METHOD_CONTEXT_H_ +#define EMBEDDER_TIZEN_INPUT_METHOD_CONTEXT_H_ + +#include + +#include +#include +#include + +namespace flutter { + +using OnCommit = std::function; +using OnPreeditChanged = std::function; +using OnPreeditStart = std::function; +using OnPreeditEnd = std::function; +using OnInputPanelStateChanged = std::function; + +struct InputPanelGeometry { + int32_t x = 0, y = 0, w = 0, h = 0; +}; + +class TizenInputMethodContext { + public: + TizenInputMethodContext(uintptr_t window_id); + ~TizenInputMethodContext(); + + bool HandleTcoreWlEventKey(void* event, bool is_down); + +#ifdef NUI_SUPPORT + bool HandleNuiKeyEvent(const char* device_name, + uint32_t device_class, + uint32_t device_subclass, + const char* key, + const char* string, + uint32_t modifiers, + uint32_t scan_code, + size_t timestamp, + bool is_down); +#endif + + InputPanelGeometry GetInputPanelGeometry(); + + void ResetInputMethodContext(); + + void ShowInputPanel(); + + void HideInputPanel(); + + bool IsInputPanelShown(); + + void SetInputPanelLayout(const std::string& layout); + + void SetInputPanelLayoutVariation(bool is_signed, bool is_decimal); + + void SetAutocapitalType(const std::string& type); + + void SetEditingActive(bool active); + + void SetInputPanelEnabled(bool enabled); + + void SetOnCommit(OnCommit callback) { on_commit_ = callback; } + + void SetOnPreeditChanged(OnPreeditChanged callback) { + on_preedit_changed_ = callback; + } + + void SetOnPreeditStart(OnPreeditStart callback) { + on_preedit_start_ = callback; + } + + void SetOnPreeditEnd(OnPreeditEnd callback) { on_preedit_end_ = callback; } + + void SetOnInputPanelStateChanged(OnInputPanelStateChanged callback) { + on_input_panel_state_changed_ = callback; + } + + void RegisterInputPanelEventCallback(); + void UnregisterInputPanelEventCallback(); + + private: + static void InputPanelStateChangedCallback(tizen_core_imf_context_h ctx, + int value, + void* data); + + void RegisterEventCallbacks(); + void UnregisterEventCallbacks(); + + void SetContextOptions(); + void SetInputPanelOptions(); + + tizen_core_imf_context_h imf_context_ = nullptr; + OnCommit on_commit_; + OnPreeditChanged on_preedit_changed_; + OnPreeditStart on_preedit_start_; + OnPreeditEnd on_preedit_end_; + OnInputPanelStateChanged on_input_panel_state_changed_; + std::unordered_map + event_callbacks_; +}; + +} // namespace flutter + +#endif // EMBEDDER_TIZEN_INPUT_METHOD_CONTEXT_H_ diff --git a/flutter/shell/platform/tizen/tizen_renderer_egl.cc b/flutter/shell/platform/tizen/tizen_renderer_egl.cc index 5da84ad7..f2847091 100644 --- a/flutter/shell/platform/tizen/tizen_renderer_egl.cc +++ b/flutter/shell/platform/tizen/tizen_renderer_egl.cc @@ -4,8 +4,12 @@ #include "flutter/shell/platform/tizen/tizen_renderer_egl.h" +#ifdef USE_TCORE_WL +#include +#else #define EFL_BETA_API_SUPPORT #include +#endif #include #include #include @@ -89,8 +93,13 @@ bool TizenRendererEgl::CreateSurface(void* render_target, const EGLint attribs[] = {EGL_NONE}; if (render_target_display) { +#ifdef USE_TCORE_WL + void* egl_window = tizen_core_wl_egl_window_native_get( + static_cast(render_target)); +#else const auto egl_window = ecore_wl2_egl_window_native_get( static_cast(render_target)); +#endif egl_surface_ = eglCreateWindowSurface( egl_display_, egl_config_, reinterpret_cast(egl_window), attribs); diff --git a/flutter/shell/platform/tizen/tizen_view_base.h b/flutter/shell/platform/tizen/tizen_view_base.h index 40a1b8f9..9354dd4a 100644 --- a/flutter/shell/platform/tizen/tizen_view_base.h +++ b/flutter/shell/platform/tizen/tizen_view_base.h @@ -10,7 +10,11 @@ #include #include +#ifdef USE_TCORE_WL +#include "flutter/shell/platform/tizen/tizen_input_method_context_tcore.h" +#else #include "flutter/shell/platform/tizen/tizen_input_method_context.h" +#endif #include "flutter/shell/platform/tizen/tizen_view_event_handler_delegate.h" namespace flutter { diff --git a/flutter/shell/platform/tizen/tizen_window_ecore_wl2.cc b/flutter/shell/platform/tizen/tizen_window_ecore_wl2.cc index 98bfb7cd..c8e5946e 100644 --- a/flutter/shell/platform/tizen/tizen_window_ecore_wl2.cc +++ b/flutter/shell/platform/tizen/tizen_window_ecore_wl2.cc @@ -7,10 +7,11 @@ #ifdef TV_PROFILE #include #include -#include #include #include #include + +#include "flutter/shell/platform/tizen/tizen_window_util.h" #endif #include "flutter/shell/platform/embedder/embedder.h" @@ -268,29 +269,8 @@ void TizenWindowEcoreWl2::SetWindowOptions() { void TizenWindowEcoreWl2::EnableCursor() { #ifdef TV_PROFILE - // dlopen is used here because the TV-specific library libvd-win-util.so - // and the relevant headers are not present in the rootstrap. - void* handle = dlopen("libvd-win-util.so", RTLD_LAZY); - if (!handle) { - FT_LOG(Error) << "Could not open a shared library libvd-win-util.so."; - return; - } - - // These functions are defined in vd-win-util's cursor_module.h. - int (*CursorModule_Initialize)(wl_display* display, wl_registry* registry, - wl_seat* seat, unsigned int id); - int (*Cursor_Set_Config)(wl_surface* surface, uint32_t config_type, - void* data); - void (*CursorModule_Finalize)(void); - *(void**)(&CursorModule_Initialize) = - dlsym(handle, "CursorModule_Initialize"); - *(void**)(&Cursor_Set_Config) = dlsym(handle, "Cursor_Set_Config"); - *(void**)(&CursorModule_Finalize) = dlsym(handle, "CursorModule_Finalize"); - - if (!CursorModule_Initialize || !Cursor_Set_Config || - !CursorModule_Finalize) { - FT_LOG(Error) << "Could not load symbols from the library."; - dlclose(handle); + TizenWindowUtil window_util; + if (!window_util.IsValid()) { return; } @@ -300,7 +280,6 @@ void TizenWindowEcoreWl2::EnableCursor() { if (!registry || !seat) { FT_LOG(Error) << "Could not retreive wl_registry or wl_seat from the display."; - dlclose(handle); return; } @@ -309,9 +288,7 @@ void TizenWindowEcoreWl2::EnableCursor() { EINA_ITERATOR_FOREACH(iter, global) { if (strcmp(global->interface, "tizen_cursor") == 0) { - if (!CursorModule_Initialize(wl2_display_, registry, seat, global->id)) { - FT_LOG(Error) << "Failed to initialize the cursor module."; - } + window_util.InitializeCursorModule(wl2_display_, registry, seat, global->id); } } eina_iterator_free(iter); @@ -319,92 +296,28 @@ void TizenWindowEcoreWl2::EnableCursor() { ecore_wl2_sync(); wl_surface* surface = ecore_wl2_window_surface_get(ecore_wl2_window_); - // The config_type 1 refers to TIZEN_CURSOR_CONFIG_CURSOR_AVAILABLE - // defined in the TV extension protocol tizen-extension-tv.xml. - if (!Cursor_Set_Config(surface, 1, nullptr)) { - FT_LOG(Error) << "Failed to set a cursor config value."; - } + window_util.SetCursorConfig( + surface, TizenWindowUtil::kCursorConfigCursorAvailable, nullptr); - CursorModule_Finalize(); - dlclose(handle); + window_util.FinalizeCursorModule(); #endif } #ifdef TV_PROFILE -typedef enum _MouseSupport { DISABLE = 0, ENABLE } MouseSupport; -typedef enum _Device_Type { MOUSE_DEVICE = 3, TOUCH_DEVICE } Device_Type; - void TizenWindowEcoreWl2::SetPointingDeviceSupport() { - // dlopen is used here because the TV-specific library libvd-win-util.so - // and the relevant headers are not present in the rootstrap. - void* handle = dlopen("libvd-win-util.so", RTLD_LAZY); - if (!handle) { - FT_LOG(Error) << "Could not open a shared library libvd-win-util.so."; - return; - } - - // These functions are defined in vd-win-util's cursor_module.h. - int (*Mouse_Pointer_Support)(MouseSupport type, void* ecore_wl2_win); - *(void**)(&Mouse_Pointer_Support) = dlsym(handle, "Mouse_Pointer_Support"); - - if (!Mouse_Pointer_Support) { - FT_LOG(Error) << "Could not load symbols from the library."; - dlclose(handle); - return; - } - - Mouse_Pointer_Support(pointing_device_support_ ? ENABLE : DISABLE, - ecore_wl2_window_); - dlclose(handle); + TizenWindowUtil window_util; + window_util.SetMousePointerSupport(pointing_device_support_, ecore_wl2_window_); } void TizenWindowEcoreWl2::SetFloatingMenuSupport() { - // dlopen is used here because the TV-specific library libvd-win-util.so - // and the relevant headers are not present in the rootstrap. - void* handle = dlopen("libvd-win-util.so", RTLD_LAZY); - if (!handle) { - FT_LOG(Error) << "Could not open a shared library libvd-win-util.so."; - return; - } - - // These functions are defined in vd-win-util's cursor_module.h. - int (*Mouse_Pointer_Not_Allow)(int enable, void* ecore_wl2_win); - *(void**)(&Mouse_Pointer_Not_Allow) = - dlsym(handle, "Mouse_Pointer_Not_Allow"); - - if (!Mouse_Pointer_Not_Allow) { - FT_LOG(Error) << "Could not load symbols from the library."; - dlclose(handle); - return; - } - - Mouse_Pointer_Not_Allow(!floating_menu_support_, ecore_wl2_window_); - dlclose(handle); + TizenWindowUtil window_util; + window_util.SetMousePointerNotAllow(!floating_menu_support_, ecore_wl2_window_); } void TizenWindowEcoreWl2::ShowUnsupportedToast() { - // dlopen is used here because the TV-specific library libvd-win-util.so - // and the relevant headers are not present in the rootstrap. - void* handle = dlopen("libvd-win-util.so", RTLD_LAZY); - if (!handle) { - FT_LOG(Error) << "Could not open a shared library libvd-win-util.so."; - return; - } - - // These functions are defined in vd-win-util's cursor_module.h. - void (*Unsupported_Toast_Launch)(Device_Type type, int show, int enable, - void* ecore_wl2_win); - *(void**)(&Unsupported_Toast_Launch) = - dlsym(handle, "Unsupported_Toast_Launch"); - - if (!Unsupported_Toast_Launch) { - FT_LOG(Error) << "Could not load symbols from the library."; - dlclose(handle); - return; - } - - Unsupported_Toast_Launch(MOUSE_DEVICE, 1, 1, ecore_wl2_window_); - dlclose(handle); + TizenWindowUtil window_util; + window_util.LaunchUnsupportedToast(TizenWindowUtil::kDeviceTypeMouse, true, + true, ecore_wl2_window_); } #endif diff --git a/flutter/shell/platform/tizen/tizen_window_tcore_wl.cc b/flutter/shell/platform/tizen/tizen_window_tcore_wl.cc new file mode 100644 index 00000000..89da9fd0 --- /dev/null +++ b/flutter/shell/platform/tizen/tizen_window_tcore_wl.cc @@ -0,0 +1,874 @@ +// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "tizen_window_tcore_wl.h" + +#ifdef TV_PROFILE +#include +#include +#include +#include +#include + +#include "flutter/shell/platform/tizen/tizen_window_util.h" +#endif + +#include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/tizen/logger.h" +#include "flutter/shell/platform/tizen/tizen_view_event_handler_delegate.h" + +namespace flutter { + +namespace { + +constexpr int kScrollDirectionVertical = 0; +constexpr int kScrollDirectionHorizontal = 1; + +#ifdef TV_PROFILE +constexpr char kSysMouseCursorPointerSizeVConfKey[] = + "db/menu/system/mouse-pointer-size"; +constexpr char kSysPointingDeviceSupportToastSharedPreferenceKey[] = + "flutter-tizen/preference/pointing-device-support-toast"; +constexpr char kTcoreWlInputCursorThemeName[] = "vd-cursors"; +#endif + +FlutterPointerMouseButtons ToFlutterPointerButton(int32_t button) { + if (button == 2) { + return kFlutterPointerButtonMouseMiddle; + } else if (button == 3) { + return kFlutterPointerButtonMouseSecondary; + } else { + return kFlutterPointerButtonMousePrimary; + } +} + +// FlutterPointerDeviceKind GetDeviceKindFromEventType(int event_type) { +// if (event_type == TIZEN_CORE_WL_EVENT_MOUSE_BUTTON_DOWN || +// event_type == TIZEN_CORE_WL_EVENT_MOUSE_BUTTON_UP || +// event_type == TIZEN_CORE_WL_EVENT_MOUSE_MOVE || +// event_type == TIZEN_CORE_WL_EVENT_MOUSE_WHEEL) { +// // The event system doesn't distinguish mouse vs touch at event level. +// // Default to touch; mouse events can be differentiated by touch_id == 0. +// return kFlutterPointerDeviceKindTouch; +// } +// return kFlutterPointerDeviceKindTouch; +// } + +#ifdef TV_PROFILE +time_t GetBootTimeEpoch() { + struct timespec now, boot_time; + if (clock_gettime(CLOCK_REALTIME, &now) != 0) { + FT_LOG(Error) << "Fail to get clock_gettime(CLOCK_REALTIME)."; + return -1; + } + if (clock_gettime(CLOCK_BOOTTIME, &boot_time) != 0) { + FT_LOG(Error) << "Fail to get clock_gettime(CLOCK_BOOTTIME)."; + return -1; + } + time_t boot_time_epoch = now.tv_sec - boot_time.tv_sec; + return (boot_time_epoch / 10) * 10; +} + +bool PreferenceItemCallback(const char* key, void* user_data) { + char* app_id = (char*)user_data; + if (!app_id || !key) { + return true; + } + + std::string preference_key = + std::string(kSysPointingDeviceSupportToastSharedPreferenceKey) + "/" + + app_id; + if (!strncmp(key, preference_key.c_str(), preference_key.length())) { + preference_remove(key); + } + return true; +} + +std::string GetPreferenceKey(bool clear_exist_key) { + time_t boot_time = GetBootTimeEpoch(); + if (boot_time == -1) { + return ""; + } + + char* id = nullptr; + int ret = app_get_id(&id); + if (ret != APP_CONTROL_ERROR_NONE || !id) { + FT_LOG(Error) << "Fail to get app id."; + return std::string(); + } + + std::string app_id = id; + free(id); + + std::ostringstream boot_time_buffer; + boot_time_buffer << boot_time; + + std::string preference_key = + std::string(kSysPointingDeviceSupportToastSharedPreferenceKey) + "/" + + app_id + "/" + boot_time_buffer.str(); + + if (clear_exist_key) { + preference_foreach_item(PreferenceItemCallback, (void*)app_id.c_str()); + } + return preference_key; +} + +bool GetPointingDeviceToastPreference() { + bool show_unsupported_toast = false; + std::string preference_key = GetPreferenceKey(false); + if (preference_key.empty()) { + return false; + } + + int ret = + preference_get_boolean(preference_key.c_str(), &show_unsupported_toast); + if (ret != PREFERENCE_ERROR_NONE) { + return false; + } + return show_unsupported_toast; +} + +void SetPointingDevicePreference() { + std::string preference_key = GetPreferenceKey(true); + if (preference_key.empty()) { + return; + } + + int ret = preference_set_boolean(preference_key.c_str(), true); + if (ret != PREFERENCE_ERROR_NONE) { + FT_LOG(Error) << "Fail to set toasted preference."; + } +} +#endif + +} // namespace + +TizenWindowTcoreWl::TizenWindowTcoreWl(TizenGeometry geometry, + bool transparent, + bool focusable, + bool top_level, + bool pointing_device_support, + bool floating_menu_support, + void* window_handle = nullptr, + bool is_vulkan = false) + : TizenWindow(geometry, transparent, focusable, top_level) +#ifdef TV_PROFILE + , + pointing_device_support_(pointing_device_support), + floating_menu_support_(floating_menu_support) +#endif + , + is_vulkan_(is_vulkan) { + if (!CreateWindow(window_handle)) { + FT_LOG(Error) << "Failed to create a platform window."; + return; + } + + SetWindowOptions(); + RegisterEventHandlers(); + PrepareInputMethod(); + Show(); +} + +TizenWindowTcoreWl::~TizenWindowTcoreWl() { + UnregisterEventHandlers(); + DestroyWindow(); +} + +bool TizenWindowTcoreWl::CreateWindow(void* window_handle) { + if (tizen_core_wl_init() != TIZEN_CORE_WL_ERROR_NONE) { + FT_LOG(Error) << "Could not initialize tizen core wl."; + return false; + } + + if (tizen_core_wl_display_create(&tcore_wl_display_) != + TIZEN_CORE_WL_ERROR_NONE) { + FT_LOG(Error) << "Could not create tizen core wl display."; + return false; + } + + if (tizen_core_wl_display_connect(tcore_wl_display_, nullptr) != + TIZEN_CORE_WL_ERROR_NONE) { + FT_LOG(Error) << "Tizen core wl display not found."; + return false; + } + + tizen_core_wl_display_private_get_wl_display(tcore_wl_display_, + &wl2_display_); + + if (tizen_core_wl_display_sync(tcore_wl_display_) != + TIZEN_CORE_WL_ERROR_NONE) { + FT_LOG(Error) << "Failed to sync tizen core wl display."; + return false; + } + + if (tizen_core_wl_display_get_event(tcore_wl_display_, &tcore_wl_event_) != + TIZEN_CORE_WL_ERROR_NONE) { + FT_LOG(Error) << "Could not get tizen core wl event handle."; + return false; + } + + int32_t width = 0, height = 0; + GList* output_list = nullptr; + tizen_core_wl_display_get_output_device_list(tcore_wl_display_, &output_list); + if (output_list) { + tizen_core_wl_output_h output = + static_cast(output_list->data); + tizen_core_wl_output_device_get_geometry(output, &width, &height); + g_list_free(output_list); + } + if (width == 0 || height == 0) { + FT_LOG(Error) << "Invalid screen size: " << width << " x " << height; + return false; + } + + if (initial_geometry_.width == 0) { + initial_geometry_.width = width; + } + if (initial_geometry_.height == 0) { + initial_geometry_.height = height; + } + + if (window_handle == nullptr) { + if (tizen_core_wl_create_window( + tcore_wl_display_, nullptr, initial_geometry_.left, + initial_geometry_.top, initial_geometry_.width, + initial_geometry_.height, + &tcore_wl_window_) != TIZEN_CORE_WL_ERROR_NONE) { + FT_LOG(Error) << "Could not create tizen core wl window."; + return false; + } + } else { + tcore_wl_window_ = static_cast(window_handle); + } + + if (is_vulkan_) { + tizen_core_wl_window_private_get_wl_surface(tcore_wl_window_, + &wl2_surface_); + return wl2_surface_ && wl2_display_; + } else { + if (tizen_core_wl_create_egl_window( + tcore_wl_window_, initial_geometry_.width, initial_geometry_.height, + &tcore_wl_egl_window_) != TIZEN_CORE_WL_ERROR_NONE) { + FT_LOG(Error) << "Could not create tizen core wl egl window."; + return false; + } + return tcore_wl_egl_window_ && wl2_display_; + } +} + +void TizenWindowTcoreWl::SetWindowOptions() { + tizen_core_wl_window_set_type( + tcore_wl_window_, top_level_ ? TIZEN_CORE_WL_WINDOW_TYPE_NOTIFICATION + : TIZEN_CORE_WL_WINDOW_TYPE_TOPLEVEL); + if (top_level_) { + SetNotificationLevel(TIZEN_CORE_WL_NOTIFICATION_LEVEL_TOP); + } + + tizen_core_wl_window_set_position(tcore_wl_window_, initial_geometry_.left, + initial_geometry_.top); + tizen_core_wl_window_set_aux_hint(tcore_wl_window_, + "wm.policy.win.user.geometry", "1"); + + tizen_core_wl_window_set_alpha(tcore_wl_window_, transparent_); + + if (!focusable_) { + tizen_core_wl_window_set_focus_skip(tcore_wl_window_, true); + } + +#ifdef TV_PROFILE + tizen_core_wl_window_angle_e rotations[1] = {TIZEN_CORE_WL_WINDOW_ANGLE_0}; +#else + tizen_core_wl_window_angle_e rotations[4] = { + TIZEN_CORE_WL_WINDOW_ANGLE_0, TIZEN_CORE_WL_WINDOW_ANGLE_90, + TIZEN_CORE_WL_WINDOW_ANGLE_180, TIZEN_CORE_WL_WINDOW_ANGLE_270}; +#endif + tizen_core_wl_window_set_available_rotation_angle_list( + tcore_wl_window_, rotations, sizeof(rotations) / sizeof(rotations[0])); + EnableCursor(); +} + +void TizenWindowTcoreWl::EnableCursor() { +#ifdef TV_PROFILE + TizenWindowUtil window_util; + if (!window_util.IsValid()) { + return; + } + + tizen_core_wl_seat_h default_seat = nullptr; + tizen_core_wl_display_get_default_seat(tcore_wl_display_, &default_seat); + if (!default_seat) { + FT_LOG(Error) << "Could not get default seat."; + return; + } + struct wl_seat* seat = nullptr; + tizen_core_wl_seat_private_get_wl_seat(default_seat, &seat); + if (!seat) { + FT_LOG(Error) << "Could not get wl_seat from the default seat."; + return; + } + + int cursor_global_id = 0; + if (tizen_core_wl_display_private_get_global_id( + tcore_wl_display_, "tizen_cursor", &cursor_global_id) == + TIZEN_CORE_WL_ERROR_NONE && + cursor_global_id > 0) { + struct wl_registry* registry = wl_display_get_registry(wl2_display_); + if (registry) { + window_util.InitializeCursorModule(wl2_display_, registry, seat, + cursor_global_id); + wl_registry_destroy(registry); + } + } + + tizen_core_wl_display_sync(tcore_wl_display_); + + struct wl_surface* surface = nullptr; + tizen_core_wl_window_private_get_wl_surface(tcore_wl_window_, &surface); + if (surface) { + window_util.SetCursorConfig( + surface, TizenWindowUtil::kCursorConfigCursorAvailable, nullptr); + } + + window_util.FinalizeCursorModule(); +#endif +} + +#ifdef TV_PROFILE +void TizenWindowTcoreWl::SetPointingDeviceSupport() { + TizenWindowUtil window_util; + window_util.SetMousePointerSupport(pointing_device_support_, tcore_wl_window_); +} + +void TizenWindowTcoreWl::SetFloatingMenuSupport() { + TizenWindowUtil window_util; + window_util.SetMousePointerNotAllow(!floating_menu_support_, tcore_wl_window_); +} + +void TizenWindowTcoreWl::ShowUnsupportedToast() { + TizenWindowUtil window_util; + window_util.LaunchUnsupportedToast(TizenWindowUtil::kDeviceTypeMouse, true, + true, tcore_wl_window_); +} +#endif + +void TizenWindowTcoreWl::AddEventListener(tizen_core_wl_event_type_e type, + tizen_core_wl_event_cb callback) { + tizen_core_wl_event_listener_h listener = nullptr; + if (tizen_core_wl_event_add_listener(tcore_wl_event_, type, callback, this, + &listener) != TIZEN_CORE_WL_ERROR_NONE) { + FT_LOG(Error) << "Failed to add tizen core wl event listener."; + return; + } + tcore_event_listeners_.push_back(listener); +} + +void TizenWindowTcoreWl::RegisterEventHandlers() { + // Window rotation event. + AddEventListener( + TIZEN_CORE_WL_EVENT_WINDOW_ROTATION, + [](void* event, tizen_core_wl_event_type_e type, void* data) { + auto* self = static_cast(data); + if (self->view_delegate_) { + auto* base_event = + static_cast(event); + tizen_core_wl_window_h event_window = nullptr; + tizen_core_wl_event_window_base_get_window(base_event, &event_window); + if (event_window == self->tcore_wl_window_) { + tizen_core_wl_event_window_rotation_h rot_event = nullptr; + tizen_core_wl_event_window_base_to_window_rotation(base_event, + &rot_event); + tizen_core_wl_window_angle_e angle = TIZEN_CORE_WL_WINDOW_ANGLE_0; + tizen_core_wl_event_window_rotation_get_angle(rot_event, &angle); + int degree = static_cast(angle); + self->view_delegate_->OnRotate(degree); + tizen_core_wl_window_set_rotation_angle( + self->tcore_wl_window_, + static_cast(degree)); + tizen_core_wl_window_send_rotation_change_done( + self->tcore_wl_window_, degree); + } + } + }); + + // Window configure event. + if (!is_vulkan_) { + AddEventListener( + TIZEN_CORE_WL_EVENT_WINDOW_CONFIGURE_COMPLETE, + [](void* event, tizen_core_wl_event_type_e type, void* data) { + auto* self = static_cast(data); + if (self->view_delegate_) { + auto* base_event = + static_cast(event); + tizen_core_wl_window_h event_window = nullptr; + tizen_core_wl_event_window_base_get_window(base_event, + &event_window); + if (event_window == self->tcore_wl_window_) { + int x = 0, y = 0, w = 0, h = 0; + tizen_core_wl_window_get_geometry(self->tcore_wl_window_, &x, &y, + &w, &h); + int32_t rotation = self->GetRotation(); + tizen_core_wl_egl_window_resize(self->tcore_wl_egl_window_, w, h); + tizen_core_wl_egl_window_set_window_transform( + self->tcore_wl_egl_window_, rotation / 90); + self->view_delegate_->OnResize(x, y, w, h); + } + } + }); + } + + // Mouse button down. + AddEventListener( + TIZEN_CORE_WL_EVENT_MOUSE_BUTTON_DOWN, + [](void* event, tizen_core_wl_event_type_e type, void* data) { + auto* self = static_cast(data); +#ifdef TV_PROFILE + if ((!self->pointing_device_support_ || + !self->floating_menu_support_) && + !self->show_unsupported_toast_) { + bool shown = GetPointingDeviceToastPreference(); + if (!self->floating_menu_support_) { + self->SetFloatingMenuSupport(); + } + if (!shown) { + self->ShowUnsupportedToast(); + SetPointingDevicePreference(); + } + self->show_unsupported_toast_ = true; + if (self->floating_menu_support_ && !self->pointing_device_support_) { + self->SetPointingDeviceSupport(); + } + return; + } +#endif + if (self->view_delegate_) { + auto* ev = static_cast(event); + tizen_core_wl_window_h event_window = nullptr; + tizen_core_wl_event_input_base_get_window(ev, &event_window); + if (event_window == self->tcore_wl_window_) { + int x = 0, y = 0; + tizen_core_wl_event_mouse_button_get_position(ev, &x, &y); + unsigned int buttons = 0; + tizen_core_wl_event_mouse_button_get_buttons(ev, &buttons); + uint32_t timestamp = 0; + tizen_core_wl_event_input_base_get_timestamp(ev, ×tamp); + unsigned int touch_id = 0; + tizen_core_wl_event_mouse_button_get_touch_id(ev, &touch_id); + self->view_delegate_->OnPointerDown( + x, y, ToFlutterPointerButton(buttons), timestamp, + touch_id == 0 ? kFlutterPointerDeviceKindMouse + : kFlutterPointerDeviceKindTouch, + touch_id); + } + } + }); + + // Mouse button up. + AddEventListener( + TIZEN_CORE_WL_EVENT_MOUSE_BUTTON_UP, + [](void* event, tizen_core_wl_event_type_e type, void* data) { + auto* self = static_cast(data); + if (self->view_delegate_) { + auto* ev = static_cast(event); + tizen_core_wl_window_h event_window = nullptr; + tizen_core_wl_event_input_base_get_window(ev, &event_window); + if (event_window == self->tcore_wl_window_) { + int x = 0, y = 0; + tizen_core_wl_event_mouse_button_get_position(ev, &x, &y); + unsigned int buttons = 0; + tizen_core_wl_event_mouse_button_get_buttons(ev, &buttons); + uint32_t timestamp = 0; + tizen_core_wl_event_input_base_get_timestamp(ev, ×tamp); + unsigned int touch_id = 0; + tizen_core_wl_event_mouse_button_get_touch_id(ev, &touch_id); + self->view_delegate_->OnPointerUp( + x, y, ToFlutterPointerButton(buttons), timestamp, + touch_id == 0 ? kFlutterPointerDeviceKindMouse + : kFlutterPointerDeviceKindTouch, + touch_id); + } + } + }); + + // Mouse move. + AddEventListener( + TIZEN_CORE_WL_EVENT_MOUSE_MOVE, + [](void* event, tizen_core_wl_event_type_e type, void* data) { + auto* self = static_cast(data); + if (self->view_delegate_) { + auto* ev = static_cast(event); + tizen_core_wl_window_h event_window = nullptr; + tizen_core_wl_event_input_base_get_window(ev, &event_window); + if (event_window == self->tcore_wl_window_) { + int x = 0, y = 0; + tizen_core_wl_event_mouse_move_get_position(ev, &x, &y); + uint32_t timestamp = 0; + tizen_core_wl_event_input_base_get_timestamp(ev, ×tamp); + unsigned int touch_id = 0; + tizen_core_wl_event_mouse_move_get_touch_id(ev, &touch_id); + self->view_delegate_->OnPointerMove( + x, y, timestamp, + touch_id == 0 ? kFlutterPointerDeviceKindMouse + : kFlutterPointerDeviceKindTouch, + touch_id); + } + } + }); + + // Mouse wheel. + AddEventListener( + TIZEN_CORE_WL_EVENT_MOUSE_WHEEL, + [](void* event, tizen_core_wl_event_type_e type, void* data) { + auto* self = static_cast(data); + if (self->view_delegate_) { + auto* ev = static_cast(event); + tizen_core_wl_window_h event_window = nullptr; + tizen_core_wl_event_input_base_get_window(ev, &event_window); + if (event_window == self->tcore_wl_window_) { + int x = 0, y = 0; + tizen_core_wl_event_mouse_wheel_get_position(ev, &x, &y); + int direction = 0; + tizen_core_wl_event_mouse_wheel_get_direction(ev, &direction); + int z = 0; + tizen_core_wl_event_mouse_wheel_get_z(ev, &z); + uint32_t timestamp = 0; + tizen_core_wl_event_input_base_get_timestamp(ev, ×tamp); + + double delta_x = 0.0; + double delta_y = 0.0; + if (direction == kScrollDirectionVertical) { + delta_y += z; + } else if (direction == kScrollDirectionHorizontal) { + delta_x += z; + } + + self->view_delegate_->OnScroll(x, y, delta_x, delta_y, timestamp, + kFlutterPointerDeviceKindMouse, 0); + } + } + }); + + // Key down. + AddEventListener( + TIZEN_CORE_WL_EVENT_KEY_DOWN, + [](void* event, tizen_core_wl_event_type_e type, void* data) { + auto* self = static_cast(data); + if (!self->view_delegate_) { + return; + } + auto* ev = static_cast(event); + tizen_core_wl_window_h event_window = nullptr; + tizen_core_wl_event_input_base_get_window(ev, &event_window); + if (event_window != self->tcore_wl_window_) { + return; + } + + char* keyname = nullptr; + tizen_core_wl_event_key_get_keyname(ev, &keyname); + char* keysymbol = nullptr; + tizen_core_wl_event_key_get_keysymbol(ev, &keysymbol); + char* compose = nullptr; + tizen_core_wl_event_key_get_compose(ev, &compose); + unsigned int modifiers = 0; + tizen_core_wl_event_key_get_modifiers(ev, &modifiers); + unsigned int keycode = 0; + tizen_core_wl_event_key_get_keycode(ev, &keycode); + char* dev_identifier = nullptr; + tizen_core_wl_event_input_base_get_device_identifier(ev, + &dev_identifier); + + bool handled = false; + if (self->input_method_context_ && + self->input_method_context_->IsInputPanelShown()) { + handled = + self->input_method_context_->HandleTcoreWlEventKey(event, true); + } + if (!handled) { + // Match TizenWindowEcoreWl2 OnKey arg semantics: + // param 1 (key) = XKB key symbol name (e.g. "period") + // param 2 (string) = composed printable string (e.g. ".") + // tcore's keysymbol corresponds to ecore's key_event->key, and + // tcore's compose corresponds to ecore's key_event->string. + // Passing keysymbol as `string` breaks the printable-key fallback + // in TextInputChannel::HandleKey() for keys whose symbol name is + // multi-char ("period", "minus", "equal", ...). + self->view_delegate_->OnKey(keysymbol, compose, compose, modifiers, + keycode, dev_identifier, true); + } + free(keyname); + free(keysymbol); + free(compose); + free(dev_identifier); + }); + + // Key up. + AddEventListener( + TIZEN_CORE_WL_EVENT_KEY_UP, + [](void* event, tizen_core_wl_event_type_e type, void* data) { + auto* self = static_cast(data); + if (!self->view_delegate_) { + return; + } + auto* ev = static_cast(event); + tizen_core_wl_window_h event_window = nullptr; + tizen_core_wl_event_input_base_get_window(ev, &event_window); + if (event_window != self->tcore_wl_window_) { + return; + } + + char* keyname = nullptr; + tizen_core_wl_event_key_get_keyname(ev, &keyname); + char* keysymbol = nullptr; + tizen_core_wl_event_key_get_keysymbol(ev, &keysymbol); + char* compose = nullptr; + tizen_core_wl_event_key_get_compose(ev, &compose); + unsigned int modifiers = 0; + tizen_core_wl_event_key_get_modifiers(ev, &modifiers); + unsigned int keycode = 0; + tizen_core_wl_event_key_get_keycode(ev, &keycode); + + bool handled = false; + if (self->input_method_context_ && + self->input_method_context_->IsInputPanelShown()) { + handled = + self->input_method_context_->HandleTcoreWlEventKey(event, false); + } + if (!handled) { + // See key-down handler for the keysymbol vs compose rationale. + self->view_delegate_->OnKey(keysymbol, compose, compose, modifiers, + keycode, nullptr, false); + } + free(keyname); + free(keysymbol); + free(compose); + }); +} + +void TizenWindowTcoreWl::UnregisterEventHandlers() { + for (tizen_core_wl_event_listener_h listener : tcore_event_listeners_) { + tizen_core_wl_event_remove_listener(tcore_wl_event_, listener); + } + tcore_event_listeners_.clear(); +} + +void TizenWindowTcoreWl::DestroyWindow() { + if (tcore_wl_egl_window_) { + tizen_core_wl_egl_window_destroy(tcore_wl_egl_window_); + tcore_wl_egl_window_ = nullptr; + } + + if (tcore_wl_window_) { + tizen_core_wl_window_destroy(tcore_wl_window_); + tcore_wl_window_ = nullptr; + } + + if (tcore_wl_display_) { + tizen_core_wl_display_disconnect(tcore_wl_display_); + tizen_core_wl_display_destroy(tcore_wl_display_); + tcore_wl_display_ = nullptr; + } + tizen_core_wl_shutdown(); +} + +TizenGeometry TizenWindowTcoreWl::GetGeometry() { + TizenGeometry result; + tizen_core_wl_window_get_geometry(tcore_wl_window_, &result.left, &result.top, + &result.width, &result.height); + return result; +} + +bool TizenWindowTcoreWl::SetGeometry(TizenGeometry geometry) { + tizen_core_wl_window_set_geometry(tcore_wl_window_, geometry.left, + geometry.top, geometry.width, + geometry.height); + tizen_core_wl_window_set_position(tcore_wl_window_, geometry.left, + geometry.top); + return true; +} + +TizenGeometry TizenWindowTcoreWl::GetScreenGeometry() { + TizenGeometry result = {}; + GList* output_list = nullptr; + tizen_core_wl_display_get_output_device_list(tcore_wl_display_, &output_list); + if (output_list) { + tizen_core_wl_output_h output = + static_cast(output_list->data); + tizen_core_wl_output_device_get_geometry(output, &result.width, + &result.height); + g_list_free(output_list); + } + return result; +} + +int32_t TizenWindowTcoreWl::GetRotation() { + tizen_core_wl_window_angle_e angle = TIZEN_CORE_WL_WINDOW_ANGLE_0; + tizen_core_wl_window_get_rotation_angle(tcore_wl_window_, &angle); + return static_cast(angle); +} + +int32_t TizenWindowTcoreWl::GetDpi() { + GList* output_list = nullptr; + tizen_core_wl_display_get_output_device_list(tcore_wl_display_, &output_list); + if (!output_list) { + FT_LOG(Error) << "Could not find an output device."; + return 0; + } + tizen_core_wl_output_h output = + static_cast(output_list->data); + g_list_free(output_list); + int dpi = 0; + tizen_core_wl_output_device_get_dpi(output, &dpi); + return dpi; +} + +uintptr_t TizenWindowTcoreWl::GetWindowId() { + return reinterpret_cast(tcore_wl_window_); +} + +uint32_t TizenWindowTcoreWl::GetResourceId() { + if (resource_id_ > 0) { + return resource_id_; + } + + unsigned int res_id = 0; + if (tizen_core_wl_window_private_get_resource_id(tcore_wl_window_, &res_id) == + TIZEN_CORE_WL_ERROR_NONE) { + resource_id_ = res_id; + } + return resource_id_; +} + +void TizenWindowTcoreWl::SetPreferredOrientations( + const std::vector& rotations) { + std::vector angles; + for (int rot : rotations) { + angles.push_back(static_cast(rot)); + } + tizen_core_wl_window_set_available_rotation_angle_list( + tcore_wl_window_, angles.data(), angles.size()); +} + +void TizenWindowTcoreWl::BindKeys(const std::vector& keys) { + for (const std::string& key : keys) { + tizen_core_wl_keygrab_info_h info = nullptr; + tizen_core_wl_keygrab_info_create(key.c_str(), + TIZEN_CORE_WL_KEYGRAB_TOPMOST, &info); + if (info) { + GList* list = g_list_append(nullptr, info); + tizen_core_wl_window_set_keygrab_list(tcore_wl_window_, list); + g_list_free(list); + tizen_core_wl_keygrab_info_destroy(info); + } + } +} + +void TizenWindowTcoreWl::Show() { + tizen_core_wl_window_show(tcore_wl_window_); +} + +void TizenWindowTcoreWl::UpdateFlutterCursor(const std::string& kind) { +#ifdef TV_PROFILE + int pointer_size = -1; + if (vconf_get_int(kSysMouseCursorPointerSizeVConfKey, &pointer_size) < 0) { + FT_LOG(Info) << "Failed to load cursor size."; + } + + std::string cursor_name = "normal_default"; + if (kind == "basic") { + if (pointer_size == 0) { + cursor_name = "large_normal"; + } else if (pointer_size == 1) { + cursor_name = "medium_normal"; + } else if (pointer_size == 2) { + cursor_name = "small_normal"; + } else { + cursor_name = "normal_default"; + } + } else if (kind == "click") { + if (pointer_size == 0) { + cursor_name = "large_normal_pnh"; + } else if (pointer_size == 1) { + cursor_name = "medium_normal_pnh"; + } else if (pointer_size == 2) { + cursor_name = "small_normal_pnh"; + } else { + cursor_name = "normal_pnh"; + } + } else if (kind == "text") { + if (pointer_size == 0) { + cursor_name = "large_normal_input_field"; + } else if (pointer_size == 1) { + cursor_name = "medium_normal_input_field"; + } else if (pointer_size == 2) { + cursor_name = "small_normal_input_field"; + } else { + cursor_name = "normal_input_field"; + } + } else if (kind == "none") { + cursor_name = "normal_transparent"; + } else { + FT_LOG(Info) << kind << " cursor is not supported."; + } + tizen_core_wl_seat_h default_seat = nullptr; + tizen_core_wl_display_get_default_seat(tcore_wl_display_, &default_seat); + if (default_seat) { + tizen_core_wl_seat_set_cursor_theme(default_seat, + kTcoreWlInputCursorThemeName); + tizen_core_wl_seat_set_cursor_name(default_seat, cursor_name.c_str()); + } else { + FT_LOG(Error) << "Failed to get default seat; cannot update cursor."; + } +#else + tizen_core_wl_seat_h default_seat = nullptr; + tizen_core_wl_display_get_default_seat(tcore_wl_display_, &default_seat); + if (default_seat) { + tizen_core_wl_seat_set_cursor_theme(default_seat, "default"); + tizen_core_wl_seat_set_cursor_name(default_seat, "left_ptr"); + } + FT_LOG(Info) << "UpdateFlutterCursor is not supported."; +#endif +} + +void TizenWindowTcoreWl::SetNotificationLevel(int level) { + tizen_core_wl_notification_set_level( + tcore_wl_window_, static_cast(level)); +} + +void TizenWindowTcoreWl::PrepareInputMethod() { + input_method_context_ = + std::make_unique(GetWindowId()); + + input_method_context_->SetOnPreeditStart( + [this]() { view_delegate_->OnComposeBegin(); }); + input_method_context_->SetOnPreeditChanged( + [this](std::string str, int cursor_pos) { + view_delegate_->OnComposeChange(str, cursor_pos); + }); + input_method_context_->SetOnPreeditEnd( + [this]() { view_delegate_->OnComposeEnd(); }); + input_method_context_->SetOnCommit( + [this](std::string str) { view_delegate_->OnCommit(str); }); +} + +void* TizenWindowTcoreWl::GetRenderTarget() { + if (is_vulkan_) { + return wl2_surface_; + } else { + return tcore_wl_egl_window_; + } +} + +void TizenWindowTcoreWl::ActivateWindow() { + tizen_core_wl_window_activate(tcore_wl_window_); +} + +void TizenWindowTcoreWl::RaiseWindow() { + tizen_core_wl_window_raise(tcore_wl_window_); +} + +void TizenWindowTcoreWl::LowerWindow() { + tizen_core_wl_window_lower(tcore_wl_window_); +} + +} // namespace flutter diff --git a/flutter/shell/platform/tizen/tizen_window_tcore_wl.h b/flutter/shell/platform/tizen/tizen_window_tcore_wl.h new file mode 100644 index 00000000..1f124b0f --- /dev/null +++ b/flutter/shell/platform/tizen/tizen_window_tcore_wl.h @@ -0,0 +1,114 @@ +// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef EMBEDDER_TIZEN_WINDOW_TCORE_WL_H_ +#define EMBEDDER_TIZEN_WINDOW_TCORE_WL_H_ + +#include +#include +#include + +#include +#include +#include + +#include "flutter/shell/platform/tizen/tizen_window.h" + +namespace flutter { + +class TizenWindowTcoreWl : public TizenWindow { + public: + TizenWindowTcoreWl(TizenGeometry geometry, + bool transparent, + bool focusable, + bool top_level, + bool pointing_device_support, + bool floating_menu_support, + void* window_handle, + bool is_vulkan); + + ~TizenWindowTcoreWl(); + + TizenGeometry GetGeometry() override; + + bool SetGeometry(TizenGeometry geometry) override; + + TizenGeometry GetScreenGeometry() override; + + void* GetRenderTarget() override; + + void* GetRenderTargetDisplay() override { return wl2_display_; } + + void* GetNativeHandle() override { return tcore_wl_window_; } + + int32_t GetRotation() override; + + int32_t GetDpi() override; + + uintptr_t GetWindowId() override; + + uint32_t GetResourceId() override; + + void SetPreferredOrientations(const std::vector& rotations) override; + + void BindKeys(const std::vector& keys) override; + + void Show() override; + + void UpdateFlutterCursor(const std::string& kind) override; + + void ActivateWindow() override; + + void RaiseWindow() override; + + void LowerWindow() override; + + private: + bool CreateWindow(void* window_handle); + + void DestroyWindow(); + + void SetWindowOptions(); + + void EnableCursor(); + +#ifdef TV_PROFILE + void SetPointingDeviceSupport(); + + void SetFloatingMenuSupport(); + + void ShowUnsupportedToast(); +#endif + + void RegisterEventHandlers(); + + void AddEventListener(tizen_core_wl_event_type_e type, + tizen_core_wl_event_cb callback); + + void UnregisterEventHandlers(); + + void SetNotificationLevel(int level); + + void PrepareInputMethod(); + + tizen_core_wl_display_h tcore_wl_display_ = nullptr; + tizen_core_wl_window_h tcore_wl_window_ = nullptr; + tizen_core_wl_egl_window_h tcore_wl_egl_window_ = nullptr; + tizen_core_event_h tcore_wl_event_ = nullptr; + wl_display* wl2_display_ = nullptr; + wl_surface* wl2_surface_ = nullptr; + std::vector tcore_event_listeners_; + uint32_t resource_id_ = 0; + +#ifdef TV_PROFILE + bool pointing_device_support_ = true; + bool floating_menu_support_ = true; + bool show_unsupported_toast_ = false; +#endif + bool is_vulkan_ = false; +}; + +} // namespace flutter + +#endif // EMBEDDER_TIZEN_WINDOW_TCORE_WL_H_ diff --git a/flutter/shell/platform/tizen/tizen_window_util.cc b/flutter/shell/platform/tizen/tizen_window_util.cc new file mode 100644 index 00000000..ed103d4d --- /dev/null +++ b/flutter/shell/platform/tizen/tizen_window_util.cc @@ -0,0 +1,117 @@ +// Copyright 2026 Samsung Electronics Co., Ltd. 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/tizen/tizen_window_util.h" + +#ifdef TV_PROFILE + +#include + +#include "flutter/shell/platform/tizen/logger.h" + +namespace flutter { + +namespace { + +constexpr char kLibraryName[] = "libvd-win-util.so"; + +} // namespace + +TizenWindowUtil::TizenWindowUtil() { + handle_ = dlopen(kLibraryName, RTLD_LAZY); + if (!handle_) { + FT_LOG(Error) << "Could not open a shared library " << kLibraryName << "."; + return; + } + + *(void**)(&cursor_module_initialize_) = Resolve("CursorModule_Initialize"); + *(void**)(&cursor_set_config_) = Resolve("Cursor_Set_Config"); + *(void**)(&cursor_module_finalize_) = Resolve("CursorModule_Finalize"); + *(void**)(&mouse_pointer_support_) = Resolve("Mouse_Pointer_Support"); + *(void**)(&mouse_pointer_not_allow_) = Resolve("Mouse_Pointer_Not_Allow"); + *(void**)(&unsupported_toast_launch_) = Resolve("Unsupported_Toast_Launch"); +} + +TizenWindowUtil::~TizenWindowUtil() { + if (handle_) { + dlclose(handle_); + handle_ = nullptr; + } +} + +void* TizenWindowUtil::Resolve(const char* name) { + void* symbol = dlsym(handle_, name); + if (!symbol) { + FT_LOG(Error) << "Could not load the symbol " << name << " from " + << kLibraryName << "."; + } + return symbol; +} + +bool TizenWindowUtil::InitializeCursorModule(wl_display* display, + wl_registry* registry, + wl_seat* seat, + unsigned int cursor_global_id) { + if (!cursor_module_initialize_) { + return false; + } + if (!cursor_module_initialize_(display, registry, seat, cursor_global_id)) { + FT_LOG(Error) << "Failed to initialize the cursor module."; + return false; + } + return true; +} + +bool TizenWindowUtil::SetCursorConfig(wl_surface* surface, + uint32_t config_type, + void* data) { + if (!cursor_set_config_) { + return false; + } + if (!cursor_set_config_(surface, config_type, data)) { + FT_LOG(Error) << "Failed to set a cursor config value."; + return false; + } + return true; +} + +void TizenWindowUtil::FinalizeCursorModule() { + if (cursor_module_finalize_) { + cursor_module_finalize_(); + } +} + +bool TizenWindowUtil::SetMousePointerSupport(bool enable, + void* native_window) { + if (!mouse_pointer_support_) { + return false; + } + mouse_pointer_support_(enable ? kMouseSupportEnable : kMouseSupportDisable, + native_window); + return true; +} + +bool TizenWindowUtil::SetMousePointerNotAllow(bool not_allow, + void* native_window) { + if (!mouse_pointer_not_allow_) { + return false; + } + mouse_pointer_not_allow_(not_allow, native_window); + return true; +} + +bool TizenWindowUtil::LaunchUnsupportedToast(DeviceType type, + bool show, + bool enable, + void* native_window) { + if (!unsupported_toast_launch_) { + return false; + } + unsupported_toast_launch_(type, show, enable, native_window); + return true; +} + +} // namespace flutter + +#endif // TV_PROFILE diff --git a/flutter/shell/platform/tizen/tizen_window_util.h b/flutter/shell/platform/tizen/tizen_window_util.h new file mode 100644 index 00000000..5216e485 --- /dev/null +++ b/flutter/shell/platform/tizen/tizen_window_util.h @@ -0,0 +1,96 @@ +// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef EMBEDDER_TIZEN_WINDOW_UTIL_H_ +#define EMBEDDER_TIZEN_WINDOW_UTIL_H_ + +#ifdef TV_PROFILE + +#include + +struct wl_display; +struct wl_registry; +struct wl_seat; +struct wl_surface; + +namespace flutter { + +// Loads the TV-specific library libvd-win-util.so and exposes the subset of +// its cursor / mouse-pointer / toast entry points that the window backends +// need. +// +// dlopen is used because neither the library nor its headers +// (vd-win-util's cursor_module.h) are present in the rootstrap. The symbols +// are internal to the Samsung TV platform and may be renamed or removed in +// future releases, so every accessor tolerates a missing library or symbol by +// logging and returning false. Callers must keep working with the +// corresponding feature disabled. +// +// The library is opened for the lifetime of the instance. Create one where the +// original per-call dlopen used to be; a single instance must stay alive +// across an InitializeCursorModule / SetCursorConfig / FinalizeCursorModule +// sequence. +class TizenWindowUtil { + public: + // Values are defined by libvd-win-util and must not be renumbered. + enum MouseSupport { kMouseSupportDisable = 0, kMouseSupportEnable = 1 }; + enum DeviceType { kDeviceTypeMouse = 3, kDeviceTypeTouch = 4 }; + + // The config_type 1 refers to TIZEN_CURSOR_CONFIG_CURSOR_AVAILABLE defined + // in the TV extension protocol tizen-extension-tv.xml. + static constexpr uint32_t kCursorConfigCursorAvailable = 1; + + TizenWindowUtil(); + ~TizenWindowUtil(); + + // Prevent copying. + TizenWindowUtil(const TizenWindowUtil&) = delete; + TizenWindowUtil& operator=(const TizenWindowUtil&) = delete; + + // Returns true if the library and all required symbols were resolved. + bool IsValid() const { return handle_ != nullptr; } + + bool InitializeCursorModule(wl_display* display, + wl_registry* registry, + wl_seat* seat, + unsigned int cursor_global_id); + bool SetCursorConfig(wl_surface* surface, uint32_t config_type, void* data); + void FinalizeCursorModule(); + + // |native_window| is the backend-specific window handle + // (Ecore_Wl2_Window* or tizen_core_wl_window_h). + bool SetMousePointerSupport(bool enable, void* native_window); + bool SetMousePointerNotAllow(bool not_allow, void* native_window); + bool LaunchUnsupportedToast(DeviceType type, + bool show, + bool enable, + void* native_window); + + private: + // Resolves |name| from the library, logging on failure. + void* Resolve(const char* name); + + void* handle_ = nullptr; + + int (*cursor_module_initialize_)(wl_display* display, + wl_registry* registry, + wl_seat* seat, + unsigned int id) = nullptr; + int (*cursor_set_config_)(wl_surface* surface, + uint32_t config_type, + void* data) = nullptr; + void (*cursor_module_finalize_)(void) = nullptr; + int (*mouse_pointer_support_)(int type, void* window) = nullptr; + int (*mouse_pointer_not_allow_)(int enable, void* window) = nullptr; + void (*unsupported_toast_launch_)(int type, + int show, + int enable, + void* window) = nullptr; +}; + +} // namespace flutter + +#endif // TV_PROFILE + +#endif // EMBEDDER_TIZEN_WINDOW_UTIL_H_ diff --git a/tools/generate_sysroot.py b/tools/generate_sysroot.py index 21cab37f..4afe988c 100755 --- a/tools/generate_sysroot.py +++ b/tools/generate_sysroot.py @@ -95,6 +95,18 @@ 'wayland-devel', ] +# Required for --use-tcore builds. Only available for Tizen 10.0 and above. +tcore_packages = [ + 'tizen-core', + 'tizen-core-devel', + 'tizen-core-imf', + 'tizen-core-imf-devel', + 'tizen-core-wl', + 'tizen-core-wl-devel', + 'libwayland-egl-tizen', + 'libwayland-egl-tizen-devel', +] + def generate_sysroot(sysroot: Path, api_version: float, arch: str, quiet=False): target = 'standard' @@ -137,6 +149,8 @@ def generate_sysroot(sysroot: Path, api_version: float, arch: str, quiet=False): existing_rpms = [f for f in download_path.iterdir() if f.suffix == '.rpm'] packages = base_packages + unified_packages + if api_version > 10.0: + packages += tcore_packages for package in packages: quoted = urllib.parse.quote(package) diff --git a/tools/gn b/tools/gn index 7038677f..aba985c7 100755 --- a/tools/gn +++ b/tools/gn @@ -14,6 +14,9 @@ from pathlib import Path def get_out_dir(args): target_dir = ['tizen', args.target_cpu] + if args.use_tcore: + target_dir.append('tcore') + if args.unoptimized: target_dir.append('unopt') @@ -78,6 +81,7 @@ def to_gn_args(args): gn_args['clang_version'] = get_clang_version(args.target_toolchain) gn_args['api_version'] = args.api_version gn_args['use_system_cxx'] = not args.no_system_cxx + gn_args['use_tcore'] = args.use_tcore for key, val in gn_args.items(): if isinstance(val, bool): @@ -104,6 +108,8 @@ def parse_args(args): parser.add_argument('--api-version', default='6.0', type=str) parser.add_argument('--no-system-cxx', default=False, action='store_true') + parser.add_argument('--use-tcore', default=False, action='store_true', + help='Use tizen-core-wl API instead of ecore') parser.add_argument('--target-dir', type=str) parser.add_argument('--verbose', default=False, action='store_true') @@ -114,6 +120,16 @@ def parse_args(args): def main(): args = parse_args(sys.argv) + if args.use_tcore: + try: + api_value = float(args.api_version) + except ValueError: + api_value = 0.0 + if api_value < 10.0: + sys.exit('--use-tcore requires --api-version 10.0 or higher ' + '(tizen-core packages are only published for Tizen 11+). ' + 'Got --api-version {}.'.format(args.api_version)) + root_dir = Path(__file__).parent.parent.resolve() gn = root_dir / 'third_party' / 'gn' / 'gn' command = [gn, 'gen', '--check', '--export-compile-commands']