diff --git a/flutter/shell/platform/tizen/BUILD.gn b/flutter/shell/platform/tizen/BUILD.gn index d942cdec..3c21911a 100644 --- a/flutter/shell/platform/tizen/BUILD.gn +++ b/flutter/shell/platform/tizen/BUILD.gn @@ -235,6 +235,7 @@ executable("flutter_tizen_unittests") { "flutter_project_bundle_unittests.cc", "flutter_tizen_engine_unittest.cc", "flutter_tizen_texture_registrar_unittests.cc", + "flutter_tizen_view_unittests.cc", ] ldflags = [ "-Wl,--unresolved-symbols=ignore-in-shared-libs" ] diff --git a/flutter/shell/platform/tizen/flutter_tizen.cc b/flutter/shell/platform/tizen/flutter_tizen.cc index a89a2574..91a29171 100644 --- a/flutter/shell/platform/tizen/flutter_tizen.cc +++ b/flutter/shell/platform/tizen/flutter_tizen.cc @@ -167,15 +167,27 @@ void FlutterDesktopEngineNotifyLowMemoryWarning( } void FlutterDesktopEngineNotifyAppIsInactive(FlutterDesktopEngineRef engine) { - EngineFromHandle(engine)->lifecycle_channel()->AppIsInactive(); + flutter::FlutterTizenEngine* tizen_engine = EngineFromHandle(engine); + tizen_engine->lifecycle_channel()->AppIsInactive(); + if (tizen_engine->view()) { + tizen_engine->view()->OnFocus(kUnfocused); + } } void FlutterDesktopEngineNotifyAppIsResumed(FlutterDesktopEngineRef engine) { - EngineFromHandle(engine)->lifecycle_channel()->AppIsResumed(); + flutter::FlutterTizenEngine* tizen_engine = EngineFromHandle(engine); + tizen_engine->lifecycle_channel()->AppIsResumed(); + if (tizen_engine->view()) { + tizen_engine->view()->OnFocus(kFocused); + } } void FlutterDesktopEngineNotifyAppIsPaused(FlutterDesktopEngineRef engine) { - EngineFromHandle(engine)->lifecycle_channel()->AppIsPaused(); + flutter::FlutterTizenEngine* tizen_engine = EngineFromHandle(engine); + tizen_engine->lifecycle_channel()->AppIsPaused(); + if (tizen_engine->view()) { + tizen_engine->view()->OnFocus(kUnfocused); + } } void FlutterDesktopEngineNotifyAppIsDetached(FlutterDesktopEngineRef engine) { diff --git a/flutter/shell/platform/tizen/flutter_tizen_engine.cc b/flutter/shell/platform/tizen/flutter_tizen_engine.cc index 3c3611ea..2de1a11a 100644 --- a/flutter/shell/platform/tizen/flutter_tizen_engine.cc +++ b/flutter/shell/platform/tizen/flutter_tizen_engine.cc @@ -195,6 +195,13 @@ bool FlutterTizenEngine::RunEngine() { auto* engine = static_cast(user_data); engine->OnUpdateSemantics(update); }; + args.view_focus_change_request_callback = + [](const FlutterViewFocusChangeRequest* request, void* user_data) { + auto* engine = static_cast(user_data); + if (engine->view()) { + engine->view()->OnFocusChangeRequest(*request); + } + }; if (IsHeaded() && dynamic_cast(renderer_.get())) { vsync_waiter_ = std::make_unique(this); @@ -341,6 +348,13 @@ void FlutterTizenEngine::SendPointerEvent(const FlutterPointerEvent& event) { embedder_api_.SendPointerEvent(engine_, &event, 1); } +void FlutterTizenEngine::SendViewFocusEvent( + const FlutterViewFocusEvent& event) { + if (engine_) { + embedder_api_.SendViewFocusEvent(engine_, &event); + } +} + void FlutterTizenEngine::SendWindowMetrics(int32_t x, int32_t y, int32_t width, diff --git a/flutter/shell/platform/tizen/flutter_tizen_engine.h b/flutter/shell/platform/tizen/flutter_tizen_engine.h index c6cfa341..ffe34d4d 100644 --- a/flutter/shell/platform/tizen/flutter_tizen_engine.h +++ b/flutter/shell/platform/tizen/flutter_tizen_engine.h @@ -144,6 +144,9 @@ class FlutterTizenEngine { // Informs the engine of an incoming pointer event. void SendPointerEvent(const FlutterPointerEvent& event); + // Informs the engine of a native view focus change. + void SendViewFocusEvent(const FlutterViewFocusEvent& event); + // Sends a window metrics update to the Flutter engine using current window // dimensions in physical void SendWindowMetrics(int32_t x, diff --git a/flutter/shell/platform/tizen/flutter_tizen_engine_unittest.cc b/flutter/shell/platform/tizen/flutter_tizen_engine_unittest.cc index 55978b69..15665584 100644 --- a/flutter/shell/platform/tizen/flutter_tizen_engine_unittest.cc +++ b/flutter/shell/platform/tizen/flutter_tizen_engine_unittest.cc @@ -65,6 +65,7 @@ TEST_F(FlutterTizenEngineTest, RunDoesExpectedInitialization) { EXPECT_STREQ(args->icu_data_path, "/foo/icudtl.dat"); EXPECT_EQ(args->dart_entrypoint_argc, 0); EXPECT_NE(args->platform_message_callback, nullptr); + EXPECT_NE(args->view_focus_change_request_callback, nullptr); EXPECT_NE(args->custom_task_runners, nullptr); EXPECT_EQ(args->custom_task_runners->platform_task_runner, args->custom_task_runners->ui_task_runner); diff --git a/flutter/shell/platform/tizen/flutter_tizen_view.cc b/flutter/shell/platform/tizen/flutter_tizen_view.cc index 030e12db..f748c356 100644 --- a/flutter/shell/platform/tizen/flutter_tizen_view.cc +++ b/flutter/shell/platform/tizen/flutter_tizen_view.cc @@ -151,6 +151,37 @@ void FlutterTizenView::OnRotate(int32_t degree) { SendWindowMetrics(geometry.left, geometry.top, width, height, 0.0); } +void FlutterTizenView::OnFocus(FlutterViewFocusState focus_state) { + if (focus_state == kFocused) { + auto* window = dynamic_cast(tizen_view_.get()); + if (window && !window->focusable()) { + return; + } + } + if (focus_state == last_focus_state_) { + return; + } + last_focus_state_ = focus_state; + FlutterViewFocusEvent event = {}; + event.struct_size = sizeof(event); + event.view_id = view_id_; + event.state = focus_state; + event.direction = FlutterViewFocusDirection::kUndefined; + engine_->SendViewFocusEvent(event); +} + +void FlutterTizenView::OnFocusChangeRequest( + const FlutterViewFocusChangeRequest& request) { + if (request.view_id != view_id_ || request.state != kFocused) { + return; + } + if (auto* window = dynamic_cast(tizen_view_.get())) { + if (window->focusable()) { + window->ActivateWindow(); + } + } +} + FlutterTizenView::PointerState* FlutterTizenView::GetOrCreatePointerState( FlutterPointerDeviceKind device_kind, int32_t device_id) { diff --git a/flutter/shell/platform/tizen/flutter_tizen_view.h b/flutter/shell/platform/tizen/flutter_tizen_view.h index 4533acc8..821ded2d 100644 --- a/flutter/shell/platform/tizen/flutter_tizen_view.h +++ b/flutter/shell/platform/tizen/flutter_tizen_view.h @@ -51,6 +51,10 @@ class FlutterTizenView : public TizenViewEventHandlerDelegate { void OnRotate(int32_t degree) override; + void OnFocus(FlutterViewFocusState focus_state); + + void OnFocusChangeRequest(const FlutterViewFocusChangeRequest& request); + void OnPointerMove(double x, double y, size_t timestamp, @@ -166,6 +170,11 @@ class FlutterTizenView : public TizenViewEventHandlerDelegate { // Keeps track of pointer states. std::unordered_map> pointer_states_; + // The focus state most recently sent to the engine. Views start out + // unfocused. Used to avoid sending duplicate focus events when multiple + // lifecycle transitions map to the same focus state. + FlutterViewFocusState last_focus_state_ = kUnfocused; + // The plugin registrar managing internal plugins. std::unique_ptr internal_plugin_registrar_; diff --git a/flutter/shell/platform/tizen/flutter_tizen_view_unittests.cc b/flutter/shell/platform/tizen/flutter_tizen_view_unittests.cc new file mode 100644 index 00000000..0ac71df2 --- /dev/null +++ b/flutter/shell/platform/tizen/flutter_tizen_view_unittests.cc @@ -0,0 +1,118 @@ +// 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/flutter_tizen_view.h" + +#include + +#include + +#include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" +#include "flutter/shell/platform/tizen/testing/engine_modifier.h" +#include "flutter/shell/platform/tizen/tizen_window.h" +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { +namespace { + +class TestTizenView : public TizenWindow { + public: + TestTizenView() : TizenWindow({}, false, true, false) { + input_method_context_ = std::make_unique(0); + } + + void* GetRenderTarget() override { return nullptr; } + void* GetNativeHandle() override { return nullptr; } + uintptr_t GetWindowId() override { return 0; } + TizenGeometry GetGeometry() override { return {0, 0, 100, 100}; } + bool SetGeometry(TizenGeometry geometry) override { return true; } + int32_t GetDpi() override { return 160; } + uint32_t GetResourceId() override { return 0; } + void UpdateFlutterCursor(const std::string& kind) override {} + void Show() override {} + int32_t GetRotation() override { return 0; } + void SetPreferredOrientations(const std::vector& rotations) override {} + void* GetRenderTargetDisplay() override { return nullptr; } + TizenGeometry GetScreenGeometry() override { return GetGeometry(); } + void BindKeys(const std::vector& keys) override {} + void ActivateWindow() override { activated = true; } + void RaiseWindow() override {} + void LowerWindow() override {} + + void SetFocusable(bool focusable) { focusable_ = focusable; } + + bool activated = false; +}; + +TEST(FlutterTizenViewTest, SendsAndRequestsViewFocus) { + ecore_init(); + { + FlutterDesktopEngineProperties properties = {}; + properties.assets_path = "/foo/flutter_assets"; + properties.icu_data_path = "/foo/icudtl.dat"; + properties.aot_library_path = "/foo/libapp.so"; + + FlutterProjectBundle project(properties); + auto engine = std::make_unique(project); + std::vector sent_events; + EngineModifier modifier(engine.get()); + modifier.SetEngine(reinterpret_cast(1)); + modifier.embedder_api().SendViewFocusEvent = MOCK_ENGINE_PROC( + SendViewFocusEvent, + ([&sent_events](auto engine, const FlutterViewFocusEvent* event) { + sent_events.push_back(*event); + return kSuccess; + })); + modifier.embedder_api().Shutdown = [](auto engine) { return kSuccess; }; + + auto tizen_view = std::make_unique(); + TestTizenView* tizen_view_ptr = tizen_view.get(); + FlutterTizenView view(kImplicitViewId, std::move(tizen_view), + std::move(engine), kEVulkan); + + view.OnFocus(kFocused); + view.OnFocus(kFocused); + ASSERT_EQ(sent_events.size(), 1u); + EXPECT_EQ(sent_events[0].struct_size, sizeof(FlutterViewFocusEvent)); + EXPECT_EQ(sent_events[0].view_id, kImplicitViewId); + EXPECT_EQ(sent_events[0].state, kFocused); + EXPECT_EQ(sent_events[0].direction, kUndefined); + + view.OnFocus(kUnfocused); + view.OnFocus(kUnfocused); + ASSERT_EQ(sent_events.size(), 2u); + EXPECT_EQ(sent_events[1].state, kUnfocused); + + FlutterViewFocusChangeRequest request = { + .struct_size = sizeof(FlutterViewFocusChangeRequest), + .view_id = kImplicitViewId, + .state = kUnfocused, + .direction = kUndefined, + }; + view.OnFocusChangeRequest(request); + EXPECT_FALSE(tizen_view_ptr->activated); + + request.state = kFocused; + request.view_id = kImplicitViewId + 1; + view.OnFocusChangeRequest(request); + EXPECT_FALSE(tizen_view_ptr->activated); + + request.view_id = kImplicitViewId; + view.OnFocusChangeRequest(request); + EXPECT_TRUE(tizen_view_ptr->activated); + + tizen_view_ptr->activated = false; + tizen_view_ptr->SetFocusable(false); + view.OnFocus(kFocused); + EXPECT_EQ(sent_events.size(), 2u); + view.OnFocusChangeRequest(request); + EXPECT_FALSE(tizen_view_ptr->activated); + } + ecore_shutdown(); +} + +} // namespace +} // namespace testing +} // namespace flutter diff --git a/flutter/shell/platform/tizen/testing/engine_modifier.h b/flutter/shell/platform/tizen/testing/engine_modifier.h index 4075ff77..0d2988f8 100644 --- a/flutter/shell/platform/tizen/testing/engine_modifier.h +++ b/flutter/shell/platform/tizen/testing/engine_modifier.h @@ -23,6 +23,10 @@ class EngineModifier { // engine unless overwritten again. FlutterEngineProcTable& embedder_api() { return engine_->embedder_api_; } + void SetEngine(FLUTTER_API_SYMBOL(FlutterEngine) engine) { + engine_->engine_ = engine; + } + private: FlutterTizenEngine* engine_; }; diff --git a/flutter/shell/platform/tizen/tizen_window.h b/flutter/shell/platform/tizen/tizen_window.h index 90395bdc..a77b8508 100644 --- a/flutter/shell/platform/tizen/tizen_window.h +++ b/flutter/shell/platform/tizen/tizen_window.h @@ -36,6 +36,8 @@ class TizenWindow : public TizenViewBase { virtual void LowerWindow() = 0; + bool focusable() { return focusable_; } + protected: explicit TizenWindow(TizenGeometry geometry, bool transparent,