tests: remove legacy test tree
This commit is contained in:
@@ -1,175 +0,0 @@
|
||||
set(EDITOR_UI_UNIT_TEST_SOURCES
|
||||
test_ui_editor_command_dispatcher.cpp
|
||||
test_ui_editor_command_registry.cpp
|
||||
test_ui_editor_dock_host_interaction.cpp
|
||||
test_ui_editor_menu_model.cpp
|
||||
test_ui_editor_menu_session.cpp
|
||||
test_ui_editor_menu_bar.cpp
|
||||
test_ui_editor_menu_popup.cpp
|
||||
test_ui_editor_panel_content_host.cpp
|
||||
test_ui_editor_panel_host_lifecycle.cpp
|
||||
test_ui_editor_panel_input_filter.cpp
|
||||
test_ui_editor_property_grid.cpp
|
||||
test_ui_editor_property_grid_interaction.cpp
|
||||
test_ui_editor_shell_compose.cpp
|
||||
test_ui_editor_shell_interaction.cpp
|
||||
test_ui_editor_collection_primitives.cpp
|
||||
test_ui_editor_drag_drop.cpp
|
||||
test_ui_editor_field_row_layout.cpp
|
||||
test_ui_editor_hosted_field_builders.cpp
|
||||
test_ui_editor_bool_field.cpp
|
||||
test_ui_editor_bool_field_interaction.cpp
|
||||
test_ui_editor_color_field.cpp
|
||||
test_ui_editor_color_field_interaction.cpp
|
||||
test_ui_editor_asset_field.cpp
|
||||
test_ui_editor_asset_field_interaction.cpp
|
||||
test_ui_editor_dock_host.cpp
|
||||
test_ui_editor_inline_rename_session.cpp
|
||||
test_ui_editor_list_view.cpp
|
||||
test_ui_editor_list_view_interaction.cpp
|
||||
test_ui_editor_panel_frame.cpp
|
||||
test_ui_editor_enum_field.cpp
|
||||
test_ui_editor_enum_field_interaction.cpp
|
||||
test_ui_editor_number_field.cpp
|
||||
test_ui_editor_number_field_interaction.cpp
|
||||
test_ui_editor_object_field.cpp
|
||||
test_ui_editor_object_field_interaction.cpp
|
||||
test_ui_editor_text_field.cpp
|
||||
test_ui_editor_text_field_interaction.cpp
|
||||
test_ui_editor_vector2_field.cpp
|
||||
test_ui_editor_vector2_field_interaction.cpp
|
||||
test_ui_editor_vector3_field.cpp
|
||||
test_ui_editor_vector3_field_interaction.cpp
|
||||
test_ui_editor_vector4_field.cpp
|
||||
test_ui_editor_vector4_field_interaction.cpp
|
||||
test_ui_editor_scroll_view.cpp
|
||||
test_ui_editor_scroll_view_interaction.cpp
|
||||
test_ui_editor_status_bar.cpp
|
||||
test_ui_editor_tab_strip.cpp
|
||||
test_ui_editor_tab_strip_interaction.cpp
|
||||
test_ui_editor_tree_panel_behavior.cpp
|
||||
test_ui_editor_tree_view.cpp
|
||||
test_ui_editor_tree_view_interaction.cpp
|
||||
test_ui_editor_viewport_input_bridge.cpp
|
||||
test_ui_editor_viewport_shell.cpp
|
||||
test_ui_editor_viewport_slot.cpp
|
||||
test_ui_editor_workspace_compose.cpp
|
||||
test_ui_editor_workspace_interaction.cpp
|
||||
test_ui_editor_shortcut_manager.cpp
|
||||
test_ui_editor_workspace_controller.cpp
|
||||
test_ui_editor_workspace_layout_persistence.cpp
|
||||
test_ui_editor_workspace_model.cpp
|
||||
test_ui_editor_workspace_splitter_drag_correction.cpp
|
||||
test_ui_editor_workspace_session.cpp
|
||||
test_ui_editor_window_workspace_controller.cpp
|
||||
)
|
||||
|
||||
add_executable(editor_ui_tests ${EDITOR_UI_UNIT_TEST_SOURCES})
|
||||
|
||||
target_link_libraries(editor_ui_tests
|
||||
PRIVATE
|
||||
XCUIEditorLib
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
target_include_directories(editor_ui_tests
|
||||
PRIVATE
|
||||
${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/include
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(editor_ui_tests PRIVATE /utf-8 /FS)
|
||||
set_target_properties(editor_ui_tests PROPERTIES
|
||||
MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>:Embedded>"
|
||||
COMPILE_PDB_NAME "editor_ui_tests-compile"
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb"
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Debug"
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Release"
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY_MINSIZEREL "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/MinSizeRel"
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/RelWithDebInfo"
|
||||
)
|
||||
set_property(TARGET editor_ui_tests PROPERTY
|
||||
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||
endif()
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(editor_ui_tests
|
||||
DISCOVERY_MODE PRE_TEST
|
||||
)
|
||||
|
||||
if(TARGET XCUIEditorAppLib)
|
||||
set(EDITOR_APP_FEATURE_TEST_SOURCES
|
||||
test_editor_project_runtime.cpp
|
||||
test_project_browser_model.cpp
|
||||
test_project_panel.cpp
|
||||
test_hierarchy_scene_binding.cpp
|
||||
test_scene_viewport_render_plan.cpp
|
||||
test_scene_viewport_runtime.cpp
|
||||
)
|
||||
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_inspector_presentation.cpp")
|
||||
list(APPEND EDITOR_APP_FEATURE_TEST_SOURCES
|
||||
test_inspector_presentation.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
list(APPEND EDITOR_APP_FEATURE_TEST_SOURCES
|
||||
test_input_modifier_tracker.cpp
|
||||
test_editor_host_command_bridge.cpp
|
||||
test_editor_shell_asset_validation.cpp
|
||||
test_editor_window_tab_drag_drop_target.cpp
|
||||
test_editor_window_workspace_store.cpp
|
||||
test_structured_editor_shell.cpp
|
||||
test_editor_window_input_routing.cpp
|
||||
test_ui_editor_panel_registry.cpp
|
||||
test_viewport_object_id_picker.cpp
|
||||
)
|
||||
|
||||
add_executable(editor_app_feature_tests
|
||||
${EDITOR_APP_FEATURE_TEST_SOURCES}
|
||||
)
|
||||
|
||||
target_link_libraries(editor_app_feature_tests
|
||||
PRIVATE
|
||||
XCUIEditorAppLib
|
||||
XCUIEditorAppCore
|
||||
XCUIEditorLib
|
||||
XCUIEditorHost
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
target_include_directories(editor_app_feature_tests
|
||||
PRIVATE
|
||||
${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/include
|
||||
${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(editor_app_feature_tests PRIVATE /utf-8 /FS)
|
||||
set_target_properties(editor_app_feature_tests PROPERTIES
|
||||
MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>:Embedded>"
|
||||
COMPILE_PDB_NAME "editor_app_feature_tests-compile"
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb"
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Debug"
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Release"
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY_MINSIZEREL "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/MinSizeRel"
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/RelWithDebInfo"
|
||||
)
|
||||
set_property(TARGET editor_app_feature_tests PROPERTY
|
||||
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||
endif()
|
||||
|
||||
if(WIN32 AND EXISTS "${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll")
|
||||
add_custom_command(TARGET editor_app_feature_tests POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll
|
||||
$<TARGET_FILE_DIR:editor_app_feature_tests>/assimp-vc143-mt.dll
|
||||
)
|
||||
endif()
|
||||
|
||||
gtest_discover_tests(editor_app_feature_tests
|
||||
DISCOVERY_MODE PRE_TEST
|
||||
)
|
||||
endif()
|
||||
@@ -1,235 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Composition/EditorPanelIds.h"
|
||||
#include "Commands/EditorEditCommandRoute.h"
|
||||
#include "Commands/EditorHostCommandBridge.h"
|
||||
#include "State/EditorCommandFocusService.h"
|
||||
#include "State/EditorSession.h"
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Editor::App::EditorActionRoute;
|
||||
using XCEngine::UI::Editor::App::EditorCommandFocusService;
|
||||
using XCEngine::UI::Editor::App::EditorEditCommandRoute;
|
||||
using XCEngine::UI::Editor::App::EditorHostCommandBridge;
|
||||
using XCEngine::UI::Editor::App::EditorSession;
|
||||
using XCEngine::UI::Editor::UIEditorHostCommandDispatchResult;
|
||||
using XCEngine::UI::Editor::UIEditorHostCommandEvaluationResult;
|
||||
|
||||
class StubEditCommandRoute final : public EditorEditCommandRoute {
|
||||
public:
|
||||
UIEditorHostCommandEvaluationResult EvaluateAssetCommand(
|
||||
std::string_view commandId) const override {
|
||||
lastEvaluatedAssetCommandId = std::string(commandId);
|
||||
return assetEvaluationResult;
|
||||
}
|
||||
|
||||
UIEditorHostCommandDispatchResult DispatchAssetCommand(
|
||||
std::string_view commandId) override {
|
||||
lastDispatchedAssetCommandId = std::string(commandId);
|
||||
return assetDispatchResult;
|
||||
}
|
||||
|
||||
UIEditorHostCommandEvaluationResult EvaluateEditCommand(
|
||||
std::string_view commandId) const override {
|
||||
lastEvaluatedCommandId = std::string(commandId);
|
||||
return evaluationResult;
|
||||
}
|
||||
|
||||
UIEditorHostCommandDispatchResult DispatchEditCommand(
|
||||
std::string_view commandId) override {
|
||||
lastDispatchedCommandId = std::string(commandId);
|
||||
return dispatchResult;
|
||||
}
|
||||
|
||||
mutable std::string lastEvaluatedAssetCommandId = {};
|
||||
std::string lastDispatchedAssetCommandId = {};
|
||||
mutable std::string lastEvaluatedCommandId = {};
|
||||
std::string lastDispatchedCommandId = {};
|
||||
UIEditorHostCommandEvaluationResult assetEvaluationResult = {};
|
||||
UIEditorHostCommandDispatchResult assetDispatchResult = {};
|
||||
UIEditorHostCommandEvaluationResult evaluationResult = {};
|
||||
UIEditorHostCommandDispatchResult dispatchResult = {};
|
||||
};
|
||||
|
||||
TEST(EditorHostCommandBridgeTest, HierarchyEditCommandsDelegateToBoundRoute) {
|
||||
EditorSession session = {};
|
||||
EditorCommandFocusService commandFocus = {};
|
||||
commandFocus.ClaimFocus(EditorActionRoute::Hierarchy);
|
||||
|
||||
StubEditCommandRoute hierarchyRoute = {};
|
||||
hierarchyRoute.evaluationResult.executable = true;
|
||||
hierarchyRoute.evaluationResult.message = "Hierarchy route owns rename.";
|
||||
hierarchyRoute.dispatchResult.commandExecuted = true;
|
||||
hierarchyRoute.dispatchResult.message = "Hierarchy rename dispatched.";
|
||||
|
||||
EditorHostCommandBridge bridge = {};
|
||||
bridge.BindSession(session);
|
||||
bridge.BindCommandFocusService(commandFocus);
|
||||
bridge.BindEditCommandRoutes(&hierarchyRoute, nullptr, nullptr);
|
||||
|
||||
const UIEditorHostCommandEvaluationResult evaluation =
|
||||
bridge.EvaluateHostCommand("edit.rename");
|
||||
EXPECT_TRUE(evaluation.executable);
|
||||
EXPECT_EQ(evaluation.message, "Hierarchy route owns rename.");
|
||||
EXPECT_EQ(hierarchyRoute.lastEvaluatedCommandId, "edit.rename");
|
||||
|
||||
const UIEditorHostCommandDispatchResult dispatch =
|
||||
bridge.DispatchHostCommand("edit.rename");
|
||||
EXPECT_TRUE(dispatch.commandExecuted);
|
||||
EXPECT_EQ(dispatch.message, "Hierarchy rename dispatched.");
|
||||
EXPECT_EQ(hierarchyRoute.lastDispatchedCommandId, "edit.rename");
|
||||
}
|
||||
|
||||
TEST(EditorHostCommandBridgeTest, UnsupportedHostCommandsUseHonestMessages) {
|
||||
EditorSession session = {};
|
||||
|
||||
EditorHostCommandBridge bridge = {};
|
||||
bridge.BindSession(session);
|
||||
|
||||
const UIEditorHostCommandEvaluationResult aboutEvaluation =
|
||||
bridge.EvaluateHostCommand("help.about");
|
||||
EXPECT_FALSE(aboutEvaluation.executable);
|
||||
EXPECT_EQ(
|
||||
aboutEvaluation.message,
|
||||
"About dialog is unavailable in the current shell.");
|
||||
|
||||
const UIEditorHostCommandEvaluationResult fileEvaluation =
|
||||
bridge.EvaluateHostCommand("file.save_scene");
|
||||
EXPECT_FALSE(fileEvaluation.executable);
|
||||
EXPECT_EQ(
|
||||
fileEvaluation.message,
|
||||
"Only file.exit has a bound host owner in the current shell.");
|
||||
}
|
||||
|
||||
TEST(EditorHostCommandBridgeTest, AssetCommandsDelegateToProjectRoute) {
|
||||
EditorSession session = {};
|
||||
|
||||
StubEditCommandRoute projectRoute = {};
|
||||
projectRoute.assetEvaluationResult.executable = true;
|
||||
projectRoute.assetEvaluationResult.message = "Project route owns create folder.";
|
||||
projectRoute.assetDispatchResult.commandExecuted = true;
|
||||
projectRoute.assetDispatchResult.message = "Project create folder dispatched.";
|
||||
|
||||
EditorHostCommandBridge bridge = {};
|
||||
bridge.BindSession(session);
|
||||
bridge.BindEditCommandRoutes(nullptr, &projectRoute, nullptr);
|
||||
|
||||
const UIEditorHostCommandEvaluationResult evaluation =
|
||||
bridge.EvaluateHostCommand("assets.create_folder");
|
||||
EXPECT_TRUE(evaluation.executable);
|
||||
EXPECT_EQ(evaluation.message, "Project route owns create folder.");
|
||||
EXPECT_EQ(projectRoute.lastEvaluatedAssetCommandId, "assets.create_folder");
|
||||
|
||||
const UIEditorHostCommandDispatchResult dispatch =
|
||||
bridge.DispatchHostCommand("assets.create_folder");
|
||||
EXPECT_TRUE(dispatch.commandExecuted);
|
||||
EXPECT_EQ(dispatch.message, "Project create folder dispatched.");
|
||||
EXPECT_EQ(projectRoute.lastDispatchedAssetCommandId, "assets.create_folder");
|
||||
}
|
||||
|
||||
TEST(EditorHostCommandBridgeTest, SceneEditCommandsDelegateToBoundSceneRoute) {
|
||||
EditorSession session = {};
|
||||
EditorCommandFocusService commandFocus = {};
|
||||
commandFocus.ClaimFocus(EditorActionRoute::Scene);
|
||||
|
||||
StubEditCommandRoute sceneRoute = {};
|
||||
sceneRoute.evaluationResult.executable = true;
|
||||
sceneRoute.evaluationResult.message = "Scene route owns undo.";
|
||||
sceneRoute.dispatchResult.commandExecuted = true;
|
||||
sceneRoute.dispatchResult.message = "Scene undo dispatched.";
|
||||
|
||||
EditorHostCommandBridge bridge = {};
|
||||
bridge.BindSession(session);
|
||||
bridge.BindCommandFocusService(commandFocus);
|
||||
bridge.BindEditCommandRoutes(nullptr, nullptr, &sceneRoute);
|
||||
|
||||
const UIEditorHostCommandEvaluationResult evaluation =
|
||||
bridge.EvaluateHostCommand("edit.undo");
|
||||
EXPECT_TRUE(evaluation.executable);
|
||||
EXPECT_EQ(evaluation.message, "Scene route owns undo.");
|
||||
EXPECT_EQ(sceneRoute.lastEvaluatedCommandId, "edit.undo");
|
||||
|
||||
const UIEditorHostCommandDispatchResult dispatch =
|
||||
bridge.DispatchHostCommand("edit.undo");
|
||||
EXPECT_TRUE(dispatch.commandExecuted);
|
||||
EXPECT_EQ(dispatch.message, "Scene undo dispatched.");
|
||||
EXPECT_EQ(sceneRoute.lastDispatchedCommandId, "edit.undo");
|
||||
}
|
||||
|
||||
TEST(EditorHostCommandBridgeTest, InspectorEditCommandsDelegateToBoundInspectorRoute) {
|
||||
EditorSession session = {};
|
||||
EditorCommandFocusService commandFocus = {};
|
||||
commandFocus.ClaimFocus(EditorActionRoute::Inspector);
|
||||
|
||||
StubEditCommandRoute inspectorRoute = {};
|
||||
inspectorRoute.evaluationResult.executable = true;
|
||||
inspectorRoute.evaluationResult.message = "Inspector route owns delete.";
|
||||
inspectorRoute.dispatchResult.commandExecuted = true;
|
||||
inspectorRoute.dispatchResult.message = "Inspector delete dispatched.";
|
||||
|
||||
EditorHostCommandBridge bridge = {};
|
||||
bridge.BindSession(session);
|
||||
bridge.BindCommandFocusService(commandFocus);
|
||||
bridge.BindEditCommandRoutes(nullptr, nullptr, nullptr, &inspectorRoute);
|
||||
|
||||
const UIEditorHostCommandEvaluationResult evaluation =
|
||||
bridge.EvaluateHostCommand("edit.delete");
|
||||
EXPECT_TRUE(evaluation.executable);
|
||||
EXPECT_EQ(evaluation.message, "Inspector route owns delete.");
|
||||
EXPECT_EQ(inspectorRoute.lastEvaluatedCommandId, "edit.delete");
|
||||
|
||||
const UIEditorHostCommandDispatchResult dispatch =
|
||||
bridge.DispatchHostCommand("edit.delete");
|
||||
EXPECT_TRUE(dispatch.commandExecuted);
|
||||
EXPECT_EQ(dispatch.message, "Inspector delete dispatched.");
|
||||
EXPECT_EQ(inspectorRoute.lastDispatchedCommandId, "edit.delete");
|
||||
}
|
||||
|
||||
TEST(EditorHostCommandBridgeTest, ActivePanelRouteIsUsedAsFallbackWhenNoExplicitCommandFocusExists) {
|
||||
EditorSession session = {};
|
||||
session.activePanelId = XCEngine::UI::Editor::App::kHierarchyPanelId;
|
||||
|
||||
StubEditCommandRoute hierarchyRoute = {};
|
||||
hierarchyRoute.evaluationResult.executable = true;
|
||||
hierarchyRoute.evaluationResult.message = "Hierarchy route owns rename.";
|
||||
|
||||
EditorHostCommandBridge bridge = {};
|
||||
bridge.BindSession(session);
|
||||
bridge.BindEditCommandRoutes(&hierarchyRoute, nullptr, nullptr);
|
||||
|
||||
const UIEditorHostCommandEvaluationResult evaluation =
|
||||
bridge.EvaluateHostCommand("edit.rename");
|
||||
EXPECT_TRUE(evaluation.executable);
|
||||
EXPECT_EQ(evaluation.message, "Hierarchy route owns rename.");
|
||||
}
|
||||
|
||||
TEST(EditorHostCommandBridgeTest, ExplicitCommandFocusOverridesActivePanelFallback) {
|
||||
EditorSession session = {};
|
||||
session.activePanelId = XCEngine::UI::Editor::App::kProjectPanelId;
|
||||
|
||||
EditorCommandFocusService commandFocus = {};
|
||||
commandFocus.ClaimFocus(EditorActionRoute::Scene);
|
||||
|
||||
StubEditCommandRoute projectRoute = {};
|
||||
projectRoute.evaluationResult.executable = true;
|
||||
projectRoute.evaluationResult.message = "Project route.";
|
||||
|
||||
StubEditCommandRoute sceneRoute = {};
|
||||
sceneRoute.evaluationResult.executable = true;
|
||||
sceneRoute.evaluationResult.message = "Scene route.";
|
||||
|
||||
EditorHostCommandBridge bridge = {};
|
||||
bridge.BindSession(session);
|
||||
bridge.BindCommandFocusService(commandFocus);
|
||||
bridge.BindEditCommandRoutes(nullptr, &projectRoute, &sceneRoute);
|
||||
|
||||
const UIEditorHostCommandEvaluationResult evaluation =
|
||||
bridge.EvaluateHostCommand("edit.undo");
|
||||
EXPECT_TRUE(evaluation.executable);
|
||||
EXPECT_EQ(evaluation.message, "Scene route.");
|
||||
EXPECT_EQ(sceneRoute.lastEvaluatedCommandId, "edit.undo");
|
||||
EXPECT_TRUE(projectRoute.lastEvaluatedCommandId.empty());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,194 +0,0 @@
|
||||
#include "Project/EditorProjectRuntime.h"
|
||||
#include "State/EditorSelectionService.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
namespace {
|
||||
|
||||
class TemporaryRepo final {
|
||||
public:
|
||||
TemporaryRepo() {
|
||||
const auto uniqueSuffix =
|
||||
std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
m_root =
|
||||
std::filesystem::temp_directory_path() /
|
||||
("xcengine_editor_project_runtime_" + std::to_string(uniqueSuffix));
|
||||
}
|
||||
|
||||
~TemporaryRepo() {
|
||||
std::error_code errorCode = {};
|
||||
std::filesystem::remove_all(m_root, errorCode);
|
||||
}
|
||||
|
||||
const std::filesystem::path& Root() const {
|
||||
return m_root;
|
||||
}
|
||||
|
||||
bool CreateDirectory(const std::filesystem::path& relativePath) const {
|
||||
std::error_code errorCode = {};
|
||||
std::filesystem::create_directories(m_root / relativePath, errorCode);
|
||||
return !errorCode;
|
||||
}
|
||||
|
||||
bool WriteFile(
|
||||
const std::filesystem::path& relativePath,
|
||||
std::string_view contents = "test") const {
|
||||
const std::filesystem::path absolutePath = m_root / relativePath;
|
||||
std::error_code errorCode = {};
|
||||
std::filesystem::create_directories(absolutePath.parent_path(), errorCode);
|
||||
if (errorCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream stream(absolutePath, std::ios::binary);
|
||||
if (!stream.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stream << contents;
|
||||
return stream.good();
|
||||
}
|
||||
|
||||
private:
|
||||
std::filesystem::path m_root = {};
|
||||
};
|
||||
|
||||
TEST(EditorProjectRuntimeTests, NavigateToFolderClearsCurrentProjectSelection) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc"));
|
||||
|
||||
EditorProjectRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(repo.Root()));
|
||||
ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scenes"));
|
||||
ASSERT_TRUE(runtime.SetSelection("Assets/Scenes/Main.xc"));
|
||||
|
||||
ASSERT_TRUE(runtime.NavigateToFolder("Assets"));
|
||||
EXPECT_FALSE(runtime.HasSelection());
|
||||
EXPECT_EQ(runtime.GetSelection().kind, EditorSelectionKind::None);
|
||||
}
|
||||
|
||||
TEST(EditorProjectRuntimeTests, OpenSceneItemQueuesSceneOpenRequest) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc"));
|
||||
|
||||
EditorProjectRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(repo.Root()));
|
||||
ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scenes"));
|
||||
|
||||
ASSERT_TRUE(runtime.OpenItem("Assets/Scenes/Main.xc"));
|
||||
const std::optional<std::filesystem::path> openedScene =
|
||||
runtime.ConsumePendingSceneOpenPath();
|
||||
ASSERT_TRUE(openedScene.has_value());
|
||||
EXPECT_EQ(openedScene.value(), (repo.Root() / "project/Assets/Scenes/Main.xc"));
|
||||
EXPECT_FALSE(runtime.ConsumePendingSceneOpenPath().has_value());
|
||||
}
|
||||
|
||||
TEST(EditorProjectRuntimeTests, ResolveCommandTargetsFollowRuntimeSelectionAndCurrentFolder) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc"));
|
||||
|
||||
EditorProjectRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(repo.Root()));
|
||||
ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scenes"));
|
||||
ASSERT_TRUE(runtime.SetSelection("Assets/Scenes/Main.xc"));
|
||||
|
||||
const EditorProjectRuntime::AssetCommandTarget assetTarget =
|
||||
runtime.ResolveAssetCommandTarget();
|
||||
EXPECT_EQ(assetTarget.subjectItemId, "Assets/Scenes/Main.xc");
|
||||
EXPECT_EQ(assetTarget.subjectRelativePath, "Assets/Scenes/Main.xc");
|
||||
ASSERT_NE(assetTarget.containerFolder, nullptr);
|
||||
EXPECT_EQ(assetTarget.containerFolder->itemId, "Assets/Scenes");
|
||||
|
||||
const std::optional<EditorProjectRuntime::EditCommandTarget> editTarget =
|
||||
runtime.ResolveEditCommandTarget();
|
||||
ASSERT_TRUE(editTarget.has_value());
|
||||
EXPECT_EQ(editTarget->itemId, "Assets/Scenes/Main.xc");
|
||||
EXPECT_FALSE(editTarget->directory);
|
||||
|
||||
runtime.ClearSelection();
|
||||
|
||||
const EditorProjectRuntime::AssetCommandTarget folderTarget =
|
||||
runtime.ResolveAssetCommandTarget();
|
||||
EXPECT_EQ(folderTarget.subjectItemId, "Assets/Scenes");
|
||||
EXPECT_EQ(folderTarget.subjectRelativePath, "Assets/Scenes");
|
||||
EXPECT_FALSE(folderTarget.showInExplorerSelectTarget);
|
||||
|
||||
const std::optional<EditorProjectRuntime::EditCommandTarget> folderEditTarget =
|
||||
runtime.ResolveEditCommandTarget();
|
||||
ASSERT_TRUE(folderEditTarget.has_value());
|
||||
EXPECT_EQ(folderEditTarget->itemId, "Assets/Scenes");
|
||||
EXPECT_TRUE(folderEditTarget->directory);
|
||||
EXPECT_FALSE(folderEditTarget->assetsRoot);
|
||||
}
|
||||
|
||||
TEST(EditorProjectRuntimeTests, ResolveEditCommandTargetMarksAssetsRootAndIgnoresBackgroundFallback) {
|
||||
TemporaryRepo repo = {};
|
||||
|
||||
EditorProjectRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(repo.Root()));
|
||||
|
||||
const std::optional<EditorProjectRuntime::EditCommandTarget> rootTarget =
|
||||
runtime.ResolveEditCommandTarget();
|
||||
ASSERT_TRUE(rootTarget.has_value());
|
||||
EXPECT_EQ(rootTarget->itemId, "Assets");
|
||||
EXPECT_TRUE(rootTarget->directory);
|
||||
EXPECT_TRUE(rootTarget->assetsRoot);
|
||||
|
||||
EXPECT_FALSE(runtime.ResolveEditCommandTarget({}, true).has_value());
|
||||
}
|
||||
|
||||
TEST(EditorProjectRuntimeTests, RenameSelectedItemRemapsSelectionAndDeleteClearsIt) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs.meta"));
|
||||
|
||||
EditorProjectRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(repo.Root()));
|
||||
ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scripts"));
|
||||
ASSERT_TRUE(runtime.SetSelection("Assets/Scripts/Player.cs"));
|
||||
|
||||
std::string renamedItemId = {};
|
||||
ASSERT_TRUE(runtime.RenameItem("Assets/Scripts/Player.cs", "Hero", &renamedItemId));
|
||||
EXPECT_EQ(renamedItemId, "Assets/Scripts/Hero.cs");
|
||||
EXPECT_TRUE(runtime.HasSelection());
|
||||
EXPECT_EQ(runtime.GetSelection().itemId, "Assets/Scripts/Hero.cs");
|
||||
EXPECT_EQ(runtime.GetSelection().displayName, "Hero");
|
||||
|
||||
ASSERT_TRUE(runtime.DeleteItem("Assets/Scripts/Hero.cs"));
|
||||
EXPECT_FALSE(runtime.HasSelection());
|
||||
EXPECT_EQ(runtime.GetSelection().kind, EditorSelectionKind::None);
|
||||
}
|
||||
|
||||
TEST(EditorProjectRuntimeTests, BoundSelectionServiceBecomesTheSingleProjectSelectionSource) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs"));
|
||||
|
||||
EditorSelectionService selectionService = {};
|
||||
EditorProjectRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(repo.Root()));
|
||||
runtime.BindSelectionService(&selectionService);
|
||||
ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scripts"));
|
||||
|
||||
ASSERT_TRUE(runtime.SetSelection("Assets/Scripts/Player.cs"));
|
||||
EXPECT_EQ(selectionService.GetSelection().kind, EditorSelectionKind::ProjectItem);
|
||||
EXPECT_EQ(selectionService.GetSelection().itemId, "Assets/Scripts/Player.cs");
|
||||
EXPECT_EQ(runtime.GetSelection().itemId, "Assets/Scripts/Player.cs");
|
||||
|
||||
runtime.ClearSelection();
|
||||
EXPECT_EQ(selectionService.GetSelection().kind, EditorSelectionKind::None);
|
||||
EXPECT_FALSE(runtime.HasSelection());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace XCEngine::UI::Editor::App
|
||||
@@ -1,191 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Composition/EditorShellAssetBuilder.h"
|
||||
|
||||
#include <XCEditor/Shell/UIEditorShellAsset.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::Editor::App::BuildEditorApplicationShellAsset;
|
||||
using XCEngine::UI::Editor::EditorShellAssetValidationCode;
|
||||
using XCEngine::UI::Editor::FindUIEditorPanelDescriptor;
|
||||
using XCEngine::UI::Editor::UIEditorCommandPanelSource;
|
||||
using XCEngine::UI::Editor::UIEditorMenuItemKind;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
|
||||
using XCEngine::UI::Editor::ValidateEditorShellAsset;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIShortcutBinding;
|
||||
using XCEngine::UI::UIShortcutScope;
|
||||
|
||||
XCEngine::UI::Editor::UIEditorWorkspaceNode* FindWorkspacePanelNode(
|
||||
XCEngine::UI::Editor::UIEditorWorkspaceNode& node,
|
||||
std::string_view panelId) {
|
||||
if (node.kind == XCEngine::UI::Editor::UIEditorWorkspaceNodeKind::Panel &&
|
||||
node.panel.panelId == panelId) {
|
||||
return &node;
|
||||
}
|
||||
|
||||
for (auto& child : node.children) {
|
||||
if (auto* panel = FindWorkspacePanelNode(child, panelId); panel != nullptr) {
|
||||
return panel;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIShortcutBinding MakeBinding(std::string commandId, KeyCode keyCode) {
|
||||
UIShortcutBinding binding = {};
|
||||
binding.commandId = std::move(commandId);
|
||||
binding.scope = UIShortcutScope::Global;
|
||||
binding.triggerEventType = UIInputEventType::KeyDown;
|
||||
binding.chord.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
binding.chord.modifiers.control = true;
|
||||
return binding;
|
||||
}
|
||||
|
||||
TEST(EditorShellAssetValidationTest, DefaultShellAssetPassesValidation) {
|
||||
const auto shellAsset = BuildEditorApplicationShellAsset(".");
|
||||
|
||||
const auto validation = ValidateEditorShellAsset(shellAsset);
|
||||
EXPECT_TRUE(validation.IsValid()) << validation.message;
|
||||
EXPECT_TRUE(shellAsset.documentPath.empty());
|
||||
ASSERT_EQ(
|
||||
shellAsset.shellDefinition.workspacePresentations.size(),
|
||||
shellAsset.panelRegistry.panels.size());
|
||||
EXPECT_EQ(
|
||||
shellAsset.shellDefinition.workspacePresentations.front().panelId,
|
||||
shellAsset.panelRegistry.panels.front().panelId);
|
||||
EXPECT_EQ(
|
||||
shellAsset.shellDefinition.workspacePresentations.front().kind,
|
||||
shellAsset.panelRegistry.panels.front().presentationKind);
|
||||
}
|
||||
|
||||
TEST(EditorShellAssetValidationTest, ValidationRejectsWorkspacePanelMissingFromRegistry) {
|
||||
auto shellAsset = BuildEditorApplicationShellAsset(".");
|
||||
|
||||
auto* documentPanel =
|
||||
const_cast<XCEngine::UI::Editor::UIEditorPanelDescriptor*>(
|
||||
FindUIEditorPanelDescriptor(
|
||||
shellAsset.panelRegistry,
|
||||
shellAsset.panelRegistry.panels.front().panelId));
|
||||
ASSERT_NE(documentPanel, nullptr);
|
||||
documentPanel->panelId = "renamed-panel";
|
||||
|
||||
const auto validation = ValidateEditorShellAsset(shellAsset);
|
||||
EXPECT_EQ(validation.code, EditorShellAssetValidationCode::MissingPanelDescriptor)
|
||||
<< validation.message;
|
||||
}
|
||||
|
||||
TEST(EditorShellAssetValidationTest, ValidationRejectsWorkspaceTitleDriftFromRegistry) {
|
||||
auto shellAsset = BuildEditorApplicationShellAsset(".");
|
||||
auto* scenePanel = FindWorkspacePanelNode(shellAsset.workspace.root, "scene");
|
||||
ASSERT_NE(scenePanel, nullptr);
|
||||
shellAsset.workspace.activePanelId = scenePanel->panel.panelId;
|
||||
scenePanel->panel.title = "Drifted Title";
|
||||
|
||||
const auto validation = ValidateEditorShellAsset(shellAsset);
|
||||
EXPECT_EQ(validation.code, EditorShellAssetValidationCode::PanelTitleMismatch);
|
||||
}
|
||||
|
||||
TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidWorkspaceSessionState) {
|
||||
auto shellAsset = BuildEditorApplicationShellAsset(".");
|
||||
ASSERT_FALSE(shellAsset.workspaceSession.panelStates.empty());
|
||||
shellAsset.workspaceSession.panelStates.front().open = false;
|
||||
|
||||
const auto validation = ValidateEditorShellAsset(shellAsset);
|
||||
EXPECT_EQ(validation.code, EditorShellAssetValidationCode::InvalidWorkspaceSession);
|
||||
}
|
||||
|
||||
TEST(EditorShellAssetValidationTest, ValidationRejectsShellPresentationMissingFromRegistry) {
|
||||
auto shellAsset = BuildEditorApplicationShellAsset(".");
|
||||
ASSERT_EQ(
|
||||
shellAsset.shellDefinition.workspacePresentations.size(),
|
||||
shellAsset.panelRegistry.panels.size());
|
||||
shellAsset.shellDefinition.workspacePresentations.front().panelId =
|
||||
"renamed-panel";
|
||||
|
||||
const auto validation = ValidateEditorShellAsset(shellAsset);
|
||||
EXPECT_EQ(
|
||||
validation.code,
|
||||
EditorShellAssetValidationCode::MissingShellPresentationPanelDescriptor);
|
||||
}
|
||||
|
||||
TEST(EditorShellAssetValidationTest, ValidationRejectsDuplicateShellPresentationPanelId) {
|
||||
auto shellAsset = BuildEditorApplicationShellAsset(".");
|
||||
ASSERT_EQ(
|
||||
shellAsset.shellDefinition.workspacePresentations.size(),
|
||||
shellAsset.panelRegistry.panels.size());
|
||||
shellAsset.shellDefinition.workspacePresentations.push_back(
|
||||
shellAsset.shellDefinition.workspacePresentations.front());
|
||||
|
||||
const auto validation = ValidateEditorShellAsset(shellAsset);
|
||||
EXPECT_EQ(
|
||||
validation.code,
|
||||
EditorShellAssetValidationCode::DuplicateShellPresentationPanelId);
|
||||
}
|
||||
|
||||
TEST(EditorShellAssetValidationTest, ValidationRejectsMissingRequiredShellPresentation) {
|
||||
auto shellAsset = BuildEditorApplicationShellAsset(".");
|
||||
shellAsset.shellDefinition.workspacePresentations.clear();
|
||||
|
||||
const auto validation = ValidateEditorShellAsset(shellAsset);
|
||||
EXPECT_EQ(
|
||||
validation.code,
|
||||
EditorShellAssetValidationCode::MissingRequiredShellPresentation);
|
||||
}
|
||||
|
||||
TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidShellMenuModel) {
|
||||
auto shellAsset = BuildEditorApplicationShellAsset(".");
|
||||
shellAsset.shellDefinition.menuModel.menus = {
|
||||
{
|
||||
"window",
|
||||
"Window",
|
||||
{
|
||||
{
|
||||
UIEditorMenuItemKind::Command,
|
||||
"reset-layout",
|
||||
{},
|
||||
"workspace.reset_layout",
|
||||
{},
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const auto validation = ValidateEditorShellAsset(shellAsset);
|
||||
EXPECT_EQ(
|
||||
validation.code,
|
||||
EditorShellAssetValidationCode::InvalidShellMenuModel);
|
||||
}
|
||||
|
||||
TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidShortcutConfiguration) {
|
||||
auto shellAsset = BuildEditorApplicationShellAsset(".");
|
||||
shellAsset.shortcutAsset.bindings.push_back(
|
||||
MakeBinding("missing.command", KeyCode::R));
|
||||
|
||||
const auto validation = ValidateEditorShellAsset(shellAsset);
|
||||
EXPECT_EQ(
|
||||
validation.code,
|
||||
EditorShellAssetValidationCode::InvalidShortcutConfiguration);
|
||||
}
|
||||
|
||||
TEST(EditorShellAssetValidationTest, ValidationRejectsShellPresentationKindMismatch) {
|
||||
auto shellAsset = BuildEditorApplicationShellAsset(".");
|
||||
ASSERT_EQ(
|
||||
shellAsset.shellDefinition.workspacePresentations.size(),
|
||||
shellAsset.panelRegistry.panels.size());
|
||||
shellAsset.shellDefinition.workspacePresentations.front().kind =
|
||||
XCEngine::UI::Editor::UIEditorPanelPresentationKind::ViewportShell;
|
||||
|
||||
const auto validation = ValidateEditorShellAsset(shellAsset);
|
||||
EXPECT_EQ(
|
||||
validation.code,
|
||||
EditorShellAssetValidationCode::ShellPresentationKindMismatch);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,125 +0,0 @@
|
||||
#include <XCEditor/Docking/UIEditorDockHost.h>
|
||||
#include <XCEditor/Shell/UIEditorShellCapturePolicy.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceCompose.h>
|
||||
|
||||
#include "app/Platform/Win32/EditorWindowPointerCapture.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::ShouldStartImmediateUIEditorShellPointerCapture;
|
||||
using XCEngine::UI::Editor::UIEditorShellInteractionFrame;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceViewportComposeFrame;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostSplitterLayout;
|
||||
using XCEngine::UI::Editor::App::CanConsumeEditorWindowChromeHover;
|
||||
using XCEngine::UI::Editor::App::CanRouteEditorWindowBorderlessChromePointerMessages;
|
||||
using XCEngine::UI::Editor::App::CanRouteEditorWindowBorderlessResizePointerMessages;
|
||||
using XCEngine::UI::Editor::App::CanRouteEditorWindowGlobalTabDragPointerMessages;
|
||||
using XCEngine::UI::Editor::App::EditorWindowPointerCaptureOwner;
|
||||
|
||||
UIEditorShellInteractionFrame BuildFrameWithViewportInputRect(
|
||||
const UIRect& inputRect,
|
||||
bool captureOnPointerDownInside = true) {
|
||||
UIEditorShellInteractionFrame frame = {};
|
||||
|
||||
UIEditorWorkspaceViewportComposeFrame viewportFrame = {};
|
||||
viewportFrame.panelId = "scene";
|
||||
viewportFrame.viewportShellModel.spec.inputBridgeConfig.capturePointerOnPointerDownInside =
|
||||
captureOnPointerDownInside;
|
||||
viewportFrame.viewportShellFrame.slotLayout.bounds = inputRect;
|
||||
viewportFrame.viewportShellFrame.slotLayout.inputRect = inputRect;
|
||||
frame.workspaceInteractionFrame.composeFrame.viewportFrames.push_back(std::move(viewportFrame));
|
||||
return frame;
|
||||
}
|
||||
|
||||
TEST(EditorWindowInputRoutingTests, ViewportInputRectStartsImmediateCaptureWhenEnabled) {
|
||||
const UIEditorShellInteractionFrame frame =
|
||||
BuildFrameWithViewportInputRect(UIRect(100.0f, 80.0f, 640.0f, 360.0f));
|
||||
|
||||
EXPECT_TRUE(ShouldStartImmediateUIEditorShellPointerCapture(frame, UIPoint(240.0f, 180.0f)));
|
||||
EXPECT_FALSE(ShouldStartImmediateUIEditorShellPointerCapture(frame, UIPoint(80.0f, 60.0f)));
|
||||
}
|
||||
|
||||
TEST(EditorWindowInputRoutingTests, ViewportInputRectRespectsCaptureConfig) {
|
||||
const UIEditorShellInteractionFrame frame =
|
||||
BuildFrameWithViewportInputRect(
|
||||
UIRect(100.0f, 80.0f, 640.0f, 360.0f),
|
||||
false);
|
||||
|
||||
EXPECT_FALSE(ShouldStartImmediateUIEditorShellPointerCapture(frame, UIPoint(240.0f, 180.0f)));
|
||||
}
|
||||
|
||||
TEST(EditorWindowInputRoutingTests, ViewportTopBarStartsImmediateCaptureWhenInsideShellBounds) {
|
||||
UIEditorShellInteractionFrame frame = {};
|
||||
|
||||
UIEditorWorkspaceViewportComposeFrame viewportFrame = {};
|
||||
viewportFrame.panelId = "scene";
|
||||
viewportFrame.viewportShellModel.spec.inputBridgeConfig.capturePointerOnPointerDownInside =
|
||||
true;
|
||||
viewportFrame.viewportShellFrame.slotLayout.bounds =
|
||||
UIRect(100.0f, 80.0f, 640.0f, 406.0f);
|
||||
viewportFrame.viewportShellFrame.slotLayout.inputRect =
|
||||
UIRect(100.0f, 104.0f, 640.0f, 382.0f);
|
||||
frame.workspaceInteractionFrame.composeFrame.viewportFrames.push_back(std::move(viewportFrame));
|
||||
|
||||
EXPECT_TRUE(ShouldStartImmediateUIEditorShellPointerCapture(frame, UIPoint(240.0f, 92.0f)));
|
||||
}
|
||||
|
||||
TEST(EditorWindowInputRoutingTests, SplitterHandleStartsImmediateCapture) {
|
||||
UIEditorShellInteractionFrame frame = {};
|
||||
|
||||
UIEditorDockHostSplitterLayout splitter = {};
|
||||
splitter.nodeId = "root-split";
|
||||
splitter.handleHitRect = UIRect(400.0f, 0.0f, 12.0f, 720.0f);
|
||||
frame.workspaceInteractionFrame.dockHostFrame.layout.splitters.push_back(std::move(splitter));
|
||||
|
||||
EXPECT_TRUE(ShouldStartImmediateUIEditorShellPointerCapture(frame, UIPoint(405.0f, 220.0f)));
|
||||
EXPECT_FALSE(ShouldStartImmediateUIEditorShellPointerCapture(frame, UIPoint(520.0f, 220.0f)));
|
||||
}
|
||||
|
||||
TEST(EditorWindowInputRoutingTests, PointerCaptureOwnerRoutesPointerStreamToMatchingSubsystem) {
|
||||
EXPECT_TRUE(CanRouteEditorWindowGlobalTabDragPointerMessages(
|
||||
EditorWindowPointerCaptureOwner::GlobalTabDrag,
|
||||
true));
|
||||
EXPECT_FALSE(CanRouteEditorWindowGlobalTabDragPointerMessages(
|
||||
EditorWindowPointerCaptureOwner::Shell,
|
||||
true));
|
||||
EXPECT_TRUE(CanRouteEditorWindowBorderlessResizePointerMessages(
|
||||
EditorWindowPointerCaptureOwner::BorderlessResize));
|
||||
EXPECT_FALSE(CanRouteEditorWindowBorderlessResizePointerMessages(
|
||||
EditorWindowPointerCaptureOwner::Shell));
|
||||
EXPECT_TRUE(CanRouteEditorWindowBorderlessChromePointerMessages(
|
||||
EditorWindowPointerCaptureOwner::BorderlessChrome));
|
||||
EXPECT_FALSE(CanRouteEditorWindowBorderlessChromePointerMessages(
|
||||
EditorWindowPointerCaptureOwner::HostedContent));
|
||||
}
|
||||
|
||||
TEST(EditorWindowInputRoutingTests, ChromeHoverConsumptionStopsWhileShellOrHostedCaptureOwnsPointerStream) {
|
||||
EXPECT_TRUE(CanConsumeEditorWindowChromeHover(
|
||||
EditorWindowPointerCaptureOwner::None,
|
||||
false,
|
||||
false));
|
||||
EXPECT_FALSE(CanConsumeEditorWindowChromeHover(
|
||||
EditorWindowPointerCaptureOwner::Shell,
|
||||
false,
|
||||
false));
|
||||
EXPECT_FALSE(CanConsumeEditorWindowChromeHover(
|
||||
EditorWindowPointerCaptureOwner::HostedContent,
|
||||
false,
|
||||
false));
|
||||
EXPECT_FALSE(CanConsumeEditorWindowChromeHover(
|
||||
EditorWindowPointerCaptureOwner::None,
|
||||
true,
|
||||
false));
|
||||
EXPECT_FALSE(CanConsumeEditorWindowChromeHover(
|
||||
EditorWindowPointerCaptureOwner::None,
|
||||
false,
|
||||
true));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,83 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/Platform/Win32/WindowManager/TabDragDropTarget.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::App::Internal::EditorWindowTabDragDropTarget;
|
||||
using XCEngine::UI::Editor::App::Internal::ResolveEditorWindowTabDragDropTarget;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceDockPlacement;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostTabItemLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostTabStackLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex;
|
||||
|
||||
UIEditorDockHostLayout BuildDockLayout() {
|
||||
UIEditorDockHostLayout layout = {};
|
||||
layout.bounds = UIRect(0.0f, 0.0f, 420.0f, 280.0f);
|
||||
|
||||
UIEditorDockHostTabStackLayout tabStack = {};
|
||||
tabStack.nodeId = "stack-a";
|
||||
tabStack.bounds = UIRect(20.0f, 20.0f, 300.0f, 200.0f);
|
||||
tabStack.items = {
|
||||
UIEditorDockHostTabItemLayout{ "doc-a", "Document A", false },
|
||||
UIEditorDockHostTabItemLayout{ "doc-b", "Document B", true },
|
||||
UIEditorDockHostTabItemLayout{ "doc-c", "Document C", false },
|
||||
};
|
||||
tabStack.tabStripLayout.headerRect = UIRect(20.0f, 20.0f, 180.0f, 24.0f);
|
||||
tabStack.tabStripLayout.tabHeaderRects = {
|
||||
UIRect(20.0f, 20.0f, 40.0f, 24.0f),
|
||||
UIRect(64.0f, 20.0f, 44.0f, 24.0f),
|
||||
UIRect(112.0f, 20.0f, 48.0f, 24.0f),
|
||||
};
|
||||
layout.tabStacks.push_back(std::move(tabStack));
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(EditorWindowTabDragDropTargetTests, ReturnsInvalidTargetOutsideDockHostBounds) {
|
||||
const UIEditorDockHostLayout layout = BuildDockLayout();
|
||||
|
||||
const EditorWindowTabDragDropTarget target =
|
||||
ResolveEditorWindowTabDragDropTarget(layout, UIPoint(460.0f, 40.0f));
|
||||
|
||||
EXPECT_FALSE(target.valid);
|
||||
EXPECT_TRUE(target.nodeId.empty());
|
||||
EXPECT_EQ(target.placement, UIEditorWorkspaceDockPlacement::Center);
|
||||
EXPECT_EQ(target.insertionIndex, UIEditorTabStripInvalidIndex);
|
||||
}
|
||||
|
||||
TEST(EditorWindowTabDragDropTargetTests, HeaderGapResolvesCenterPlacementAndAppendInsertionIndex) {
|
||||
const UIEditorDockHostLayout layout = BuildDockLayout();
|
||||
|
||||
const EditorWindowTabDragDropTarget target =
|
||||
ResolveEditorWindowTabDragDropTarget(layout, UIPoint(190.0f, 32.0f));
|
||||
|
||||
ASSERT_TRUE(target.valid);
|
||||
EXPECT_EQ(target.nodeId, "stack-a");
|
||||
EXPECT_EQ(target.placement, UIEditorWorkspaceDockPlacement::Center);
|
||||
EXPECT_EQ(target.insertionIndex, 3u);
|
||||
}
|
||||
|
||||
TEST(EditorWindowTabDragDropTargetTests, BodyEdgesResolveDirectionalDockPlacement) {
|
||||
const UIEditorDockHostLayout layout = BuildDockLayout();
|
||||
|
||||
EXPECT_EQ(
|
||||
ResolveEditorWindowTabDragDropTarget(layout, UIPoint(24.0f, 120.0f)).placement,
|
||||
UIEditorWorkspaceDockPlacement::Left);
|
||||
EXPECT_EQ(
|
||||
ResolveEditorWindowTabDragDropTarget(layout, UIPoint(314.0f, 120.0f)).placement,
|
||||
UIEditorWorkspaceDockPlacement::Right);
|
||||
EXPECT_EQ(
|
||||
ResolveEditorWindowTabDragDropTarget(layout, UIPoint(120.0f, 48.0f)).placement,
|
||||
UIEditorWorkspaceDockPlacement::Top);
|
||||
EXPECT_EQ(
|
||||
ResolveEditorWindowTabDragDropTarget(layout, UIPoint(120.0f, 214.0f)).placement,
|
||||
UIEditorWorkspaceDockPlacement::Bottom);
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Composition/EditorWindowWorkspaceStore.h"
|
||||
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceController.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceModel.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceQueries.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Editor::App::Internal::EditorWindowWorkspaceStore;
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::ContainsUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::FindUIEditorWindowWorkspaceState;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorWindowWorkspaceOperationStatus;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommand;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandResult;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "inspector", "Inspector", {}, true, true, true },
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.7f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true),
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspaceSingleTabStack(
|
||||
"inspector-panel",
|
||||
"inspector",
|
||||
"Inspector",
|
||||
true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(EditorWindowWorkspaceStoreTest, RegistersPrimaryWindowProjectionAsAuthoritativeState) {
|
||||
EditorWindowWorkspaceStore store(BuildPanelRegistry());
|
||||
auto workspaceController =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
std::string error = {};
|
||||
|
||||
ASSERT_TRUE(store.RegisterWindowProjection("main", true, workspaceController, error))
|
||||
<< error;
|
||||
ASSERT_TRUE(store.HasState());
|
||||
ASSERT_EQ(store.GetWindowSet().primaryWindowId, "main");
|
||||
ASSERT_EQ(store.GetWindowSet().activeWindowId, "main");
|
||||
ASSERT_EQ(store.GetWindowSet().windows.size(), 1u);
|
||||
|
||||
const auto* mainWindow = FindUIEditorWindowWorkspaceState(store.GetWindowSet(), "main");
|
||||
ASSERT_NE(mainWindow, nullptr);
|
||||
EXPECT_EQ(mainWindow->workspace.activePanelId, "doc-a");
|
||||
EXPECT_TRUE(ContainsUIEditorWorkspacePanel(mainWindow->workspace, "doc-b"));
|
||||
}
|
||||
|
||||
TEST(EditorWindowWorkspaceStoreTest, CommitsWindowLocalProjectionBackIntoCentralStore) {
|
||||
EditorWindowWorkspaceStore store(BuildPanelRegistry());
|
||||
auto workspaceController =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
std::string error = {};
|
||||
ASSERT_TRUE(store.RegisterWindowProjection("main", true, workspaceController, error))
|
||||
<< error;
|
||||
|
||||
const UIEditorWorkspaceCommandResult activateResult = workspaceController.Dispatch(
|
||||
UIEditorWorkspaceCommand{
|
||||
UIEditorWorkspaceCommandKind::ActivatePanel,
|
||||
"doc-b",
|
||||
});
|
||||
ASSERT_EQ(activateResult.status, UIEditorWorkspaceCommandStatus::Changed);
|
||||
|
||||
ASSERT_TRUE(store.CommitWindowProjection("main", workspaceController, error))
|
||||
<< error;
|
||||
|
||||
const auto* mainWindow = FindUIEditorWindowWorkspaceState(store.GetWindowSet(), "main");
|
||||
ASSERT_NE(mainWindow, nullptr);
|
||||
EXPECT_EQ(mainWindow->workspace.activePanelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(EditorWindowWorkspaceStoreTest, AppliesCrossWindowMutationAgainstCentralStore) {
|
||||
EditorWindowWorkspaceStore store(BuildPanelRegistry());
|
||||
const auto workspaceController =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
std::string error = {};
|
||||
ASSERT_TRUE(store.RegisterWindowProjection("main", true, workspaceController, error))
|
||||
<< error;
|
||||
|
||||
auto mutationController = store.BuildMutationController();
|
||||
const auto detachResult = mutationController.DetachPanelToNewWindow(
|
||||
"main",
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
"doc-b-window");
|
||||
ASSERT_EQ(detachResult.status, UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
ASSERT_TRUE(store.ValidateWindowSet(mutationController.GetWindowSet(), error))
|
||||
<< error;
|
||||
store.ReplaceWindowSet(mutationController.GetWindowSet());
|
||||
|
||||
ASSERT_EQ(store.GetWindowSet().windows.size(), 2u);
|
||||
EXPECT_EQ(store.GetWindowSet().activeWindowId, "doc-b-window");
|
||||
|
||||
const auto* detachedWindow =
|
||||
FindUIEditorWindowWorkspaceState(store.GetWindowSet(), "doc-b-window");
|
||||
ASSERT_NE(detachedWindow, nullptr);
|
||||
EXPECT_TRUE(ContainsUIEditorWorkspacePanel(detachedWindow->workspace, "doc-b"));
|
||||
}
|
||||
|
||||
TEST(EditorWindowWorkspaceStoreTest, RemovingDetachedWindowRepairsActiveWindowReference) {
|
||||
EditorWindowWorkspaceStore store(BuildPanelRegistry());
|
||||
const auto workspaceController =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
std::string error = {};
|
||||
ASSERT_TRUE(store.RegisterWindowProjection("main", true, workspaceController, error))
|
||||
<< error;
|
||||
|
||||
auto mutationController = store.BuildMutationController();
|
||||
ASSERT_EQ(
|
||||
mutationController.DetachPanelToNewWindow(
|
||||
"main",
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
"doc-b-window").status,
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
ASSERT_TRUE(store.ValidateWindowSet(mutationController.GetWindowSet(), error))
|
||||
<< error;
|
||||
store.ReplaceWindowSet(mutationController.GetWindowSet());
|
||||
|
||||
store.RemoveWindow("doc-b-window", false);
|
||||
|
||||
EXPECT_EQ(store.GetWindowSet().windows.size(), 1u);
|
||||
EXPECT_EQ(store.GetWindowSet().primaryWindowId, "main");
|
||||
EXPECT_EQ(store.GetWindowSet().activeWindowId, "main");
|
||||
EXPECT_EQ(
|
||||
FindUIEditorWindowWorkspaceState(store.GetWindowSet(), "doc-b-window"),
|
||||
nullptr);
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
#include "Features/Hierarchy/HierarchyModel.h"
|
||||
#include "Scene/EditorSceneBridge.h"
|
||||
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Scene/Scene.h>
|
||||
#include <XCEngine/Scene/SceneManager.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::Components::GameObject;
|
||||
using ::XCEngine::Components::Scene;
|
||||
using ::XCEngine::Components::SceneManager;
|
||||
|
||||
class ScopedSceneManagerReset final {
|
||||
public:
|
||||
ScopedSceneManagerReset() {
|
||||
Reset();
|
||||
}
|
||||
|
||||
~ScopedSceneManagerReset() {
|
||||
Reset();
|
||||
}
|
||||
|
||||
private:
|
||||
static void Reset() {
|
||||
SceneManager& manager = SceneManager::Get();
|
||||
const auto scenes = manager.GetAllScenes();
|
||||
for (Scene* scene : scenes) {
|
||||
manager.UnloadScene(scene);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class TemporaryProjectRoot final {
|
||||
public:
|
||||
TemporaryProjectRoot() {
|
||||
const auto uniqueSuffix =
|
||||
std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
m_root =
|
||||
std::filesystem::temp_directory_path() /
|
||||
("xcui_hierarchy_scene_bridge_" + std::to_string(uniqueSuffix));
|
||||
}
|
||||
|
||||
~TemporaryProjectRoot() {
|
||||
std::error_code errorCode = {};
|
||||
std::filesystem::remove_all(m_root, errorCode);
|
||||
}
|
||||
|
||||
const std::filesystem::path& Root() const {
|
||||
return m_root;
|
||||
}
|
||||
|
||||
private:
|
||||
std::filesystem::path m_root = {};
|
||||
};
|
||||
|
||||
TEST(HierarchySceneBindingTests, BuildFromSceneUsesRealGameObjectIds) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
|
||||
Scene* scene = SceneManager::Get().CreateScene("Main");
|
||||
ASSERT_NE(scene, nullptr);
|
||||
SceneManager::Get().SetActiveScene(scene);
|
||||
|
||||
GameObject* root = scene->CreateGameObject("Root");
|
||||
ASSERT_NE(root, nullptr);
|
||||
GameObject* child = scene->CreateGameObject("Child", root);
|
||||
ASSERT_NE(child, nullptr);
|
||||
|
||||
const HierarchyModel model = HierarchyModel::BuildFromScene(scene);
|
||||
const HierarchyNode* rootNode =
|
||||
model.FindNode(MakeEditorGameObjectItemId(root->GetID()));
|
||||
ASSERT_NE(rootNode, nullptr);
|
||||
EXPECT_EQ(rootNode->label, "Root");
|
||||
ASSERT_EQ(rootNode->children.size(), 1u);
|
||||
EXPECT_EQ(
|
||||
rootNode->children.front().nodeId,
|
||||
MakeEditorGameObjectItemId(child->GetID()));
|
||||
EXPECT_EQ(rootNode->children.front().label, "Child");
|
||||
}
|
||||
|
||||
TEST(HierarchySceneBindingTests, DuplicateGameObjectClonesHierarchyIntoScene) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
|
||||
Scene* scene = SceneManager::Get().CreateScene("Main");
|
||||
ASSERT_NE(scene, nullptr);
|
||||
SceneManager::Get().SetActiveScene(scene);
|
||||
|
||||
GameObject* root = scene->CreateGameObject("Root");
|
||||
ASSERT_NE(root, nullptr);
|
||||
GameObject* child = scene->CreateGameObject("Child", root);
|
||||
ASSERT_NE(child, nullptr);
|
||||
|
||||
const std::string duplicateId =
|
||||
DuplicateEditorGameObject(MakeEditorGameObjectItemId(root->GetID()));
|
||||
ASSERT_FALSE(duplicateId.empty());
|
||||
|
||||
const HierarchyModel model = HierarchyModel::BuildFromScene(scene);
|
||||
const HierarchyNode* duplicateNode = model.FindNode(duplicateId);
|
||||
ASSERT_NE(duplicateNode, nullptr);
|
||||
EXPECT_EQ(duplicateNode->label, "Root");
|
||||
ASSERT_EQ(duplicateNode->children.size(), 1u);
|
||||
EXPECT_EQ(duplicateNode->children.front().label, "Child");
|
||||
|
||||
const auto roots = scene->GetRootGameObjects();
|
||||
EXPECT_EQ(roots.size(), 2u);
|
||||
}
|
||||
|
||||
TEST(HierarchySceneBindingTests, RenameGameObjectUpdatesRealSceneAndProjection) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
|
||||
Scene* scene = SceneManager::Get().CreateScene("Main");
|
||||
ASSERT_NE(scene, nullptr);
|
||||
SceneManager::Get().SetActiveScene(scene);
|
||||
|
||||
GameObject* gameObject = scene->CreateGameObject("Camera");
|
||||
ASSERT_NE(gameObject, nullptr);
|
||||
|
||||
const std::string itemId =
|
||||
MakeEditorGameObjectItemId(gameObject->GetID());
|
||||
ASSERT_TRUE(RenameEditorGameObject(itemId, "PlayerCamera"));
|
||||
EXPECT_EQ(gameObject->GetName(), "PlayerCamera");
|
||||
|
||||
const HierarchyModel model = HierarchyModel::BuildFromScene(scene);
|
||||
const HierarchyNode* node = model.FindNode(itemId);
|
||||
ASSERT_NE(node, nullptr);
|
||||
EXPECT_EQ(node->label, "PlayerCamera");
|
||||
}
|
||||
|
||||
TEST(HierarchySceneBindingTests, ReparentAndMoveToRootOperateOnRealScene) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
|
||||
Scene* scene = SceneManager::Get().CreateScene("Main");
|
||||
ASSERT_NE(scene, nullptr);
|
||||
SceneManager::Get().SetActiveScene(scene);
|
||||
|
||||
GameObject* parentA = scene->CreateGameObject("ParentA");
|
||||
ASSERT_NE(parentA, nullptr);
|
||||
GameObject* parentB = scene->CreateGameObject("ParentB");
|
||||
ASSERT_NE(parentB, nullptr);
|
||||
GameObject* child = scene->CreateGameObject("Child", parentA);
|
||||
ASSERT_NE(child, nullptr);
|
||||
|
||||
ASSERT_TRUE(ReparentEditorGameObject(
|
||||
MakeEditorGameObjectItemId(child->GetID()),
|
||||
MakeEditorGameObjectItemId(parentB->GetID())));
|
||||
EXPECT_EQ(child->GetParent(), parentB);
|
||||
|
||||
HierarchyModel model = HierarchyModel::BuildFromScene(scene);
|
||||
const HierarchyNode* parentBNode =
|
||||
model.FindNode(MakeEditorGameObjectItemId(parentB->GetID()));
|
||||
ASSERT_NE(parentBNode, nullptr);
|
||||
ASSERT_EQ(parentBNode->children.size(), 1u);
|
||||
EXPECT_EQ(parentBNode->children.front().label, "Child");
|
||||
|
||||
ASSERT_TRUE(MoveEditorGameObjectToRoot(
|
||||
MakeEditorGameObjectItemId(child->GetID())));
|
||||
EXPECT_EQ(child->GetParent(), nullptr);
|
||||
|
||||
model = HierarchyModel::BuildFromScene(scene);
|
||||
const auto roots = scene->GetRootGameObjects();
|
||||
EXPECT_EQ(roots.size(), 3u);
|
||||
}
|
||||
|
||||
TEST(HierarchySceneBindingTests, EnsureStartupSceneLoadsMainSceneAndSetsActive) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
|
||||
const std::filesystem::path scenePath =
|
||||
projectRoot.Root() / "Assets" / "Scenes" / "Main.xc";
|
||||
std::filesystem::create_directories(scenePath.parent_path());
|
||||
{
|
||||
Scene scene("Main");
|
||||
scene.CreateGameObject("Camera");
|
||||
scene.Save(scenePath.string());
|
||||
}
|
||||
|
||||
const EditorStartupSceneResult result =
|
||||
EnsureEditorStartupScene(projectRoot.Root());
|
||||
EXPECT_TRUE(result.ready);
|
||||
EXPECT_TRUE(result.loadedFromDisk);
|
||||
ASSERT_NE(GetActiveEditorScene(), nullptr);
|
||||
EXPECT_EQ(GetActiveEditorScene()->GetName(), "Main");
|
||||
|
||||
const HierarchyModel model =
|
||||
HierarchyModel::BuildFromScene(GetActiveEditorScene());
|
||||
EXPECT_FALSE(model.Empty());
|
||||
|
||||
SceneManager& sceneManager = SceneManager::Get();
|
||||
const auto scenes = sceneManager.GetAllScenes();
|
||||
for (Scene* scene : scenes) {
|
||||
sceneManager.UnloadScene(scene);
|
||||
}
|
||||
::XCEngine::Resources::ResourceManager::Get().Shutdown();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace XCEngine::UI::Editor::App
|
||||
@@ -1,111 +0,0 @@
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/Platform/Win32/InputModifierTracker.h"
|
||||
|
||||
#include <XCEngine/UI/Types.h>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Editor::Host::InputModifierTracker;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
|
||||
TEST(InputModifierTrackerTest, ControlStatePersistsAcrossChordKeyDownAndClearsOnKeyUp) {
|
||||
InputModifierTracker tracker = {};
|
||||
|
||||
const auto ctrlDown = tracker.ApplyKeyMessage(
|
||||
UIInputEventType::KeyDown,
|
||||
VK_CONTROL,
|
||||
0x001D0001);
|
||||
EXPECT_TRUE(ctrlDown.control);
|
||||
EXPECT_FALSE(ctrlDown.shift);
|
||||
EXPECT_FALSE(ctrlDown.alt);
|
||||
|
||||
const auto chordKeyDown = tracker.ApplyKeyMessage(
|
||||
UIInputEventType::KeyDown,
|
||||
'P',
|
||||
0x00190001);
|
||||
EXPECT_TRUE(chordKeyDown.control);
|
||||
|
||||
const auto ctrlUp = tracker.ApplyKeyMessage(
|
||||
UIInputEventType::KeyUp,
|
||||
VK_CONTROL,
|
||||
static_cast<LPARAM>(0xC01D0001u));
|
||||
EXPECT_FALSE(ctrlUp.control);
|
||||
|
||||
const auto nextKeyDown = tracker.ApplyKeyMessage(
|
||||
UIInputEventType::KeyDown,
|
||||
'P',
|
||||
0x00190001);
|
||||
EXPECT_FALSE(nextKeyDown.control);
|
||||
}
|
||||
|
||||
TEST(InputModifierTrackerTest, PointerModifiersMergeMouseFlagsWithTrackedKeyboardState) {
|
||||
InputModifierTracker tracker = {};
|
||||
tracker.ApplyKeyMessage(
|
||||
UIInputEventType::KeyDown,
|
||||
VK_MENU,
|
||||
0x00380001);
|
||||
|
||||
const auto modifiers = tracker.BuildPointerModifiers(MK_SHIFT | MK_RBUTTON);
|
||||
EXPECT_TRUE(modifiers.shift);
|
||||
EXPECT_TRUE(modifiers.alt);
|
||||
EXPECT_FALSE(modifiers.control);
|
||||
EXPECT_FALSE(modifiers.super);
|
||||
EXPECT_FALSE(modifiers.leftMouse);
|
||||
EXPECT_TRUE(modifiers.rightMouse);
|
||||
}
|
||||
|
||||
TEST(InputModifierTrackerTest, PointerMessagesUpdateTrackedMouseButtonState) {
|
||||
InputModifierTracker tracker = {};
|
||||
|
||||
const auto leftDown = tracker.ApplyPointerMessage(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
UIPointerButton::Left,
|
||||
0u);
|
||||
EXPECT_TRUE(leftDown.leftMouse);
|
||||
EXPECT_TRUE(tracker.GetCurrentModifiers().leftMouse);
|
||||
|
||||
const auto leftUp = tracker.ApplyPointerMessage(
|
||||
UIInputEventType::PointerButtonUp,
|
||||
UIPointerButton::Left,
|
||||
0u);
|
||||
EXPECT_FALSE(leftUp.leftMouse);
|
||||
EXPECT_FALSE(tracker.GetCurrentModifiers().leftMouse);
|
||||
}
|
||||
|
||||
TEST(InputModifierTrackerTest, RightControlIsTrackedIndependentlyFromLeftControl) {
|
||||
InputModifierTracker tracker = {};
|
||||
|
||||
tracker.ApplyKeyMessage(
|
||||
UIInputEventType::KeyDown,
|
||||
VK_CONTROL,
|
||||
static_cast<LPARAM>(0x011D0001u));
|
||||
EXPECT_TRUE(tracker.GetCurrentModifiers().control);
|
||||
|
||||
tracker.ApplyKeyMessage(
|
||||
UIInputEventType::KeyDown,
|
||||
VK_CONTROL,
|
||||
0x001D0001);
|
||||
EXPECT_TRUE(tracker.GetCurrentModifiers().control);
|
||||
|
||||
tracker.ApplyKeyMessage(
|
||||
UIInputEventType::KeyUp,
|
||||
VK_CONTROL,
|
||||
static_cast<LPARAM>(0xC11D0001u));
|
||||
EXPECT_TRUE(tracker.GetCurrentModifiers().control);
|
||||
|
||||
tracker.ApplyKeyMessage(
|
||||
UIInputEventType::KeyUp,
|
||||
VK_CONTROL,
|
||||
static_cast<LPARAM>(0xC01D0001u));
|
||||
EXPECT_FALSE(tracker.GetCurrentModifiers().control);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,316 +0,0 @@
|
||||
#include "Features/Inspector/Components/IInspectorComponentEditor.h"
|
||||
#include "Features/Inspector/Components/InspectorComponentEditorRegistry.h"
|
||||
#include "Features/Inspector/InspectorPresentationModel.h"
|
||||
#include "Features/Inspector/InspectorSubject.h"
|
||||
#include "Scene/EditorSceneRuntime.h"
|
||||
|
||||
#include <XCEngine/Components/CameraComponent.h>
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Scene/Scene.h>
|
||||
#include <XCEngine/Scene/SceneManager.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::Components::GameObject;
|
||||
using ::XCEngine::Components::Scene;
|
||||
using ::XCEngine::Components::SceneManager;
|
||||
using Widgets::UIEditorPropertyGridField;
|
||||
using Widgets::UIEditorPropertyGridSection;
|
||||
|
||||
class ScopedSceneManagerReset final {
|
||||
public:
|
||||
ScopedSceneManagerReset() {
|
||||
Reset();
|
||||
}
|
||||
|
||||
~ScopedSceneManagerReset() {
|
||||
Reset();
|
||||
::XCEngine::Resources::ResourceManager::Get().Shutdown();
|
||||
}
|
||||
|
||||
private:
|
||||
static void Reset() {
|
||||
SceneManager& manager = SceneManager::Get();
|
||||
const auto scenes = manager.GetAllScenes();
|
||||
for (Scene* scene : scenes) {
|
||||
manager.UnloadScene(scene);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class TemporaryProjectRoot final {
|
||||
public:
|
||||
TemporaryProjectRoot() {
|
||||
const auto uniqueSuffix =
|
||||
std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
m_root =
|
||||
std::filesystem::temp_directory_path() /
|
||||
("xcui_inspector_presentation_" + std::to_string(uniqueSuffix));
|
||||
}
|
||||
|
||||
~TemporaryProjectRoot() {
|
||||
std::error_code errorCode = {};
|
||||
std::filesystem::remove_all(m_root, errorCode);
|
||||
}
|
||||
|
||||
const std::filesystem::path& Root() const {
|
||||
return m_root;
|
||||
}
|
||||
|
||||
std::filesystem::path MainScenePath() const {
|
||||
return m_root / "Assets" / "Scenes" / "Main.xc";
|
||||
}
|
||||
|
||||
private:
|
||||
std::filesystem::path m_root = {};
|
||||
};
|
||||
|
||||
void SaveMainScene(const TemporaryProjectRoot& projectRoot) {
|
||||
const std::filesystem::path scenePath = projectRoot.MainScenePath();
|
||||
std::filesystem::create_directories(scenePath.parent_path());
|
||||
|
||||
Scene scene("Main");
|
||||
GameObject* parent = scene.CreateGameObject("Parent");
|
||||
ASSERT_NE(parent, nullptr);
|
||||
ASSERT_NE(parent->GetTransform(), nullptr);
|
||||
parent->GetTransform()->SetLocalPosition(::XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f));
|
||||
parent->GetTransform()->SetLocalScale(::XCEngine::Math::Vector3(4.0f, 5.0f, 6.0f));
|
||||
ASSERT_NE(scene.CreateGameObject("Child", parent), nullptr);
|
||||
ASSERT_NE(parent->AddComponent<::XCEngine::Components::CameraComponent>(), nullptr);
|
||||
scene.Save(scenePath.string());
|
||||
}
|
||||
|
||||
const UIEditorPropertyGridSection* FindSection(
|
||||
const InspectorPresentationModel& model,
|
||||
std::string_view title) {
|
||||
for (const UIEditorPropertyGridSection& section : model.sections) {
|
||||
if (section.title == title) {
|
||||
return §ion;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UIEditorPropertyGridField* FindField(
|
||||
const UIEditorPropertyGridSection& section,
|
||||
std::string_view label) {
|
||||
for (const UIEditorPropertyGridField& field : section.fields) {
|
||||
if (field.label == label) {
|
||||
return &field;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const InspectorPresentationComponentBinding* FindBinding(
|
||||
const InspectorPresentationModel& model,
|
||||
std::string_view typeName) {
|
||||
for (const InspectorPresentationComponentBinding& binding :
|
||||
model.componentBindings) {
|
||||
if (binding.typeName == typeName) {
|
||||
return &binding;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TEST(InspectorPresentationModelTests, EmptySubjectBuildsDefaultEmptyState) {
|
||||
EditorSceneRuntime runtime = {};
|
||||
const InspectorPresentationModel model =
|
||||
BuildInspectorPresentationModel(
|
||||
{},
|
||||
runtime,
|
||||
InspectorComponentEditorRegistry::Get());
|
||||
|
||||
EXPECT_FALSE(model.hasSelection);
|
||||
EXPECT_EQ(model.title, "Nothing selected");
|
||||
EXPECT_EQ(model.subtitle, "Select a hierarchy item or project asset.");
|
||||
EXPECT_TRUE(model.sections.empty());
|
||||
EXPECT_TRUE(model.componentBindings.empty());
|
||||
}
|
||||
|
||||
TEST(InspectorPresentationModelTests, ProjectAssetSubjectBuildsIdentityAndLocationSections) {
|
||||
InspectorSubject subject = {};
|
||||
subject.kind = InspectorSubjectKind::ProjectAsset;
|
||||
subject.source = InspectorSelectionSource::Project;
|
||||
subject.projectAsset.selection.kind = EditorSelectionKind::ProjectItem;
|
||||
subject.projectAsset.selection.itemId = "asset:materials/test";
|
||||
subject.projectAsset.selection.displayName = "TestMaterial";
|
||||
subject.projectAsset.selection.absolutePath =
|
||||
std::filesystem::path("D:/Xuanchi/Main/XCEngine/project/Assets/Materials/Test.mat");
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
const InspectorPresentationModel model =
|
||||
BuildInspectorPresentationModel(
|
||||
subject,
|
||||
runtime,
|
||||
InspectorComponentEditorRegistry::Get());
|
||||
|
||||
ASSERT_TRUE(model.hasSelection);
|
||||
EXPECT_EQ(model.title, "TestMaterial");
|
||||
EXPECT_EQ(model.subtitle, "Asset");
|
||||
ASSERT_EQ(model.sections.size(), 2u);
|
||||
|
||||
const auto* identity = FindSection(model, "Identity");
|
||||
ASSERT_NE(identity, nullptr);
|
||||
ASSERT_EQ(identity->fields.size(), 3u);
|
||||
const auto* typeField = FindField(*identity, "Type");
|
||||
const auto* nameField = FindField(*identity, "Name");
|
||||
const auto* idField = FindField(*identity, "Id");
|
||||
ASSERT_NE(typeField, nullptr);
|
||||
ASSERT_NE(nameField, nullptr);
|
||||
ASSERT_NE(idField, nullptr);
|
||||
EXPECT_EQ(typeField->valueText, "Asset");
|
||||
EXPECT_EQ(nameField->valueText, "TestMaterial");
|
||||
EXPECT_EQ(idField->valueText, "asset:materials/test");
|
||||
|
||||
const auto* location = FindSection(model, "Location");
|
||||
ASSERT_NE(location, nullptr);
|
||||
ASSERT_EQ(location->fields.size(), 1u);
|
||||
const auto* pathField = FindField(*location, "Path");
|
||||
ASSERT_NE(pathField, nullptr);
|
||||
EXPECT_NE(
|
||||
pathField->valueText.find("Assets/Materials/Test.mat"),
|
||||
std::string::npos);
|
||||
}
|
||||
|
||||
TEST(InspectorPresentationModelTests, SceneObjectSubjectBuildsRegisteredComponentSections) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot);
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
Scene* scene = runtime.GetActiveScene();
|
||||
ASSERT_NE(scene, nullptr);
|
||||
GameObject* parent = scene->Find("Parent");
|
||||
ASSERT_NE(parent, nullptr);
|
||||
ASSERT_TRUE(runtime.SetSelection(parent->GetID()));
|
||||
|
||||
const InspectorSubject subject =
|
||||
BuildInspectorSubject(EditorSession{}, runtime);
|
||||
ASSERT_EQ(subject.kind, InspectorSubjectKind::SceneObject);
|
||||
|
||||
const InspectorPresentationModel model =
|
||||
BuildInspectorPresentationModel(
|
||||
subject,
|
||||
runtime,
|
||||
InspectorComponentEditorRegistry::Get());
|
||||
|
||||
ASSERT_TRUE(model.hasSelection);
|
||||
EXPECT_EQ(model.title, "Parent");
|
||||
EXPECT_EQ(model.subtitle, "GameObject");
|
||||
ASSERT_EQ(model.sections.size(), 4u);
|
||||
ASSERT_EQ(model.componentBindings.size(), 2u);
|
||||
|
||||
const auto* identity = FindSection(model, "Identity");
|
||||
ASSERT_NE(identity, nullptr);
|
||||
const auto* sceneTypeField = FindField(*identity, "Type");
|
||||
const auto* sceneNameField = FindField(*identity, "Name");
|
||||
const auto* sceneIdField = FindField(*identity, "Id");
|
||||
ASSERT_NE(sceneTypeField, nullptr);
|
||||
ASSERT_NE(sceneNameField, nullptr);
|
||||
ASSERT_NE(sceneIdField, nullptr);
|
||||
EXPECT_EQ(sceneTypeField->valueText, "GameObject");
|
||||
EXPECT_EQ(sceneNameField->valueText, "Parent");
|
||||
EXPECT_EQ(sceneIdField->valueText, runtime.GetSelectedItemId());
|
||||
|
||||
const auto* hierarchy = FindSection(model, "Hierarchy");
|
||||
ASSERT_NE(hierarchy, nullptr);
|
||||
const auto* childrenField = FindField(*hierarchy, "Children");
|
||||
const auto* parentField = FindField(*hierarchy, "Parent");
|
||||
ASSERT_NE(childrenField, nullptr);
|
||||
ASSERT_NE(parentField, nullptr);
|
||||
EXPECT_EQ(childrenField->valueText, "1");
|
||||
EXPECT_EQ(parentField->valueText, "Scene Root");
|
||||
|
||||
const auto* transform = FindSection(model, "Transform");
|
||||
ASSERT_NE(transform, nullptr);
|
||||
const auto* positionField = FindField(*transform, "Position");
|
||||
const auto* rotationField = FindField(*transform, "Rotation");
|
||||
const auto* scaleField = FindField(*transform, "Scale");
|
||||
ASSERT_NE(positionField, nullptr);
|
||||
ASSERT_NE(rotationField, nullptr);
|
||||
ASSERT_NE(scaleField, nullptr);
|
||||
EXPECT_EQ(positionField->kind, Widgets::UIEditorPropertyGridFieldKind::Vector3);
|
||||
EXPECT_DOUBLE_EQ(positionField->vector3Value.values[0], 1.0);
|
||||
EXPECT_DOUBLE_EQ(positionField->vector3Value.values[1], 2.0);
|
||||
EXPECT_DOUBLE_EQ(positionField->vector3Value.values[2], 3.0);
|
||||
EXPECT_EQ(rotationField->kind, Widgets::UIEditorPropertyGridFieldKind::Vector3);
|
||||
EXPECT_EQ(scaleField->kind, Widgets::UIEditorPropertyGridFieldKind::Vector3);
|
||||
EXPECT_DOUBLE_EQ(scaleField->vector3Value.values[0], 4.0);
|
||||
EXPECT_DOUBLE_EQ(scaleField->vector3Value.values[1], 5.0);
|
||||
EXPECT_DOUBLE_EQ(scaleField->vector3Value.values[2], 6.0);
|
||||
|
||||
const auto* camera = FindSection(model, "Camera");
|
||||
ASSERT_NE(camera, nullptr);
|
||||
const auto* projectionField = FindField(*camera, "Projection");
|
||||
const auto* primaryField = FindField(*camera, "Primary");
|
||||
const auto* clearColorField = FindField(*camera, "Clear Color");
|
||||
ASSERT_NE(projectionField, nullptr);
|
||||
ASSERT_NE(primaryField, nullptr);
|
||||
ASSERT_NE(clearColorField, nullptr);
|
||||
EXPECT_EQ(projectionField->kind, Widgets::UIEditorPropertyGridFieldKind::Enum);
|
||||
EXPECT_EQ(projectionField->enumValue.selectedIndex, 0u);
|
||||
EXPECT_EQ(primaryField->kind, Widgets::UIEditorPropertyGridFieldKind::Bool);
|
||||
EXPECT_TRUE(primaryField->boolValue);
|
||||
EXPECT_EQ(clearColorField->kind, Widgets::UIEditorPropertyGridFieldKind::Color);
|
||||
|
||||
const auto* transformBinding = FindBinding(model, "Transform");
|
||||
const auto* cameraBinding = FindBinding(model, "Camera");
|
||||
ASSERT_NE(transformBinding, nullptr);
|
||||
ASSERT_NE(cameraBinding, nullptr);
|
||||
EXPECT_FALSE(transformBinding->removable);
|
||||
EXPECT_TRUE(cameraBinding->removable);
|
||||
EXPECT_EQ(transformBinding->fieldIds.size(), 3u);
|
||||
EXPECT_GE(cameraBinding->fieldIds.size(), 8u);
|
||||
}
|
||||
|
||||
TEST(InspectorPresentationModelTests, CameraSkyboxMaterialBuildsAssetField) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot);
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
Scene* scene = runtime.GetActiveScene();
|
||||
ASSERT_NE(scene, nullptr);
|
||||
GameObject* parent = scene->Find("Parent");
|
||||
ASSERT_NE(parent, nullptr);
|
||||
auto* camera = parent->GetComponent<::XCEngine::Components::CameraComponent>();
|
||||
ASSERT_NE(camera, nullptr);
|
||||
camera->SetSkyboxEnabled(true);
|
||||
camera->SetSkyboxMaterialPath("Assets/Materials/Skybox.mat");
|
||||
ASSERT_TRUE(runtime.SetSelection(parent->GetID()));
|
||||
|
||||
const InspectorPresentationModel model =
|
||||
BuildInspectorPresentationModel(
|
||||
BuildInspectorSubject(EditorSession{}, runtime),
|
||||
runtime,
|
||||
InspectorComponentEditorRegistry::Get());
|
||||
|
||||
const auto* cameraSection = FindSection(model, "Camera");
|
||||
ASSERT_NE(cameraSection, nullptr);
|
||||
const auto* skyboxMaterialField = FindField(*cameraSection, "Skybox Material");
|
||||
ASSERT_NE(skyboxMaterialField, nullptr);
|
||||
EXPECT_EQ(skyboxMaterialField->kind, Widgets::UIEditorPropertyGridFieldKind::Asset);
|
||||
EXPECT_EQ(
|
||||
skyboxMaterialField->assetValue.assetId,
|
||||
"Assets/Materials/Skybox.mat");
|
||||
EXPECT_EQ(
|
||||
skyboxMaterialField->assetValue.displayName,
|
||||
"Skybox.mat");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace XCEngine::UI::Editor::App
|
||||
@@ -1,262 +0,0 @@
|
||||
#include "Features/Project/ProjectBrowserModel.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
namespace {
|
||||
|
||||
class TemporaryRepo final {
|
||||
public:
|
||||
TemporaryRepo() {
|
||||
const auto uniqueSuffix =
|
||||
std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
m_root =
|
||||
std::filesystem::temp_directory_path() /
|
||||
("xcengine_project_browser_model_" + std::to_string(uniqueSuffix));
|
||||
}
|
||||
|
||||
~TemporaryRepo() {
|
||||
std::error_code errorCode = {};
|
||||
std::filesystem::remove_all(m_root, errorCode);
|
||||
}
|
||||
|
||||
const std::filesystem::path& Root() const {
|
||||
return m_root;
|
||||
}
|
||||
|
||||
bool CreateDirectory(const std::filesystem::path& relativePath) const {
|
||||
std::error_code errorCode = {};
|
||||
std::filesystem::create_directories(m_root / relativePath, errorCode);
|
||||
return !errorCode;
|
||||
}
|
||||
|
||||
bool WriteFile(
|
||||
const std::filesystem::path& relativePath,
|
||||
std::string_view contents = "test") const {
|
||||
const std::filesystem::path absolutePath = m_root / relativePath;
|
||||
std::error_code errorCode = {};
|
||||
std::filesystem::create_directories(absolutePath.parent_path(), errorCode);
|
||||
if (errorCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream stream(absolutePath, std::ios::binary);
|
||||
if (!stream.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stream << contents;
|
||||
return stream.good();
|
||||
}
|
||||
|
||||
private:
|
||||
std::filesystem::path m_root = {};
|
||||
};
|
||||
|
||||
TEST(ProjectBrowserModelTests, ReparentFolderMovesFolderMetaAndRemapsCurrentFolder) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/A/Child"));
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/B"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/A.meta"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/A/Child.meta"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/B.meta"));
|
||||
|
||||
ProjectBrowserModel model = {};
|
||||
model.Initialize(repo.Root());
|
||||
|
||||
ASSERT_TRUE(model.NavigateToFolder("Assets/A/Child"));
|
||||
|
||||
std::string movedFolderId = {};
|
||||
ASSERT_TRUE(model.ReparentFolder("Assets/A", "Assets/B", &movedFolderId));
|
||||
|
||||
EXPECT_EQ(movedFolderId, "Assets/B/A");
|
||||
EXPECT_EQ(model.GetCurrentFolderId(), "Assets/B/A/Child");
|
||||
EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/B/A"));
|
||||
EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/B/A.meta"));
|
||||
EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/A"));
|
||||
EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/A.meta"));
|
||||
}
|
||||
|
||||
TEST(ProjectBrowserModelTests, MoveFolderToRootMovesFolderMetaAndRemapsCurrentFolder) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Parent/Nested"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Parent.meta"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Parent/Nested.meta"));
|
||||
|
||||
ProjectBrowserModel model = {};
|
||||
model.Initialize(repo.Root());
|
||||
|
||||
ASSERT_TRUE(model.NavigateToFolder("Assets/Parent/Nested"));
|
||||
|
||||
std::string movedFolderId = {};
|
||||
ASSERT_TRUE(model.MoveFolderToRoot("Assets/Parent/Nested", &movedFolderId));
|
||||
|
||||
EXPECT_EQ(movedFolderId, "Assets/Nested");
|
||||
EXPECT_EQ(model.GetCurrentFolderId(), "Assets/Nested");
|
||||
EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Nested"));
|
||||
EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Nested.meta"));
|
||||
EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Parent/Nested"));
|
||||
EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Parent/Nested.meta"));
|
||||
}
|
||||
|
||||
TEST(ProjectBrowserModelTests, CreateFolderCreatesUniqueDirectoryUnderTargetFolder) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes"));
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes/New Folder"));
|
||||
|
||||
ProjectBrowserModel model = {};
|
||||
model.Initialize(repo.Root());
|
||||
|
||||
std::string createdFolderId = {};
|
||||
ASSERT_TRUE(model.CreateFolder("Assets/Scenes", "New Folder", &createdFolderId));
|
||||
|
||||
EXPECT_EQ(createdFolderId, "Assets/Scenes/New Folder 1");
|
||||
EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/New Folder 1"));
|
||||
}
|
||||
|
||||
TEST(ProjectBrowserModelTests, CreateMaterialCreatesUniqueMaterialFileAndExposesRelativePath) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Materials"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Materials/New Material.mat"));
|
||||
|
||||
ProjectBrowserModel model = {};
|
||||
model.Initialize(repo.Root());
|
||||
|
||||
std::string createdItemId = {};
|
||||
ASSERT_TRUE(model.CreateMaterial("Assets/Materials", "New Material", &createdItemId));
|
||||
|
||||
EXPECT_EQ(createdItemId, "Assets/Materials/New Material 1.mat");
|
||||
EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Materials/New Material 1.mat"));
|
||||
EXPECT_EQ(
|
||||
model.BuildProjectRelativePath(createdItemId),
|
||||
"Assets/Materials/New Material 1.mat");
|
||||
|
||||
ASSERT_TRUE(model.NavigateToFolder("Assets/Materials"));
|
||||
const ProjectBrowserModel::AssetEntry* createdEntry =
|
||||
model.FindAssetEntry(createdItemId);
|
||||
ASSERT_NE(createdEntry, nullptr);
|
||||
EXPECT_EQ(createdEntry->kind, ProjectBrowserModel::ItemKind::Material);
|
||||
EXPECT_EQ(createdEntry->displayName, "New Material 1");
|
||||
EXPECT_EQ(createdEntry->nameWithExtension, "New Material 1.mat");
|
||||
}
|
||||
|
||||
TEST(ProjectBrowserModelTests, CanMoveItemToFolderRejectsDescendantFolderTargets) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/FolderA/Nested"));
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/FolderB"));
|
||||
|
||||
ProjectBrowserModel model = {};
|
||||
model.Initialize(repo.Root());
|
||||
|
||||
EXPECT_FALSE(model.CanMoveItemToFolder("Assets/FolderA", "Assets/FolderA/Nested"));
|
||||
EXPECT_TRUE(model.CanMoveItemToFolder("Assets/FolderA", "Assets/FolderB"));
|
||||
}
|
||||
|
||||
TEST(ProjectBrowserModelTests, MoveItemToFolderMovesFileMetaAndRefreshesCurrentListing) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts"));
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Archive"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs.meta"));
|
||||
|
||||
ProjectBrowserModel model = {};
|
||||
model.Initialize(repo.Root());
|
||||
ASSERT_TRUE(model.NavigateToFolder("Assets/Scripts"));
|
||||
|
||||
std::string movedItemId = {};
|
||||
ASSERT_TRUE(model.MoveItemToFolder(
|
||||
"Assets/Scripts/Player.cs",
|
||||
"Assets/Archive",
|
||||
&movedItemId));
|
||||
|
||||
EXPECT_EQ(movedItemId, "Assets/Archive/Player.cs");
|
||||
EXPECT_EQ(model.GetCurrentFolderId(), "Assets/Scripts");
|
||||
EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Scripts/Player.cs"));
|
||||
EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Scripts/Player.cs.meta"));
|
||||
EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Archive/Player.cs"));
|
||||
EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Archive/Player.cs.meta"));
|
||||
EXPECT_EQ(model.FindAssetEntry("Assets/Scripts/Player.cs"), nullptr);
|
||||
|
||||
ASSERT_TRUE(model.NavigateToFolder("Assets/Archive"));
|
||||
EXPECT_NE(model.FindAssetEntry("Assets/Archive/Player.cs"), nullptr);
|
||||
}
|
||||
|
||||
TEST(ProjectBrowserModelTests, RenameFilePreservesExtensionAndUpdatesCurrentListing) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc.meta"));
|
||||
|
||||
ProjectBrowserModel model = {};
|
||||
model.Initialize(repo.Root());
|
||||
ASSERT_TRUE(model.NavigateToFolder("Assets/Scenes"));
|
||||
|
||||
std::string renamedItemId = {};
|
||||
ASSERT_TRUE(model.RenameItem("Assets/Scenes/Main.xc", "Gameplay", &renamedItemId));
|
||||
|
||||
EXPECT_EQ(renamedItemId, "Assets/Scenes/Gameplay.xc");
|
||||
EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/Gameplay.xc"));
|
||||
EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/Gameplay.xc.meta"));
|
||||
EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/Main.xc"));
|
||||
EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/Main.xc.meta"));
|
||||
|
||||
const ProjectBrowserModel::AssetEntry* renamedEntry =
|
||||
model.FindAssetEntry("Assets/Scenes/Gameplay.xc");
|
||||
ASSERT_NE(renamedEntry, nullptr);
|
||||
EXPECT_EQ(renamedEntry->displayName, "Gameplay");
|
||||
EXPECT_EQ(renamedEntry->nameWithExtension, "Gameplay.xc");
|
||||
}
|
||||
|
||||
TEST(ProjectBrowserModelTests, DeleteFolderRemovesMetaAndFallsBackCurrentFolder) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Parent/Nested"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Parent.meta"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Parent/Nested.meta"));
|
||||
|
||||
ProjectBrowserModel model = {};
|
||||
model.Initialize(repo.Root());
|
||||
ASSERT_TRUE(model.NavigateToFolder("Assets/Parent/Nested"));
|
||||
|
||||
ASSERT_TRUE(model.DeleteItem("Assets/Parent"));
|
||||
|
||||
EXPECT_EQ(model.GetCurrentFolderId(), "Assets");
|
||||
EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Parent"));
|
||||
EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Parent.meta"));
|
||||
}
|
||||
|
||||
TEST(ProjectBrowserModelTests, AssetEntriesExposeKindAndOpenCapability) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/mesh.fbx"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/readme.txt"));
|
||||
|
||||
ProjectBrowserModel model = {};
|
||||
model.Initialize(repo.Root());
|
||||
|
||||
const ProjectBrowserModel::AssetEntry* sceneEntry =
|
||||
model.FindAssetEntry("Assets/mesh.fbx");
|
||||
ASSERT_NE(sceneEntry, nullptr);
|
||||
EXPECT_EQ(sceneEntry->kind, ProjectBrowserModel::ItemKind::Model);
|
||||
EXPECT_FALSE(sceneEntry->canOpen);
|
||||
|
||||
const ProjectBrowserModel::AssetEntry* fileEntry =
|
||||
model.FindAssetEntry("Assets/readme.txt");
|
||||
ASSERT_NE(fileEntry, nullptr);
|
||||
EXPECT_EQ(fileEntry->kind, ProjectBrowserModel::ItemKind::File);
|
||||
EXPECT_FALSE(fileEntry->canOpen);
|
||||
|
||||
ASSERT_TRUE(model.NavigateToFolder("Assets/Scenes"));
|
||||
const ProjectBrowserModel::AssetEntry* openedSceneEntry =
|
||||
model.FindAssetEntry("Assets/Scenes/Main.xc");
|
||||
ASSERT_NE(openedSceneEntry, nullptr);
|
||||
EXPECT_EQ(openedSceneEntry->kind, ProjectBrowserModel::ItemKind::Scene);
|
||||
EXPECT_TRUE(openedSceneEntry->canOpen);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace XCEngine::UI::Editor::App
|
||||
@@ -1,326 +0,0 @@
|
||||
#include "Features/Project/ProjectPanel.h"
|
||||
#include "Ports/SystemInteractionPort.h"
|
||||
#include "Rendering/Assets/BuiltInIcons.h"
|
||||
|
||||
#include "Composition/EditorPanelIds.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
namespace {
|
||||
|
||||
class FakeSystemInteractionHost final : public Ports::SystemInteractionPort {
|
||||
public:
|
||||
bool CopyTextToClipboard(std::string_view text) override {
|
||||
lastClipboardText = std::string(text);
|
||||
++clipboardCallCount;
|
||||
return clipboardResult;
|
||||
}
|
||||
|
||||
bool RevealPathInFileBrowser(
|
||||
const std::filesystem::path& path,
|
||||
bool selectTarget) override {
|
||||
lastRevealPath = path;
|
||||
lastRevealSelectTarget = selectTarget;
|
||||
++revealCallCount;
|
||||
return revealResult;
|
||||
}
|
||||
|
||||
bool clipboardResult = true;
|
||||
bool revealResult = true;
|
||||
int clipboardCallCount = 0;
|
||||
int revealCallCount = 0;
|
||||
bool lastRevealSelectTarget = false;
|
||||
std::string lastClipboardText = {};
|
||||
std::filesystem::path lastRevealPath = {};
|
||||
};
|
||||
|
||||
class TemporaryRepo final {
|
||||
public:
|
||||
TemporaryRepo() {
|
||||
const auto uniqueSuffix =
|
||||
std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
m_root =
|
||||
std::filesystem::temp_directory_path() /
|
||||
("xcengine_project_panel_" + std::to_string(uniqueSuffix));
|
||||
}
|
||||
|
||||
~TemporaryRepo() {
|
||||
std::error_code errorCode = {};
|
||||
std::filesystem::remove_all(m_root, errorCode);
|
||||
}
|
||||
|
||||
const std::filesystem::path& Root() const {
|
||||
return m_root;
|
||||
}
|
||||
|
||||
bool CreateDirectory(const std::filesystem::path& relativePath) const {
|
||||
std::error_code errorCode = {};
|
||||
std::filesystem::create_directories(m_root / relativePath, errorCode);
|
||||
return !errorCode;
|
||||
}
|
||||
|
||||
bool WriteFile(
|
||||
const std::filesystem::path& relativePath,
|
||||
std::string_view contents = "test") const {
|
||||
const std::filesystem::path absolutePath = m_root / relativePath;
|
||||
std::error_code errorCode = {};
|
||||
std::filesystem::create_directories(absolutePath.parent_path(), errorCode);
|
||||
if (errorCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream stream(absolutePath, std::ios::binary);
|
||||
if (!stream.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stream << contents;
|
||||
return stream.good();
|
||||
}
|
||||
|
||||
private:
|
||||
std::filesystem::path m_root = {};
|
||||
};
|
||||
|
||||
UIEditorPanelContentHostFrame MakeProjectHostFrame() {
|
||||
UIEditorPanelContentHostFrame frame = {};
|
||||
UIEditorPanelContentHostPanelState panelState = {};
|
||||
panelState.panelId = std::string(kProjectPanelId);
|
||||
panelState.kind = UIEditorPanelPresentationKind::HostedContent;
|
||||
panelState.mounted = true;
|
||||
panelState.bounds = ::XCEngine::UI::UIRect(0.0f, 0.0f, 640.0f, 360.0f);
|
||||
frame.panelStates.push_back(std::move(panelState));
|
||||
return frame;
|
||||
}
|
||||
|
||||
::XCEngine::UI::UIInputEvent MakePointerButtonDown(
|
||||
float x,
|
||||
float y,
|
||||
::XCEngine::UI::UIPointerButton button) {
|
||||
::XCEngine::UI::UIInputEvent event = {};
|
||||
event.type = ::XCEngine::UI::UIInputEventType::PointerButtonDown;
|
||||
event.position = ::XCEngine::UI::UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
PanelInputContext MakeFocusedPanelInputContext() {
|
||||
PanelInputContext inputContext = {};
|
||||
inputContext.allowInteraction = true;
|
||||
inputContext.hasInputFocus = true;
|
||||
return inputContext;
|
||||
}
|
||||
|
||||
TEST(ProjectPanelTests, CreateFolderCommandCreatesDirectoryAndQueuesRename) {
|
||||
TemporaryRepo repo = {};
|
||||
|
||||
ProjectPanel panel = {};
|
||||
panel.Initialize(repo.Root());
|
||||
|
||||
const UIEditorHostCommandEvaluationResult evaluation =
|
||||
panel.EvaluateAssetCommand("assets.create_folder");
|
||||
EXPECT_TRUE(evaluation.executable);
|
||||
|
||||
const UIEditorHostCommandDispatchResult dispatch =
|
||||
panel.DispatchAssetCommand("assets.create_folder");
|
||||
EXPECT_TRUE(dispatch.commandExecuted);
|
||||
EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/New Folder"));
|
||||
|
||||
const auto& events = panel.GetFrameEvents();
|
||||
ASSERT_EQ(events.size(), 2u);
|
||||
EXPECT_EQ(events[0].kind, ProjectPanel::EventKind::AssetSelected);
|
||||
EXPECT_EQ(events[0].source, ProjectPanel::EventSource::Command);
|
||||
EXPECT_EQ(events[0].itemId, "Assets/New Folder");
|
||||
EXPECT_EQ(events[1].kind, ProjectPanel::EventKind::RenameRequested);
|
||||
EXPECT_EQ(events[1].source, ProjectPanel::EventSource::Command);
|
||||
EXPECT_EQ(events[1].itemId, "Assets/New Folder");
|
||||
}
|
||||
|
||||
TEST(ProjectPanelTests, CreateMaterialCommandCreatesFileAndQueuesRename) {
|
||||
TemporaryRepo repo = {};
|
||||
|
||||
ProjectPanel panel = {};
|
||||
panel.Initialize(repo.Root());
|
||||
|
||||
const UIEditorHostCommandEvaluationResult evaluation =
|
||||
panel.EvaluateAssetCommand("assets.create_material");
|
||||
EXPECT_TRUE(evaluation.executable);
|
||||
|
||||
const UIEditorHostCommandDispatchResult dispatch =
|
||||
panel.DispatchAssetCommand("assets.create_material");
|
||||
EXPECT_TRUE(dispatch.commandExecuted);
|
||||
EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/New Material.mat"));
|
||||
|
||||
const auto& events = panel.GetFrameEvents();
|
||||
ASSERT_EQ(events.size(), 2u);
|
||||
EXPECT_EQ(events[0].kind, ProjectPanel::EventKind::AssetSelected);
|
||||
EXPECT_EQ(events[0].itemId, "Assets/New Material.mat");
|
||||
EXPECT_EQ(events[1].kind, ProjectPanel::EventKind::RenameRequested);
|
||||
EXPECT_EQ(events[1].itemId, "Assets/New Material.mat");
|
||||
}
|
||||
|
||||
TEST(ProjectPanelTests, BackgroundContextMenuCreateFolderUsesCurrentFolder) {
|
||||
TemporaryRepo repo = {};
|
||||
|
||||
ProjectPanel panel = {};
|
||||
panel.Initialize(repo.Root());
|
||||
|
||||
const UIEditorPanelContentHostFrame hostFrame = MakeProjectHostFrame();
|
||||
panel.Update(
|
||||
hostFrame,
|
||||
{ MakePointerButtonDown(520.0f, 180.0f, ::XCEngine::UI::UIPointerButton::Right) },
|
||||
MakeFocusedPanelInputContext());
|
||||
panel.Update(
|
||||
hostFrame,
|
||||
{ MakePointerButtonDown(520.0f, 194.0f, ::XCEngine::UI::UIPointerButton::Left) },
|
||||
MakeFocusedPanelInputContext());
|
||||
|
||||
EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/New Folder"));
|
||||
const UIEditorHostCommandEvaluationResult renameEvaluation =
|
||||
panel.EvaluateEditCommand("edit.rename");
|
||||
EXPECT_TRUE(renameEvaluation.executable);
|
||||
EXPECT_EQ(renameEvaluation.message, "Rename project item 'New Folder'.");
|
||||
|
||||
const auto& events = panel.GetFrameEvents();
|
||||
ASSERT_FALSE(events.empty());
|
||||
EXPECT_EQ(events.back().kind, ProjectPanel::EventKind::RenameRequested);
|
||||
EXPECT_EQ(events.back().itemId, "Assets/New Folder");
|
||||
}
|
||||
|
||||
TEST(ProjectPanelTests, FolderContextMenuCreateFolderUsesFolderTarget) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(std::filesystem::create_directories(repo.Root() / "project/Assets/FolderA"));
|
||||
|
||||
ProjectPanel panel = {};
|
||||
panel.Initialize(repo.Root());
|
||||
|
||||
const UIEditorPanelContentHostFrame hostFrame = MakeProjectHostFrame();
|
||||
panel.Update(
|
||||
hostFrame,
|
||||
{ MakePointerButtonDown(300.0f, 80.0f, ::XCEngine::UI::UIPointerButton::Right) },
|
||||
MakeFocusedPanelInputContext());
|
||||
panel.Update(
|
||||
hostFrame,
|
||||
{ MakePointerButtonDown(320.0f, 120.0f, ::XCEngine::UI::UIPointerButton::Left) },
|
||||
MakeFocusedPanelInputContext());
|
||||
|
||||
EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/FolderA/New Folder"));
|
||||
}
|
||||
|
||||
TEST(ProjectPanelTests, InjectedRuntimeSelectionDrivesRenameWithoutPanelSelectionSync) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs.meta"));
|
||||
|
||||
EditorProjectRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(repo.Root()));
|
||||
|
||||
ProjectPanel panel = {};
|
||||
panel.SetProjectRuntime(&runtime);
|
||||
|
||||
ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scripts"));
|
||||
ASSERT_TRUE(runtime.SetSelection("Assets/Scripts/Player.cs"));
|
||||
|
||||
const UIEditorHostCommandDispatchResult dispatch =
|
||||
panel.DispatchEditCommand("edit.rename");
|
||||
EXPECT_TRUE(dispatch.commandExecuted);
|
||||
|
||||
const auto& events = panel.GetFrameEvents();
|
||||
ASSERT_EQ(events.size(), 1u);
|
||||
EXPECT_EQ(events[0].kind, ProjectPanel::EventKind::RenameRequested);
|
||||
EXPECT_EQ(events[0].source, ProjectPanel::EventSource::GridPrimary);
|
||||
EXPECT_EQ(events[0].itemId, "Assets/Scripts/Player.cs");
|
||||
}
|
||||
|
||||
TEST(ProjectPanelTests, InjectedRuntimeCurrentFolderDrivesRenameFallbackWithoutTreeSync) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/FolderA"));
|
||||
|
||||
EditorProjectRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(repo.Root()));
|
||||
|
||||
ProjectPanel panel = {};
|
||||
panel.SetProjectRuntime(&runtime);
|
||||
|
||||
ASSERT_TRUE(runtime.NavigateToFolder("Assets/FolderA"));
|
||||
|
||||
const UIEditorHostCommandEvaluationResult evaluation =
|
||||
panel.EvaluateEditCommand("edit.rename");
|
||||
EXPECT_TRUE(evaluation.executable);
|
||||
EXPECT_EQ(evaluation.message, "Rename project item 'FolderA'.");
|
||||
}
|
||||
|
||||
TEST(ProjectPanelTests, BuiltInIconsCanBeConfiguredBeforeRuntimeInitialization) {
|
||||
TemporaryRepo repo = {};
|
||||
|
||||
ProjectPanel panel = {};
|
||||
BuiltInIcons icons = {};
|
||||
|
||||
panel.SetBuiltInIcons(&icons);
|
||||
panel.Initialize(repo.Root());
|
||||
|
||||
const UIEditorHostCommandEvaluationResult evaluation =
|
||||
panel.EvaluateAssetCommand("assets.create_folder");
|
||||
EXPECT_TRUE(evaluation.executable);
|
||||
}
|
||||
|
||||
TEST(ProjectPanelTests, CopyPathCommandUsesInjectedSystemHost) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs"));
|
||||
|
||||
EditorProjectRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(repo.Root()));
|
||||
ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scripts"));
|
||||
ASSERT_TRUE(runtime.SetSelection("Assets/Scripts/Player.cs"));
|
||||
|
||||
ProjectPanel panel = {};
|
||||
FakeSystemInteractionHost systemHost = {};
|
||||
panel.SetProjectRuntime(&runtime);
|
||||
panel.SetSystemInteractionHost(&systemHost);
|
||||
|
||||
const UIEditorHostCommandEvaluationResult evaluation =
|
||||
panel.EvaluateAssetCommand("assets.copy_path");
|
||||
EXPECT_TRUE(evaluation.executable);
|
||||
|
||||
const UIEditorHostCommandDispatchResult dispatch =
|
||||
panel.DispatchAssetCommand("assets.copy_path");
|
||||
EXPECT_TRUE(dispatch.commandExecuted);
|
||||
EXPECT_EQ(systemHost.clipboardCallCount, 1);
|
||||
EXPECT_EQ(systemHost.lastClipboardText, "Assets/Scripts/Player.cs");
|
||||
}
|
||||
|
||||
TEST(ProjectPanelTests, ShowInExplorerCommandUsesInjectedSystemHost) {
|
||||
TemporaryRepo repo = {};
|
||||
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts"));
|
||||
ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs"));
|
||||
|
||||
EditorProjectRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(repo.Root()));
|
||||
ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scripts"));
|
||||
ASSERT_TRUE(runtime.SetSelection("Assets/Scripts/Player.cs"));
|
||||
|
||||
ProjectPanel panel = {};
|
||||
FakeSystemInteractionHost systemHost = {};
|
||||
panel.SetProjectRuntime(&runtime);
|
||||
panel.SetSystemInteractionHost(&systemHost);
|
||||
|
||||
const UIEditorHostCommandDispatchResult dispatch =
|
||||
panel.DispatchAssetCommand("assets.show_in_explorer");
|
||||
EXPECT_TRUE(dispatch.commandExecuted);
|
||||
EXPECT_EQ(systemHost.revealCallCount, 1);
|
||||
EXPECT_EQ(
|
||||
systemHost.lastRevealPath.lexically_normal(),
|
||||
(repo.Root() / "project/Assets/Scripts/Player.cs").lexically_normal());
|
||||
EXPECT_TRUE(systemHost.lastRevealSelectTarget);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace XCEngine::UI::Editor::App
|
||||
@@ -1,246 +0,0 @@
|
||||
#include "Rendering/Viewport/SceneViewportRenderPlan.h"
|
||||
|
||||
#include <XCEngine/Components/CameraComponent.h>
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::RHI::Format;
|
||||
using XCEngine::RHI::RHIResourceView;
|
||||
using XCEngine::RHI::ResourceStates;
|
||||
using XCEngine::RHI::ResourceViewDimension;
|
||||
using XCEngine::RHI::ResourceViewType;
|
||||
using XCEngine::Rendering::RenderPass;
|
||||
using XCEngine::Rendering::RenderPassContext;
|
||||
using XCEngine::Rendering::RenderSurface;
|
||||
using XCEngine::UI::Editor::App::BuildSceneViewportGridPassData;
|
||||
using XCEngine::UI::Editor::App::ApplySceneViewportRenderPlan;
|
||||
using XCEngine::UI::Editor::App::BuildSceneViewportRenderPlan;
|
||||
using XCEngine::UI::Editor::App::MarkSceneViewportRenderSuccess;
|
||||
using XCEngine::UI::Editor::App::SceneViewportGridPassData;
|
||||
using XCEngine::UI::Editor::App::SceneViewportRenderRequest;
|
||||
using XCEngine::UI::Editor::App::SceneViewportSelectionOutlineStyle;
|
||||
using XCEngine::UI::Editor::App::ViewportRenderTargets;
|
||||
|
||||
class DummyResourceView final : public RHIResourceView {
|
||||
public:
|
||||
explicit DummyResourceView(
|
||||
ResourceViewType viewType = ResourceViewType::RenderTarget,
|
||||
Format format = Format::R8G8B8A8_UNorm)
|
||||
: m_viewType(viewType)
|
||||
, m_format(format) {
|
||||
}
|
||||
|
||||
void Shutdown() override {
|
||||
}
|
||||
|
||||
void* GetNativeHandle() override {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsValid() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
ResourceViewType GetViewType() const override {
|
||||
return m_viewType;
|
||||
}
|
||||
|
||||
ResourceViewDimension GetDimension() const override {
|
||||
return ResourceViewDimension::Texture2D;
|
||||
}
|
||||
|
||||
Format GetFormat() const override {
|
||||
return m_format;
|
||||
}
|
||||
|
||||
private:
|
||||
ResourceViewType m_viewType = ResourceViewType::RenderTarget;
|
||||
Format m_format = Format::R8G8B8A8_UNorm;
|
||||
};
|
||||
|
||||
class NoopRenderPass final : public RenderPass {
|
||||
public:
|
||||
const char* GetName() const override {
|
||||
return "NoopRenderPass";
|
||||
}
|
||||
|
||||
bool Execute(const RenderPassContext&) override {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
SceneViewportRenderRequest CreateValidRequest(
|
||||
XCEngine::Components::GameObject& cameraObject) {
|
||||
auto* camera =
|
||||
cameraObject.AddComponent<XCEngine::Components::CameraComponent>();
|
||||
EXPECT_NE(camera, nullptr);
|
||||
EXPECT_NE(cameraObject.GetTransform(), nullptr);
|
||||
cameraObject.GetTransform()->SetPosition(
|
||||
XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f));
|
||||
SceneViewportRenderRequest request = {};
|
||||
request.camera = camera;
|
||||
request.orbitDistance = 9.0f;
|
||||
return request;
|
||||
}
|
||||
|
||||
TEST(SceneViewportRenderPlanTests, BuildRenderPlanCreatesOutlinePassWhenSelectionResourcesExist) {
|
||||
DummyResourceView depthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt);
|
||||
DummyResourceView depthShaderView(ResourceViewType::ShaderResource, Format::D24_UNorm_S8_UInt);
|
||||
DummyResourceView selectionMaskView(ResourceViewType::RenderTarget);
|
||||
DummyResourceView selectionMaskShaderView(ResourceViewType::ShaderResource);
|
||||
|
||||
ViewportRenderTargets targets = {};
|
||||
targets.depthView = &depthView;
|
||||
targets.depthShaderView = &depthShaderView;
|
||||
targets.selectionMaskView = &selectionMaskView;
|
||||
targets.selectionMaskShaderView = &selectionMaskShaderView;
|
||||
|
||||
XCEngine::Components::GameObject cameraObject("SceneCamera");
|
||||
SceneViewportRenderRequest request = CreateValidRequest(cameraObject);
|
||||
request.selectedObjectIds = { 7u, 11u };
|
||||
|
||||
std::size_t gridFactoryCallCount = 0u;
|
||||
std::size_t outlineFactoryCallCount = 0u;
|
||||
const auto result = BuildSceneViewportRenderPlan(
|
||||
targets,
|
||||
request,
|
||||
[&gridFactoryCallCount](const SceneViewportGridPassData& data) {
|
||||
++gridFactoryCallCount;
|
||||
EXPECT_TRUE(data.valid);
|
||||
EXPECT_FLOAT_EQ(data.orbitDistance, 9.0f);
|
||||
return std::make_unique<NoopRenderPass>();
|
||||
},
|
||||
[&outlineFactoryCallCount](
|
||||
ViewportRenderTargets* outlineTargets,
|
||||
const std::vector<std::uint64_t>& selectedObjectIds,
|
||||
const SceneViewportSelectionOutlineStyle& style) {
|
||||
++outlineFactoryCallCount;
|
||||
EXPECT_NE(outlineTargets, nullptr);
|
||||
EXPECT_EQ(selectedObjectIds.size(), 2u);
|
||||
EXPECT_FLOAT_EQ(style.outlineWidthPixels, 2.0f);
|
||||
EXPECT_FALSE(style.debugSelectionMask);
|
||||
return std::make_unique<NoopRenderPass>();
|
||||
});
|
||||
|
||||
EXPECT_EQ(result.plan.postScenePasses.GetPassCount(), 2u);
|
||||
EXPECT_TRUE(result.plan.usesGridPass);
|
||||
EXPECT_TRUE(result.plan.usesSelectionOutline);
|
||||
EXPECT_EQ(gridFactoryCallCount, 1u);
|
||||
EXPECT_EQ(outlineFactoryCallCount, 1u);
|
||||
EXPECT_EQ(result.warningStatusText, nullptr);
|
||||
}
|
||||
|
||||
TEST(SceneViewportRenderPlanTests, BuildRenderPlanWarnsWhenSelectionResourcesAreUnavailable) {
|
||||
ViewportRenderTargets targets = {};
|
||||
XCEngine::Components::GameObject cameraObject("SceneCamera");
|
||||
SceneViewportRenderRequest request = CreateValidRequest(cameraObject);
|
||||
request.selectedObjectIds = { 42u };
|
||||
|
||||
std::size_t gridFactoryCallCount = 0u;
|
||||
const auto result = BuildSceneViewportRenderPlan(
|
||||
targets,
|
||||
request,
|
||||
[&gridFactoryCallCount](const SceneViewportGridPassData& data) {
|
||||
++gridFactoryCallCount;
|
||||
EXPECT_TRUE(data.valid);
|
||||
return std::make_unique<NoopRenderPass>();
|
||||
},
|
||||
[](
|
||||
ViewportRenderTargets*,
|
||||
const std::vector<std::uint64_t>&,
|
||||
const SceneViewportSelectionOutlineStyle&) {
|
||||
return std::make_unique<NoopRenderPass>();
|
||||
});
|
||||
|
||||
EXPECT_EQ(result.plan.postScenePasses.GetPassCount(), 1u);
|
||||
EXPECT_TRUE(result.plan.usesGridPass);
|
||||
EXPECT_FALSE(result.plan.usesSelectionOutline);
|
||||
EXPECT_EQ(gridFactoryCallCount, 1u);
|
||||
EXPECT_STREQ(
|
||||
result.warningStatusText,
|
||||
"Scene selection outline resources are unavailable");
|
||||
}
|
||||
|
||||
TEST(SceneViewportRenderPlanTests, BuildSceneViewportGridPassDataCopiesCameraTransformAndLens) {
|
||||
XCEngine::Components::GameObject cameraObject("SceneCamera");
|
||||
SceneViewportRenderRequest request = CreateValidRequest(cameraObject);
|
||||
|
||||
const SceneViewportGridPassData gridData =
|
||||
BuildSceneViewportGridPassData(request);
|
||||
|
||||
ASSERT_TRUE(gridData.valid);
|
||||
EXPECT_FLOAT_EQ(gridData.cameraPosition.x, 1.0f);
|
||||
EXPECT_FLOAT_EQ(gridData.cameraPosition.y, 2.0f);
|
||||
EXPECT_FLOAT_EQ(gridData.cameraPosition.z, 3.0f);
|
||||
EXPECT_FLOAT_EQ(gridData.verticalFovDegrees, request.camera->GetFieldOfView());
|
||||
EXPECT_FLOAT_EQ(gridData.nearClipPlane, request.camera->GetNearClipPlane());
|
||||
EXPECT_FLOAT_EQ(gridData.farClipPlane, request.camera->GetFarClipPlane());
|
||||
EXPECT_FLOAT_EQ(gridData.orbitDistance, 9.0f);
|
||||
}
|
||||
|
||||
TEST(SceneViewportRenderPlanTests, ApplyRenderPlanAttachesPassesAndMarksRenderStates) {
|
||||
DummyResourceView depthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt);
|
||||
DummyResourceView depthShaderView(ResourceViewType::ShaderResource, Format::D24_UNorm_S8_UInt);
|
||||
DummyResourceView selectionMaskView(ResourceViewType::RenderTarget);
|
||||
DummyResourceView selectionMaskShaderView(ResourceViewType::ShaderResource);
|
||||
DummyResourceView objectIdDepthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt);
|
||||
DummyResourceView objectIdView(ResourceViewType::RenderTarget);
|
||||
|
||||
ViewportRenderTargets targets = {};
|
||||
targets.width = 800u;
|
||||
targets.height = 600u;
|
||||
targets.depthView = &depthView;
|
||||
targets.depthShaderView = &depthShaderView;
|
||||
targets.selectionMaskView = &selectionMaskView;
|
||||
targets.selectionMaskShaderView = &selectionMaskShaderView;
|
||||
targets.objectIdDepthView = &objectIdDepthView;
|
||||
targets.objectIdView = &objectIdView;
|
||||
targets.colorState = ResourceStates::Common;
|
||||
targets.objectIdState = ResourceStates::Common;
|
||||
targets.selectionMaskState = ResourceStates::Common;
|
||||
|
||||
XCEngine::Components::GameObject cameraObject("SceneCamera");
|
||||
SceneViewportRenderRequest request = CreateValidRequest(cameraObject);
|
||||
request.selectedObjectIds = { 24u };
|
||||
|
||||
auto result = BuildSceneViewportRenderPlan(
|
||||
targets,
|
||||
request,
|
||||
[](const SceneViewportGridPassData& data) {
|
||||
EXPECT_TRUE(data.valid);
|
||||
return std::make_unique<NoopRenderPass>();
|
||||
},
|
||||
[](
|
||||
ViewportRenderTargets*,
|
||||
const std::vector<std::uint64_t>&,
|
||||
const SceneViewportSelectionOutlineStyle&) {
|
||||
return std::make_unique<NoopRenderPass>();
|
||||
});
|
||||
|
||||
XCEngine::Rendering::CameraFramePlan framePlan = {};
|
||||
framePlan.request.surface = RenderSurface(800u, 600u);
|
||||
framePlan.request.surface.SetRenderArea(XCEngine::Math::RectInt(10, 20, 300, 200));
|
||||
|
||||
ApplySceneViewportRenderPlan(targets, result.plan, framePlan);
|
||||
|
||||
EXPECT_EQ(framePlan.postScenePasses, &result.plan.postScenePasses);
|
||||
EXPECT_TRUE(framePlan.request.objectId.IsRequested());
|
||||
EXPECT_TRUE(framePlan.request.hasClearColorOverride);
|
||||
EXPECT_FLOAT_EQ(framePlan.request.clearColorOverride.r, 0.27f);
|
||||
EXPECT_FLOAT_EQ(framePlan.request.clearColorOverride.g, 0.27f);
|
||||
EXPECT_FLOAT_EQ(framePlan.request.clearColorOverride.b, 0.27f);
|
||||
ASSERT_EQ(framePlan.request.objectId.surface.GetColorAttachments().size(), 1u);
|
||||
EXPECT_EQ(framePlan.request.objectId.surface.GetColorAttachments()[0], &objectIdView);
|
||||
EXPECT_EQ(framePlan.request.objectId.surface.GetDepthAttachment(), &objectIdDepthView);
|
||||
|
||||
MarkSceneViewportRenderSuccess(targets, result.plan, framePlan);
|
||||
EXPECT_EQ(targets.colorState, ResourceStates::PixelShaderResource);
|
||||
EXPECT_EQ(targets.objectIdState, ResourceStates::PixelShaderResource);
|
||||
EXPECT_EQ(targets.selectionMaskState, ResourceStates::PixelShaderResource);
|
||||
EXPECT_TRUE(targets.hasValidObjectIdFrame);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,978 +0,0 @@
|
||||
#include "Scene/EditorSceneRuntime.h"
|
||||
#include "Features/Scene/SceneViewportController.h"
|
||||
#include "Features/Inspector/InspectorSubject.h"
|
||||
#include "Rendering/Viewport/SceneViewportRenderService.h"
|
||||
#include "Rendering/Viewport/ViewportHostService.h"
|
||||
#include "Rendering/Viewport/ViewportRenderTargetInternal.h"
|
||||
#include "State/EditorSelectionService.h"
|
||||
|
||||
#include "Composition/EditorPanelIds.h"
|
||||
#include <XCEditor/Viewport/UIEditorViewportInputBridge.h>
|
||||
#include <XCEditor/Viewport/UIEditorViewportSlot.h>
|
||||
|
||||
#include <XCEngine/Components/CameraComponent.h>
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#include <XCEngine/Scene/Scene.h>
|
||||
#include <XCEngine/Scene/SceneManager.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <cstdint>
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::Components::GameObject;
|
||||
using ::XCEngine::Components::Scene;
|
||||
using ::XCEngine::Components::SceneManager;
|
||||
using ::XCEngine::Input::KeyCode;
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIInputModifiers;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIPointerButton;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using ::XCEngine::UI::UISize;
|
||||
using ::XCEngine::UI::Editor::UIEditorViewportInputBridgeState;
|
||||
using ::XCEngine::UI::Editor::UIEditorWorkspaceComposeFrame;
|
||||
using ::XCEngine::UI::Editor::UIEditorWorkspaceComposeState;
|
||||
using ::XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationState;
|
||||
using ::XCEngine::UI::Editor::UIEditorWorkspaceViewportComposeFrame;
|
||||
using ::XCEngine::UI::Editor::UpdateUIEditorViewportInputBridge;
|
||||
|
||||
class ScopedSceneManagerReset final {
|
||||
public:
|
||||
ScopedSceneManagerReset() {
|
||||
Reset();
|
||||
}
|
||||
|
||||
~ScopedSceneManagerReset() {
|
||||
Reset();
|
||||
::XCEngine::Resources::ResourceManager::Get().Shutdown();
|
||||
}
|
||||
|
||||
private:
|
||||
static void Reset() {
|
||||
SceneManager& manager = SceneManager::Get();
|
||||
const auto scenes = manager.GetAllScenes();
|
||||
for (Scene* scene : scenes) {
|
||||
manager.UnloadScene(scene);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class TemporaryProjectRoot final {
|
||||
public:
|
||||
TemporaryProjectRoot() {
|
||||
const auto uniqueSuffix =
|
||||
std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
m_root =
|
||||
std::filesystem::temp_directory_path() /
|
||||
("xcui_scene_viewport_runtime_" + std::to_string(uniqueSuffix));
|
||||
}
|
||||
|
||||
~TemporaryProjectRoot() {
|
||||
std::error_code errorCode = {};
|
||||
std::filesystem::remove_all(m_root, errorCode);
|
||||
}
|
||||
|
||||
const std::filesystem::path& Root() const {
|
||||
return m_root;
|
||||
}
|
||||
|
||||
std::filesystem::path MainScenePath() const {
|
||||
return m_root / "Assets" / "Scenes" / "Main.xc";
|
||||
}
|
||||
|
||||
private:
|
||||
std::filesystem::path m_root = {};
|
||||
};
|
||||
|
||||
void SaveMainScene(const TemporaryProjectRoot& projectRoot, const Math::Vector3& targetPosition) {
|
||||
const std::filesystem::path scenePath = projectRoot.MainScenePath();
|
||||
std::filesystem::create_directories(scenePath.parent_path());
|
||||
|
||||
Scene scene("Main");
|
||||
GameObject* target = scene.CreateGameObject("Target");
|
||||
ASSERT_NE(target, nullptr);
|
||||
ASSERT_NE(target->GetTransform(), nullptr);
|
||||
target->GetTransform()->SetPosition(targetPosition);
|
||||
scene.Save(scenePath.string());
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerEvent(
|
||||
UIInputEventType type,
|
||||
float x,
|
||||
float y,
|
||||
UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerEventWithModifiers(
|
||||
UIInputEventType type,
|
||||
float x,
|
||||
float y,
|
||||
const UIInputModifiers& modifiers,
|
||||
UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
event.modifiers = modifiers;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputModifiers MakePointerModifiers(UIPointerButton button) {
|
||||
UIInputModifiers modifiers = {};
|
||||
switch (button) {
|
||||
case UIPointerButton::Left:
|
||||
modifiers.leftMouse = true;
|
||||
break;
|
||||
case UIPointerButton::Right:
|
||||
modifiers.rightMouse = true;
|
||||
break;
|
||||
case UIPointerButton::Middle:
|
||||
modifiers.middleMouse = true;
|
||||
break;
|
||||
case UIPointerButton::X1:
|
||||
modifiers.x1Mouse = true;
|
||||
break;
|
||||
case UIPointerButton::X2:
|
||||
modifiers.x2Mouse = true;
|
||||
break;
|
||||
case UIPointerButton::None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
UIInputEvent MakeWheelEvent(float x, float y, float wheelDelta) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerWheel;
|
||||
event.position = UIPoint(x, y);
|
||||
event.wheelDelta = wheelDelta;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceComposeState BuildSceneComposeState(
|
||||
const UIEditorViewportInputBridgeState& inputBridgeState) {
|
||||
UIEditorWorkspaceComposeState composeState = {};
|
||||
UIEditorWorkspacePanelPresentationState panelState = {};
|
||||
panelState.panelId = std::string(::XCEngine::UI::Editor::App::kScenePanelId);
|
||||
panelState.viewportShellState.inputBridgeState = inputBridgeState;
|
||||
composeState.panelStates.push_back(std::move(panelState));
|
||||
return composeState;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceComposeFrame BuildSceneComposeFrame(
|
||||
const ::XCEngine::UI::Editor::UIEditorViewportInputBridgeFrame& inputFrame,
|
||||
const UIRect& inputRect,
|
||||
const UISize& requestedViewportSize) {
|
||||
UIEditorWorkspaceComposeFrame composeFrame = {};
|
||||
UIEditorWorkspaceViewportComposeFrame viewportFrame = {};
|
||||
viewportFrame.panelId = std::string(::XCEngine::UI::Editor::App::kScenePanelId);
|
||||
viewportFrame.viewportShellFrame.inputFrame = inputFrame;
|
||||
viewportFrame.viewportShellFrame.requestedViewportSize = requestedViewportSize;
|
||||
viewportFrame.viewportShellFrame.slotLayout.inputRect = inputRect;
|
||||
composeFrame.viewportFrames.push_back(std::move(viewportFrame));
|
||||
return composeFrame;
|
||||
}
|
||||
|
||||
const EditorSceneComponentDescriptor* FindComponentDescriptor(
|
||||
const std::vector<EditorSceneComponentDescriptor>& descriptors,
|
||||
std::string_view typeName) {
|
||||
for (const EditorSceneComponentDescriptor& descriptor : descriptors) {
|
||||
if (descriptor.typeName == typeName) {
|
||||
return &descriptor;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, ApplySceneViewportCameraInputUpdatesCameraTransform) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
|
||||
auto* camera = runtime.GetSceneViewCamera();
|
||||
ASSERT_NE(camera, nullptr);
|
||||
auto* transform = camera->GetGameObject()->GetTransform();
|
||||
ASSERT_NE(transform, nullptr);
|
||||
|
||||
const Math::Vector3 before = transform->GetPosition();
|
||||
|
||||
SceneViewportCameraInputState input = {};
|
||||
input.viewportHeight = 720.0f;
|
||||
input.zoomDelta = 1.0f;
|
||||
runtime.ApplySceneViewportCameraInput(input);
|
||||
|
||||
const Math::Vector3 after = transform->GetPosition();
|
||||
EXPECT_NE(before.x, after.x);
|
||||
EXPECT_NE(before.y, after.y);
|
||||
EXPECT_NE(before.z, after.z);
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, FocusSceneSelectionRepositionsCameraAroundSelectedObject) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot, Math::Vector3(12.0f, 3.0f, -8.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
Scene* scene = runtime.GetActiveScene();
|
||||
ASSERT_NE(scene, nullptr);
|
||||
|
||||
GameObject* target = scene->Find("Target");
|
||||
ASSERT_NE(target, nullptr);
|
||||
ASSERT_TRUE(runtime.SetSelection(target->GetID()));
|
||||
|
||||
auto* camera = runtime.GetSceneViewCamera();
|
||||
ASSERT_NE(camera, nullptr);
|
||||
auto* transform = camera->GetGameObject()->GetTransform();
|
||||
ASSERT_NE(transform, nullptr);
|
||||
const Math::Vector3 before = transform->GetPosition();
|
||||
|
||||
ASSERT_TRUE(runtime.FocusSceneSelection());
|
||||
|
||||
const Math::Vector3 after = transform->GetPosition();
|
||||
EXPECT_NE(before.x, after.x);
|
||||
EXPECT_NE(before.y, after.y);
|
||||
EXPECT_NE(before.z, after.z);
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, BuildSceneViewportRenderRequestIncludesSelectedObjectId) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
Scene* scene = runtime.GetActiveScene();
|
||||
ASSERT_NE(scene, nullptr);
|
||||
|
||||
GameObject* target = scene->Find("Target");
|
||||
ASSERT_NE(target, nullptr);
|
||||
ASSERT_TRUE(runtime.SetSelection(target->GetID()));
|
||||
|
||||
const SceneViewportRenderRequest request =
|
||||
runtime.BuildSceneViewportRenderRequest();
|
||||
ASSERT_TRUE(request.IsValid());
|
||||
ASSERT_EQ(request.selectedObjectIds.size(), 1u);
|
||||
EXPECT_EQ(request.selectedObjectIds.front(), target->GetID());
|
||||
EXPECT_GT(request.orbitDistance, 0.0f);
|
||||
EXPECT_FALSE(request.debugSelectionMask);
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, SelectedComponentsExposeTransformAndAttachedCameraDescriptors) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
Scene* scene = runtime.GetActiveScene();
|
||||
ASSERT_NE(scene, nullptr);
|
||||
GameObject* target = scene->Find("Target");
|
||||
ASSERT_NE(target, nullptr);
|
||||
ASSERT_NE(target->AddComponent<::XCEngine::Components::CameraComponent>(), nullptr);
|
||||
ASSERT_TRUE(runtime.SetSelection(target->GetID()));
|
||||
|
||||
const std::vector<EditorSceneComponentDescriptor> descriptors =
|
||||
runtime.GetSelectedComponents();
|
||||
ASSERT_EQ(descriptors.size(), 2u);
|
||||
|
||||
const auto* transformDescriptor =
|
||||
FindComponentDescriptor(descriptors, "Transform");
|
||||
const auto* cameraDescriptor =
|
||||
FindComponentDescriptor(descriptors, "Camera");
|
||||
ASSERT_NE(transformDescriptor, nullptr);
|
||||
ASSERT_NE(cameraDescriptor, nullptr);
|
||||
EXPECT_EQ(transformDescriptor->componentId, "Transform#0");
|
||||
EXPECT_FALSE(transformDescriptor->removable);
|
||||
EXPECT_EQ(cameraDescriptor->componentId, "Camera#0");
|
||||
EXPECT_TRUE(cameraDescriptor->removable);
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, RemoveSelectedComponentDropsRemovableDescriptorButKeepsTransform) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
Scene* scene = runtime.GetActiveScene();
|
||||
ASSERT_NE(scene, nullptr);
|
||||
GameObject* target = scene->Find("Target");
|
||||
ASSERT_NE(target, nullptr);
|
||||
ASSERT_NE(target->AddComponent<::XCEngine::Components::CameraComponent>(), nullptr);
|
||||
ASSERT_TRUE(runtime.SetSelection(target->GetID()));
|
||||
|
||||
EXPECT_FALSE(runtime.CanRemoveSelectedComponent("Transform#0"));
|
||||
EXPECT_TRUE(runtime.CanRemoveSelectedComponent("Camera#0"));
|
||||
EXPECT_FALSE(runtime.RemoveSelectedComponent("Transform#0"));
|
||||
ASSERT_TRUE(runtime.RemoveSelectedComponent("Camera#0"));
|
||||
|
||||
const std::vector<EditorSceneComponentDescriptor> descriptors =
|
||||
runtime.GetSelectedComponents();
|
||||
ASSERT_EQ(descriptors.size(), 1u);
|
||||
EXPECT_EQ(descriptors[0].typeName, "Transform");
|
||||
EXPECT_EQ(descriptors[0].componentId, "Transform#0");
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, TransformSetterApisWriteLocalValuesOnSelectedTransform) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
runtime.EnsureSceneSelection();
|
||||
|
||||
auto* target = const_cast<GameObject*>(runtime.GetSelectedGameObject());
|
||||
ASSERT_NE(target, nullptr);
|
||||
auto* transform = target->GetTransform();
|
||||
ASSERT_NE(transform, nullptr);
|
||||
|
||||
ASSERT_TRUE(runtime.SetSelectedTransformLocalPosition(
|
||||
"Transform#0",
|
||||
Math::Vector3(8.0f, 9.0f, 10.0f)));
|
||||
ASSERT_TRUE(runtime.SetSelectedTransformLocalEulerAngles(
|
||||
"Transform#0",
|
||||
Math::Vector3(15.0f, 25.0f, 35.0f)));
|
||||
ASSERT_TRUE(runtime.SetSelectedTransformLocalScale(
|
||||
"Transform#0",
|
||||
Math::Vector3(2.0f, 3.0f, 4.0f)));
|
||||
|
||||
const Math::Vector3 position = transform->GetLocalPosition();
|
||||
const Math::Quaternion rotation = transform->GetLocalRotation();
|
||||
const Math::Vector3 scale = transform->GetLocalScale();
|
||||
const Math::Quaternion expectedRotation =
|
||||
Math::Quaternion::FromEulerAngles(
|
||||
Math::Vector3(15.0f, 25.0f, 35.0f) * Math::DEG_TO_RAD);
|
||||
EXPECT_FLOAT_EQ(position.x, 8.0f);
|
||||
EXPECT_FLOAT_EQ(position.y, 9.0f);
|
||||
EXPECT_FLOAT_EQ(position.z, 10.0f);
|
||||
EXPECT_GT(std::abs(rotation.Dot(expectedRotation)), 0.9999f);
|
||||
EXPECT_FLOAT_EQ(scale.x, 2.0f);
|
||||
EXPECT_FLOAT_EQ(scale.y, 3.0f);
|
||||
EXPECT_FLOAT_EQ(scale.z, 4.0f);
|
||||
EXPECT_TRUE(runtime.CanUndoTransformEdit());
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, SelectionStampAdvancesOnSceneSelectionChanges) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
runtime.EnsureSceneSelection();
|
||||
|
||||
const std::uint64_t initialStamp = runtime.GetSelectionStamp();
|
||||
EXPECT_GT(initialStamp, 0u);
|
||||
|
||||
Scene* scene = runtime.GetActiveScene();
|
||||
ASSERT_NE(scene, nullptr);
|
||||
GameObject* secondary = scene->CreateGameObject("Secondary");
|
||||
ASSERT_NE(secondary, nullptr);
|
||||
ASSERT_TRUE(runtime.SetSelection(secondary->GetID()));
|
||||
const std::uint64_t selectedStamp = runtime.GetSelectionStamp();
|
||||
EXPECT_GT(selectedStamp, initialStamp);
|
||||
|
||||
runtime.ClearSelection();
|
||||
const std::uint64_t clearedStamp = runtime.GetSelectionStamp();
|
||||
EXPECT_GT(clearedStamp, selectedStamp);
|
||||
|
||||
runtime.ClearSelection();
|
||||
EXPECT_GT(runtime.GetSelectionStamp(), clearedStamp);
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, InspectorSelectionResolverFollowsUnifiedSelectionSnapshot) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f));
|
||||
|
||||
EditorSelectionService selectionService = {};
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
runtime.BindSelectionService(&selectionService);
|
||||
runtime.EnsureSceneSelection();
|
||||
ASSERT_TRUE(runtime.HasSceneSelection());
|
||||
|
||||
EditorSession session = {};
|
||||
session.selection = selectionService.GetSelection();
|
||||
EXPECT_EQ(
|
||||
ResolveInspectorSelectionSource(session, runtime),
|
||||
InspectorSelectionSource::Scene);
|
||||
const InspectorSubject sceneSubject =
|
||||
BuildInspectorSubject(session, runtime);
|
||||
EXPECT_EQ(sceneSubject.kind, InspectorSubjectKind::SceneObject);
|
||||
EXPECT_EQ(sceneSubject.source, InspectorSelectionSource::Scene);
|
||||
EXPECT_EQ(sceneSubject.sceneObject.displayName, "Target");
|
||||
|
||||
selectionService.SetProjectSelection(
|
||||
"asset:scene",
|
||||
"Main",
|
||||
projectRoot.MainScenePath(),
|
||||
false);
|
||||
session.selection = selectionService.GetSelection();
|
||||
EXPECT_EQ(
|
||||
ResolveInspectorSelectionSource(session, runtime),
|
||||
InspectorSelectionSource::Project);
|
||||
const InspectorSubject projectSubject =
|
||||
BuildInspectorSubject(session, runtime);
|
||||
EXPECT_EQ(projectSubject.kind, InspectorSubjectKind::ProjectAsset);
|
||||
EXPECT_EQ(projectSubject.source, InspectorSelectionSource::Project);
|
||||
EXPECT_EQ(projectSubject.projectAsset.selection.itemId, "asset:scene");
|
||||
|
||||
runtime.EnsureSceneSelection();
|
||||
session.selection = selectionService.GetSelection();
|
||||
EXPECT_EQ(
|
||||
ResolveInspectorSelectionSource(session, runtime),
|
||||
InspectorSelectionSource::Project);
|
||||
EXPECT_EQ(
|
||||
BuildInspectorSubject(session, runtime).kind,
|
||||
InspectorSubjectKind::ProjectAsset);
|
||||
|
||||
runtime.ClearSelection();
|
||||
session.selection = selectionService.GetSelection();
|
||||
EXPECT_EQ(
|
||||
ResolveInspectorSelectionSource(session, runtime),
|
||||
InspectorSelectionSource::None);
|
||||
runtime.EnsureSceneSelection();
|
||||
session.selection = selectionService.GetSelection();
|
||||
EXPECT_EQ(
|
||||
ResolveInspectorSelectionSource(session, runtime),
|
||||
InspectorSelectionSource::Scene);
|
||||
|
||||
selectionService.SetProjectSelection(
|
||||
"asset:scene",
|
||||
"Main",
|
||||
projectRoot.MainScenePath(),
|
||||
false);
|
||||
runtime.EnsureSceneSelection();
|
||||
EXPECT_EQ(selectionService.GetSelection().kind, EditorSelectionKind::ProjectItem);
|
||||
EXPECT_EQ(selectionService.GetSelection().itemId, "asset:scene");
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, RightMouseDragRotatesSceneCameraThroughViewportController) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
|
||||
auto* camera = runtime.GetSceneViewCamera();
|
||||
ASSERT_NE(camera, nullptr);
|
||||
auto* transform = camera->GetGameObject()->GetTransform();
|
||||
ASSERT_NE(transform, nullptr);
|
||||
const Math::Vector3 beforeForward = transform->GetForward();
|
||||
|
||||
SceneViewportController controller = {};
|
||||
SceneViewportRenderService sceneViewportRenderService = {};
|
||||
UIEditorViewportInputBridgeState inputBridgeState = {};
|
||||
const UIRect inputRect(100.0f, 80.0f, 640.0f, 360.0f);
|
||||
const UISize viewportSize(640.0f, 360.0f);
|
||||
|
||||
const auto pressFrame = UpdateUIEditorViewportInputBridge(
|
||||
inputBridgeState,
|
||||
inputRect,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 180.0f),
|
||||
MakePointerEventWithModifiers(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
220.0f,
|
||||
180.0f,
|
||||
MakePointerModifiers(UIPointerButton::Right),
|
||||
UIPointerButton::Right)
|
||||
});
|
||||
controller.Update(
|
||||
runtime,
|
||||
sceneViewportRenderService,
|
||||
BuildSceneComposeState(inputBridgeState),
|
||||
BuildSceneComposeFrame(pressFrame, inputRect, viewportSize));
|
||||
|
||||
const auto dragFrame = UpdateUIEditorViewportInputBridge(
|
||||
inputBridgeState,
|
||||
inputRect,
|
||||
{
|
||||
MakePointerEventWithModifiers(
|
||||
UIInputEventType::PointerMove,
|
||||
280.0f,
|
||||
220.0f,
|
||||
MakePointerModifiers(UIPointerButton::Right))
|
||||
});
|
||||
controller.Update(
|
||||
runtime,
|
||||
sceneViewportRenderService,
|
||||
BuildSceneComposeState(inputBridgeState),
|
||||
BuildSceneComposeFrame(dragFrame, inputRect, viewportSize));
|
||||
|
||||
const Math::Vector3 afterForward = transform->GetForward();
|
||||
EXPECT_NE(beforeForward.x, afterForward.x);
|
||||
EXPECT_NE(beforeForward.y, afterForward.y);
|
||||
EXPECT_NE(beforeForward.z, afterForward.z);
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, MoveRightInputMovesSceneCameraTowardPositiveCameraRight) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
|
||||
auto* camera = runtime.GetSceneViewCamera();
|
||||
ASSERT_NE(camera, nullptr);
|
||||
auto* transform = camera->GetGameObject()->GetTransform();
|
||||
ASSERT_NE(transform, nullptr);
|
||||
const Math::Vector3 before = transform->GetPosition();
|
||||
|
||||
SceneViewportCameraInputState input = {};
|
||||
input.viewportHeight = 720.0f;
|
||||
input.deltaTime = 1.0f;
|
||||
input.moveRight = 1.0f;
|
||||
runtime.ApplySceneViewportCameraInput(input);
|
||||
|
||||
const Math::Vector3 after = transform->GetPosition();
|
||||
EXPECT_GT(after.x, before.x);
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, MiddleMouseDragPansSceneCameraWithGrabSemantics) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
|
||||
auto* camera = runtime.GetSceneViewCamera();
|
||||
ASSERT_NE(camera, nullptr);
|
||||
auto* transform = camera->GetGameObject()->GetTransform();
|
||||
ASSERT_NE(transform, nullptr);
|
||||
const Math::Vector3 before = transform->GetPosition();
|
||||
|
||||
SceneViewportController controller = {};
|
||||
SceneViewportRenderService sceneViewportRenderService = {};
|
||||
UIEditorViewportInputBridgeState inputBridgeState = {};
|
||||
const UIRect inputRect(100.0f, 80.0f, 640.0f, 360.0f);
|
||||
const UISize viewportSize(640.0f, 360.0f);
|
||||
|
||||
const auto pressFrame = UpdateUIEditorViewportInputBridge(
|
||||
inputBridgeState,
|
||||
inputRect,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 180.0f),
|
||||
MakePointerEventWithModifiers(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
220.0f,
|
||||
180.0f,
|
||||
MakePointerModifiers(UIPointerButton::Middle),
|
||||
UIPointerButton::Middle)
|
||||
});
|
||||
controller.Update(
|
||||
runtime,
|
||||
sceneViewportRenderService,
|
||||
BuildSceneComposeState(inputBridgeState),
|
||||
BuildSceneComposeFrame(pressFrame, inputRect, viewportSize));
|
||||
|
||||
const auto dragFrame = UpdateUIEditorViewportInputBridge(
|
||||
inputBridgeState,
|
||||
inputRect,
|
||||
{
|
||||
MakePointerEventWithModifiers(
|
||||
UIInputEventType::PointerMove,
|
||||
280.0f,
|
||||
180.0f,
|
||||
MakePointerModifiers(UIPointerButton::Middle))
|
||||
});
|
||||
controller.Update(
|
||||
runtime,
|
||||
sceneViewportRenderService,
|
||||
BuildSceneComposeState(inputBridgeState),
|
||||
BuildSceneComposeFrame(dragFrame, inputRect, viewportSize));
|
||||
|
||||
const Math::Vector3 after = transform->GetPosition();
|
||||
EXPECT_LT(after.x, before.x);
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, ViewToolLeftMouseDragPansSceneCameraWithGrabSemantics) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
runtime.SetToolMode(SceneToolMode::View);
|
||||
|
||||
auto* camera = runtime.GetSceneViewCamera();
|
||||
ASSERT_NE(camera, nullptr);
|
||||
auto* transform = camera->GetGameObject()->GetTransform();
|
||||
ASSERT_NE(transform, nullptr);
|
||||
const Math::Vector3 before = transform->GetPosition();
|
||||
|
||||
SceneViewportController controller = {};
|
||||
SceneViewportRenderService sceneViewportRenderService = {};
|
||||
UIEditorViewportInputBridgeState inputBridgeState = {};
|
||||
const UIRect inputRect(100.0f, 80.0f, 640.0f, 360.0f);
|
||||
const UISize viewportSize(640.0f, 360.0f);
|
||||
|
||||
const auto pressFrame = UpdateUIEditorViewportInputBridge(
|
||||
inputBridgeState,
|
||||
inputRect,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 180.0f),
|
||||
MakePointerEventWithModifiers(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
220.0f,
|
||||
180.0f,
|
||||
MakePointerModifiers(UIPointerButton::Left),
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
controller.Update(
|
||||
runtime,
|
||||
sceneViewportRenderService,
|
||||
BuildSceneComposeState(inputBridgeState),
|
||||
BuildSceneComposeFrame(pressFrame, inputRect, viewportSize));
|
||||
|
||||
const auto dragFrame = UpdateUIEditorViewportInputBridge(
|
||||
inputBridgeState,
|
||||
inputRect,
|
||||
{
|
||||
MakePointerEventWithModifiers(
|
||||
UIInputEventType::PointerMove,
|
||||
280.0f,
|
||||
180.0f,
|
||||
MakePointerModifiers(UIPointerButton::Left))
|
||||
});
|
||||
controller.Update(
|
||||
runtime,
|
||||
sceneViewportRenderService,
|
||||
BuildSceneComposeState(inputBridgeState),
|
||||
BuildSceneComposeFrame(dragFrame, inputRect, viewportSize));
|
||||
|
||||
const Math::Vector3 after = transform->GetPosition();
|
||||
EXPECT_LT(after.x, before.x);
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, MouseWheelUsesSingleNotchNormalizationForSceneZoom) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
const float beforeDistance =
|
||||
runtime.BuildSceneViewportRenderRequest().orbitDistance;
|
||||
|
||||
SceneViewportController controller = {};
|
||||
SceneViewportRenderService sceneViewportRenderService = {};
|
||||
UIEditorViewportInputBridgeState inputBridgeState = {};
|
||||
const UIRect inputRect(100.0f, 80.0f, 640.0f, 360.0f);
|
||||
const UISize viewportSize(640.0f, 360.0f);
|
||||
|
||||
const auto hoverFrame = UpdateUIEditorViewportInputBridge(
|
||||
inputBridgeState,
|
||||
inputRect,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 180.0f)
|
||||
});
|
||||
controller.Update(
|
||||
runtime,
|
||||
sceneViewportRenderService,
|
||||
BuildSceneComposeState(inputBridgeState),
|
||||
BuildSceneComposeFrame(hoverFrame, inputRect, viewportSize));
|
||||
|
||||
const auto wheelFrame = UpdateUIEditorViewportInputBridge(
|
||||
inputBridgeState,
|
||||
inputRect,
|
||||
{
|
||||
MakeWheelEvent(220.0f, 180.0f, 120.0f)
|
||||
});
|
||||
controller.Update(
|
||||
runtime,
|
||||
sceneViewportRenderService,
|
||||
BuildSceneComposeState(inputBridgeState),
|
||||
BuildSceneComposeFrame(wheelFrame, inputRect, viewportSize));
|
||||
|
||||
const float afterDistance =
|
||||
runtime.BuildSceneViewportRenderRequest().orbitDistance;
|
||||
EXPECT_LT(afterDistance, beforeDistance);
|
||||
EXPECT_GT(afterDistance, 4.0f);
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, ToolShortcutSwitchesFocusedSceneViewportIntoTranslateMode) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
runtime.SetToolMode(SceneToolMode::View);
|
||||
EXPECT_EQ(runtime.GetToolMode(), SceneToolMode::View);
|
||||
|
||||
SceneViewportController controller = {};
|
||||
SceneViewportRenderService sceneViewportRenderService = {};
|
||||
UIEditorViewportInputBridgeState inputBridgeState = {};
|
||||
const UIRect inputRect(100.0f, 80.0f, 640.0f, 360.0f);
|
||||
const UISize viewportSize(640.0f, 360.0f);
|
||||
|
||||
const auto focusFrame = UpdateUIEditorViewportInputBridge(
|
||||
inputBridgeState,
|
||||
inputRect,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 180.0f),
|
||||
MakePointerEventWithModifiers(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
220.0f,
|
||||
180.0f,
|
||||
MakePointerModifiers(UIPointerButton::Left),
|
||||
UIPointerButton::Left),
|
||||
MakePointerEvent(
|
||||
UIInputEventType::PointerButtonUp,
|
||||
220.0f,
|
||||
180.0f,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
controller.Update(
|
||||
runtime,
|
||||
sceneViewportRenderService,
|
||||
BuildSceneComposeState(inputBridgeState),
|
||||
BuildSceneComposeFrame(focusFrame, inputRect, viewportSize));
|
||||
|
||||
const auto shortcutFrame = UpdateUIEditorViewportInputBridge(
|
||||
inputBridgeState,
|
||||
inputRect,
|
||||
{
|
||||
UIInputEvent {
|
||||
.type = UIInputEventType::KeyDown,
|
||||
.position = UIPoint(220.0f, 180.0f),
|
||||
.keyCode = static_cast<std::int32_t>(KeyCode::W)
|
||||
}
|
||||
});
|
||||
controller.Update(
|
||||
runtime,
|
||||
sceneViewportRenderService,
|
||||
BuildSceneComposeState(inputBridgeState),
|
||||
BuildSceneComposeFrame(shortcutFrame, inputRect, viewportSize));
|
||||
|
||||
EXPECT_EQ(runtime.GetToolMode(), SceneToolMode::Translate);
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, SceneToolOverlayClickSwitchesModeOnPointerDown) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
runtime.SetToolMode(SceneToolMode::Translate);
|
||||
|
||||
SceneViewportController controller = {};
|
||||
SceneViewportRenderService sceneViewportRenderService = {};
|
||||
UIEditorViewportInputBridgeState inputBridgeState = {};
|
||||
const UIRect inputRect(100.0f, 104.0f, 640.0f, 360.0f);
|
||||
const UISize viewportSize(640.0f, 360.0f);
|
||||
const UIPoint rotateButtonCenter(
|
||||
inputRect.x + 28.0f,
|
||||
inputRect.y + 82.0f);
|
||||
|
||||
const auto frame = UpdateUIEditorViewportInputBridge(
|
||||
inputBridgeState,
|
||||
inputRect,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, rotateButtonCenter.x, rotateButtonCenter.y),
|
||||
MakePointerEvent(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
rotateButtonCenter.x,
|
||||
rotateButtonCenter.y,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
controller.Update(
|
||||
runtime,
|
||||
sceneViewportRenderService,
|
||||
BuildSceneComposeState(inputBridgeState),
|
||||
BuildSceneComposeFrame(frame, inputRect, viewportSize));
|
||||
|
||||
EXPECT_EQ(runtime.GetToolMode(), SceneToolMode::Rotate);
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, SceneToolOverlayIncludesTransformButtonAndSwitchesModeOnPointerDown) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
runtime.SetToolMode(SceneToolMode::Translate);
|
||||
|
||||
SceneViewportController controller = {};
|
||||
SceneViewportRenderService sceneViewportRenderService = {};
|
||||
UIEditorViewportInputBridgeState inputBridgeState = {};
|
||||
const UIRect inputRect(100.0f, 104.0f, 640.0f, 360.0f);
|
||||
const UISize viewportSize(640.0f, 360.0f);
|
||||
const UIPoint transformButtonCenter(
|
||||
inputRect.x + 28.0f,
|
||||
inputRect.y + 148.0f);
|
||||
|
||||
const auto frame = UpdateUIEditorViewportInputBridge(
|
||||
inputBridgeState,
|
||||
inputRect,
|
||||
{
|
||||
MakePointerEvent(
|
||||
UIInputEventType::PointerMove,
|
||||
transformButtonCenter.x,
|
||||
transformButtonCenter.y),
|
||||
MakePointerEvent(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
transformButtonCenter.x,
|
||||
transformButtonCenter.y,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
controller.Update(
|
||||
runtime,
|
||||
sceneViewportRenderService,
|
||||
BuildSceneComposeState(inputBridgeState),
|
||||
BuildSceneComposeFrame(frame, inputRect, viewportSize));
|
||||
|
||||
EXPECT_EQ(runtime.GetToolMode(), SceneToolMode::Transform);
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, SceneToolOverlayHandlesCoalescedClickInSingleFrame) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
TemporaryProjectRoot projectRoot = {};
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
runtime.SetToolMode(SceneToolMode::Translate);
|
||||
|
||||
SceneViewportController controller = {};
|
||||
SceneViewportRenderService sceneViewportRenderService = {};
|
||||
UIEditorViewportInputBridgeState inputBridgeState = {};
|
||||
const UIRect inputRect(100.0f, 104.0f, 640.0f, 360.0f);
|
||||
const UISize viewportSize(640.0f, 360.0f);
|
||||
const UIPoint rotateButtonCenter(
|
||||
inputRect.x + 28.0f,
|
||||
inputRect.y + 82.0f);
|
||||
|
||||
const auto frame = UpdateUIEditorViewportInputBridge(
|
||||
inputBridgeState,
|
||||
inputRect,
|
||||
{
|
||||
MakePointerEvent(
|
||||
UIInputEventType::PointerMove,
|
||||
rotateButtonCenter.x,
|
||||
rotateButtonCenter.y),
|
||||
MakePointerEvent(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
rotateButtonCenter.x,
|
||||
rotateButtonCenter.y,
|
||||
UIPointerButton::Left),
|
||||
MakePointerEvent(
|
||||
UIInputEventType::PointerButtonUp,
|
||||
rotateButtonCenter.x,
|
||||
rotateButtonCenter.y,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
controller.Update(
|
||||
runtime,
|
||||
sceneViewportRenderService,
|
||||
BuildSceneComposeState(inputBridgeState),
|
||||
BuildSceneComposeFrame(frame, inputRect, viewportSize));
|
||||
|
||||
EXPECT_EQ(runtime.GetToolMode(), SceneToolMode::Rotate);
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, SceneViewportRendererDeclaresExplicitAuxiliaryResourceRequirements) {
|
||||
const ViewportResourceRequirements requirements =
|
||||
SceneViewportRenderService::GetViewportResourceRequirements();
|
||||
|
||||
EXPECT_TRUE(requirements.requiresDepthSampling);
|
||||
EXPECT_TRUE(requirements.requiresObjectIdSurface);
|
||||
EXPECT_TRUE(requirements.requiresSelectionMaskSurface);
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, ViewportResourceReuseQueryHonorsExplicitRequirementsInsteadOfViewportKind) {
|
||||
ViewportRenderTargets targets = {};
|
||||
targets.width = 1280u;
|
||||
targets.height = 720u;
|
||||
targets.colorTexture = reinterpret_cast<::XCEngine::RHI::RHITexture*>(1);
|
||||
targets.colorView = reinterpret_cast<::XCEngine::RHI::RHIResourceView*>(2);
|
||||
targets.depthTexture = reinterpret_cast<::XCEngine::RHI::RHITexture*>(3);
|
||||
targets.depthView = reinterpret_cast<::XCEngine::RHI::RHIResourceView*>(4);
|
||||
targets.textureHandle = {
|
||||
5u,
|
||||
1280u,
|
||||
720u,
|
||||
::XCEngine::UI::UITextureHandleKind::ShaderResourceView,
|
||||
6u
|
||||
};
|
||||
|
||||
ViewportResourceRequirements requirements = {};
|
||||
EXPECT_TRUE(CanReuseViewportResources(
|
||||
BuildViewportRenderTargetsReuseQuery(requirements, targets, 1280u, 720u)));
|
||||
|
||||
requirements.requiresDepthSampling = true;
|
||||
EXPECT_FALSE(CanReuseViewportResources(
|
||||
BuildViewportRenderTargetsReuseQuery(requirements, targets, 1280u, 720u)));
|
||||
|
||||
targets.depthShaderView = reinterpret_cast<::XCEngine::RHI::RHIResourceView*>(7);
|
||||
EXPECT_TRUE(CanReuseViewportResources(
|
||||
BuildViewportRenderTargetsReuseQuery(requirements, targets, 1280u, 720u)));
|
||||
|
||||
requirements.requiresObjectIdSurface = true;
|
||||
EXPECT_FALSE(CanReuseViewportResources(
|
||||
BuildViewportRenderTargetsReuseQuery(requirements, targets, 1280u, 720u)));
|
||||
|
||||
targets.objectIdTexture = reinterpret_cast<::XCEngine::RHI::RHITexture*>(8);
|
||||
targets.objectIdDepthTexture = reinterpret_cast<::XCEngine::RHI::RHITexture*>(9);
|
||||
targets.objectIdDepthView = reinterpret_cast<::XCEngine::RHI::RHIResourceView*>(10);
|
||||
targets.objectIdView = reinterpret_cast<::XCEngine::RHI::RHIResourceView*>(11);
|
||||
targets.objectIdShaderView = reinterpret_cast<::XCEngine::RHI::RHIResourceView*>(12);
|
||||
EXPECT_TRUE(CanReuseViewportResources(
|
||||
BuildViewportRenderTargetsReuseQuery(requirements, targets, 1280u, 720u)));
|
||||
|
||||
requirements.requiresSelectionMaskSurface = true;
|
||||
EXPECT_FALSE(CanReuseViewportResources(
|
||||
BuildViewportRenderTargetsReuseQuery(requirements, targets, 1280u, 720u)));
|
||||
|
||||
targets.selectionMaskTexture = reinterpret_cast<::XCEngine::RHI::RHITexture*>(13);
|
||||
targets.selectionMaskView = reinterpret_cast<::XCEngine::RHI::RHIResourceView*>(14);
|
||||
targets.selectionMaskShaderView = reinterpret_cast<::XCEngine::RHI::RHIResourceView*>(15);
|
||||
EXPECT_TRUE(CanReuseViewportResources(
|
||||
BuildViewportRenderTargetsReuseQuery(requirements, targets, 1280u, 720u)));
|
||||
}
|
||||
|
||||
TEST(SceneViewportRuntimeTests, ViewportHostServiceAcceptsArbitraryViewportIds) {
|
||||
ViewportHostService hostService = {};
|
||||
hostService.BeginFrame();
|
||||
|
||||
const ViewportFrame frame =
|
||||
hostService.RequestViewport("preview_inspector", UISize(320.0f, 200.0f));
|
||||
|
||||
EXPECT_TRUE(frame.wasRequested);
|
||||
EXPECT_FALSE(frame.hasTexture);
|
||||
EXPECT_FLOAT_EQ(frame.requestedSize.width, 320.0f);
|
||||
EXPECT_FLOAT_EQ(frame.requestedSize.height, 200.0f);
|
||||
EXPECT_FLOAT_EQ(frame.renderSize.width, 0.0f);
|
||||
EXPECT_FLOAT_EQ(frame.renderSize.height, 0.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace XCEngine::UI::Editor::App
|
||||
@@ -1,201 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Composition/EditorShellAssetBuilder.h"
|
||||
|
||||
#include <XCEditor/Shell/UIEditorShellAsset.h>
|
||||
#include <XCEditor/Shell/UIEditorStructuredShell.h>
|
||||
#include <XCEditor/Shell/UIEditorShellInteraction.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceModel.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceSession.h>
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#ifndef XCUIEDITOR_REPO_ROOT
|
||||
#define XCUIEDITOR_REPO_ROOT "."
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::Editor::App::BuildEditorApplicationShellAsset;
|
||||
using XCEngine::UI::Editor::BuildStructuredEditorShellBinding;
|
||||
using XCEngine::UI::Editor::BuildStructuredEditorShellServices;
|
||||
using XCEngine::UI::Editor::ResolveUIEditorShellInteractionModel;
|
||||
using XCEngine::UI::Editor::AreUIEditorWorkspaceModelsEquivalent;
|
||||
using XCEngine::UI::Editor::AreUIEditorWorkspaceSessionsEquivalent;
|
||||
using XCEngine::UI::Editor::UIEditorCommandPanelSource;
|
||||
using XCEngine::UI::Editor::UIEditorMenuItemKind;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIShortcutBinding;
|
||||
using XCEngine::UI::UIShortcutScope;
|
||||
|
||||
std::filesystem::path RepoRootPath() {
|
||||
std::string root = XCUIEDITOR_REPO_ROOT;
|
||||
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
|
||||
root = root.substr(1u, root.size() - 2u);
|
||||
}
|
||||
return std::filesystem::path(root).lexically_normal();
|
||||
}
|
||||
|
||||
bool PanelRegistriesMatch(
|
||||
const XCEngine::UI::Editor::UIEditorPanelRegistry& lhs,
|
||||
const XCEngine::UI::Editor::UIEditorPanelRegistry& rhs) {
|
||||
if (lhs.panels.size() != rhs.panels.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < lhs.panels.size(); ++index) {
|
||||
const auto& left = lhs.panels[index];
|
||||
const auto& right = rhs.panels[index];
|
||||
if (left.panelId != right.panelId ||
|
||||
left.defaultTitle != right.defaultTitle ||
|
||||
left.presentationKind != right.presentationKind ||
|
||||
left.placeholder != right.placeholder ||
|
||||
left.canHide != right.canHide ||
|
||||
left.canClose != right.canClose) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShellDefinitionsMatch(
|
||||
const XCEngine::UI::Editor::UIEditorShellInteractionDefinition& lhs,
|
||||
const XCEngine::UI::Editor::UIEditorShellInteractionDefinition& rhs) {
|
||||
if (lhs.menuModel.menus.size() != rhs.menuModel.menus.size() ||
|
||||
lhs.statusSegments.size() != rhs.statusSegments.size() ||
|
||||
lhs.workspacePresentations.size() != rhs.workspacePresentations.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < lhs.menuModel.menus.size(); ++index) {
|
||||
const auto& left = lhs.menuModel.menus[index];
|
||||
const auto& right = rhs.menuModel.menus[index];
|
||||
if (left.menuId != right.menuId ||
|
||||
left.label != right.label ||
|
||||
left.items.size() != right.items.size()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < lhs.statusSegments.size(); ++index) {
|
||||
const auto& left = lhs.statusSegments[index];
|
||||
const auto& right = rhs.statusSegments[index];
|
||||
if (left.segmentId != right.segmentId ||
|
||||
left.label != right.label ||
|
||||
left.slot != right.slot ||
|
||||
left.tone != right.tone ||
|
||||
left.interactive != right.interactive ||
|
||||
left.showSeparator != right.showSeparator ||
|
||||
left.desiredWidth != right.desiredWidth) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < lhs.workspacePresentations.size(); ++index) {
|
||||
const auto& left = lhs.workspacePresentations[index];
|
||||
const auto& right = rhs.workspacePresentations[index];
|
||||
if (left.panelId != right.panelId ||
|
||||
left.kind != right.kind ||
|
||||
left.viewportShellModel.spec.chrome.title != right.viewportShellModel.spec.chrome.title ||
|
||||
left.viewportShellModel.spec.chrome.subtitle != right.viewportShellModel.spec.chrome.subtitle ||
|
||||
left.viewportShellModel.spec.chrome.showTopBar != right.viewportShellModel.spec.chrome.showTopBar ||
|
||||
left.viewportShellModel.spec.chrome.showBottomBar != right.viewportShellModel.spec.chrome.showBottomBar ||
|
||||
left.viewportShellModel.frame.statusText != right.viewportShellModel.frame.statusText) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
UIShortcutBinding MakeBinding(std::string commandId, KeyCode keyCode) {
|
||||
UIShortcutBinding binding = {};
|
||||
binding.commandId = std::move(commandId);
|
||||
binding.scope = UIShortcutScope::Global;
|
||||
binding.triggerEventType = UIInputEventType::KeyDown;
|
||||
binding.chord.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
binding.chord.modifiers.control = true;
|
||||
return binding;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(EditorUIStructuredShellTest, StructuredEditorShellDoesNotRequireRepositoryXCUIDocument) {
|
||||
const auto shell = BuildEditorApplicationShellAsset(RepoRootPath());
|
||||
const auto binding = BuildStructuredEditorShellBinding(shell);
|
||||
|
||||
ASSERT_TRUE(binding.IsValid()) << binding.assetValidation.message;
|
||||
EXPECT_TRUE(binding.screenAsset.documentPath.empty());
|
||||
EXPECT_TRUE(shell.documentPath.empty());
|
||||
EXPECT_TRUE(binding.screenAsset.themePath.empty());
|
||||
}
|
||||
|
||||
TEST(EditorUIStructuredShellTest, StructuredShellBindingUsesEditorShellAssetAsSingleSource) {
|
||||
auto shell = BuildEditorApplicationShellAsset(RepoRootPath());
|
||||
shell.shortcutAsset.commandRegistry.commands = {
|
||||
{
|
||||
"workspace.reset_layout",
|
||||
"Reset Layout",
|
||||
{ UIEditorWorkspaceCommandKind::ResetWorkspace, UIEditorCommandPanelSource::None, {} }
|
||||
}
|
||||
};
|
||||
shell.shortcutAsset.bindings = {
|
||||
MakeBinding("workspace.reset_layout", KeyCode::R)
|
||||
};
|
||||
XCEngine::UI::Editor::UIEditorMenuItemDescriptor assetCommand = {};
|
||||
assetCommand.kind = UIEditorMenuItemKind::Command;
|
||||
assetCommand.itemId = "asset-reset-layout";
|
||||
assetCommand.commandId = "workspace.reset_layout";
|
||||
XCEngine::UI::Editor::UIEditorMenuDescriptor assetMenu = {};
|
||||
assetMenu.menuId = "asset";
|
||||
assetMenu.label = "Asset";
|
||||
assetMenu.items = { assetCommand };
|
||||
shell.shellDefinition.menuModel.menus = { assetMenu };
|
||||
const auto binding = BuildStructuredEditorShellBinding(shell);
|
||||
|
||||
ASSERT_TRUE(binding.IsValid()) << binding.assetValidation.message;
|
||||
EXPECT_EQ(binding.screenAsset.screenId, shell.screenId);
|
||||
EXPECT_TRUE(binding.screenAsset.documentPath.empty());
|
||||
EXPECT_TRUE(shell.documentPath.empty());
|
||||
EXPECT_TRUE(binding.screenAsset.themePath.empty());
|
||||
|
||||
EXPECT_TRUE(PanelRegistriesMatch(binding.workspaceController.GetPanelRegistry(), shell.panelRegistry));
|
||||
EXPECT_TRUE(AreUIEditorWorkspaceModelsEquivalent(binding.workspaceController.GetWorkspace(), shell.workspace));
|
||||
EXPECT_TRUE(AreUIEditorWorkspaceSessionsEquivalent(binding.workspaceController.GetSession(), shell.workspaceSession));
|
||||
EXPECT_TRUE(ShellDefinitionsMatch(binding.shellDefinition, shell.shellDefinition));
|
||||
EXPECT_TRUE(binding.workspaceController.ValidateState().IsValid());
|
||||
EXPECT_TRUE(binding.shortcutManager.ValidateConfiguration().IsValid());
|
||||
|
||||
const auto services = BuildStructuredEditorShellServices(binding);
|
||||
ASSERT_NE(services.commandDispatcher, nullptr);
|
||||
EXPECT_EQ(services.shortcutManager, &binding.shortcutManager);
|
||||
|
||||
const auto model = ResolveUIEditorShellInteractionModel(
|
||||
binding.workspaceController,
|
||||
binding.shellDefinition,
|
||||
services);
|
||||
|
||||
ASSERT_EQ(model.resolvedMenuModel.menus.size(), 1u);
|
||||
EXPECT_EQ(model.resolvedMenuModel.menus.front().menuId, "asset");
|
||||
EXPECT_EQ(model.resolvedMenuModel.menus.front().label, "Asset");
|
||||
ASSERT_EQ(model.resolvedMenuModel.menus.front().items.size(), 1u);
|
||||
EXPECT_EQ(model.resolvedMenuModel.menus.front().items.front().commandId, "workspace.reset_layout");
|
||||
EXPECT_EQ(model.resolvedMenuModel.menus.front().items.front().label, "Reset Layout");
|
||||
EXPECT_EQ(model.resolvedMenuModel.menus.front().items.front().shortcutText, "Ctrl+R");
|
||||
ASSERT_EQ(model.statusSegments.size(), shell.shellDefinition.statusSegments.size());
|
||||
ASSERT_EQ(model.statusSegments.size(), shell.shellDefinition.statusSegments.size());
|
||||
ASSERT_EQ(
|
||||
model.workspacePresentations.size(),
|
||||
shell.shellDefinition.workspacePresentations.size());
|
||||
EXPECT_EQ(
|
||||
model.workspacePresentations.front().panelId,
|
||||
shell.shellDefinition.workspacePresentations.front().panelId);
|
||||
EXPECT_EQ(
|
||||
model.workspacePresentations.front().kind,
|
||||
shell.shellDefinition.workspacePresentations.front().kind);
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorAssetField.h>
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIDrawData;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorAssetFieldBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorAssetFieldForeground;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorAssetFieldLayout;
|
||||
using XCEngine::UI::Editor::Widgets::HasUIEditorAssetFieldValue;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorAssetField;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorAssetFieldValueText;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorAssetFieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorAssetFieldSpec;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorAssetFieldState;
|
||||
|
||||
TEST(UIEditorAssetFieldTest, ValueTextUsesDisplayNameAndFallsBackToEmptyText) {
|
||||
UIEditorAssetFieldSpec assignedSpec = {};
|
||||
assignedSpec.displayName = "Crate_Albedo";
|
||||
assignedSpec.emptyText = "None (Texture)";
|
||||
|
||||
EXPECT_TRUE(HasUIEditorAssetFieldValue(assignedSpec));
|
||||
EXPECT_EQ(ResolveUIEditorAssetFieldValueText(assignedSpec), "Crate_Albedo");
|
||||
|
||||
UIEditorAssetFieldSpec emptySpec = {};
|
||||
emptySpec.emptyText = "None (Texture)";
|
||||
|
||||
EXPECT_FALSE(HasUIEditorAssetFieldValue(emptySpec));
|
||||
EXPECT_EQ(ResolveUIEditorAssetFieldValueText(emptySpec), "None (Texture)");
|
||||
}
|
||||
|
||||
TEST(UIEditorAssetFieldTest, LayoutReservesPreviewStatusAndActionButtons) {
|
||||
UIEditorAssetFieldSpec spec = {};
|
||||
spec.fieldId = "material.base_color";
|
||||
spec.label = "Base Map";
|
||||
spec.assetId = "assets/textures/crate_albedo";
|
||||
spec.displayName = "Crate_Albedo";
|
||||
spec.statusText = "Ready";
|
||||
|
||||
const auto layout = BuildUIEditorAssetFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.x, 236.0f);
|
||||
EXPECT_FLOAT_EQ(layout.valueRect.y, 1.0f);
|
||||
EXPECT_GT(layout.previewRect.width, 0.0f);
|
||||
EXPECT_GT(layout.statusBadgeRect.width, 0.0f);
|
||||
EXPECT_FLOAT_EQ(layout.pickerRect.width, 20.0f);
|
||||
EXPECT_FLOAT_EQ(layout.clearRect.width, 20.0f);
|
||||
EXPECT_GT(layout.textRect.width, 0.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorAssetFieldTest, HitTestResolvesClearPickerAndValueBox) {
|
||||
UIEditorAssetFieldSpec spec = {};
|
||||
spec.assetId = "assets/textures/crate_albedo";
|
||||
spec.displayName = "Crate_Albedo";
|
||||
spec.statusText = "Ready";
|
||||
|
||||
const auto layout = BuildUIEditorAssetFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
|
||||
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorAssetField(
|
||||
layout,
|
||||
UIPoint(layout.clearRect.x + 2.0f, layout.clearRect.y + 2.0f)).kind,
|
||||
UIEditorAssetFieldHitTargetKind::ClearButton);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorAssetField(
|
||||
layout,
|
||||
UIPoint(layout.pickerRect.x + 2.0f, layout.pickerRect.y + 2.0f)).kind,
|
||||
UIEditorAssetFieldHitTargetKind::PickerButton);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorAssetField(
|
||||
layout,
|
||||
UIPoint(layout.textRect.x + 2.0f, layout.textRect.y + 2.0f)).kind,
|
||||
UIEditorAssetFieldHitTargetKind::ValueBox);
|
||||
}
|
||||
|
||||
TEST(UIEditorAssetFieldTest, DrawCommandsContainPreviewStatusAndActionGlyphs) {
|
||||
UIEditorAssetFieldSpec spec = {};
|
||||
spec.label = "Base Map";
|
||||
spec.assetId = "assets/textures/crate_albedo";
|
||||
spec.displayName = "Crate_Albedo";
|
||||
spec.statusText = "Ready";
|
||||
UIEditorAssetFieldState state = {};
|
||||
|
||||
UIDrawData drawData = {};
|
||||
auto& drawList = drawData.EmplaceDrawList("AssetField");
|
||||
const auto layout = BuildUIEditorAssetFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
|
||||
AppendUIEditorAssetFieldBackground(drawList, layout, spec, state);
|
||||
AppendUIEditorAssetFieldForeground(drawList, layout, spec, state);
|
||||
|
||||
bool foundLabel = false;
|
||||
bool foundValue = false;
|
||||
bool foundStatus = false;
|
||||
bool foundPicker = false;
|
||||
bool foundClear = false;
|
||||
bool foundPreviewGradient = false;
|
||||
for (const auto& command : drawList.GetCommands()) {
|
||||
if (command.type == UIDrawCommandType::FilledRectLinearGradient &&
|
||||
command.rect.x == layout.previewRect.x) {
|
||||
foundPreviewGradient = true;
|
||||
}
|
||||
if (command.type == UIDrawCommandType::Text) {
|
||||
foundLabel = foundLabel || command.text == "Base Map";
|
||||
foundValue = foundValue || command.text == "Crate_Albedo";
|
||||
foundStatus = foundStatus || command.text == "Ready";
|
||||
foundPicker = foundPicker || command.text == "o";
|
||||
foundClear = foundClear || command.text == "X";
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(foundPreviewGradient);
|
||||
EXPECT_TRUE(foundLabel);
|
||||
EXPECT_TRUE(foundValue);
|
||||
EXPECT_TRUE(foundStatus);
|
||||
EXPECT_TRUE(foundPicker);
|
||||
EXPECT_TRUE(foundClear);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,189 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorAssetFieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::UIEditorAssetFieldInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorAssetFieldInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorAssetFieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorAssetFieldSpec;
|
||||
|
||||
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKey(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIEditorAssetFieldSpec MakeAssignedSpec() {
|
||||
UIEditorAssetFieldSpec spec = {};
|
||||
spec.fieldId = "material.base_color";
|
||||
spec.label = "Base Map";
|
||||
spec.assetId = "assets/textures/crate_albedo";
|
||||
spec.displayName = "Crate_Albedo";
|
||||
spec.statusText = "Ready";
|
||||
return spec;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorAssetFieldInteractionTest, ClickPickerRequestsPickerWithoutMutatingValue) {
|
||||
UIEditorAssetFieldSpec spec = MakeAssignedSpec();
|
||||
UIEditorAssetFieldInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorAssetFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
{});
|
||||
|
||||
frame = UpdateUIEditorAssetFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
{
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
frame.layout.pickerRect.x + 2.0f,
|
||||
frame.layout.pickerRect.y + 2.0f,
|
||||
UIPointerButton::Left),
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonUp,
|
||||
frame.layout.pickerRect.x + 2.0f,
|
||||
frame.layout.pickerRect.y + 2.0f,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.pickerRequested);
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorAssetFieldHitTargetKind::PickerButton);
|
||||
EXPECT_EQ(spec.assetId, "assets/textures/crate_albedo");
|
||||
EXPECT_EQ(spec.displayName, "Crate_Albedo");
|
||||
EXPECT_TRUE(state.fieldState.focused);
|
||||
}
|
||||
|
||||
TEST(UIEditorAssetFieldInteractionTest, ClickClearClearsAssignedValue) {
|
||||
UIEditorAssetFieldSpec spec = MakeAssignedSpec();
|
||||
UIEditorAssetFieldInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorAssetFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
{});
|
||||
|
||||
frame = UpdateUIEditorAssetFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
{
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
frame.layout.clearRect.x + 2.0f,
|
||||
frame.layout.clearRect.y + 2.0f,
|
||||
UIPointerButton::Left),
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonUp,
|
||||
frame.layout.clearRect.x + 2.0f,
|
||||
frame.layout.clearRect.y + 2.0f,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.clearRequested);
|
||||
EXPECT_TRUE(frame.result.valueChanged);
|
||||
EXPECT_EQ(frame.result.assetIdBefore, "assets/textures/crate_albedo");
|
||||
EXPECT_EQ(frame.result.displayNameBefore, "Crate_Albedo");
|
||||
EXPECT_TRUE(spec.assetId.empty());
|
||||
EXPECT_TRUE(spec.displayName.empty());
|
||||
EXPECT_TRUE(spec.statusText.empty());
|
||||
}
|
||||
|
||||
TEST(UIEditorAssetFieldInteractionTest, ClickValueBoxRequestsActivate) {
|
||||
UIEditorAssetFieldSpec spec = MakeAssignedSpec();
|
||||
UIEditorAssetFieldInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorAssetFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
{});
|
||||
|
||||
frame = UpdateUIEditorAssetFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
{
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
frame.layout.textRect.x + 2.0f,
|
||||
frame.layout.textRect.y + 2.0f,
|
||||
UIPointerButton::Left),
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonUp,
|
||||
frame.layout.textRect.x + 2.0f,
|
||||
frame.layout.textRect.y + 2.0f,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.activateRequested);
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorAssetFieldHitTargetKind::ValueBox);
|
||||
}
|
||||
|
||||
TEST(UIEditorAssetFieldInteractionTest, KeyboardEnterRequestsPickerAndDeleteClearsFocusedValue) {
|
||||
UIEditorAssetFieldSpec spec = MakeAssignedSpec();
|
||||
UIEditorAssetFieldInteractionState state = {};
|
||||
state.fieldState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorAssetFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
{ MakeKey(KeyCode::Enter) });
|
||||
EXPECT_TRUE(frame.result.pickerRequested);
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorAssetFieldHitTargetKind::PickerButton);
|
||||
|
||||
frame = UpdateUIEditorAssetFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
{ MakeKey(KeyCode::Delete) });
|
||||
EXPECT_TRUE(frame.result.clearRequested);
|
||||
EXPECT_TRUE(frame.result.valueChanged);
|
||||
EXPECT_TRUE(spec.assetId.empty());
|
||||
}
|
||||
|
||||
TEST(UIEditorAssetFieldInteractionTest, ReadOnlyFieldIgnoresPickerAndClearRequests) {
|
||||
UIEditorAssetFieldSpec spec = MakeAssignedSpec();
|
||||
spec.readOnly = true;
|
||||
UIEditorAssetFieldInteractionState state = {};
|
||||
state.fieldState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorAssetFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
{ MakeKey(KeyCode::Enter), MakeKey(KeyCode::Delete) });
|
||||
|
||||
EXPECT_FALSE(frame.result.pickerRequested);
|
||||
EXPECT_FALSE(frame.result.clearRequested);
|
||||
EXPECT_FALSE(frame.result.valueChanged);
|
||||
EXPECT_EQ(spec.assetId, "assets/textures/crate_albedo");
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEditor/Fields/UIEditorBoolField.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIColor;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorBoolFieldBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorBoolFieldForeground;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorBoolFieldLayout;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorBoolField;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldSpec;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldState;
|
||||
|
||||
TEST(UIEditorBoolFieldTest, LayoutBuildsLabelAndToggleRects) {
|
||||
UIEditorBoolFieldSpec spec = { "visible", "Visible", true, false };
|
||||
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
|
||||
|
||||
EXPECT_GT(layout.labelRect.width, 0.0f);
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.x, 236.0f);
|
||||
EXPECT_FLOAT_EQ(layout.checkboxRect.width, 18.0f);
|
||||
EXPECT_FLOAT_EQ(layout.checkmarkRect.width, layout.checkboxRect.width);
|
||||
EXPECT_FLOAT_EQ(layout.checkboxRect.y, 2.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorBoolFieldTest, HitTestResolvesToggleAndRow) {
|
||||
UIEditorBoolFieldSpec spec = { "visible", "Visible", false, false };
|
||||
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
|
||||
|
||||
const auto checkboxHit = HitTestUIEditorBoolField(
|
||||
layout,
|
||||
UIPoint(layout.controlRect.x + layout.controlRect.width - 2.0f, layout.controlRect.y + 2.0f));
|
||||
EXPECT_EQ(checkboxHit.kind, UIEditorBoolFieldHitTargetKind::Checkbox);
|
||||
|
||||
const auto rowHit = HitTestUIEditorBoolField(layout, UIPoint(20.0f, 11.0f));
|
||||
EXPECT_EQ(rowHit.kind, UIEditorBoolFieldHitTargetKind::Row);
|
||||
}
|
||||
|
||||
TEST(UIEditorBoolFieldTest, BackgroundAndForegroundEmitCheckboxOnlyChromeAndCenteredText) {
|
||||
UIEditorBoolFieldSpec spec = { "visible", "Visible", true, false };
|
||||
UIEditorBoolFieldState state = {};
|
||||
state.focused = true;
|
||||
state.hoveredTarget = UIEditorBoolFieldHitTargetKind::Checkbox;
|
||||
const XCEngine::UI::Editor::Widgets::UIEditorBoolFieldPalette palette = {};
|
||||
|
||||
XCEngine::UI::UIDrawData drawData = {};
|
||||
auto& drawList = drawData.EmplaceDrawList("BoolField");
|
||||
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
|
||||
AppendUIEditorBoolFieldBackground(drawList, layout, spec, state, palette);
|
||||
AppendUIEditorBoolFieldForeground(drawList, layout, spec, palette);
|
||||
|
||||
const auto& commands = drawList.GetCommands();
|
||||
ASSERT_EQ(commands.size(), 6u);
|
||||
EXPECT_EQ(commands[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(commands[0].rect.x, layout.checkboxRect.x);
|
||||
EXPECT_EQ(commands[0].rect.y, layout.checkboxRect.y);
|
||||
EXPECT_EQ(commands[1].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_FLOAT_EQ(commands[1].color.r, palette.checkboxBorderColor.r);
|
||||
EXPECT_EQ(commands[2].type, UIDrawCommandType::PushClipRect);
|
||||
EXPECT_EQ(commands[3].type, UIDrawCommandType::Text);
|
||||
EXPECT_FLOAT_EQ(commands[3].position.y, 2.0f);
|
||||
EXPECT_EQ(commands[4].type, UIDrawCommandType::PopClipRect);
|
||||
EXPECT_EQ(commands[5].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(commands[5].text, "V");
|
||||
EXPECT_FLOAT_EQ(commands[5].position.y, 2.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,109 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorBoolFieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::UIEditorBoolFieldInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorBoolFieldInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldSpec;
|
||||
|
||||
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKey(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
return event;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorBoolFieldInteractionTest, ClickToggleFlipsValue) {
|
||||
UIEditorBoolFieldSpec spec = { "visible", "Visible", false, false };
|
||||
UIEditorBoolFieldInteractionState state = {};
|
||||
bool value = false;
|
||||
|
||||
auto frame = UpdateUIEditorBoolFieldInteraction(
|
||||
state,
|
||||
value,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
spec,
|
||||
{});
|
||||
const auto checkbox = frame.layout.checkboxRect;
|
||||
|
||||
frame = UpdateUIEditorBoolFieldInteraction(
|
||||
state,
|
||||
value,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
spec,
|
||||
{
|
||||
MakePointer(UIInputEventType::PointerButtonDown, checkbox.x + 4.0f, checkbox.y + 4.0f, UIPointerButton::Left),
|
||||
MakePointer(UIInputEventType::PointerButtonUp, checkbox.x + 4.0f, checkbox.y + 4.0f, UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.valueChanged);
|
||||
EXPECT_TRUE(value);
|
||||
}
|
||||
|
||||
TEST(UIEditorBoolFieldInteractionTest, SpaceAndEnterToggleWhenFocused) {
|
||||
UIEditorBoolFieldSpec spec = { "visible", "Visible", false, false };
|
||||
UIEditorBoolFieldInteractionState state = {};
|
||||
state.fieldState.focused = true;
|
||||
bool value = false;
|
||||
|
||||
auto frame = UpdateUIEditorBoolFieldInteraction(
|
||||
state,
|
||||
value,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
spec,
|
||||
{ MakeKey(KeyCode::Space) });
|
||||
EXPECT_TRUE(frame.result.valueChanged);
|
||||
EXPECT_TRUE(value);
|
||||
|
||||
frame = UpdateUIEditorBoolFieldInteraction(
|
||||
state,
|
||||
value,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
spec,
|
||||
{ MakeKey(KeyCode::Enter) });
|
||||
EXPECT_TRUE(frame.result.valueChanged);
|
||||
EXPECT_FALSE(value);
|
||||
}
|
||||
|
||||
TEST(UIEditorBoolFieldInteractionTest, HoverTracksToggleHitTarget) {
|
||||
UIEditorBoolFieldSpec spec = { "visible", "Visible", false, false };
|
||||
UIEditorBoolFieldInteractionState state = {};
|
||||
bool value = false;
|
||||
|
||||
auto frame = UpdateUIEditorBoolFieldInteraction(
|
||||
state,
|
||||
value,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
spec,
|
||||
{});
|
||||
const auto checkbox = frame.layout.checkboxRect;
|
||||
|
||||
frame = UpdateUIEditorBoolFieldInteraction(
|
||||
state,
|
||||
value,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
spec,
|
||||
{ MakePointer(UIInputEventType::PointerMove, checkbox.x + 4.0f, checkbox.y + 4.0f) });
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorBoolFieldHitTargetKind::Checkbox);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Widgets/UIEditorCollectionPrimitives.h>
|
||||
|
||||
namespace {
|
||||
|
||||
namespace UIWidgets = XCEngine::UI::Editor::Widgets;
|
||||
|
||||
TEST(UIEditorCollectionPrimitivesTest, ClassifyAndFlagsMatchEditorCollectionTags) {
|
||||
using Kind = UIWidgets::UIEditorCollectionPrimitiveKind;
|
||||
|
||||
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("ScrollView"), Kind::ScrollView);
|
||||
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("TreeView"), Kind::TreeView);
|
||||
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("TreeItem"), Kind::TreeItem);
|
||||
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("ListView"), Kind::ListView);
|
||||
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("ListItem"), Kind::ListItem);
|
||||
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("PropertySection"), Kind::PropertySection);
|
||||
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("FieldRow"), Kind::FieldRow);
|
||||
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("Column"), Kind::None);
|
||||
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveContainer(Kind::ScrollView));
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveContainer(Kind::TreeView));
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveContainer(Kind::ListView));
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveContainer(Kind::PropertySection));
|
||||
EXPECT_FALSE(UIWidgets::IsUIEditorCollectionPrimitiveContainer(Kind::TreeItem));
|
||||
|
||||
EXPECT_TRUE(UIWidgets::UsesUIEditorCollectionPrimitiveColumnLayout(Kind::TreeView));
|
||||
EXPECT_TRUE(UIWidgets::UsesUIEditorCollectionPrimitiveColumnLayout(Kind::ListView));
|
||||
EXPECT_TRUE(UIWidgets::UsesUIEditorCollectionPrimitiveColumnLayout(Kind::PropertySection));
|
||||
EXPECT_FALSE(UIWidgets::UsesUIEditorCollectionPrimitiveColumnLayout(Kind::ScrollView));
|
||||
|
||||
EXPECT_TRUE(UIWidgets::DoesUIEditorCollectionPrimitiveClipChildren(Kind::ScrollView));
|
||||
EXPECT_TRUE(UIWidgets::DoesUIEditorCollectionPrimitiveClipChildren(Kind::TreeView));
|
||||
EXPECT_TRUE(UIWidgets::DoesUIEditorCollectionPrimitiveClipChildren(Kind::ListView));
|
||||
EXPECT_FALSE(UIWidgets::DoesUIEditorCollectionPrimitiveClipChildren(Kind::PropertySection));
|
||||
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::TreeItem));
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::ListItem));
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::PropertySection));
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::FieldRow));
|
||||
EXPECT_FALSE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::TreeView));
|
||||
}
|
||||
|
||||
TEST(UIEditorCollectionPrimitivesTest, ResolveMetricsUseFixedEditorDefaults) {
|
||||
using Kind = UIWidgets::UIEditorCollectionPrimitiveKind;
|
||||
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitivePadding(Kind::TreeView), 12.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitivePadding(Kind::ListView), 12.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitivePadding(Kind::PropertySection), 12.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitivePadding(Kind::ScrollView), 0.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::TreeItem), 28.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::ListItem), 60.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::FieldRow), 32.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::PropertySection), 148.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveIndent(Kind::TreeItem, 2.0f), 36.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveIndent(Kind::ListItem, 2.0f), 0.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,140 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEditor/Fields/UIEditorColorField.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorColorField;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorColorFieldLayout;
|
||||
using XCEngine::UI::Editor::Widgets::FormatUIEditorColorFieldHexText;
|
||||
using XCEngine::UI::Editor::Widgets::FormatUIEditorColorFieldRgbaText;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorColorField;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorColorFieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorColorFieldSpec;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorColorFieldState;
|
||||
|
||||
TEST(UIEditorColorFieldTest, FormatsHexTextWithAndWithoutAlpha) {
|
||||
UIEditorColorFieldSpec spec = {};
|
||||
spec.value = XCEngine::UI::UIColor(1.0f, 0.5f, 0.25f, 0.75f);
|
||||
spec.showAlpha = true;
|
||||
|
||||
EXPECT_EQ(FormatUIEditorColorFieldHexText(spec), "#FF8040BF");
|
||||
|
||||
spec.showAlpha = false;
|
||||
EXPECT_EQ(FormatUIEditorColorFieldHexText(spec), "#FF8040");
|
||||
}
|
||||
|
||||
TEST(UIEditorColorFieldTest, FormatsRgbaReadoutForInspectorSummary) {
|
||||
UIEditorColorFieldSpec spec = {};
|
||||
spec.value = XCEngine::UI::UIColor(0.25f, 0.5f, 0.75f, 1.0f);
|
||||
|
||||
EXPECT_EQ(FormatUIEditorColorFieldRgbaText(spec), "RGBA 64, 128, 191, 255");
|
||||
}
|
||||
|
||||
TEST(UIEditorColorFieldTest, LayoutKeepsInspectorColumnAndCompactSwatch) {
|
||||
UIEditorColorFieldSpec spec = {};
|
||||
spec.fieldId = "albedo";
|
||||
spec.label = "Albedo";
|
||||
|
||||
const auto layout = BuildUIEditorColorFieldLayout(
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
spec);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.swatchRect.x, 236.0f);
|
||||
EXPECT_FLOAT_EQ(layout.swatchRect.width, 54.0f);
|
||||
EXPECT_FLOAT_EQ(layout.swatchRect.height, 18.0f);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorColorField(
|
||||
layout,
|
||||
false,
|
||||
UIPoint(layout.swatchRect.x + 2.0f, layout.swatchRect.y + 2.0f)).kind,
|
||||
UIEditorColorFieldHitTargetKind::Swatch);
|
||||
}
|
||||
|
||||
TEST(UIEditorColorFieldTest, PopupLayoutExposesHueWheelAndChannelTargets) {
|
||||
UIEditorColorFieldSpec spec = {};
|
||||
spec.showAlpha = true;
|
||||
|
||||
const auto layout = BuildUIEditorColorFieldLayout(
|
||||
UIRect(10.0f, 20.0f, 360.0f, 22.0f),
|
||||
spec,
|
||||
{},
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f));
|
||||
|
||||
EXPECT_GT(layout.saturationValueRect.width, 0.0f);
|
||||
EXPECT_GT(layout.hueWheelOuterRadius, layout.hueWheelInnerRadius);
|
||||
EXPECT_GT(layout.redSliderRect.width, 0.0f);
|
||||
EXPECT_GT(layout.alphaSliderRect.width, 0.0f);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorColorField(
|
||||
layout,
|
||||
true,
|
||||
UIPoint(
|
||||
layout.hueWheelCenter.x + (layout.hueWheelInnerRadius + layout.hueWheelOuterRadius) * 0.5f,
|
||||
layout.hueWheelCenter.y)).kind,
|
||||
UIEditorColorFieldHitTargetKind::HueWheel);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorColorField(
|
||||
layout,
|
||||
true,
|
||||
UIPoint(layout.saturationValueRect.x + 5.0f, layout.saturationValueRect.y + 5.0f)).kind,
|
||||
UIEditorColorFieldHitTargetKind::SaturationValue);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorColorField(
|
||||
layout,
|
||||
true,
|
||||
UIPoint(layout.redSliderRect.x + 2.0f, layout.redSliderRect.y + 2.0f)).kind,
|
||||
UIEditorColorFieldHitTargetKind::RedChannel);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorColorField(
|
||||
layout,
|
||||
true,
|
||||
UIPoint(layout.alphaSliderRect.x + 2.0f, layout.alphaSliderRect.y + 2.0f)).kind,
|
||||
UIEditorColorFieldHitTargetKind::AlphaChannel);
|
||||
}
|
||||
|
||||
TEST(UIEditorColorFieldTest, PopupDrawEmitsHeaderWheelHandlesAndHexadecimalLabel) {
|
||||
UIEditorColorFieldSpec spec = {};
|
||||
spec.label = "Tint";
|
||||
spec.value = XCEngine::UI::UIColor(0.8f, 0.4f, 0.2f, 0.5f);
|
||||
spec.showAlpha = true;
|
||||
|
||||
UIEditorColorFieldState state = {};
|
||||
state.popupOpen = true;
|
||||
|
||||
XCEngine::UI::UIDrawData drawData = {};
|
||||
auto& drawList = drawData.EmplaceDrawList("ColorField");
|
||||
AppendUIEditorColorField(
|
||||
drawList,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
spec,
|
||||
state,
|
||||
{},
|
||||
{},
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f));
|
||||
|
||||
bool hasFilledCircle = false;
|
||||
bool hasCircleOutline = false;
|
||||
bool hasLine = false;
|
||||
bool hasTitleText = false;
|
||||
bool hasHexLabel = false;
|
||||
for (const auto& command : drawList.GetCommands()) {
|
||||
hasFilledCircle = hasFilledCircle || command.type == UIDrawCommandType::FilledCircle;
|
||||
hasCircleOutline = hasCircleOutline || command.type == UIDrawCommandType::CircleOutline;
|
||||
hasLine = hasLine || command.type == UIDrawCommandType::Line;
|
||||
hasTitleText = hasTitleText || command.text == "Color";
|
||||
hasHexLabel = hasHexLabel || command.text == "Hexadecimal";
|
||||
}
|
||||
|
||||
EXPECT_TRUE(hasFilledCircle);
|
||||
EXPECT_TRUE(hasCircleOutline);
|
||||
EXPECT_TRUE(hasLine);
|
||||
EXPECT_TRUE(hasTitleText);
|
||||
EXPECT_TRUE(hasHexLabel);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,138 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorColorFieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::UIEditorColorFieldInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorColorFieldInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorColorFieldSpec;
|
||||
|
||||
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKey(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
return event;
|
||||
}
|
||||
|
||||
TEST(UIEditorColorFieldInteractionTest, ClickSwatchOpensPopupAndEscapeClosesIt) {
|
||||
UIEditorColorFieldSpec spec = {};
|
||||
spec.fieldId = "tint";
|
||||
spec.label = "Tint";
|
||||
spec.showAlpha = true;
|
||||
spec.value = XCEngine::UI::UIColor(0.8f, 0.4f, 0.2f, 0.5f);
|
||||
|
||||
UIEditorColorFieldInteractionState state = {};
|
||||
auto frame = UpdateUIEditorColorFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{});
|
||||
|
||||
frame = UpdateUIEditorColorFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
frame.layout.swatchRect.x + 2.0f,
|
||||
frame.layout.swatchRect.y + 2.0f,
|
||||
UIPointerButton::Left),
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonUp,
|
||||
frame.layout.swatchRect.x + 2.0f,
|
||||
frame.layout.swatchRect.y + 2.0f,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
EXPECT_TRUE(frame.result.popupOpened);
|
||||
EXPECT_TRUE(state.colorFieldState.popupOpen);
|
||||
|
||||
frame = UpdateUIEditorColorFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Escape) });
|
||||
EXPECT_TRUE(frame.result.popupClosed);
|
||||
EXPECT_FALSE(state.colorFieldState.popupOpen);
|
||||
}
|
||||
|
||||
TEST(UIEditorColorFieldInteractionTest, DraggingHueWheelAndAlphaChannelUpdatesColor) {
|
||||
UIEditorColorFieldSpec spec = {};
|
||||
spec.fieldId = "tint";
|
||||
spec.label = "Tint";
|
||||
spec.showAlpha = true;
|
||||
spec.value = XCEngine::UI::UIColor(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
UIEditorColorFieldInteractionState state = {};
|
||||
auto frame = UpdateUIEditorColorFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{});
|
||||
|
||||
frame = UpdateUIEditorColorFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
frame.layout.swatchRect.x + 2.0f,
|
||||
frame.layout.swatchRect.y + 2.0f,
|
||||
UIPointerButton::Left),
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonUp,
|
||||
frame.layout.swatchRect.x + 2.0f,
|
||||
frame.layout.swatchRect.y + 2.0f,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
ASSERT_TRUE(state.colorFieldState.popupOpen);
|
||||
|
||||
const float hueRadius = (frame.layout.hueWheelInnerRadius + frame.layout.hueWheelOuterRadius) * 0.5f;
|
||||
const float hueX = frame.layout.hueWheelCenter.x - hueRadius;
|
||||
const float hueY = frame.layout.hueWheelCenter.y;
|
||||
frame = UpdateUIEditorColorFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{
|
||||
MakePointer(UIInputEventType::PointerButtonDown, hueX, hueY, UIPointerButton::Left),
|
||||
MakePointer(UIInputEventType::PointerMove, hueX, hueY),
|
||||
MakePointer(UIInputEventType::PointerButtonUp, hueX, hueY, UIPointerButton::Left)
|
||||
});
|
||||
EXPECT_TRUE(frame.result.colorChanged);
|
||||
EXPECT_GT(spec.value.b, 0.0f);
|
||||
|
||||
const float alphaX = frame.layout.alphaSliderRect.x + frame.layout.alphaSliderRect.width * 0.25f;
|
||||
const float alphaY = frame.layout.alphaSliderRect.y + frame.layout.alphaSliderRect.height * 0.5f;
|
||||
frame = UpdateUIEditorColorFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{
|
||||
MakePointer(UIInputEventType::PointerButtonDown, alphaX, alphaY, UIPointerButton::Left),
|
||||
MakePointer(UIInputEventType::PointerMove, alphaX, alphaY),
|
||||
MakePointer(UIInputEventType::PointerButtonUp, alphaX, alphaY, UIPointerButton::Left)
|
||||
});
|
||||
EXPECT_TRUE(frame.result.colorChanged);
|
||||
EXPECT_LT(spec.value.a, 0.5f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,118 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Foundation/UIEditorCommandDispatcher.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::GetUIEditorCommandDispatchStatusName;
|
||||
using XCEngine::UI::Editor::UIEditorCommandDispatchStatus;
|
||||
using XCEngine::UI::Editor::UIEditorCommandDispatcher;
|
||||
using XCEngine::UI::Editor::UIEditorCommandEvaluationCode;
|
||||
using XCEngine::UI::Editor::UIEditorCommandPanelSource;
|
||||
using XCEngine::UI::Editor::UIEditorCommandRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
|
||||
UIEditorCommandRegistry BuildCommandRegistry() {
|
||||
UIEditorCommandRegistry registry = {};
|
||||
registry.commands = {
|
||||
{
|
||||
"workspace.hide_active",
|
||||
"Hide Active",
|
||||
{ UIEditorWorkspaceCommandKind::HidePanel, UIEditorCommandPanelSource::ActivePanel, {} }
|
||||
},
|
||||
{
|
||||
"workspace.activate_details",
|
||||
"Activate Details",
|
||||
{ UIEditorWorkspaceCommandKind::ActivatePanel, UIEditorCommandPanelSource::FixedPanelId, "details" }
|
||||
},
|
||||
{
|
||||
"workspace.reset",
|
||||
"Reset Workspace",
|
||||
{ UIEditorWorkspaceCommandKind::ResetWorkspace, UIEditorCommandPanelSource::None, {} }
|
||||
}
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.66f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorCommandDispatcherTest, EvaluateResolvesActivePanelCommandToCurrentWorkspaceTarget) {
|
||||
UIEditorCommandDispatcher dispatcher(BuildCommandRegistry());
|
||||
const auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
|
||||
const auto evaluation = dispatcher.Evaluate("workspace.hide_active", controller);
|
||||
|
||||
EXPECT_EQ(evaluation.code, UIEditorCommandEvaluationCode::None);
|
||||
EXPECT_TRUE(evaluation.executable);
|
||||
EXPECT_EQ(evaluation.commandId, "workspace.hide_active");
|
||||
EXPECT_EQ(evaluation.workspaceCommand.kind, UIEditorWorkspaceCommandKind::HidePanel);
|
||||
EXPECT_EQ(evaluation.workspaceCommand.panelId, "doc-a");
|
||||
EXPECT_EQ(evaluation.previewResult.status, UIEditorWorkspaceCommandStatus::Changed);
|
||||
}
|
||||
|
||||
TEST(UIEditorCommandDispatcherTest, EvaluateRejectsActivePanelCommandWhenWorkspaceHasNoActivePanel) {
|
||||
UIEditorCommandDispatcher dispatcher(BuildCommandRegistry());
|
||||
UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
workspace.activePanelId.clear();
|
||||
|
||||
const auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), workspace);
|
||||
|
||||
const auto evaluation = dispatcher.Evaluate("workspace.hide_active", controller);
|
||||
|
||||
EXPECT_EQ(evaluation.code, UIEditorCommandEvaluationCode::MissingActivePanel);
|
||||
EXPECT_FALSE(evaluation.executable);
|
||||
EXPECT_EQ(evaluation.previewResult.status, UIEditorWorkspaceCommandStatus::Rejected);
|
||||
}
|
||||
|
||||
TEST(UIEditorCommandDispatcherTest, DispatchUsesResolvedWorkspaceCommandAndMutatesController) {
|
||||
UIEditorCommandDispatcher dispatcher(BuildCommandRegistry());
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
|
||||
const auto result = dispatcher.Dispatch("workspace.activate_details", controller);
|
||||
|
||||
EXPECT_EQ(result.status, UIEditorCommandDispatchStatus::Dispatched);
|
||||
EXPECT_STREQ(GetUIEditorCommandDispatchStatusName(result.status).data(), "Dispatched");
|
||||
EXPECT_TRUE(result.commandExecuted);
|
||||
EXPECT_EQ(result.commandId, "workspace.activate_details");
|
||||
EXPECT_EQ(result.commandResult.status, UIEditorWorkspaceCommandStatus::Changed);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "details");
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Foundation/UIEditorCommandRegistry.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Editor::FindUIEditorCommandDescriptor;
|
||||
using XCEngine::UI::Editor::UIEditorCommandDescriptor;
|
||||
using XCEngine::UI::Editor::UIEditorCommandPanelSource;
|
||||
using XCEngine::UI::Editor::UIEditorCommandRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorCommandRegistryValidationCode;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
|
||||
using XCEngine::UI::Editor::ValidateUIEditorCommandRegistry;
|
||||
|
||||
UIEditorCommandRegistry BuildCommandRegistry() {
|
||||
UIEditorCommandRegistry registry = {};
|
||||
registry.commands = {
|
||||
{
|
||||
"workspace.hide_active",
|
||||
"Hide Active",
|
||||
{ UIEditorWorkspaceCommandKind::HidePanel, UIEditorCommandPanelSource::ActivePanel, {} }
|
||||
},
|
||||
{
|
||||
"workspace.activate_details",
|
||||
"Activate Details",
|
||||
{ UIEditorWorkspaceCommandKind::ActivatePanel, UIEditorCommandPanelSource::FixedPanelId, "details" }
|
||||
},
|
||||
{
|
||||
"workspace.reset",
|
||||
"Reset Workspace",
|
||||
{ UIEditorWorkspaceCommandKind::ResetWorkspace, UIEditorCommandPanelSource::None, {} }
|
||||
}
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorCommandRegistryTest, FindDescriptorReturnsRegisteredCommand) {
|
||||
const UIEditorCommandRegistry registry = BuildCommandRegistry();
|
||||
|
||||
const UIEditorCommandDescriptor* descriptor =
|
||||
FindUIEditorCommandDescriptor(registry, "workspace.activate_details");
|
||||
|
||||
ASSERT_NE(descriptor, nullptr);
|
||||
EXPECT_EQ(descriptor->displayName, "Activate Details");
|
||||
}
|
||||
|
||||
TEST(UIEditorCommandRegistryTest, ValidationRejectsDuplicateCommandIdsAndMissingDisplayName) {
|
||||
UIEditorCommandRegistry registry = BuildCommandRegistry();
|
||||
registry.commands.push_back(registry.commands.front());
|
||||
|
||||
auto validation = ValidateUIEditorCommandRegistry(registry);
|
||||
EXPECT_EQ(validation.code, UIEditorCommandRegistryValidationCode::DuplicateCommandId);
|
||||
|
||||
registry = BuildCommandRegistry();
|
||||
registry.commands[1].displayName.clear();
|
||||
validation = ValidateUIEditorCommandRegistry(registry);
|
||||
EXPECT_EQ(validation.code, UIEditorCommandRegistryValidationCode::EmptyDisplayName);
|
||||
}
|
||||
|
||||
TEST(UIEditorCommandRegistryTest, ValidationRejectsInvalidPanelSourceUsage) {
|
||||
UIEditorCommandRegistry registry = BuildCommandRegistry();
|
||||
registry.commands[0].workspaceCommand.panelSource = UIEditorCommandPanelSource::None;
|
||||
|
||||
auto validation = ValidateUIEditorCommandRegistry(registry);
|
||||
EXPECT_EQ(validation.code, UIEditorCommandRegistryValidationCode::MissingPanelSource);
|
||||
|
||||
registry = BuildCommandRegistry();
|
||||
registry.commands[2].workspaceCommand.panelSource =
|
||||
UIEditorCommandPanelSource::FixedPanelId;
|
||||
registry.commands[2].workspaceCommand.panelId = "details";
|
||||
validation = ValidateUIEditorCommandRegistry(registry);
|
||||
EXPECT_EQ(validation.code, UIEditorCommandRegistryValidationCode::UnexpectedPanelSource);
|
||||
}
|
||||
@@ -1,334 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEditor/Panels/UIEditorPanelRegistry.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceModel.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceSession.h>
|
||||
#include <XCEditor/Docking/UIEditorDockHost.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::TryHideUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorDockHostBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorDockHostForeground;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorDockHostLayout;
|
||||
using XCEngine::UI::Editor::Widgets::FindUIEditorDockHostSplitterLayout;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorDockHost;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorDockHostCursorKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostCursorKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTarget;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostMetrics;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostForegroundOptions;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostState;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex;
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true },
|
||||
{ "console", "Console", {}, true, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.5f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
1u),
|
||||
BuildUIEditorWorkspaceSplit(
|
||||
"right-split",
|
||||
UIEditorWorkspaceSplitAxis::Vertical,
|
||||
0.6f,
|
||||
BuildUIEditorWorkspaceSingleTabStack("details-node", "details", "Details", true),
|
||||
BuildUIEditorWorkspaceSingleTabStack("console-node", "console", "Console", true)));
|
||||
workspace.activePanelId = "doc-b";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
bool ContainsTextCommand(const UIDrawList& drawList, std::string_view text) {
|
||||
for (const auto& command : drawList.GetCommands()) {
|
||||
if (command.type == UIDrawCommandType::Text && command.text == text) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorDockHostTest, LayoutComposesOnlyUnifiedTabStacksFromWorkspaceTree) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
const UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
const UIEditorDockHostLayout layout = BuildUIEditorDockHostLayout(
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session);
|
||||
|
||||
ASSERT_EQ(layout.splitters.size(), 2u);
|
||||
ASSERT_EQ(layout.tabStacks.size(), 3u);
|
||||
|
||||
const auto* rootSplitter = FindUIEditorDockHostSplitterLayout(layout, "root-split");
|
||||
ASSERT_NE(rootSplitter, nullptr);
|
||||
const UIEditorDockHostMetrics metrics = {};
|
||||
EXPECT_FLOAT_EQ(
|
||||
rootSplitter->splitterLayout.handleRect.width,
|
||||
metrics.splitterMetrics.thickness);
|
||||
EXPECT_FLOAT_EQ(
|
||||
rootSplitter->splitterLayout.handleRect.x +
|
||||
rootSplitter->splitterLayout.handleRect.width * 0.5f,
|
||||
400.0f);
|
||||
|
||||
const auto& tabStack = layout.tabStacks.front();
|
||||
EXPECT_EQ(tabStack.nodeId, "document-tabs");
|
||||
EXPECT_EQ(tabStack.selectedPanelId, "doc-b");
|
||||
ASSERT_EQ(tabStack.items.size(), 2u);
|
||||
EXPECT_EQ(tabStack.items[0].panelId, "doc-a");
|
||||
EXPECT_EQ(tabStack.items[1].panelId, "doc-b");
|
||||
EXPECT_EQ(tabStack.tabStripState.selectedIndex, 1u);
|
||||
|
||||
EXPECT_EQ(layout.tabStacks[1].nodeId, "details-node");
|
||||
EXPECT_EQ(layout.tabStacks[1].selectedPanelId, "details");
|
||||
EXPECT_EQ(layout.tabStacks[2].nodeId, "console-node");
|
||||
EXPECT_EQ(layout.tabStacks[2].selectedPanelId, "console");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostTest, HiddenBranchCollapsesAndVisibleBranchUsesFullBounds) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
ASSERT_TRUE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "details"));
|
||||
ASSERT_TRUE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "console"));
|
||||
|
||||
const UIEditorDockHostLayout layout = BuildUIEditorDockHostLayout(
|
||||
UIRect(10.0f, 20.0f, 640.0f, 480.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session);
|
||||
|
||||
EXPECT_TRUE(layout.splitters.empty());
|
||||
ASSERT_EQ(layout.tabStacks.size(), 1u);
|
||||
EXPECT_FLOAT_EQ(layout.tabStacks.front().bounds.x, 10.0f);
|
||||
EXPECT_FLOAT_EQ(layout.tabStacks.front().bounds.y, 20.0f);
|
||||
EXPECT_FLOAT_EQ(layout.tabStacks.front().bounds.width, 640.0f);
|
||||
EXPECT_FLOAT_EQ(layout.tabStacks.front().bounds.height, 480.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostTest, HitTestPrioritizesSplitterThenTabThenPanelBody) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
const UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
UIEditorDockHostState state = {};
|
||||
state.focused = true;
|
||||
const UIEditorDockHostLayout layout = BuildUIEditorDockHostLayout(
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
state);
|
||||
|
||||
const auto splitterHit = HitTestUIEditorDockHost(
|
||||
layout,
|
||||
UIPoint(396.0f, 120.0f));
|
||||
EXPECT_EQ(splitterHit.kind, UIEditorDockHostHitTargetKind::SplitterHandle);
|
||||
EXPECT_EQ(splitterHit.nodeId, "root-split");
|
||||
|
||||
ASSERT_EQ(layout.tabStacks.size(), 3u);
|
||||
const auto& tabRect = layout.tabStacks.front().tabStripLayout.tabHeaderRects[1];
|
||||
const auto tabHit = HitTestUIEditorDockHost(
|
||||
layout,
|
||||
UIPoint(tabRect.x + tabRect.width - 6.0f, tabRect.y + tabRect.height * 0.5f));
|
||||
EXPECT_EQ(tabHit.kind, UIEditorDockHostHitTargetKind::Tab);
|
||||
EXPECT_EQ(tabHit.nodeId, "document-tabs");
|
||||
EXPECT_EQ(tabHit.panelId, "doc-b");
|
||||
EXPECT_EQ(tabHit.index, 1u);
|
||||
|
||||
const auto panelBodyHit = HitTestUIEditorDockHost(
|
||||
layout,
|
||||
UIPoint(40.0f, 90.0f));
|
||||
EXPECT_EQ(panelBodyHit.kind, UIEditorDockHostHitTargetKind::PanelBody);
|
||||
EXPECT_EQ(panelBodyHit.nodeId, "document-tabs");
|
||||
EXPECT_EQ(panelBodyHit.panelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostTest, BackgroundAndForegroundEmitStableCompositeCommands) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
const UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
UIEditorDockHostState state = {};
|
||||
state.focused = true;
|
||||
state.hoveredTarget = UIEditorDockHostHitTarget{
|
||||
UIEditorDockHostHitTargetKind::Tab,
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
1u
|
||||
};
|
||||
state.activeSplitterNodeId = "root-split";
|
||||
|
||||
const UIEditorDockHostLayout layout = BuildUIEditorDockHostLayout(
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
state);
|
||||
|
||||
UIDrawList background("DockHostBackground");
|
||||
AppendUIEditorDockHostBackground(background, layout);
|
||||
EXPECT_GT(background.GetCommandCount(), 10u);
|
||||
EXPECT_EQ(background.GetCommands().front().type, UIDrawCommandType::FilledRect);
|
||||
|
||||
UIDrawList foreground("DockHostForeground");
|
||||
AppendUIEditorDockHostForeground(foreground, layout);
|
||||
EXPECT_GT(foreground.GetCommandCount(), 10u);
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostTest, CursorFallsBackToArrowWhenNoSplitterIsHoveredOrActive) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
const UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
const UIEditorDockHostLayout layout = BuildUIEditorDockHostLayout(
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session);
|
||||
|
||||
EXPECT_EQ(ResolveUIEditorDockHostCursorKind(layout), UIEditorDockHostCursorKind::Arrow);
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostTest, CursorUsesHoveredSplitterAxisWhenPointerRestsOnHandle) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
const UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
UIEditorDockHostState state = {};
|
||||
state.hoveredTarget = UIEditorDockHostHitTarget{
|
||||
UIEditorDockHostHitTargetKind::SplitterHandle,
|
||||
"root-split",
|
||||
{},
|
||||
UIEditorTabStripInvalidIndex
|
||||
};
|
||||
const UIEditorDockHostLayout horizontalLayout = BuildUIEditorDockHostLayout(
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
state);
|
||||
EXPECT_EQ(ResolveUIEditorDockHostCursorKind(horizontalLayout), UIEditorDockHostCursorKind::ResizeEW);
|
||||
|
||||
state.hoveredTarget.nodeId = "right-split";
|
||||
const UIEditorDockHostLayout verticalLayout = BuildUIEditorDockHostLayout(
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
state);
|
||||
EXPECT_EQ(ResolveUIEditorDockHostCursorKind(verticalLayout), UIEditorDockHostCursorKind::ResizeNS);
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostTest, CursorPrefersActiveSplitterOverHoveredSplitter) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
const UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
UIEditorDockHostState state = {};
|
||||
state.hoveredTarget = UIEditorDockHostHitTarget{
|
||||
UIEditorDockHostHitTargetKind::SplitterHandle,
|
||||
"root-split",
|
||||
{},
|
||||
UIEditorTabStripInvalidIndex
|
||||
};
|
||||
state.activeSplitterNodeId = "right-split";
|
||||
|
||||
const UIEditorDockHostLayout layout = BuildUIEditorDockHostLayout(
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
state);
|
||||
|
||||
EXPECT_EQ(ResolveUIEditorDockHostCursorKind(layout), UIEditorDockHostCursorKind::ResizeNS);
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostTest, ForegroundDrawsUnifiedTabTitlesAcrossAllLeafStacks) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
const UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
const UIEditorDockHostLayout layout = BuildUIEditorDockHostLayout(
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session);
|
||||
|
||||
UIDrawList foreground("DockHostForegroundDefault");
|
||||
AppendUIEditorDockHostForeground(foreground, layout);
|
||||
|
||||
EXPECT_TRUE(ContainsTextCommand(foreground, "Document A"));
|
||||
EXPECT_TRUE(ContainsTextCommand(foreground, "Document B"));
|
||||
EXPECT_TRUE(ContainsTextCommand(foreground, "Details"));
|
||||
EXPECT_TRUE(ContainsTextCommand(foreground, "Console"));
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostTest, ForegroundWithExternalBodyStillDrawsUnifiedTabTitles) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
const UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
const UIEditorDockHostLayout layout = BuildUIEditorDockHostLayout(
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session);
|
||||
|
||||
UIDrawList foreground("DockHostForegroundExternalBody");
|
||||
UIEditorDockHostForegroundOptions options = {};
|
||||
options.externalBodyPanelIds = { "doc-b" };
|
||||
AppendUIEditorDockHostForeground(foreground, layout, options);
|
||||
|
||||
EXPECT_TRUE(ContainsTextCommand(foreground, "Document A"));
|
||||
EXPECT_TRUE(ContainsTextCommand(foreground, "Document B"));
|
||||
EXPECT_TRUE(ContainsTextCommand(foreground, "Details"));
|
||||
EXPECT_TRUE(ContainsTextCommand(foreground, "Console"));
|
||||
}
|
||||
@@ -1,723 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#include <XCEditor/Docking/UIEditorDockHostInteraction.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceController.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceModel.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceQueries.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::FindUIEditorPanelSessionState;
|
||||
using XCEngine::UI::Editor::FindUIEditorWorkspaceNode;
|
||||
using XCEngine::UI::Editor::UIEditorDockHostInteractionState;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorDockHostInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostTabStackLayout;
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "doc-c", "Document C", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true },
|
||||
{ "console", "Console", {}, true, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.5f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
1u),
|
||||
BuildUIEditorWorkspaceSplit(
|
||||
"right-split",
|
||||
UIEditorWorkspaceSplitAxis::Vertical,
|
||||
0.6f,
|
||||
BuildUIEditorWorkspaceSingleTabStack("details-node", "details", "Details", true),
|
||||
BuildUIEditorWorkspaceSingleTabStack("console-node", "console", "Console", true)));
|
||||
workspace.activePanelId = "doc-b";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildThreeDocumentWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.5f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true),
|
||||
BuildUIEditorWorkspacePanel("doc-c-node", "doc-c", "Document C", true)
|
||||
},
|
||||
2u),
|
||||
BuildUIEditorWorkspaceSplit(
|
||||
"right-split",
|
||||
UIEditorWorkspaceSplitAxis::Vertical,
|
||||
0.6f,
|
||||
BuildUIEditorWorkspaceSingleTabStack("details-node", "details", "Details", true),
|
||||
BuildUIEditorWorkspaceSingleTabStack("console-node", "console", "Console", true)));
|
||||
workspace.activePanelId = "doc-c";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerMove(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerMove;
|
||||
event.position = UIPoint(x, y);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerDown(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonDown;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = UIPointerButton::Left;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerUp(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonUp;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = UIPointerButton::Left;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeFocusLost() {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::FocusLost;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKeyDown(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIPoint RectCenter(const UIRect& rect) {
|
||||
return UIPoint(rect.x + rect.width * 0.5f, rect.y + rect.height * 0.5f);
|
||||
}
|
||||
|
||||
const UIEditorDockHostTabStackLayout* FindTabStackByNodeId(
|
||||
const UIEditorDockHostLayout& layout,
|
||||
std::string_view nodeId) {
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
if (tabStack.nodeId == nodeId) {
|
||||
return &tabStack;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const XCEngine::UI::Editor::Widgets::UIEditorDockHostSplitterLayout* FindSplitterByNodeId(
|
||||
const UIEditorDockHostLayout& layout,
|
||||
std::string_view nodeId) {
|
||||
for (const auto& splitter : layout.splitters) {
|
||||
if (splitter.nodeId == nodeId) {
|
||||
return &splitter;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, SplitterDragUpdatesWorkspaceSplitRatio) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(396.0f, 120.0f) });
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::SplitterHandle);
|
||||
EXPECT_EQ(frame.result.hitTarget.nodeId, "root-split");
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerDown(396.0f, 120.0f) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
EXPECT_EQ(frame.result.activeSplitterNodeId, "root-split");
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(520.0f, 120.0f) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.layoutChanged);
|
||||
EXPECT_GT(controller.GetWorkspace().root.splitRatio, 0.5f);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(520.0f, 120.0f) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(state.dockHostState.activeSplitterNodeId.empty());
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, FocusLostWhileDraggingSplitterRequestsPointerRelease) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(396.0f, 120.0f) });
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::SplitterHandle);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerDown(396.0f, 120.0f) });
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
EXPECT_FALSE(state.dockHostState.activeSplitterNodeId.empty());
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakeFocusLost() });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(state.dockHostState.activeSplitterNodeId.empty());
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, SplitterGestureDoesNotLeaveGhostTabDragWhenHitZoneOverlapsTabHeader) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* consoleStack = FindTabStackByNodeId(frame.layout, "console-node");
|
||||
const auto* rightSplitter = FindSplitterByNodeId(frame.layout, "right-split");
|
||||
ASSERT_NE(consoleStack, nullptr);
|
||||
ASSERT_NE(rightSplitter, nullptr);
|
||||
|
||||
const UIRect overlapRect(
|
||||
(std::max)(consoleStack->tabStripLayout.headerRect.x, rightSplitter->handleHitRect.x),
|
||||
(std::max)(consoleStack->tabStripLayout.headerRect.y, rightSplitter->handleHitRect.y),
|
||||
(std::min)(
|
||||
consoleStack->tabStripLayout.headerRect.x + consoleStack->tabStripLayout.headerRect.width,
|
||||
rightSplitter->handleHitRect.x + rightSplitter->handleHitRect.width) -
|
||||
(std::max)(consoleStack->tabStripLayout.headerRect.x, rightSplitter->handleHitRect.x),
|
||||
(std::min)(
|
||||
consoleStack->tabStripLayout.headerRect.y + consoleStack->tabStripLayout.headerRect.height,
|
||||
rightSplitter->handleHitRect.y + rightSplitter->handleHitRect.height) -
|
||||
(std::max)(consoleStack->tabStripLayout.headerRect.y, rightSplitter->handleHitRect.y));
|
||||
ASSERT_GT(overlapRect.width, 0.0f);
|
||||
ASSERT_GT(overlapRect.height, 0.0f);
|
||||
const UIPoint overlapPoint = RectCenter(overlapRect);
|
||||
const UIPoint splitterDragPoint(overlapPoint.x, overlapPoint.y + 24.0f);
|
||||
const UIPoint probeMovePoint(
|
||||
consoleStack->tabStripLayout.headerRect.x + 8.0f,
|
||||
consoleStack->tabStripLayout.headerRect.y + consoleStack->tabStripLayout.headerRect.height * 0.5f);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(overlapPoint.x, overlapPoint.y) });
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerDown(overlapPoint.x, overlapPoint.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
EXPECT_EQ(frame.result.activeSplitterNodeId, "right-split");
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(splitterDragPoint.x, splitterDragPoint.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.layoutChanged);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(splitterDragPoint.x, splitterDragPoint.y) });
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(state.dockHostState.activeSplitterNodeId.empty());
|
||||
EXPECT_TRUE(state.activeTabDragNodeId.empty());
|
||||
EXPECT_TRUE(state.activeTabDragPanelId.empty());
|
||||
EXPECT_FALSE(state.dockHostState.dropPreview.visible);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(probeMovePoint.x, probeMovePoint.y) });
|
||||
EXPECT_FALSE(frame.result.requestPointerCapture);
|
||||
EXPECT_FALSE(frame.result.releasePointerCapture);
|
||||
EXPECT_FALSE(frame.result.layoutChanged);
|
||||
EXPECT_TRUE(state.activeTabDragNodeId.empty());
|
||||
EXPECT_TRUE(state.activeTabDragPanelId.empty());
|
||||
EXPECT_FALSE(state.dockHostState.dropPreview.visible);
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, ClickingTabActivatesTargetPanel) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
const UIRect docARect = documentStack->tabStripLayout.tabHeaderRects[0];
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(RectCenter(docARect).x, RectCenter(docARect).y) });
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::Tab);
|
||||
EXPECT_EQ(frame.result.hitTarget.panelId, "doc-a");
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerDown(RectCenter(docARect).x, RectCenter(docARect).y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_FALSE(frame.result.commandExecuted);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(RectCenter(docARect).x, RectCenter(docARect).y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.commandExecuted);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a");
|
||||
documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
EXPECT_EQ(documentStack->selectedPanelId, "doc-a");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, ReleasingActiveTabDragOutsideDockHostRequestsDetach) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildThreeDocumentWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
const UIRect draggedTabRect = documentStack->tabStripLayout.tabHeaderRects[2];
|
||||
const UIPoint draggedTabCenter = RectCenter(draggedTabRect);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(draggedTabCenter.x, draggedTabCenter.y) });
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::Tab);
|
||||
EXPECT_EQ(frame.result.hitTarget.panelId, "doc-c");
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerDown(draggedTabCenter.x, draggedTabCenter.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(860.0f, draggedTabCenter.y) });
|
||||
EXPECT_EQ(state.activeTabDragPanelId, "doc-c");
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(860.0f, draggedTabCenter.y) });
|
||||
EXPECT_TRUE(frame.result.detachRequested);
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_EQ(frame.result.detachedNodeId, "document-tabs");
|
||||
EXPECT_EQ(frame.result.detachedPanelId, "doc-c");
|
||||
EXPECT_TRUE(state.activeTabDragNodeId.empty());
|
||||
EXPECT_TRUE(state.activeTabDragPanelId.empty());
|
||||
EXPECT_FALSE(state.dockHostState.dropPreview.visible);
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, FocusedTabStripHandlesKeyboardNavigationThroughTabStripInteraction) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
const UIRect docARect = documentStack->tabStripLayout.tabHeaderRects[0];
|
||||
const UIPoint docACenter = RectCenter(docARect);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(docACenter.x, docACenter.y) });
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::Tab);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerDown(docACenter.x, docACenter.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(docACenter.x, docACenter.y) });
|
||||
EXPECT_TRUE(frame.result.commandExecuted);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a");
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakeKeyDown(KeyCode::Right) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.commandExecuted);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-b");
|
||||
documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
EXPECT_EQ(documentStack->selectedPanelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, BatchedPointerMoveDownUpActivatesTabInSameUpdateCall) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
const UIPoint docACenter =
|
||||
RectCenter(documentStack->tabStripLayout.tabHeaderRects[0]);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{
|
||||
MakePointerMove(docACenter.x, docACenter.y),
|
||||
MakePointerDown(docACenter.x, docACenter.y),
|
||||
MakePointerUp(docACenter.x, docACenter.y)
|
||||
});
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.commandExecuted);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a");
|
||||
documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
EXPECT_EQ(documentStack->selectedPanelId, "doc-a");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, ClickingSingleTabStackBodyActivatesTargetPanel) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* detailsStack = FindTabStackByNodeId(frame.layout, "details-node");
|
||||
ASSERT_NE(detailsStack, nullptr);
|
||||
const UIRect detailsBodyRect = detailsStack->contentFrameLayout.bodyRect;
|
||||
const UIPoint detailsBodyCenter = RectCenter(detailsBodyRect);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(detailsBodyCenter.x, detailsBodyCenter.y) });
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::PanelBody);
|
||||
EXPECT_EQ(frame.result.hitTarget.panelId, "details");
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(detailsBodyCenter.x, detailsBodyCenter.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.commandExecuted);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "details");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, ReleasingDraggedTabOutsideHeaderCancelsWithoutChangingOrder) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
const UIPoint sourceCenter = RectCenter(documentStack->tabStripLayout.tabHeaderRects[0]);
|
||||
const UIPoint activatePoint(
|
||||
documentStack->tabStripLayout.headerRect.x +
|
||||
documentStack->tabStripLayout.headerRect.width - 2.0f,
|
||||
sourceCenter.y);
|
||||
const UIPoint cancelPoint = RectCenter(documentStack->contentFrameLayout.bodyRect);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{
|
||||
MakePointerDown(sourceCenter.x, sourceCenter.y),
|
||||
MakePointerMove(activatePoint.x, activatePoint.y),
|
||||
MakePointerMove(cancelPoint.x, cancelPoint.y),
|
||||
MakePointerUp(cancelPoint.x, cancelPoint.y)
|
||||
});
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_FALSE(frame.result.layoutChanged);
|
||||
|
||||
const auto* documentTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "document-tabs");
|
||||
ASSERT_NE(documentTabs, nullptr);
|
||||
ASSERT_EQ(documentTabs->children.size(), 2u);
|
||||
EXPECT_EQ(documentTabs->children[0].panel.panelId, "doc-a");
|
||||
EXPECT_EQ(documentTabs->children[1].panel.panelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, DraggingTabOntoAnotherStackHeaderMergesIntoTargetStack) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
const auto* detailsStack = FindTabStackByNodeId(frame.layout, "details-node");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
ASSERT_NE(detailsStack, nullptr);
|
||||
const UIPoint sourceCenter =
|
||||
RectCenter(documentStack->tabStripLayout.tabHeaderRects[0]);
|
||||
const UIPoint targetHeaderCenter =
|
||||
RectCenter(detailsStack->tabStripLayout.headerRect);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerDown(sourceCenter.x, sourceCenter.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(targetHeaderCenter.x, targetHeaderCenter.y) });
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
EXPECT_TRUE(state.dockHostState.dropPreview.visible);
|
||||
EXPECT_EQ(state.dockHostState.dropPreview.targetNodeId, "details-node");
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(targetHeaderCenter.x, targetHeaderCenter.y) });
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(frame.result.layoutChanged);
|
||||
|
||||
const auto* documentTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "document-tabs");
|
||||
const auto* detailsTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "details-node");
|
||||
ASSERT_NE(documentTabs, nullptr);
|
||||
ASSERT_NE(detailsTabs, nullptr);
|
||||
ASSERT_EQ(documentTabs->children.size(), 1u);
|
||||
ASSERT_EQ(detailsTabs->children.size(), 2u);
|
||||
EXPECT_EQ(documentTabs->children[0].panel.panelId, "doc-b");
|
||||
EXPECT_EQ(detailsTabs->children[1].panel.panelId, "doc-a");
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, DraggingTabOntoPanelBodyEdgeCreatesDockSplit) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
const auto* detailsStack = FindTabStackByNodeId(frame.layout, "details-node");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
ASSERT_NE(detailsStack, nullptr);
|
||||
const UIPoint sourceCenter =
|
||||
RectCenter(documentStack->tabStripLayout.tabHeaderRects[0]);
|
||||
const UIPoint targetBottomCenter(
|
||||
detailsStack->contentFrameLayout.bodyRect.x +
|
||||
detailsStack->contentFrameLayout.bodyRect.width * 0.5f,
|
||||
detailsStack->contentFrameLayout.bodyRect.y +
|
||||
detailsStack->contentFrameLayout.bodyRect.height - 4.0f);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerDown(sourceCenter.x, sourceCenter.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(targetBottomCenter.x, targetBottomCenter.y) });
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
EXPECT_TRUE(state.dockHostState.dropPreview.visible);
|
||||
EXPECT_EQ(
|
||||
state.dockHostState.dropPreview.placement,
|
||||
XCEngine::UI::Editor::UIEditorWorkspaceDockPlacement::Bottom);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(targetBottomCenter.x, targetBottomCenter.y) });
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(frame.result.layoutChanged);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a");
|
||||
|
||||
bool foundDockedTab = false;
|
||||
for (const std::string candidate : { "details-node__dock_doc-a_stack", "details-node__dock_doc-a_stack-1" }) {
|
||||
const auto* docked =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), candidate);
|
||||
if (docked != nullptr &&
|
||||
docked->children.size() == 1u &&
|
||||
docked->children[0].panel.panelId == "doc-a") {
|
||||
foundDockedTab = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(foundDockedTab);
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, FocusLostWhileDraggingTabCancelsAndReleasesCapture) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
const UIPoint sourceCenter = RectCenter(documentStack->tabStripLayout.tabHeaderRects[0]);
|
||||
const UIPoint activatePoint(
|
||||
documentStack->tabStripLayout.headerRect.x +
|
||||
documentStack->tabStripLayout.headerRect.width - 2.0f,
|
||||
sourceCenter.y);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{
|
||||
MakePointerDown(sourceCenter.x, sourceCenter.y),
|
||||
MakePointerMove(activatePoint.x, activatePoint.y)
|
||||
});
|
||||
ASSERT_TRUE(frame.result.requestPointerCapture);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakeFocusLost() });
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_FALSE(frame.result.layoutChanged);
|
||||
|
||||
const auto* documentTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "document-tabs");
|
||||
ASSERT_NE(documentTabs, nullptr);
|
||||
ASSERT_EQ(documentTabs->children.size(), 2u);
|
||||
EXPECT_EQ(documentTabs->children[0].panel.panelId, "doc-a");
|
||||
EXPECT_EQ(documentTabs->children[1].panel.panelId, "doc-b");
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Collections/UIEditorGridDragDrop.h>
|
||||
#include <XCEditor/Collections/UIEditorTreeDragDrop.h>
|
||||
|
||||
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
namespace GridDrag = XCEngine::UI::Editor::Collections::GridDragDrop;
|
||||
namespace TreeDrag = XCEngine::UI::Editor::Collections::TreeDragDrop;
|
||||
namespace Widgets = XCEngine::UI::Editor::Widgets;
|
||||
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIPointerButton;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
|
||||
UIInputEvent MakePointerButtonDown(float x, float y, UIPointerButton button) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonDown;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerMove(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerMove;
|
||||
event.position = UIPoint(x, y);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerButtonUp(float x, float y, UIPointerButton button) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonUp;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeFocusLost() {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::FocusLost;
|
||||
return event;
|
||||
}
|
||||
|
||||
struct GridCallbacks {
|
||||
std::string committedDraggedItemId = {};
|
||||
std::string committedDropTargetItemId = {};
|
||||
int selectCount = 0;
|
||||
int commitCount = 0;
|
||||
bool selected = false;
|
||||
|
||||
std::string ResolveDraggableItem(const UIPoint& point) const {
|
||||
return point.x <= 20.0f ? "AssetA" : "";
|
||||
}
|
||||
|
||||
std::string ResolveDropTargetItem(
|
||||
std::string_view,
|
||||
const UIPoint& point) const {
|
||||
return point.x >= 60.0f ? "FolderB" : "";
|
||||
}
|
||||
|
||||
bool IsItemSelected(std::string_view) const {
|
||||
return selected;
|
||||
}
|
||||
|
||||
bool SelectDraggedItem(std::string_view) {
|
||||
selected = true;
|
||||
++selectCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CanDropOnItem(
|
||||
std::string_view draggedItemId,
|
||||
std::string_view targetItemId) const {
|
||||
return !draggedItemId.empty() &&
|
||||
targetItemId == "FolderB" &&
|
||||
draggedItemId != targetItemId;
|
||||
}
|
||||
|
||||
bool CommitDropOnItem(
|
||||
std::string_view draggedItemId,
|
||||
std::string_view targetItemId) {
|
||||
committedDraggedItemId = std::string(draggedItemId);
|
||||
committedDropTargetItemId = std::string(targetItemId);
|
||||
++commitCount;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
TEST(UIEditorGridDragDropTests, ProcessInputEventsCapturesAndCommitsDrop) {
|
||||
GridDrag::State state = {};
|
||||
GridCallbacks callbacks = {};
|
||||
|
||||
const GridDrag::ProcessResult result =
|
||||
GridDrag::ProcessInputEvents(
|
||||
state,
|
||||
{
|
||||
MakePointerButtonDown(10.0f, 12.0f, UIPointerButton::Left),
|
||||
MakePointerMove(24.0f, 12.0f),
|
||||
MakePointerMove(80.0f, 12.0f),
|
||||
MakePointerButtonUp(80.0f, 12.0f, UIPointerButton::Left)
|
||||
},
|
||||
callbacks);
|
||||
|
||||
EXPECT_TRUE(result.selectionForced);
|
||||
EXPECT_TRUE(result.dropCommitted);
|
||||
EXPECT_EQ(result.draggedItemId, "AssetA");
|
||||
EXPECT_EQ(result.dropTargetItemId, "FolderB");
|
||||
EXPECT_EQ(callbacks.selectCount, 1);
|
||||
EXPECT_EQ(callbacks.commitCount, 1);
|
||||
EXPECT_EQ(callbacks.committedDraggedItemId, "AssetA");
|
||||
EXPECT_EQ(callbacks.committedDropTargetItemId, "FolderB");
|
||||
EXPECT_TRUE(state.requestPointerCapture);
|
||||
EXPECT_TRUE(state.requestPointerRelease);
|
||||
EXPECT_FALSE(state.armed);
|
||||
EXPECT_FALSE(state.dragging);
|
||||
EXPECT_FALSE(state.validDropTarget);
|
||||
}
|
||||
|
||||
TEST(UIEditorGridDragDropTests, FocusLostWhileDraggingRequestsReleaseAndClearsState) {
|
||||
GridDrag::State state = {};
|
||||
GridCallbacks callbacks = {};
|
||||
|
||||
const GridDrag::ProcessResult result =
|
||||
GridDrag::ProcessInputEvents(
|
||||
state,
|
||||
{
|
||||
MakePointerButtonDown(10.0f, 12.0f, UIPointerButton::Left),
|
||||
MakePointerMove(24.0f, 12.0f),
|
||||
MakeFocusLost()
|
||||
},
|
||||
callbacks);
|
||||
|
||||
EXPECT_FALSE(result.dropCommitted);
|
||||
EXPECT_TRUE(state.requestPointerRelease);
|
||||
EXPECT_FALSE(state.requestPointerCapture);
|
||||
EXPECT_FALSE(state.armed);
|
||||
EXPECT_FALSE(state.dragging);
|
||||
EXPECT_TRUE(state.armedItemId.empty());
|
||||
EXPECT_TRUE(state.draggedItemId.empty());
|
||||
EXPECT_TRUE(state.dropTargetItemId.empty());
|
||||
}
|
||||
|
||||
struct TreeFixtureData {
|
||||
std::vector<Widgets::UIEditorTreeViewItem> items = {};
|
||||
::XCEngine::UI::Widgets::UIExpansionModel expansion = {};
|
||||
Widgets::UIEditorTreeViewLayout layout = {};
|
||||
};
|
||||
|
||||
TreeFixtureData BuildTreeFixture() {
|
||||
TreeFixtureData fixture = {};
|
||||
fixture.items = {
|
||||
Widgets::UIEditorTreeViewItem{ .itemId = "NodeA", .label = "NodeA" },
|
||||
Widgets::UIEditorTreeViewItem{ .itemId = "NodeB", .label = "NodeB" }
|
||||
};
|
||||
fixture.layout = Widgets::BuildUIEditorTreeViewLayout(
|
||||
UIRect(0.0f, 0.0f, 200.0f, 120.0f),
|
||||
fixture.items,
|
||||
fixture.expansion);
|
||||
return fixture;
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeDragDropTests, BuildInteractionInputEventsSuppressesTreeViewDragGesture) {
|
||||
const TreeFixtureData fixture = BuildTreeFixture();
|
||||
TreeDrag::State state = {};
|
||||
|
||||
ASSERT_GE(fixture.layout.rowRects.size(), 2u);
|
||||
const UIRect sourceRow = fixture.layout.rowRects[0];
|
||||
const UIRect targetRow = fixture.layout.rowRects[1];
|
||||
|
||||
const std::vector<UIInputEvent> filteredEvents =
|
||||
TreeDrag::BuildInteractionInputEvents(
|
||||
state,
|
||||
fixture.layout,
|
||||
fixture.items,
|
||||
{
|
||||
MakePointerButtonDown(
|
||||
sourceRow.x + 12.0f,
|
||||
sourceRow.y + sourceRow.height * 0.5f,
|
||||
UIPointerButton::Left),
|
||||
MakePointerMove(
|
||||
sourceRow.x + 24.0f,
|
||||
sourceRow.y + sourceRow.height * 0.5f),
|
||||
MakePointerMove(
|
||||
targetRow.x + 12.0f,
|
||||
targetRow.y + targetRow.height * 0.5f),
|
||||
MakePointerButtonUp(
|
||||
targetRow.x + 12.0f,
|
||||
targetRow.y + targetRow.height * 0.5f,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
ASSERT_EQ(filteredEvents.size(), 1u);
|
||||
EXPECT_EQ(filteredEvents[0].type, UIInputEventType::PointerButtonDown);
|
||||
}
|
||||
|
||||
struct TreeCallbacks {
|
||||
int selectCount = 0;
|
||||
int commitToRootCount = 0;
|
||||
bool selected = false;
|
||||
|
||||
bool IsItemSelected(std::string_view) const {
|
||||
return selected;
|
||||
}
|
||||
|
||||
bool SelectDraggedItem(std::string_view) {
|
||||
selected = true;
|
||||
++selectCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CanDropOnItem(std::string_view, std::string_view) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CanDropToRoot(std::string_view draggedItemId) const {
|
||||
return !draggedItemId.empty();
|
||||
}
|
||||
|
||||
bool CommitDropOnItem(std::string_view, std::string_view) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CommitDropToRoot(std::string_view) {
|
||||
++commitToRootCount;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
TEST(UIEditorTreeDragDropTests, ProcessInputEventsSupportsDropToRoot) {
|
||||
TreeFixtureData fixture = BuildTreeFixture();
|
||||
fixture.items.resize(1u);
|
||||
fixture.layout = Widgets::BuildUIEditorTreeViewLayout(
|
||||
fixture.layout.bounds,
|
||||
fixture.items,
|
||||
fixture.expansion);
|
||||
|
||||
TreeDrag::State state = {};
|
||||
TreeCallbacks callbacks = {};
|
||||
|
||||
ASSERT_FALSE(fixture.layout.rowRects.empty());
|
||||
const UIRect sourceRow = fixture.layout.rowRects.front();
|
||||
const UIPoint sourcePoint(
|
||||
sourceRow.x + 12.0f,
|
||||
sourceRow.y + sourceRow.height * 0.5f);
|
||||
const UIPoint rootDropPoint(
|
||||
fixture.layout.bounds.x + fixture.layout.bounds.width * 0.5f,
|
||||
fixture.layout.bounds.y + fixture.layout.bounds.height - 8.0f);
|
||||
|
||||
const TreeDrag::ProcessResult result =
|
||||
TreeDrag::ProcessInputEvents(
|
||||
state,
|
||||
fixture.layout,
|
||||
fixture.items,
|
||||
{
|
||||
MakePointerButtonDown(sourcePoint.x, sourcePoint.y, UIPointerButton::Left),
|
||||
MakePointerMove(rootDropPoint.x, rootDropPoint.y),
|
||||
MakePointerButtonUp(rootDropPoint.x, rootDropPoint.y, UIPointerButton::Left)
|
||||
},
|
||||
fixture.layout.bounds,
|
||||
callbacks);
|
||||
|
||||
EXPECT_TRUE(result.selectionForced);
|
||||
EXPECT_TRUE(result.dropCommitted);
|
||||
EXPECT_TRUE(result.droppedToRoot);
|
||||
EXPECT_EQ(result.draggedItemId, "NodeA");
|
||||
EXPECT_TRUE(result.dropTargetItemId.empty());
|
||||
EXPECT_EQ(callbacks.selectCount, 1);
|
||||
EXPECT_EQ(callbacks.commitToRootCount, 1);
|
||||
EXPECT_TRUE(state.requestPointerCapture);
|
||||
EXPECT_TRUE(state.requestPointerRelease);
|
||||
EXPECT_FALSE(state.armed);
|
||||
EXPECT_FALSE(state.dragging);
|
||||
EXPECT_FALSE(state.dropToRoot);
|
||||
EXPECT_FALSE(state.validDropTarget);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,109 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEditor/Fields/UIEditorEnumField.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorEnumFieldBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorEnumFieldForeground;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorEnumFieldLayout;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorEnumField;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorEnumFieldValueText;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldSpec;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldState;
|
||||
|
||||
TEST(UIEditorEnumFieldTest, ValueTextUsesSelectedOption) {
|
||||
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
|
||||
EXPECT_EQ(ResolveUIEditorEnumFieldValueText(spec), "Cutout");
|
||||
}
|
||||
|
||||
TEST(UIEditorEnumFieldTest, LayoutKeepsInspectorControlColumnAndUnityArrowWidth) {
|
||||
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
|
||||
const auto layout = BuildUIEditorEnumFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.x, 236.0f);
|
||||
EXPECT_FLOAT_EQ(layout.valueRect.y, 1.0f);
|
||||
EXPECT_FLOAT_EQ(layout.valueRect.height, 20.0f);
|
||||
EXPECT_FLOAT_EQ(layout.arrowRect.width, 14.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorEnumFieldTest, HitTestResolvesArrowAndValueBox) {
|
||||
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
|
||||
const auto layout = BuildUIEditorEnumFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
|
||||
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorEnumField(layout, UIPoint(layout.arrowRect.x + 2.0f, layout.arrowRect.y + 2.0f)).kind,
|
||||
UIEditorEnumFieldHitTargetKind::DropdownArrow);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorEnumField(layout, UIPoint(layout.valueRect.x + 4.0f, layout.valueRect.y + 4.0f)).kind,
|
||||
UIEditorEnumFieldHitTargetKind::ValueBox);
|
||||
}
|
||||
|
||||
TEST(UIEditorEnumFieldTest, BackgroundAndForegroundEmitInspectorChromeAndChevron) {
|
||||
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
|
||||
UIEditorEnumFieldState state = {};
|
||||
state.popupOpen = true;
|
||||
state.hoveredTarget = UIEditorEnumFieldHitTargetKind::DropdownArrow;
|
||||
|
||||
XCEngine::UI::UIDrawData drawData = {};
|
||||
auto& drawList = drawData.EmplaceDrawList("EnumField");
|
||||
const auto layout = BuildUIEditorEnumFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
|
||||
AppendUIEditorEnumFieldBackground(drawList, layout, spec, state);
|
||||
AppendUIEditorEnumFieldForeground(drawList, layout, spec);
|
||||
|
||||
const auto& commands = drawList.GetCommands();
|
||||
std::size_t filledRectCount = 0u;
|
||||
std::size_t rectOutlineCount = 0u;
|
||||
std::size_t pushClipCount = 0u;
|
||||
std::size_t popClipCount = 0u;
|
||||
std::size_t lineCount = 0u;
|
||||
bool foundLabelText = false;
|
||||
bool foundValueText = false;
|
||||
|
||||
for (const auto& command : commands) {
|
||||
switch (command.type) {
|
||||
case UIDrawCommandType::FilledRect:
|
||||
++filledRectCount;
|
||||
break;
|
||||
case UIDrawCommandType::RectOutline:
|
||||
++rectOutlineCount;
|
||||
break;
|
||||
case UIDrawCommandType::PushClipRect:
|
||||
++pushClipCount;
|
||||
break;
|
||||
case UIDrawCommandType::PopClipRect:
|
||||
++popClipCount;
|
||||
break;
|
||||
case UIDrawCommandType::Line:
|
||||
++lineCount;
|
||||
break;
|
||||
case UIDrawCommandType::Text:
|
||||
if (command.text == "Blend") {
|
||||
foundLabelText = true;
|
||||
EXPECT_FLOAT_EQ(command.position.y, 2.0f);
|
||||
}
|
||||
if (command.text == "Cutout") {
|
||||
foundValueText = true;
|
||||
EXPECT_FLOAT_EQ(command.position.y, 1.0f);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_EQ(filledRectCount, 2u);
|
||||
EXPECT_EQ(rectOutlineCount, 1u);
|
||||
EXPECT_EQ(pushClipCount, 2u);
|
||||
EXPECT_EQ(popClipCount, 2u);
|
||||
EXPECT_EQ(lineCount, 3u);
|
||||
EXPECT_TRUE(foundLabelText);
|
||||
EXPECT_TRUE(foundValueText);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,100 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorEnumFieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::UIEditorEnumFieldInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorEnumFieldInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldSpec;
|
||||
|
||||
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKey(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
return event;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorEnumFieldInteractionTest, ClickValueBoxOpensPopupAndSelectsItem) {
|
||||
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
|
||||
UIEditorEnumFieldInteractionState state = {};
|
||||
std::size_t selectedIndex = 1u;
|
||||
|
||||
auto frame = UpdateUIEditorEnumFieldInteraction(
|
||||
state,
|
||||
selectedIndex,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
spec,
|
||||
{});
|
||||
|
||||
frame = UpdateUIEditorEnumFieldInteraction(
|
||||
state,
|
||||
selectedIndex,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
spec,
|
||||
{
|
||||
MakePointer(UIInputEventType::PointerButtonDown, frame.layout.valueRect.x + 2.0f, frame.layout.valueRect.y + 2.0f, UIPointerButton::Left),
|
||||
MakePointer(UIInputEventType::PointerButtonUp, frame.layout.valueRect.x + 2.0f, frame.layout.valueRect.y + 2.0f, UIPointerButton::Left)
|
||||
});
|
||||
EXPECT_TRUE(frame.result.popupOpened);
|
||||
EXPECT_TRUE(frame.popupOpen);
|
||||
|
||||
ASSERT_FALSE(frame.popupLayout.itemRects.empty());
|
||||
const auto itemRect = frame.popupLayout.itemRects[2];
|
||||
frame = UpdateUIEditorEnumFieldInteraction(
|
||||
state,
|
||||
selectedIndex,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
spec,
|
||||
{
|
||||
MakePointer(UIInputEventType::PointerButtonDown, itemRect.x + 2.0f, itemRect.y + 2.0f, UIPointerButton::Left),
|
||||
MakePointer(UIInputEventType::PointerButtonUp, itemRect.x + 2.0f, itemRect.y + 2.0f, UIPointerButton::Left)
|
||||
});
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_EQ(selectedIndex, 2u);
|
||||
EXPECT_FALSE(frame.popupOpen);
|
||||
}
|
||||
|
||||
TEST(UIEditorEnumFieldInteractionTest, KeyboardCanOpenMoveAndCommitPopupSelection) {
|
||||
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
|
||||
UIEditorEnumFieldInteractionState state = {};
|
||||
state.fieldState.focused = true;
|
||||
std::size_t selectedIndex = 1u;
|
||||
|
||||
auto frame = UpdateUIEditorEnumFieldInteraction(
|
||||
state,
|
||||
selectedIndex,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
spec,
|
||||
{ MakeKey(KeyCode::Enter) });
|
||||
EXPECT_TRUE(frame.result.popupOpened);
|
||||
EXPECT_TRUE(frame.popupOpen);
|
||||
|
||||
frame = UpdateUIEditorEnumFieldInteraction(
|
||||
state,
|
||||
selectedIndex,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
spec,
|
||||
{ MakeKey(KeyCode::Down), MakeKey(KeyCode::Enter) });
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_EQ(selectedIndex, 2u);
|
||||
EXPECT_FALSE(frame.popupOpen);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Widgets/UIEditorFieldRowLayout.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorFieldRowLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorFieldRowLayoutMetrics;
|
||||
|
||||
TEST(UIEditorFieldRowLayoutTest, WideRowsKeepStableControlColumnAnchor) {
|
||||
UIEditorFieldRowLayoutMetrics metrics = {};
|
||||
const auto layout = BuildUIEditorFieldRowLayout(
|
||||
UIRect(10.0f, 20.0f, 392.0f, 22.0f),
|
||||
96.0f,
|
||||
metrics);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.bounds.height, 22.0f);
|
||||
EXPECT_FLOAT_EQ(layout.labelRect.x, 22.0f);
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.x, 246.0f);
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.y, 21.0f);
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.height, 20.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorFieldRowLayoutTest, NarrowRowsCompressFromRightWithoutMagicRatioFallback) {
|
||||
UIEditorFieldRowLayoutMetrics metrics = {};
|
||||
const auto layout = BuildUIEditorFieldRowLayout(
|
||||
UIRect(10.0f, 20.0f, 280.0f, 22.0f),
|
||||
96.0f,
|
||||
metrics);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.x, 186.0f);
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.width, 96.0f);
|
||||
EXPECT_FLOAT_EQ(layout.labelRect.width, 144.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorFieldRowLayoutTest, ZeroHeightFallsBackToMetricRowHeight) {
|
||||
UIEditorFieldRowLayoutMetrics metrics = {};
|
||||
metrics.rowHeight = 24.0f;
|
||||
metrics.controlInsetY = 2.0f;
|
||||
|
||||
const auto layout = BuildUIEditorFieldRowLayout(
|
||||
UIRect(0.0f, 0.0f, 360.0f, 0.0f),
|
||||
120.0f,
|
||||
metrics);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.bounds.height, 24.0f);
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.y, 2.0f);
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.height, 20.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,157 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorFieldStyle.h>
|
||||
|
||||
namespace {
|
||||
|
||||
namespace UI = XCEngine::UI;
|
||||
namespace Editor = XCEngine::UI::Editor;
|
||||
|
||||
TEST(UIEditorHostedFieldBuildersTest, HostedFieldBuildersInheritPropertyGridMetricsAndPalette) {
|
||||
UI::Editor::Widgets::UIEditorPropertyGridMetrics propertyMetrics = {};
|
||||
propertyMetrics.fieldRowHeight = 25.0f;
|
||||
propertyMetrics.horizontalPadding = 7.0f;
|
||||
propertyMetrics.labelControlGap = 11.0f;
|
||||
propertyMetrics.controlColumnStart = 210.0f;
|
||||
propertyMetrics.labelTextInsetY = 4.0f;
|
||||
propertyMetrics.labelFontSize = 10.0f;
|
||||
propertyMetrics.valueTextInsetY = 3.0f;
|
||||
propertyMetrics.valueFontSize = 12.0f;
|
||||
propertyMetrics.tagFontSize = 9.0f;
|
||||
propertyMetrics.valueBoxInsetY = 2.0f;
|
||||
propertyMetrics.valueBoxInsetX = 5.0f;
|
||||
propertyMetrics.cornerRounding = 1.0f;
|
||||
propertyMetrics.valueBoxRounding = 2.0f;
|
||||
propertyMetrics.borderThickness = 1.0f;
|
||||
propertyMetrics.focusedBorderThickness = 2.0f;
|
||||
|
||||
UI::Editor::Widgets::UIEditorPropertyGridPalette propertyPalette = {};
|
||||
propertyPalette.valueBoxColor = UI::UIColor(0.2f, 0.2f, 0.2f, 1.0f);
|
||||
propertyPalette.valueBoxHoverColor = UI::UIColor(0.3f, 0.3f, 0.3f, 1.0f);
|
||||
propertyPalette.valueBoxEditingColor = UI::UIColor(0.4f, 0.4f, 0.4f, 1.0f);
|
||||
propertyPalette.valueBoxReadOnlyColor = UI::UIColor(0.15f, 0.15f, 0.15f, 1.0f);
|
||||
propertyPalette.valueBoxBorderColor = UI::UIColor(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
propertyPalette.valueBoxEditingBorderColor = UI::UIColor(0.55f, 0.55f, 0.55f, 1.0f);
|
||||
propertyPalette.labelTextColor = UI::UIColor(0.8f, 0.8f, 0.8f, 1.0f);
|
||||
propertyPalette.valueTextColor = UI::UIColor(0.9f, 0.9f, 0.9f, 1.0f);
|
||||
propertyPalette.readOnlyValueTextColor = UI::UIColor(0.6f, 0.6f, 0.6f, 1.0f);
|
||||
propertyPalette.editTagColor = UI::UIColor(0.52f, 0.68f, 0.94f, 1.0f);
|
||||
|
||||
const auto boolMetrics = Editor::BuildUIEditorPropertyGridBoolFieldMetrics(propertyMetrics);
|
||||
const auto boolPalette = Editor::BuildUIEditorPropertyGridBoolFieldPalette(propertyPalette);
|
||||
EXPECT_FLOAT_EQ(boolMetrics.rowHeight, 25.0f);
|
||||
EXPECT_FLOAT_EQ(boolMetrics.controlTrailingInset, 5.0f);
|
||||
EXPECT_FLOAT_EQ(boolMetrics.labelFontSize, 10.0f);
|
||||
EXPECT_FLOAT_EQ(boolMetrics.checkboxGlyphFontSize, 12.0f);
|
||||
EXPECT_FLOAT_EQ(boolPalette.checkboxColor.r, 0.2f);
|
||||
EXPECT_FLOAT_EQ(boolPalette.labelColor.r, 0.8f);
|
||||
|
||||
const auto numberMetrics = Editor::BuildUIEditorPropertyGridNumberFieldMetrics(propertyMetrics);
|
||||
const auto numberPalette = Editor::BuildUIEditorPropertyGridNumberFieldPalette(propertyPalette);
|
||||
EXPECT_FLOAT_EQ(numberMetrics.controlTrailingInset, 5.0f);
|
||||
EXPECT_FLOAT_EQ(numberMetrics.controlInsetY, 2.0f);
|
||||
EXPECT_FLOAT_EQ(numberMetrics.valueTextInsetX, 5.0f);
|
||||
EXPECT_FLOAT_EQ(numberMetrics.valueFontSize, 12.0f);
|
||||
EXPECT_FLOAT_EQ(numberPalette.valueBoxEditingColor.r, 0.4f);
|
||||
EXPECT_FLOAT_EQ(numberPalette.readOnlyValueColor.r, 0.6f);
|
||||
EXPECT_FLOAT_EQ(numberPalette.controlFocusedBorderColor.r, 0.55f);
|
||||
|
||||
const auto textMetrics = Editor::BuildUIEditorPropertyGridTextFieldMetrics(propertyMetrics);
|
||||
const auto textPalette = Editor::BuildUIEditorPropertyGridTextFieldPalette(propertyPalette);
|
||||
EXPECT_FLOAT_EQ(textMetrics.controlTrailingInset, 5.0f);
|
||||
EXPECT_FLOAT_EQ(textMetrics.controlInsetY, 2.0f);
|
||||
EXPECT_FLOAT_EQ(textMetrics.valueTextInsetX, 5.0f);
|
||||
EXPECT_FLOAT_EQ(textMetrics.valueFontSize, 12.0f);
|
||||
EXPECT_FLOAT_EQ(textPalette.valueBoxEditingColor.r, 0.4f);
|
||||
EXPECT_FLOAT_EQ(textPalette.readOnlyValueColor.r, 0.6f);
|
||||
EXPECT_FLOAT_EQ(textPalette.controlFocusedBorderColor.r, 0.55f);
|
||||
|
||||
const auto objectMetrics = Editor::BuildUIEditorPropertyGridObjectFieldMetrics(propertyMetrics);
|
||||
const auto objectPalette = Editor::BuildUIEditorPropertyGridObjectFieldPalette(propertyPalette);
|
||||
EXPECT_FLOAT_EQ(objectMetrics.controlTrailingInset, 5.0f);
|
||||
EXPECT_FLOAT_EQ(objectMetrics.controlInsetY, 2.0f);
|
||||
EXPECT_FLOAT_EQ(objectMetrics.valueTextInsetX, 5.0f);
|
||||
EXPECT_FLOAT_EQ(objectMetrics.typeTextInsetX, 5.0f);
|
||||
EXPECT_FLOAT_EQ(objectMetrics.typeTextInsetY, 3.0f);
|
||||
EXPECT_FLOAT_EQ(objectMetrics.typeFontSize, 9.0f);
|
||||
EXPECT_FLOAT_EQ(objectMetrics.buttonGlyphFontSize, 12.0f);
|
||||
EXPECT_FLOAT_EQ(objectPalette.valueBoxColor.r, 0.2f);
|
||||
EXPECT_FLOAT_EQ(objectPalette.buttonColor.r, 0.15f);
|
||||
EXPECT_FLOAT_EQ(objectPalette.buttonHoverColor.r, 0.3f);
|
||||
EXPECT_FLOAT_EQ(objectPalette.buttonActiveColor.r, 0.4f);
|
||||
EXPECT_FLOAT_EQ(objectPalette.buttonGlyphColor.r, 0.9f);
|
||||
EXPECT_FLOAT_EQ(objectPalette.separatorColor.r, 0.5f);
|
||||
EXPECT_FLOAT_EQ(objectPalette.typeColor.r, 0.6f);
|
||||
|
||||
const auto vector2Metrics = Editor::BuildUIEditorPropertyGridVector2FieldMetrics(propertyMetrics);
|
||||
const auto vector2Palette = Editor::BuildUIEditorPropertyGridVector2FieldPalette(propertyPalette);
|
||||
EXPECT_FLOAT_EQ(vector2Metrics.controlInsetY, 2.0f);
|
||||
EXPECT_FLOAT_EQ(vector2Metrics.controlTrailingInset, 5.0f);
|
||||
EXPECT_FLOAT_EQ(vector2Metrics.valueTextInsetX, 5.0f);
|
||||
EXPECT_FLOAT_EQ(vector2Metrics.componentRounding, 2.0f);
|
||||
EXPECT_FLOAT_EQ(vector2Palette.componentEditingColor.r, 0.4f);
|
||||
EXPECT_FLOAT_EQ(vector2Palette.componentFocusedBorderColor.r, 0.55f);
|
||||
|
||||
const auto vector3Metrics = Editor::BuildUIEditorPropertyGridVector3FieldMetrics(propertyMetrics);
|
||||
const auto vector3Palette = Editor::BuildUIEditorPropertyGridVector3FieldPalette(propertyPalette);
|
||||
EXPECT_FLOAT_EQ(vector3Metrics.controlInsetY, 2.0f);
|
||||
EXPECT_FLOAT_EQ(vector3Metrics.controlTrailingInset, 5.0f);
|
||||
EXPECT_FLOAT_EQ(vector3Metrics.valueTextInsetX, 5.0f);
|
||||
EXPECT_FLOAT_EQ(vector3Metrics.componentRounding, 2.0f);
|
||||
EXPECT_FLOAT_EQ(vector3Palette.componentEditingColor.r, 0.4f);
|
||||
EXPECT_FLOAT_EQ(vector3Palette.componentFocusedBorderColor.r, 0.55f);
|
||||
|
||||
const auto vector4Metrics = Editor::BuildUIEditorPropertyGridVector4FieldMetrics(propertyMetrics);
|
||||
const auto vector4Palette = Editor::BuildUIEditorPropertyGridVector4FieldPalette(propertyPalette);
|
||||
EXPECT_FLOAT_EQ(vector4Metrics.controlInsetY, 2.0f);
|
||||
EXPECT_FLOAT_EQ(vector4Metrics.controlTrailingInset, 5.0f);
|
||||
EXPECT_FLOAT_EQ(vector4Metrics.valueTextInsetX, 5.0f);
|
||||
EXPECT_FLOAT_EQ(vector4Metrics.componentRounding, 2.0f);
|
||||
EXPECT_FLOAT_EQ(vector4Palette.componentEditingColor.r, 0.4f);
|
||||
EXPECT_FLOAT_EQ(vector4Palette.componentFocusedBorderColor.r, 0.55f);
|
||||
|
||||
const auto enumMetrics = Editor::BuildUIEditorPropertyGridEnumFieldMetrics(propertyMetrics);
|
||||
const auto enumPalette = Editor::BuildUIEditorPropertyGridEnumFieldPalette(propertyPalette);
|
||||
EXPECT_FLOAT_EQ(enumMetrics.controlTrailingInset, 5.0f);
|
||||
EXPECT_FLOAT_EQ(enumMetrics.controlInsetY, 2.0f);
|
||||
EXPECT_FLOAT_EQ(enumMetrics.valueTextInsetX, 5.0f);
|
||||
EXPECT_FLOAT_EQ(enumMetrics.dropdownArrowFontSize, 12.0f);
|
||||
EXPECT_FLOAT_EQ(enumPalette.arrowColor.r, 0.9f);
|
||||
|
||||
const auto colorMetrics = Editor::BuildUIEditorPropertyGridColorFieldMetrics(propertyMetrics);
|
||||
const auto colorPalette = Editor::BuildUIEditorPropertyGridColorFieldPalette(propertyPalette);
|
||||
const auto defaultColorPalette = UI::Editor::Widgets::UIEditorColorFieldPalette {};
|
||||
EXPECT_FLOAT_EQ(colorMetrics.controlTrailingInset, 5.0f);
|
||||
EXPECT_FLOAT_EQ(colorMetrics.swatchInsetY, 2.0f);
|
||||
EXPECT_FLOAT_EQ(colorMetrics.labelFontSize, 10.0f);
|
||||
EXPECT_FLOAT_EQ(colorMetrics.valueFontSize, 12.0f);
|
||||
EXPECT_FLOAT_EQ(colorMetrics.popupHeaderHeight, 30.0f);
|
||||
EXPECT_FLOAT_EQ(colorPalette.labelColor.r, 0.8f);
|
||||
EXPECT_FLOAT_EQ(colorPalette.popupBorderColor.r, 0.15f);
|
||||
EXPECT_FLOAT_EQ(colorPalette.popupHeaderColor.r, defaultColorPalette.popupHeaderColor.r);
|
||||
EXPECT_FLOAT_EQ(colorPalette.swatchBorderColor.r, 0.5f);
|
||||
|
||||
const auto assetMetrics = Editor::BuildUIEditorPropertyGridAssetFieldMetrics(propertyMetrics);
|
||||
const auto assetPalette = Editor::BuildUIEditorPropertyGridAssetFieldPalette(propertyPalette);
|
||||
EXPECT_FLOAT_EQ(assetMetrics.controlTrailingInset, 5.0f);
|
||||
EXPECT_FLOAT_EQ(assetMetrics.controlInsetY, 2.0f);
|
||||
EXPECT_FLOAT_EQ(assetMetrics.valueTextInsetX, 5.0f);
|
||||
EXPECT_FLOAT_EQ(assetMetrics.previewGlyphFontSize, 12.0f);
|
||||
EXPECT_FLOAT_EQ(assetMetrics.statusBadgeFontSize, 9.0f);
|
||||
EXPECT_FLOAT_EQ(assetMetrics.actionGlyphFontSize, 12.0f);
|
||||
EXPECT_FLOAT_EQ(assetMetrics.previewRounding, 2.0f);
|
||||
EXPECT_FLOAT_EQ(assetMetrics.badgeRounding, 2.0f);
|
||||
EXPECT_FLOAT_EQ(assetPalette.valueBoxActiveColor.r, 0.4f);
|
||||
EXPECT_FLOAT_EQ(assetPalette.previewBaseColor.r, 0.2f);
|
||||
EXPECT_FLOAT_EQ(assetPalette.previewEmptyColor.r, 0.15f);
|
||||
EXPECT_FLOAT_EQ(assetPalette.previewGlyphColor.r, 0.9f);
|
||||
EXPECT_FLOAT_EQ(assetPalette.statusBadgeColor.r, 0.52f);
|
||||
EXPECT_FLOAT_EQ(assetPalette.statusBadgeColor.g, 0.68f);
|
||||
EXPECT_FLOAT_EQ(assetPalette.statusBadgeBorderColor.r, 0.5f);
|
||||
EXPECT_FLOAT_EQ(assetPalette.actionButtonColor.r, 0.15f);
|
||||
EXPECT_FLOAT_EQ(assetPalette.actionButtonHoverColor.r, 0.3f);
|
||||
EXPECT_FLOAT_EQ(assetPalette.actionButtonActiveColor.r, 0.4f);
|
||||
EXPECT_FLOAT_EQ(assetPalette.clearGlyphColor.r, 0.6f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,144 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Collections/UIEditorInlineRenameSession.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::AppendUIEditorInlineRenameSession;
|
||||
using XCEngine::UI::Editor::UIEditorInlineRenameSessionRequest;
|
||||
using XCEngine::UI::Editor::UIEditorInlineRenameSessionFrame;
|
||||
using XCEngine::UI::Editor::UIEditorInlineRenameSessionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorInlineRenameSession;
|
||||
|
||||
UIInputEvent MakeKeyDown(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeCharacter(char character) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::Character;
|
||||
event.character = static_cast<std::uint32_t>(character);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeFocusLost() {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::FocusLost;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIEditorInlineRenameSessionRequest MakeRequest(bool beginSession = false) {
|
||||
UIEditorInlineRenameSessionRequest request = {};
|
||||
request.beginSession = beginSession;
|
||||
request.itemId = "camera";
|
||||
request.initialText = "Camera";
|
||||
request.bounds = UIRect(10.0f, 20.0f, 180.0f, 24.0f);
|
||||
return request;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorInlineRenameSessionTest, BeginRequestStartsActiveEditingSession) {
|
||||
UIEditorInlineRenameSessionState state = {};
|
||||
|
||||
const auto frame = UpdateUIEditorInlineRenameSession(
|
||||
state,
|
||||
MakeRequest(true),
|
||||
{});
|
||||
|
||||
EXPECT_TRUE(frame.result.sessionStarted);
|
||||
EXPECT_TRUE(frame.result.active);
|
||||
EXPECT_EQ(frame.result.itemId, "camera");
|
||||
EXPECT_TRUE(state.active);
|
||||
EXPECT_EQ(state.itemId, "camera");
|
||||
EXPECT_TRUE(state.textFieldInteraction.textFieldState.focused);
|
||||
EXPECT_TRUE(state.textFieldInteraction.textFieldState.editing);
|
||||
EXPECT_EQ(state.textFieldInteraction.textFieldState.displayText, "Camera");
|
||||
}
|
||||
|
||||
TEST(UIEditorInlineRenameSessionTest, EnterCommitsEditedValueAndClosesSession) {
|
||||
UIEditorInlineRenameSessionState state = {};
|
||||
UpdateUIEditorInlineRenameSession(state, MakeRequest(true), {});
|
||||
|
||||
UpdateUIEditorInlineRenameSession(
|
||||
state,
|
||||
MakeRequest(false),
|
||||
{ MakeCharacter('2') });
|
||||
|
||||
const auto frame = UpdateUIEditorInlineRenameSession(
|
||||
state,
|
||||
MakeRequest(false),
|
||||
{ MakeKeyDown(KeyCode::Enter) });
|
||||
|
||||
EXPECT_TRUE(frame.result.sessionCommitted);
|
||||
EXPECT_TRUE(frame.result.valueChanged);
|
||||
EXPECT_EQ(frame.result.valueBefore, "Camera");
|
||||
EXPECT_EQ(frame.result.valueAfter, "Camera2");
|
||||
EXPECT_FALSE(state.active);
|
||||
}
|
||||
|
||||
TEST(UIEditorInlineRenameSessionTest, EscapeCancelsEditedValueAndClosesSession) {
|
||||
UIEditorInlineRenameSessionState state = {};
|
||||
UpdateUIEditorInlineRenameSession(state, MakeRequest(true), {});
|
||||
|
||||
UpdateUIEditorInlineRenameSession(
|
||||
state,
|
||||
MakeRequest(false),
|
||||
{ MakeCharacter('X') });
|
||||
|
||||
const auto frame = UpdateUIEditorInlineRenameSession(
|
||||
state,
|
||||
MakeRequest(false),
|
||||
{ MakeKeyDown(KeyCode::Escape) });
|
||||
|
||||
EXPECT_TRUE(frame.result.sessionCanceled);
|
||||
EXPECT_EQ(frame.result.valueBefore, "Camera");
|
||||
EXPECT_EQ(frame.result.valueAfter, "Camera");
|
||||
EXPECT_FALSE(state.active);
|
||||
}
|
||||
|
||||
TEST(UIEditorInlineRenameSessionTest, FocusLostCommitsEditedValue) {
|
||||
UIEditorInlineRenameSessionState state = {};
|
||||
UpdateUIEditorInlineRenameSession(state, MakeRequest(true), {});
|
||||
|
||||
UpdateUIEditorInlineRenameSession(
|
||||
state,
|
||||
MakeRequest(false),
|
||||
{ MakeCharacter('X') });
|
||||
|
||||
const auto frame = UpdateUIEditorInlineRenameSession(
|
||||
state,
|
||||
MakeRequest(false),
|
||||
{ MakeFocusLost() });
|
||||
|
||||
EXPECT_TRUE(frame.result.sessionCommitted);
|
||||
EXPECT_EQ(frame.result.valueAfter, "CameraX");
|
||||
EXPECT_FALSE(state.active);
|
||||
}
|
||||
|
||||
TEST(UIEditorInlineRenameSessionTest, AppendUsesActiveSessionStateToEmitTextFieldCommands) {
|
||||
UIEditorInlineRenameSessionState state = {};
|
||||
UIDrawList drawList("InlineRename");
|
||||
const UIEditorInlineRenameSessionFrame inactiveFrame = {};
|
||||
|
||||
AppendUIEditorInlineRenameSession(drawList, inactiveFrame, state);
|
||||
EXPECT_TRUE(drawList.Empty());
|
||||
|
||||
const UIEditorInlineRenameSessionFrame activeFrame =
|
||||
UpdateUIEditorInlineRenameSession(state, MakeRequest(true), {});
|
||||
AppendUIEditorInlineRenameSession(drawList, activeFrame, state);
|
||||
|
||||
ASSERT_FALSE(drawList.Empty());
|
||||
EXPECT_EQ(drawList.GetCommands().front().type, UIDrawCommandType::FilledRect);
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Collections/UIEditorListView.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Widgets::UISelectionModel;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorListViewBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorListViewForeground;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorListViewLayout;
|
||||
using XCEngine::UI::Editor::Widgets::FindUIEditorListViewItemIndex;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorListView;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorListViewHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorListViewItem;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorListViewState;
|
||||
|
||||
bool ContainsTextCommand(
|
||||
const XCEngine::UI::UIDrawData& drawData,
|
||||
std::string_view text) {
|
||||
for (const auto& drawList : drawData.GetDrawLists()) {
|
||||
for (const auto& command : drawList.GetCommands()) {
|
||||
if (command.type == XCEngine::UI::UIDrawCommandType::Text &&
|
||||
command.text == text) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<UIEditorListViewItem> BuildListItems() {
|
||||
return {
|
||||
{ "scene", "Scene.unity", "最近修改: 1 分钟前", 0.0f },
|
||||
{ "material", "Metal.mat", "Material | 4 个属性", 0.0f },
|
||||
{ "script", "PlayerController.cs", "", 0.0f }
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorListViewTest, FindItemIndexReturnsMatchingStableIndex) {
|
||||
const auto items = BuildListItems();
|
||||
|
||||
EXPECT_EQ(FindUIEditorListViewItemIndex(items, "scene"), 0u);
|
||||
EXPECT_EQ(FindUIEditorListViewItemIndex(items, "material"), 1u);
|
||||
EXPECT_EQ(FindUIEditorListViewItemIndex(items, "missing"), static_cast<std::size_t>(-1));
|
||||
}
|
||||
|
||||
TEST(UIEditorListViewTest, LayoutBuildsRowsAndSecondaryTextRects) {
|
||||
const auto items = BuildListItems();
|
||||
const auto layout = BuildUIEditorListViewLayout(
|
||||
UIRect(10.0f, 20.0f, 320.0f, 240.0f),
|
||||
items);
|
||||
|
||||
ASSERT_EQ(layout.rowRects.size(), items.size());
|
||||
EXPECT_EQ(layout.itemIndices[0], 0u);
|
||||
EXPECT_EQ(layout.itemIndices[2], 2u);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.rowRects[0].x, 10.0f);
|
||||
EXPECT_FLOAT_EQ(layout.rowRects[0].y, 20.0f);
|
||||
EXPECT_FLOAT_EQ(layout.rowRects[1].y, 66.0f);
|
||||
EXPECT_FLOAT_EQ(layout.primaryTextRects[0].x, 20.0f);
|
||||
EXPECT_TRUE(layout.hasSecondaryText[0]);
|
||||
EXPECT_TRUE(layout.hasSecondaryText[1]);
|
||||
EXPECT_FALSE(layout.hasSecondaryText[2]);
|
||||
EXPECT_GT(layout.secondaryTextRects[0].height, 0.0f);
|
||||
EXPECT_FLOAT_EQ(layout.secondaryTextRects[2].height, 0.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorListViewTest, HitTestResolvesRowsAndReturnsNoneOutsideBounds) {
|
||||
const auto items = BuildListItems();
|
||||
const auto layout = BuildUIEditorListViewLayout(
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items);
|
||||
|
||||
const auto hit = HitTestUIEditorListView(layout, UIPoint(80.0f, 72.0f));
|
||||
EXPECT_EQ(hit.kind, UIEditorListViewHitTargetKind::Row);
|
||||
EXPECT_EQ(hit.visibleIndex, 1u);
|
||||
EXPECT_EQ(hit.itemIndex, 1u);
|
||||
|
||||
const auto miss = HitTestUIEditorListView(layout, UIPoint(360.0f, 20.0f));
|
||||
EXPECT_EQ(miss.kind, UIEditorListViewHitTargetKind::None);
|
||||
}
|
||||
|
||||
TEST(UIEditorListViewTest, BackgroundAndForegroundEmitStableCommands) {
|
||||
const auto items = BuildListItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("material");
|
||||
UIEditorListViewState state = {};
|
||||
state.focused = true;
|
||||
state.hoveredItemId = "script";
|
||||
|
||||
const auto layout = BuildUIEditorListViewLayout(
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items);
|
||||
|
||||
XCEngine::UI::UIDrawData drawData = {};
|
||||
auto& drawList = drawData.EmplaceDrawList("ListView");
|
||||
AppendUIEditorListViewBackground(drawList, layout, items, selectionModel, state);
|
||||
AppendUIEditorListViewForeground(drawList, layout, items);
|
||||
|
||||
const auto& commands = drawList.GetCommands();
|
||||
ASSERT_GE(commands.size(), 7u);
|
||||
EXPECT_EQ(commands[0].type, XCEngine::UI::UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(commands[1].type, XCEngine::UI::UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(commands[2].type, XCEngine::UI::UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(commands[3].type, XCEngine::UI::UIDrawCommandType::FilledRect);
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Scene.unity"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "最近修改: 1 分钟前"));
|
||||
}
|
||||
@@ -1,418 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Collections/UIEditorListViewInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIInputModifiers;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Widgets::UISelectionModel;
|
||||
using XCEngine::UI::Editor::UIEditorListViewInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorListViewInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorListViewHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorListViewItem;
|
||||
|
||||
std::vector<UIEditorListViewItem> BuildListItems() {
|
||||
return {
|
||||
{ "scene", "Scene.unity", "最近修改: 1 分钟前", 0.0f },
|
||||
{ "material", "Metal.mat", "Material | 4 个属性", 0.0f },
|
||||
{ "script", "PlayerController.cs", "", 0.0f },
|
||||
{ "texture", "Checker.png", "Texture2D | 1024x1024", 0.0f }
|
||||
};
|
||||
}
|
||||
|
||||
UIInputModifiers MakeShiftModifiers() {
|
||||
UIInputModifiers modifiers = {};
|
||||
modifiers.shift = true;
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
UIInputModifiers MakeControlModifiers() {
|
||||
UIInputModifiers modifiers = {};
|
||||
modifiers.control = true;
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerMove(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerMove;
|
||||
event.position = UIPoint(x, y);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerDown(
|
||||
float x,
|
||||
float y,
|
||||
UIPointerButton button = UIPointerButton::Left,
|
||||
UIInputModifiers modifiers = {}) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonDown;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
event.modifiers = modifiers;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerUp(
|
||||
float x,
|
||||
float y,
|
||||
UIPointerButton button = UIPointerButton::Left,
|
||||
UIInputModifiers modifiers = {}) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonUp;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
event.modifiers = modifiers;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKeyDown(KeyCode keyCode, UIInputModifiers modifiers = {}) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
event.modifiers = modifiers;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerLeave() {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerLeave;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeFocusLost() {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::FocusLost;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIPoint RectCenter(const XCEngine::UI::UIRect& rect) {
|
||||
return UIPoint(rect.x + rect.width * 0.5f, rect.y + rect.height * 0.5f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorListViewInteractionTest, PointerMoveUpdatesHoveredItemAndHitTarget) {
|
||||
const auto items = BuildListItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIEditorListViewInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint materialCenter = RectCenter(initialFrame.layout.rowRects[1]);
|
||||
|
||||
const auto frame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakePointerMove(materialCenter.x, materialCenter.y) });
|
||||
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorListViewHitTargetKind::Row);
|
||||
EXPECT_EQ(frame.result.hitTarget.itemIndex, 1u);
|
||||
EXPECT_EQ(state.listViewState.hoveredItemId, "material");
|
||||
}
|
||||
|
||||
TEST(UIEditorListViewInteractionTest, LeftClickRowSelectsItemAndFocusesList) {
|
||||
const auto items = BuildListItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIEditorListViewInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint scriptCenter = RectCenter(initialFrame.layout.rowRects[2]);
|
||||
|
||||
const auto frame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(scriptCenter.x, scriptCenter.y),
|
||||
MakePointerUp(scriptCenter.x, scriptCenter.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_EQ(frame.result.selectedItemId, "script");
|
||||
EXPECT_EQ(frame.result.selectedIndex, 2u);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("script"));
|
||||
EXPECT_TRUE(state.listViewState.focused);
|
||||
EXPECT_TRUE(state.keyboardNavigation.HasCurrentIndex());
|
||||
EXPECT_EQ(state.keyboardNavigation.GetCurrentIndex(), 2u);
|
||||
}
|
||||
|
||||
TEST(UIEditorListViewInteractionTest, RightClickRowSelectsItemAndMarksSecondaryClick) {
|
||||
const auto items = BuildListItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIEditorListViewInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint textureCenter = RectCenter(initialFrame.layout.rowRects[3]);
|
||||
|
||||
const auto frame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(textureCenter.x, textureCenter.y, UIPointerButton::Right),
|
||||
MakePointerUp(textureCenter.x, textureCenter.y, UIPointerButton::Right)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.secondaryClicked);
|
||||
EXPECT_EQ(frame.result.selectedItemId, "texture");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("texture"));
|
||||
EXPECT_TRUE(state.listViewState.focused);
|
||||
}
|
||||
|
||||
TEST(UIEditorListViewInteractionTest, ControlClickRowTogglesMembershipWithoutDroppingExistingSelection) {
|
||||
const auto items = BuildListItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelections({ "material", "script" }, "script");
|
||||
UIEditorListViewInteractionState state = {};
|
||||
state.listViewState.focused = true;
|
||||
state.selectionAnchorId = "script";
|
||||
|
||||
const auto initialFrame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint sceneCenter = RectCenter(initialFrame.layout.rowRects[0]);
|
||||
auto frame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(sceneCenter.x, sceneCenter.y, UIPointerButton::Left, MakeControlModifiers()),
|
||||
MakePointerUp(sceneCenter.x, sceneCenter.y, UIPointerButton::Left, MakeControlModifiers())
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("scene"));
|
||||
EXPECT_TRUE(selectionModel.IsSelected("material"));
|
||||
EXPECT_TRUE(selectionModel.IsSelected("script"));
|
||||
EXPECT_EQ(selectionModel.GetSelectedId(), "scene");
|
||||
|
||||
const UIPoint scriptCenter = RectCenter(initialFrame.layout.rowRects[2]);
|
||||
frame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(scriptCenter.x, scriptCenter.y, UIPointerButton::Left, MakeControlModifiers()),
|
||||
MakePointerUp(scriptCenter.x, scriptCenter.y, UIPointerButton::Left, MakeControlModifiers())
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("scene"));
|
||||
EXPECT_TRUE(selectionModel.IsSelected("material"));
|
||||
EXPECT_FALSE(selectionModel.IsSelected("script"));
|
||||
}
|
||||
|
||||
TEST(UIEditorListViewInteractionTest, ShiftClickRowSelectsRangeFromAnchor) {
|
||||
const auto items = BuildListItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("material");
|
||||
UIEditorListViewInteractionState state = {};
|
||||
state.listViewState.focused = true;
|
||||
state.selectionAnchorId = "material";
|
||||
|
||||
const auto initialFrame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint textureCenter = RectCenter(initialFrame.layout.rowRects[3]);
|
||||
|
||||
const auto frame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(textureCenter.x, textureCenter.y, UIPointerButton::Left, MakeShiftModifiers()),
|
||||
MakePointerUp(textureCenter.x, textureCenter.y, UIPointerButton::Left, MakeShiftModifiers())
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_EQ(frame.result.selectedItemId, "texture");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("material"));
|
||||
EXPECT_TRUE(selectionModel.IsSelected("script"));
|
||||
EXPECT_TRUE(selectionModel.IsSelected("texture"));
|
||||
EXPECT_EQ(selectionModel.GetSelectionCount(), 3u);
|
||||
EXPECT_EQ(state.selectionAnchorId, "material");
|
||||
}
|
||||
|
||||
TEST(UIEditorListViewInteractionTest, RightClickSelectedRowKeepsExistingMultiSelection) {
|
||||
const auto items = BuildListItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelections({ "material", "script", "texture" }, "material");
|
||||
UIEditorListViewInteractionState state = {};
|
||||
state.listViewState.focused = true;
|
||||
state.selectionAnchorId = "material";
|
||||
|
||||
const auto initialFrame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint scriptCenter = RectCenter(initialFrame.layout.rowRects[2]);
|
||||
|
||||
const auto frame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(scriptCenter.x, scriptCenter.y, UIPointerButton::Right),
|
||||
MakePointerUp(scriptCenter.x, scriptCenter.y, UIPointerButton::Right)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.secondaryClicked);
|
||||
EXPECT_FALSE(frame.result.selectionChanged);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("material"));
|
||||
EXPECT_TRUE(selectionModel.IsSelected("script"));
|
||||
EXPECT_TRUE(selectionModel.IsSelected("texture"));
|
||||
EXPECT_EQ(selectionModel.GetSelectionCount(), 3u);
|
||||
EXPECT_EQ(selectionModel.GetSelectedId(), "script");
|
||||
}
|
||||
|
||||
TEST(UIEditorListViewInteractionTest, ArrowAndHomeEndKeysDriveSelectionWhenFocused) {
|
||||
const auto items = BuildListItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("material");
|
||||
UIEditorListViewInteractionState state = {};
|
||||
state.listViewState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::Down) });
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_EQ(frame.result.selectedItemId, "script");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("script"));
|
||||
|
||||
frame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::Home) });
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_EQ(frame.result.selectedItemId, "scene");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("scene"));
|
||||
|
||||
frame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::End) });
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_EQ(frame.result.selectedItemId, "texture");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("texture"));
|
||||
}
|
||||
|
||||
TEST(UIEditorListViewInteractionTest, ShiftArrowExtendsVisibleRangeFromAnchor) {
|
||||
const auto items = BuildListItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("material");
|
||||
UIEditorListViewInteractionState state = {};
|
||||
state.listViewState.focused = true;
|
||||
state.selectionAnchorId = "material";
|
||||
|
||||
const auto frame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::Down, MakeShiftModifiers()) });
|
||||
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("material"));
|
||||
EXPECT_TRUE(selectionModel.IsSelected("script"));
|
||||
EXPECT_EQ(selectionModel.GetSelectionCount(), 2u);
|
||||
EXPECT_EQ(frame.result.selectedItemId, "script");
|
||||
}
|
||||
|
||||
TEST(UIEditorListViewInteractionTest, F2RequestsRenameForPrimarySelectionWhenFocused) {
|
||||
const auto items = BuildListItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("script");
|
||||
UIEditorListViewInteractionState state = {};
|
||||
state.listViewState.focused = true;
|
||||
|
||||
const auto frame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::F2) });
|
||||
|
||||
EXPECT_TRUE(frame.result.renameRequested);
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_EQ(frame.result.renameItemId, "script");
|
||||
EXPECT_EQ(frame.result.selectedItemId, "script");
|
||||
EXPECT_EQ(frame.result.selectedIndex, 2u);
|
||||
EXPECT_FALSE(frame.result.selectionChanged);
|
||||
}
|
||||
|
||||
TEST(UIEditorListViewInteractionTest, OutsideClickAndFocusLostClearFocusAndHoverButKeepSelection) {
|
||||
const auto items = BuildListItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("material");
|
||||
UIEditorListViewInteractionState state = {};
|
||||
state.listViewState.focused = true;
|
||||
state.listViewState.hoveredItemId = "material";
|
||||
|
||||
auto frame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakePointerDown(360.0f, 260.0f), MakePointerUp(360.0f, 260.0f) });
|
||||
EXPECT_FALSE(state.listViewState.focused);
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorListViewHitTargetKind::None);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("material"));
|
||||
|
||||
frame = UpdateUIEditorListViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakePointerLeave(), MakeFocusLost() });
|
||||
EXPECT_FALSE(state.listViewState.focused);
|
||||
EXPECT_TRUE(state.listViewState.hoveredItemId.empty());
|
||||
EXPECT_FALSE(state.hasPointerPosition);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("material"));
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEditor/Menu/UIEditorMenuBar.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorMenuBarBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorMenuBarForeground;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorMenuBarLayout;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorMenuBar;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorMenuBarDesiredButtonWidth;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorMenuBarHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorMenuBarInvalidIndex;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorMenuBarItem;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorMenuBarState;
|
||||
|
||||
TEST(UIEditorMenuBarTest, DesiredWidthUsesLabelEstimateAndHorizontalPadding) {
|
||||
XCEngine::UI::Editor::Widgets::UIEditorMenuBarMetrics metrics = {};
|
||||
metrics.estimatedGlyphWidth = 8.0f;
|
||||
metrics.buttonPaddingX = 12.0f;
|
||||
|
||||
EXPECT_FLOAT_EQ(
|
||||
ResolveUIEditorMenuBarDesiredButtonWidth(
|
||||
UIEditorMenuBarItem{ "file", "File", true, 0.0f },
|
||||
metrics),
|
||||
56.0f);
|
||||
EXPECT_FLOAT_EQ(
|
||||
ResolveUIEditorMenuBarDesiredButtonWidth(
|
||||
UIEditorMenuBarItem{ "window", "Window", true, 50.0f },
|
||||
metrics),
|
||||
74.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuBarTest, LayoutBuildsHorizontalButtonsInsideBarInsets) {
|
||||
XCEngine::UI::Editor::Widgets::UIEditorMenuBarMetrics metrics = {};
|
||||
metrics.horizontalInset = 8.0f;
|
||||
metrics.verticalInset = 3.0f;
|
||||
metrics.buttonGap = 4.0f;
|
||||
metrics.buttonPaddingX = 10.0f;
|
||||
metrics.estimatedGlyphWidth = 8.0f;
|
||||
|
||||
const std::vector<UIEditorMenuBarItem> items = {
|
||||
{ "file", "File", true, 0.0f },
|
||||
{ "window", "Window", true, 32.0f }
|
||||
};
|
||||
|
||||
const auto layout =
|
||||
BuildUIEditorMenuBarLayout(UIRect(10.0f, 20.0f, 240.0f, 32.0f), items, metrics);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.x, 18.0f);
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.y, 23.0f);
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.width, 224.0f);
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.height, 26.0f);
|
||||
|
||||
ASSERT_EQ(layout.buttonRects.size(), 2u);
|
||||
EXPECT_FLOAT_EQ(layout.buttonRects[0].x, 18.0f);
|
||||
EXPECT_FLOAT_EQ(layout.buttonRects[0].width, 52.0f);
|
||||
EXPECT_FLOAT_EQ(layout.buttonRects[1].x, 74.0f);
|
||||
EXPECT_FLOAT_EQ(layout.buttonRects[1].width, 52.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuBarTest, HitTestResolvesButtonBeforeBarBackground) {
|
||||
const std::vector<UIEditorMenuBarItem> items = {
|
||||
{ "file", "File", true, 0.0f },
|
||||
{ "window", "Window", true, 32.0f }
|
||||
};
|
||||
const auto layout =
|
||||
BuildUIEditorMenuBarLayout(UIRect(10.0f, 20.0f, 240.0f, 32.0f), items);
|
||||
|
||||
const auto buttonHit = HitTestUIEditorMenuBar(layout, UIPoint(30.0f, 32.0f));
|
||||
EXPECT_EQ(buttonHit.kind, UIEditorMenuBarHitTargetKind::Button);
|
||||
EXPECT_EQ(buttonHit.index, 0u);
|
||||
|
||||
const auto backgroundHit = HitTestUIEditorMenuBar(layout, UIPoint(220.0f, 32.0f));
|
||||
EXPECT_EQ(backgroundHit.kind, UIEditorMenuBarHitTargetKind::BarBackground);
|
||||
EXPECT_EQ(backgroundHit.index, UIEditorMenuBarInvalidIndex);
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuBarTest, BackgroundAndForegroundEmitStableCommands) {
|
||||
const std::vector<UIEditorMenuBarItem> items = {
|
||||
{ "file", "File", true, 0.0f },
|
||||
{ "window", "Window", false, 32.0f }
|
||||
};
|
||||
|
||||
UIEditorMenuBarState state = {};
|
||||
state.openIndex = 0u;
|
||||
state.hoveredIndex = 1u;
|
||||
state.focused = true;
|
||||
const auto layout =
|
||||
BuildUIEditorMenuBarLayout(UIRect(10.0f, 20.0f, 240.0f, 32.0f), items);
|
||||
|
||||
UIDrawList background("MenuBarBackground");
|
||||
AppendUIEditorMenuBarBackground(background, layout, items, state);
|
||||
ASSERT_EQ(background.GetCommandCount(), 3u);
|
||||
const auto& backgroundCommands = background.GetCommands();
|
||||
EXPECT_EQ(backgroundCommands[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[1].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[2].type, UIDrawCommandType::FilledRect);
|
||||
|
||||
UIDrawList foreground("MenuBarForeground");
|
||||
AppendUIEditorMenuBarForeground(foreground, layout, items, state);
|
||||
ASSERT_EQ(foreground.GetCommandCount(), 6u);
|
||||
const auto& foregroundCommands = foreground.GetCommands();
|
||||
EXPECT_EQ(foregroundCommands[0].type, UIDrawCommandType::PushClipRect);
|
||||
EXPECT_EQ(foregroundCommands[1].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[1].text, "File");
|
||||
EXPECT_EQ(foregroundCommands[4].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[4].text, "Window");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,223 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Menu/UIEditorMenuModel.h>
|
||||
#include <XCEditor/Foundation/UIEditorShortcutManager.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::BuildUIEditorResolvedMenuModel;
|
||||
using XCEngine::UI::Editor::UIEditorCommandDispatcher;
|
||||
using XCEngine::UI::Editor::UIEditorCommandPanelSource;
|
||||
using XCEngine::UI::Editor::UIEditorCommandRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorMenuCheckedStateSource;
|
||||
using XCEngine::UI::Editor::UIEditorMenuItemDescriptor;
|
||||
using XCEngine::UI::Editor::UIEditorMenuItemKind;
|
||||
using XCEngine::UI::Editor::UIEditorMenuModel;
|
||||
using XCEngine::UI::Editor::UIEditorMenuModelValidationCode;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorShortcutManager;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
using XCEngine::UI::Editor::ValidateUIEditorMenuModel;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIShortcutBinding;
|
||||
using XCEngine::UI::UIShortcutScope;
|
||||
|
||||
UIEditorCommandRegistry BuildCommandRegistry() {
|
||||
UIEditorCommandRegistry registry = {};
|
||||
registry.commands = {
|
||||
{
|
||||
"workspace.show_details",
|
||||
"Show Details",
|
||||
{ UIEditorWorkspaceCommandKind::ShowPanel, UIEditorCommandPanelSource::FixedPanelId, "details" }
|
||||
},
|
||||
{
|
||||
"workspace.hide_active",
|
||||
"Hide Active",
|
||||
{ UIEditorWorkspaceCommandKind::HidePanel, UIEditorCommandPanelSource::ActivePanel, {} }
|
||||
},
|
||||
{
|
||||
"workspace.reset",
|
||||
"Reset Workspace",
|
||||
{ UIEditorWorkspaceCommandKind::ResetWorkspace, UIEditorCommandPanelSource::None, {} }
|
||||
}
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.66f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
UIShortcutBinding MakeBinding(
|
||||
std::string commandId,
|
||||
UIShortcutScope scope,
|
||||
XCEngine::UI::UIElementId ownerId,
|
||||
KeyCode keyCode) {
|
||||
UIShortcutBinding binding = {};
|
||||
binding.commandId = std::move(commandId);
|
||||
binding.scope = scope;
|
||||
binding.ownerId = ownerId;
|
||||
binding.triggerEventType = UIInputEventType::KeyDown;
|
||||
binding.chord.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
binding.chord.modifiers.control = true;
|
||||
return binding;
|
||||
}
|
||||
|
||||
UIEditorMenuModel BuildMenuModel() {
|
||||
UIEditorMenuModel model = {};
|
||||
model.menus = {
|
||||
{
|
||||
"window",
|
||||
"Window",
|
||||
{
|
||||
{
|
||||
UIEditorMenuItemKind::Command,
|
||||
"show-details",
|
||||
{},
|
||||
"workspace.show_details",
|
||||
{ UIEditorMenuCheckedStateSource::PanelVisible, "details" },
|
||||
{}
|
||||
},
|
||||
{
|
||||
UIEditorMenuItemKind::Separator,
|
||||
"separator-1",
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
},
|
||||
{
|
||||
UIEditorMenuItemKind::Submenu,
|
||||
"layout",
|
||||
"Layout",
|
||||
{},
|
||||
{},
|
||||
{
|
||||
{
|
||||
UIEditorMenuItemKind::Command,
|
||||
"reset-layout",
|
||||
{},
|
||||
"workspace.reset",
|
||||
{},
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return model;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorMenuModelTest, ValidationRejectsUnknownCommandAndInvalidSubmenuShape) {
|
||||
UIEditorMenuModel model = BuildMenuModel();
|
||||
model.menus[0].items[0].commandId = "missing.command";
|
||||
|
||||
auto validation = ValidateUIEditorMenuModel(model, BuildCommandRegistry());
|
||||
EXPECT_EQ(validation.code, UIEditorMenuModelValidationCode::UnknownCommandId);
|
||||
|
||||
model = BuildMenuModel();
|
||||
model.menus[0].items[2].children.clear();
|
||||
validation = ValidateUIEditorMenuModel(model, BuildCommandRegistry());
|
||||
EXPECT_EQ(validation.code, UIEditorMenuModelValidationCode::SubmenuEmptyChildren);
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuModelTest, ResolvedModelUsesCommandDisplayNameShortcutTextAndCheckedState) {
|
||||
UIEditorShortcutManager shortcutManager(BuildCommandRegistry());
|
||||
shortcutManager.RegisterBinding(
|
||||
MakeBinding("workspace.reset", UIShortcutScope::Panel, 22u, KeyCode::P));
|
||||
shortcutManager.RegisterBinding(
|
||||
MakeBinding("workspace.reset", UIShortcutScope::Global, 0u, KeyCode::R));
|
||||
|
||||
const auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
const UIEditorCommandDispatcher dispatcher(BuildCommandRegistry());
|
||||
|
||||
const auto resolved = BuildUIEditorResolvedMenuModel(
|
||||
BuildMenuModel(),
|
||||
dispatcher,
|
||||
controller,
|
||||
&shortcutManager);
|
||||
|
||||
ASSERT_EQ(resolved.menus.size(), 1u);
|
||||
ASSERT_EQ(resolved.menus[0].items.size(), 3u);
|
||||
EXPECT_EQ(resolved.menus[0].items[0].label, "Show Details");
|
||||
EXPECT_TRUE(resolved.menus[0].items[0].checked);
|
||||
|
||||
const auto& submenu = resolved.menus[0].items[2];
|
||||
ASSERT_EQ(submenu.children.size(), 1u);
|
||||
EXPECT_EQ(submenu.children[0].label, "Reset Workspace");
|
||||
EXPECT_EQ(submenu.children[0].shortcutText, "Ctrl+R");
|
||||
EXPECT_TRUE(submenu.children[0].enabled);
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuModelTest, ResolvedModelDisablesCommandWhenEvaluationCannotResolveActivePanel) {
|
||||
UIEditorMenuModel model = {};
|
||||
model.menus = {
|
||||
{
|
||||
"window",
|
||||
"Window",
|
||||
{
|
||||
{
|
||||
UIEditorMenuItemKind::Command,
|
||||
"hide-active",
|
||||
{},
|
||||
"workspace.hide_active",
|
||||
{},
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
workspace.activePanelId.clear();
|
||||
const auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), workspace);
|
||||
const UIEditorCommandDispatcher dispatcher(BuildCommandRegistry());
|
||||
|
||||
const auto resolved = BuildUIEditorResolvedMenuModel(model, dispatcher, controller);
|
||||
|
||||
ASSERT_EQ(resolved.menus.size(), 1u);
|
||||
ASSERT_EQ(resolved.menus[0].items.size(), 1u);
|
||||
EXPECT_FALSE(resolved.menus[0].items[0].enabled);
|
||||
EXPECT_EQ(
|
||||
resolved.menus[0].items[0].previewStatus,
|
||||
UIEditorWorkspaceCommandStatus::Rejected);
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEditor/Menu/UIEditorMenuPopup.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::UIEditorMenuItemKind;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorMenuPopupBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorMenuPopupForeground;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorMenuPopupLayout;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorMenuPopup;
|
||||
using XCEngine::UI::Editor::Widgets::MeasureUIEditorMenuPopupHeight;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorMenuPopupDesiredWidth;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorMenuPopupHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorMenuPopupInvalidIndex;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorMenuPopupItem;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorMenuPopupState;
|
||||
|
||||
std::vector<UIEditorMenuPopupItem> BuildItems() {
|
||||
return {
|
||||
{ "show-inspector", UIEditorMenuItemKind::Command, "Show Inspector", "Ctrl+I", true, true, false, 0.0f, 0.0f },
|
||||
{ "separator-1", UIEditorMenuItemKind::Separator, {}, {}, false, false, false, 0.0f, 0.0f },
|
||||
{ "layout", UIEditorMenuItemKind::Submenu, "Layout", {}, true, false, true, 0.0f, 0.0f },
|
||||
{ "close", UIEditorMenuItemKind::Command, "Close", "Ctrl+W", false, false, false, 0.0f, 0.0f }
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorMenuPopupTest, DesiredWidthAndHeightUseLabelShortcutAndSeparatorMetrics) {
|
||||
XCEngine::UI::Editor::Widgets::UIEditorMenuPopupMetrics metrics = {};
|
||||
metrics.contentPaddingX = 8.0f;
|
||||
metrics.contentPaddingY = 5.0f;
|
||||
metrics.labelInsetX = 12.0f;
|
||||
metrics.checkColumnWidth = 20.0f;
|
||||
metrics.shortcutGap = 18.0f;
|
||||
metrics.shortcutInsetRight = 22.0f;
|
||||
metrics.submenuIndicatorWidth = 14.0f;
|
||||
metrics.estimatedGlyphWidth = 8.0f;
|
||||
metrics.itemHeight = 30.0f;
|
||||
metrics.separatorHeight = 10.0f;
|
||||
|
||||
EXPECT_FLOAT_EQ(
|
||||
ResolveUIEditorMenuPopupDesiredWidth(BuildItems(), metrics),
|
||||
248.0f);
|
||||
EXPECT_FLOAT_EQ(
|
||||
MeasureUIEditorMenuPopupHeight(BuildItems(), metrics),
|
||||
110.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuPopupTest, LayoutBuildsStackedRowsAndSeparatorSlots) {
|
||||
XCEngine::UI::Editor::Widgets::UIEditorMenuPopupMetrics metrics = {};
|
||||
metrics.contentPaddingX = 6.0f;
|
||||
metrics.contentPaddingY = 4.0f;
|
||||
metrics.itemHeight = 26.0f;
|
||||
metrics.separatorHeight = 8.0f;
|
||||
|
||||
const auto layout = BuildUIEditorMenuPopupLayout(
|
||||
UIRect(100.0f, 50.0f, 220.0f, 94.0f),
|
||||
BuildItems(),
|
||||
metrics);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.x, 106.0f);
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.y, 54.0f);
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.width, 208.0f);
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.height, 86.0f);
|
||||
|
||||
ASSERT_EQ(layout.itemRects.size(), 4u);
|
||||
EXPECT_FLOAT_EQ(layout.itemRects[0].y, 54.0f);
|
||||
EXPECT_FLOAT_EQ(layout.itemRects[0].height, 26.0f);
|
||||
EXPECT_FLOAT_EQ(layout.itemRects[1].y, 80.0f);
|
||||
EXPECT_FLOAT_EQ(layout.itemRects[1].height, 8.0f);
|
||||
EXPECT_FLOAT_EQ(layout.itemRects[2].y, 88.0f);
|
||||
EXPECT_FLOAT_EQ(layout.itemRects[3].y, 114.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuPopupTest, HitTestIgnoresSeparatorsAndFallsBackToPopupSurface) {
|
||||
const auto items = BuildItems();
|
||||
const auto layout = BuildUIEditorMenuPopupLayout(
|
||||
UIRect(100.0f, 50.0f, 220.0f, 118.0f),
|
||||
items);
|
||||
|
||||
const auto itemHit = HitTestUIEditorMenuPopup(layout, items, UIPoint(130.0f, 66.0f));
|
||||
EXPECT_EQ(itemHit.kind, UIEditorMenuPopupHitTargetKind::Item);
|
||||
EXPECT_EQ(itemHit.index, 0u);
|
||||
|
||||
const UIRect separatorRect = layout.itemRects[1];
|
||||
const UIPoint separatorCenter(
|
||||
separatorRect.x + separatorRect.width * 0.5f,
|
||||
separatorRect.y + separatorRect.height * 0.5f);
|
||||
const auto separatorHit = HitTestUIEditorMenuPopup(layout, items, separatorCenter);
|
||||
EXPECT_EQ(separatorHit.kind, UIEditorMenuPopupHitTargetKind::PopupSurface);
|
||||
EXPECT_EQ(separatorHit.index, UIEditorMenuPopupInvalidIndex);
|
||||
|
||||
const auto outsideHit = HitTestUIEditorMenuPopup(layout, items, UIPoint(20.0f, 20.0f));
|
||||
EXPECT_EQ(outsideHit.kind, UIEditorMenuPopupHitTargetKind::None);
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuPopupTest, BackgroundAndForegroundEmitStableCommands) {
|
||||
const auto items = BuildItems();
|
||||
UIEditorMenuPopupState state = {};
|
||||
state.hoveredIndex = 0u;
|
||||
state.submenuOpenIndex = 2u;
|
||||
const auto layout = BuildUIEditorMenuPopupLayout(
|
||||
UIRect(100.0f, 50.0f, 220.0f, 118.0f),
|
||||
items);
|
||||
|
||||
UIDrawList background("MenuPopupBackground");
|
||||
AppendUIEditorMenuPopupBackground(background, layout, items, state);
|
||||
ASSERT_EQ(background.GetCommandCount(), 5u);
|
||||
const auto& backgroundCommands = background.GetCommands();
|
||||
EXPECT_EQ(backgroundCommands[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[1].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(backgroundCommands[2].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[4].type, UIDrawCommandType::FilledRect);
|
||||
|
||||
UIDrawList foreground("MenuPopupForeground");
|
||||
AppendUIEditorMenuPopupForeground(foreground, layout, items, state);
|
||||
ASSERT_EQ(foreground.GetCommandCount(), 13u);
|
||||
const auto& foregroundCommands = foreground.GetCommands();
|
||||
EXPECT_EQ(foregroundCommands[0].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[0].text, "*");
|
||||
EXPECT_EQ(foregroundCommands[2].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[2].text, "Show Inspector");
|
||||
EXPECT_EQ(foregroundCommands[4].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[4].text, "Ctrl+I");
|
||||
EXPECT_EQ(foregroundCommands[6].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[6].text, "Layout");
|
||||
EXPECT_EQ(foregroundCommands[8].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[8].text, ">");
|
||||
EXPECT_EQ(foregroundCommands[10].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[10].text, "Close");
|
||||
EXPECT_EQ(foregroundCommands[12].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[12].text, "Ctrl+W");
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuPopupTest, SubmenuIndicatorStaysInsideReservedRightEdgeSlot) {
|
||||
const auto items = BuildItems();
|
||||
XCEngine::UI::Editor::Widgets::UIEditorMenuPopupMetrics metrics = {};
|
||||
const auto layout = BuildUIEditorMenuPopupLayout(
|
||||
UIRect(100.0f, 50.0f, 220.0f, 118.0f),
|
||||
items,
|
||||
metrics);
|
||||
|
||||
UIDrawList foreground("MenuPopupForeground");
|
||||
AppendUIEditorMenuPopupForeground(foreground, layout, items, {}, {}, metrics);
|
||||
|
||||
const auto& commands = foreground.GetCommands();
|
||||
auto submenuIt = std::find_if(
|
||||
commands.begin(),
|
||||
commands.end(),
|
||||
[](const auto& command) {
|
||||
return command.type == UIDrawCommandType::Text && command.text == ">";
|
||||
});
|
||||
ASSERT_NE(submenuIt, commands.end());
|
||||
|
||||
const UIRect& submenuRect = layout.itemRects[2];
|
||||
const float expectedLeft =
|
||||
submenuRect.x + submenuRect.width -
|
||||
metrics.shortcutInsetRight -
|
||||
metrics.submenuIndicatorWidth;
|
||||
EXPECT_FLOAT_EQ(submenuIt->position.x, expectedLeft);
|
||||
EXPECT_LE(
|
||||
submenuIt->position.x + metrics.estimatedGlyphWidth,
|
||||
submenuRect.x + submenuRect.width - metrics.shortcutInsetRight + 0.001f);
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Menu/UIEditorMenuSession.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIInputPath;
|
||||
using XCEngine::UI::Widgets::UIPopupDismissReason;
|
||||
using XCEngine::UI::Widgets::UIPopupOverlayEntry;
|
||||
using XCEngine::UI::Widgets::UIPopupPlacement;
|
||||
using XCEngine::UI::Editor::UIEditorMenuSession;
|
||||
|
||||
UIPopupOverlayEntry MakePopup(
|
||||
const char* popupId,
|
||||
const char* parentPopupId,
|
||||
UIInputPath surfacePath,
|
||||
UIInputPath anchorPath = {},
|
||||
UIPopupPlacement placement = UIPopupPlacement::BottomStart) {
|
||||
UIPopupOverlayEntry entry = {};
|
||||
entry.popupId = popupId;
|
||||
entry.parentPopupId = parentPopupId;
|
||||
entry.surfacePath = std::move(surfacePath);
|
||||
entry.anchorPath = std::move(anchorPath);
|
||||
entry.placement = placement;
|
||||
return entry;
|
||||
}
|
||||
|
||||
void ExpectClosedIds(
|
||||
const std::vector<std::string>& actual,
|
||||
std::initializer_list<const char*> expected) {
|
||||
ASSERT_EQ(actual.size(), expected.size());
|
||||
|
||||
std::size_t index = 0u;
|
||||
for (const char* popupId : expected) {
|
||||
EXPECT_EQ(actual[index], popupId);
|
||||
++index;
|
||||
}
|
||||
}
|
||||
|
||||
void ExpectOpenSubmenuIds(
|
||||
const UIEditorMenuSession& session,
|
||||
std::initializer_list<const char*> expected) {
|
||||
const auto& actual = session.GetOpenSubmenuItemIds();
|
||||
ASSERT_EQ(actual.size(), expected.size());
|
||||
|
||||
std::size_t index = 0u;
|
||||
for (const char* itemId : expected) {
|
||||
EXPECT_EQ(actual[index], itemId);
|
||||
++index;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorMenuSessionTest, OpenMenuBarRootTracksActiveMenuAndRootPopup) {
|
||||
UIEditorMenuSession session = {};
|
||||
|
||||
const auto result = session.OpenMenuBarRoot(
|
||||
"file",
|
||||
MakePopup("menu.file.root", "", UIInputPath{100u, 110u}, UIInputPath{1u, 2u}));
|
||||
|
||||
EXPECT_TRUE(result.changed);
|
||||
EXPECT_EQ(result.openRootMenuId, "file");
|
||||
EXPECT_EQ(result.openedPopupId, "menu.file.root");
|
||||
EXPECT_TRUE(result.closedPopupIds.empty());
|
||||
EXPECT_TRUE(session.IsMenuOpen("file"));
|
||||
EXPECT_TRUE(session.IsPopupOpen("menu.file.root"));
|
||||
ASSERT_EQ(session.GetPopupOverlayModel().GetPopupCount(), 1u);
|
||||
ASSERT_EQ(session.GetPopupStates().size(), 1u);
|
||||
EXPECT_TRUE(session.GetPopupStates().front().IsRootPopup());
|
||||
EXPECT_TRUE(session.GetOpenSubmenuItemIds().empty());
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuSessionTest, OpenRootMenuSupportsGenericRootPopupEntry) {
|
||||
UIEditorMenuSession session = {};
|
||||
|
||||
const auto result = session.OpenRootMenu(
|
||||
"context.scene",
|
||||
MakePopup("menu.context.scene.root", "", UIInputPath{500u, 510u}, UIInputPath{5u, 6u}));
|
||||
|
||||
EXPECT_TRUE(result.changed);
|
||||
EXPECT_EQ(result.openRootMenuId, "context.scene");
|
||||
EXPECT_EQ(result.openedPopupId, "menu.context.scene.root");
|
||||
EXPECT_TRUE(result.closedPopupIds.empty());
|
||||
EXPECT_TRUE(session.IsMenuOpen("context.scene"));
|
||||
EXPECT_TRUE(session.IsPopupOpen("menu.context.scene.root"));
|
||||
ASSERT_EQ(session.GetPopupOverlayModel().GetPopupCount(), 1u);
|
||||
ASSERT_EQ(session.GetPopupStates().size(), 1u);
|
||||
EXPECT_TRUE(session.GetPopupStates().front().IsRootPopup());
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuSessionTest, OpenRootMenuRepositionsPopupWhenAnchorChanges) {
|
||||
UIEditorMenuSession session = {};
|
||||
ASSERT_TRUE(session.OpenRootMenu(
|
||||
"context.scene",
|
||||
MakePopup("menu.context.scene.root", "", UIInputPath{500u, 510u}, UIInputPath{5u, 6u}))
|
||||
.changed);
|
||||
|
||||
UIPopupOverlayEntry movedPopup =
|
||||
MakePopup("menu.context.scene.root", "", UIInputPath{500u, 510u}, UIInputPath{5u, 6u});
|
||||
movedPopup.anchorRect = { 320.0f, 180.0f, 1.0f, 1.0f };
|
||||
|
||||
const auto result = session.OpenRootMenu("context.scene", movedPopup);
|
||||
|
||||
EXPECT_TRUE(result.changed);
|
||||
EXPECT_EQ(result.openRootMenuId, "context.scene");
|
||||
EXPECT_EQ(result.openedPopupId, "menu.context.scene.root");
|
||||
ASSERT_NE(session.GetPopupOverlayModel().GetRootPopup(), nullptr);
|
||||
EXPECT_FLOAT_EQ(session.GetPopupOverlayModel().GetRootPopup()->anchorRect.x, 320.0f);
|
||||
EXPECT_FLOAT_EQ(session.GetPopupOverlayModel().GetRootPopup()->anchorRect.y, 180.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuSessionTest, HoverMenuBarRootReplacesOpenRootAndClearsSubmenuPath) {
|
||||
UIEditorMenuSession session = {};
|
||||
ASSERT_TRUE(session.OpenMenuBarRoot(
|
||||
"file",
|
||||
MakePopup("menu.file.root", "", UIInputPath{100u, 110u}, UIInputPath{1u, 2u}))
|
||||
.changed);
|
||||
ASSERT_TRUE(session.HoverSubmenu(
|
||||
"layout",
|
||||
MakePopup(
|
||||
"menu.file.layout",
|
||||
"menu.file.root",
|
||||
UIInputPath{200u, 210u},
|
||||
UIInputPath{100u, 110u, 120u},
|
||||
UIPopupPlacement::RightStart))
|
||||
.changed);
|
||||
|
||||
const auto result = session.HoverMenuBarRoot(
|
||||
"window",
|
||||
MakePopup("menu.window.root", "", UIInputPath{300u, 310u}, UIInputPath{3u, 4u}));
|
||||
|
||||
EXPECT_TRUE(result.changed);
|
||||
EXPECT_EQ(result.openRootMenuId, "window");
|
||||
EXPECT_EQ(result.dismissReason, UIPopupDismissReason::Programmatic);
|
||||
ExpectClosedIds(result.closedPopupIds, {"menu.file.root", "menu.file.layout"});
|
||||
EXPECT_TRUE(session.IsMenuOpen("window"));
|
||||
EXPECT_FALSE(session.IsMenuOpen("file"));
|
||||
EXPECT_TRUE(session.GetOpenSubmenuItemIds().empty());
|
||||
ASSERT_EQ(session.GetPopupOverlayModel().GetPopupCount(), 1u);
|
||||
EXPECT_EQ(session.GetPopupOverlayModel().GetRootPopup()->popupId, "menu.window.root");
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuSessionTest, HoverSubmenuSwitchesSiblingBranchAndTruncatesDescendants) {
|
||||
UIEditorMenuSession session = {};
|
||||
ASSERT_TRUE(session.OpenMenuBarRoot(
|
||||
"file",
|
||||
MakePopup("menu.file.root", "", UIInputPath{100u, 110u}, UIInputPath{1u, 2u}))
|
||||
.changed);
|
||||
ASSERT_TRUE(session.HoverSubmenu(
|
||||
"layout",
|
||||
MakePopup(
|
||||
"menu.file.layout",
|
||||
"menu.file.root",
|
||||
UIInputPath{200u, 210u},
|
||||
UIInputPath{100u, 110u, 120u},
|
||||
UIPopupPlacement::RightStart))
|
||||
.changed);
|
||||
ASSERT_TRUE(session.HoverSubmenu(
|
||||
"advanced",
|
||||
MakePopup(
|
||||
"menu.file.advanced",
|
||||
"menu.file.layout",
|
||||
UIInputPath{300u, 310u},
|
||||
UIInputPath{200u, 210u, 220u},
|
||||
UIPopupPlacement::RightStart))
|
||||
.changed);
|
||||
|
||||
const auto result = session.HoverSubmenu(
|
||||
"export",
|
||||
MakePopup(
|
||||
"menu.file.export",
|
||||
"menu.file.root",
|
||||
UIInputPath{400u, 410u},
|
||||
UIInputPath{100u, 110u, 130u},
|
||||
UIPopupPlacement::RightStart));
|
||||
|
||||
EXPECT_TRUE(result.changed);
|
||||
ExpectClosedIds(result.closedPopupIds, {"menu.file.layout", "menu.file.advanced"});
|
||||
EXPECT_EQ(result.openedPopupId, "menu.file.export");
|
||||
ASSERT_EQ(session.GetPopupOverlayModel().GetPopupCount(), 2u);
|
||||
ExpectOpenSubmenuIds(session, {"export"});
|
||||
ASSERT_NE(session.FindPopupState("menu.file.export"), nullptr);
|
||||
EXPECT_EQ(session.FindPopupState("menu.file.export")->itemId, "export");
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuSessionTest, HoveringAlreadyOpenSubmenuKeepsExistingDescendants) {
|
||||
UIEditorMenuSession session = {};
|
||||
ASSERT_TRUE(session.OpenMenuBarRoot(
|
||||
"file",
|
||||
MakePopup("menu.file.root", "", UIInputPath{100u, 110u}, UIInputPath{1u, 2u}))
|
||||
.changed);
|
||||
ASSERT_TRUE(session.HoverSubmenu(
|
||||
"layout",
|
||||
MakePopup(
|
||||
"menu.file.layout",
|
||||
"menu.file.root",
|
||||
UIInputPath{200u, 210u},
|
||||
UIInputPath{100u, 110u, 120u},
|
||||
UIPopupPlacement::RightStart))
|
||||
.changed);
|
||||
ASSERT_TRUE(session.HoverSubmenu(
|
||||
"advanced",
|
||||
MakePopup(
|
||||
"menu.file.advanced",
|
||||
"menu.file.layout",
|
||||
UIInputPath{300u, 310u},
|
||||
UIInputPath{200u, 210u, 220u},
|
||||
UIPopupPlacement::RightStart))
|
||||
.changed);
|
||||
|
||||
const auto result = session.HoverSubmenu(
|
||||
"layout",
|
||||
MakePopup(
|
||||
"menu.file.layout",
|
||||
"menu.file.root",
|
||||
UIInputPath{200u, 210u},
|
||||
UIInputPath{100u, 110u, 120u},
|
||||
UIPopupPlacement::RightStart));
|
||||
|
||||
EXPECT_FALSE(result.changed);
|
||||
EXPECT_EQ(result.openRootMenuId, "file");
|
||||
ASSERT_EQ(session.GetPopupOverlayModel().GetPopupCount(), 3u);
|
||||
ExpectOpenSubmenuIds(session, {"layout", "advanced"});
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuSessionTest, PointerDismissInsideRootClosesOnlyDescendantSubmenus) {
|
||||
UIEditorMenuSession session = {};
|
||||
ASSERT_TRUE(session.OpenMenuBarRoot(
|
||||
"file",
|
||||
MakePopup("menu.file.root", "", UIInputPath{100u, 110u}, UIInputPath{1u, 2u}))
|
||||
.changed);
|
||||
ASSERT_TRUE(session.HoverSubmenu(
|
||||
"layout",
|
||||
MakePopup(
|
||||
"menu.file.layout",
|
||||
"menu.file.root",
|
||||
UIInputPath{200u, 210u},
|
||||
UIInputPath{100u, 110u, 120u},
|
||||
UIPopupPlacement::RightStart))
|
||||
.changed);
|
||||
|
||||
const auto result = session.DismissFromPointerDown(UIInputPath{100u, 110u, 130u});
|
||||
|
||||
EXPECT_TRUE(result.changed);
|
||||
EXPECT_EQ(result.dismissReason, UIPopupDismissReason::PointerOutside);
|
||||
ExpectClosedIds(result.closedPopupIds, {"menu.file.layout"});
|
||||
EXPECT_EQ(result.openRootMenuId, "file");
|
||||
EXPECT_TRUE(session.IsMenuOpen("file"));
|
||||
EXPECT_TRUE(session.GetOpenSubmenuItemIds().empty());
|
||||
ASSERT_EQ(session.GetPopupOverlayModel().GetPopupCount(), 1u);
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuSessionTest, PointerDismissOutsideAllClosesEntireMenuChain) {
|
||||
UIEditorMenuSession session = {};
|
||||
ASSERT_TRUE(session.OpenMenuBarRoot(
|
||||
"file",
|
||||
MakePopup("menu.file.root", "", UIInputPath{100u, 110u}, UIInputPath{1u, 2u}))
|
||||
.changed);
|
||||
ASSERT_TRUE(session.HoverSubmenu(
|
||||
"layout",
|
||||
MakePopup(
|
||||
"menu.file.layout",
|
||||
"menu.file.root",
|
||||
UIInputPath{200u, 210u},
|
||||
UIInputPath{100u, 110u, 120u},
|
||||
UIPopupPlacement::RightStart))
|
||||
.changed);
|
||||
|
||||
const auto result = session.DismissFromPointerDown(UIInputPath{999u, 1000u});
|
||||
|
||||
EXPECT_TRUE(result.changed);
|
||||
EXPECT_EQ(result.dismissReason, UIPopupDismissReason::PointerOutside);
|
||||
ExpectClosedIds(result.closedPopupIds, {"menu.file.root", "menu.file.layout"});
|
||||
EXPECT_FALSE(result.HasOpenMenu());
|
||||
EXPECT_FALSE(session.HasOpenMenu());
|
||||
EXPECT_EQ(session.GetPopupOverlayModel().GetPopupCount(), 0u);
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuSessionTest, EscapeDismissClosesTopmostPopupBeforeRoot) {
|
||||
UIEditorMenuSession session = {};
|
||||
ASSERT_TRUE(session.OpenMenuBarRoot(
|
||||
"file",
|
||||
MakePopup("menu.file.root", "", UIInputPath{100u, 110u}, UIInputPath{1u, 2u}))
|
||||
.changed);
|
||||
ASSERT_TRUE(session.HoverSubmenu(
|
||||
"layout",
|
||||
MakePopup(
|
||||
"menu.file.layout",
|
||||
"menu.file.root",
|
||||
UIInputPath{200u, 210u},
|
||||
UIInputPath{100u, 110u, 120u},
|
||||
UIPopupPlacement::RightStart))
|
||||
.changed);
|
||||
|
||||
auto result = session.DismissFromEscape();
|
||||
|
||||
EXPECT_TRUE(result.changed);
|
||||
EXPECT_EQ(result.dismissReason, UIPopupDismissReason::EscapeKey);
|
||||
ExpectClosedIds(result.closedPopupIds, {"menu.file.layout"});
|
||||
EXPECT_EQ(result.openRootMenuId, "file");
|
||||
ASSERT_EQ(session.GetPopupOverlayModel().GetPopupCount(), 1u);
|
||||
|
||||
result = session.DismissFromEscape();
|
||||
|
||||
EXPECT_TRUE(result.changed);
|
||||
EXPECT_EQ(result.dismissReason, UIPopupDismissReason::EscapeKey);
|
||||
ExpectClosedIds(result.closedPopupIds, {"menu.file.root"});
|
||||
EXPECT_FALSE(session.HasOpenMenu());
|
||||
EXPECT_EQ(session.GetPopupOverlayModel().GetPopupCount(), 0u);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorNumberField.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorNumberFieldLayout;
|
||||
using XCEngine::UI::Editor::Widgets::FormatUIEditorNumberFieldValue;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorNumberField;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldSpec;
|
||||
|
||||
TEST(UIEditorNumberFieldTest, FormatSupportsIntegerAndFloatMode) {
|
||||
UIEditorNumberFieldSpec integerSpec = { "queue", "Queue", 7.0, 1.0, 0.0, 10.0, true, false };
|
||||
UIEditorNumberFieldSpec floatSpec = { "scale", "Scale", 1.25, 0.25, 0.0, 2.0, false, false };
|
||||
|
||||
EXPECT_EQ(FormatUIEditorNumberFieldValue(integerSpec), "7");
|
||||
EXPECT_EQ(FormatUIEditorNumberFieldValue(floatSpec), "1.25");
|
||||
}
|
||||
|
||||
TEST(UIEditorNumberFieldTest, LayoutBuildsValueRectWithoutStepperButtons) {
|
||||
UIEditorNumberFieldSpec spec = { "queue", "Queue", 7.0, 1.0, 0.0, 10.0, true, false };
|
||||
const auto layout = BuildUIEditorNumberFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
|
||||
|
||||
EXPECT_GT(layout.labelRect.width, 0.0f);
|
||||
EXPECT_GT(layout.controlRect.width, 0.0f);
|
||||
EXPECT_GT(layout.valueRect.width, 0.0f);
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.width, layout.valueRect.width);
|
||||
}
|
||||
|
||||
TEST(UIEditorNumberFieldTest, HitTestResolvesValueBoxAndRow) {
|
||||
UIEditorNumberFieldSpec spec = { "queue", "Queue", 7.0, 1.0, 0.0, 10.0, true, false };
|
||||
const auto layout = BuildUIEditorNumberFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
|
||||
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorNumberField(layout, UIPoint(layout.valueRect.x + 4.0f, layout.valueRect.y + 4.0f)).kind,
|
||||
UIEditorNumberFieldHitTargetKind::ValueBox);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorNumberField(layout, UIPoint(layout.labelRect.x + 4.0f, layout.labelRect.y + 4.0f)).kind,
|
||||
UIEditorNumberFieldHitTargetKind::Row);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,154 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorNumberFieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::UIEditorNumberFieldInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorNumberFieldInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldSpec;
|
||||
|
||||
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKey(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeCharacter(char character) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::Character;
|
||||
event.character = static_cast<std::uint32_t>(character);
|
||||
return event;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorNumberFieldInteractionTest, ClickValueBoxStartsEditing) {
|
||||
UIEditorNumberFieldSpec spec = { "queue", "Queue", 2.0, 1.0, 0.0, 5.0, true, false };
|
||||
UIEditorNumberFieldInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorNumberFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{});
|
||||
|
||||
frame = UpdateUIEditorNumberFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
frame.layout.valueRect.x + 2.0f,
|
||||
frame.layout.valueRect.y + 2.0f,
|
||||
UIPointerButton::Left),
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonUp,
|
||||
frame.layout.valueRect.x + 2.0f,
|
||||
frame.layout.valueRect.y + 2.0f,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.numberFieldState.editing);
|
||||
EXPECT_DOUBLE_EQ(spec.value, 2.0);
|
||||
}
|
||||
|
||||
TEST(UIEditorNumberFieldInteractionTest, KeyboardStepAndBoundsWorkWhenFocused) {
|
||||
UIEditorNumberFieldSpec spec = { "queue", "Queue", 2.0, 1.0, 0.0, 5.0, true, false };
|
||||
UIEditorNumberFieldInteractionState state = {};
|
||||
state.numberFieldState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorNumberFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Right) });
|
||||
EXPECT_TRUE(frame.result.stepApplied);
|
||||
EXPECT_DOUBLE_EQ(spec.value, 3.0);
|
||||
|
||||
frame = UpdateUIEditorNumberFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Home) });
|
||||
EXPECT_TRUE(frame.result.stepApplied);
|
||||
EXPECT_DOUBLE_EQ(spec.value, 0.0);
|
||||
|
||||
frame = UpdateUIEditorNumberFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::End) });
|
||||
EXPECT_TRUE(frame.result.stepApplied);
|
||||
EXPECT_DOUBLE_EQ(spec.value, 5.0);
|
||||
}
|
||||
|
||||
TEST(UIEditorNumberFieldInteractionTest, EnterStartsEditingAndCharacterInputCommitsValue) {
|
||||
UIEditorNumberFieldSpec spec = { "count", "Count", 7.0, 1.0, 0.0, 1000.0, true, false };
|
||||
UIEditorNumberFieldInteractionState state = {};
|
||||
state.numberFieldState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorNumberFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Enter) });
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.numberFieldState.editing);
|
||||
|
||||
frame = UpdateUIEditorNumberFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeCharacter('4'), MakeCharacter('2'), MakeKey(KeyCode::Enter) });
|
||||
|
||||
EXPECT_TRUE(frame.result.editCommitted);
|
||||
EXPECT_TRUE(frame.result.valueChanged);
|
||||
EXPECT_FALSE(state.numberFieldState.editing);
|
||||
EXPECT_EQ(frame.result.committedText, "742");
|
||||
EXPECT_DOUBLE_EQ(spec.value, 742.0);
|
||||
}
|
||||
|
||||
TEST(UIEditorNumberFieldInteractionTest, CharacterInputCanStartEditingAndEscapeCancels) {
|
||||
UIEditorNumberFieldSpec spec = { "count", "Count", 7.0, 1.0, 0.0, 200.0, true, false };
|
||||
UIEditorNumberFieldInteractionState state = {};
|
||||
state.numberFieldState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorNumberFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeCharacter('9') });
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.numberFieldState.editing);
|
||||
EXPECT_EQ(state.numberFieldState.displayText, "9");
|
||||
|
||||
frame = UpdateUIEditorNumberFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Escape) });
|
||||
EXPECT_TRUE(frame.result.editCanceled);
|
||||
EXPECT_FALSE(state.numberFieldState.editing);
|
||||
EXPECT_DOUBLE_EQ(spec.value, 7.0);
|
||||
EXPECT_EQ(state.numberFieldState.displayText, "7");
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEditor/Fields/UIEditorObjectField.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorObjectField;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorObjectFieldLayout;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorObjectField;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorObjectFieldDisplayText;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorObjectFieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorObjectFieldSpec;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorObjectFieldState;
|
||||
|
||||
TEST(UIEditorObjectFieldTest, DisplayTextFallsBackForEmptyAndUnnamedObjects) {
|
||||
UIEditorObjectFieldSpec spec = {};
|
||||
spec.emptyText = "None (Object)";
|
||||
|
||||
EXPECT_EQ(ResolveUIEditorObjectFieldDisplayText(spec), "None (Object)");
|
||||
|
||||
spec.hasValue = true;
|
||||
spec.objectName.clear();
|
||||
EXPECT_EQ(ResolveUIEditorObjectFieldDisplayText(spec), "(unnamed)");
|
||||
|
||||
spec.objectName = "Main Camera";
|
||||
EXPECT_EQ(ResolveUIEditorObjectFieldDisplayText(spec), "Main Camera");
|
||||
}
|
||||
|
||||
TEST(UIEditorObjectFieldTest, LayoutReservesClearAndPickerButtonsWhenValueExists) {
|
||||
UIEditorObjectFieldSpec spec = {};
|
||||
spec.fieldId = "target";
|
||||
spec.label = "Target";
|
||||
spec.hasValue = true;
|
||||
spec.objectName = "Main Camera";
|
||||
spec.objectTypeName = "Camera";
|
||||
|
||||
const auto layout = BuildUIEditorObjectFieldLayout(
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
spec);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.valueRect.x, 236.0f);
|
||||
EXPECT_GT(layout.clearButtonRect.width, 0.0f);
|
||||
EXPECT_GT(layout.pickerButtonRect.width, 0.0f);
|
||||
EXPECT_GT(layout.typeRect.width, 0.0f);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorObjectField(
|
||||
layout,
|
||||
UIPoint(layout.clearButtonRect.x + 2.0f, layout.clearButtonRect.y + 2.0f)).kind,
|
||||
UIEditorObjectFieldHitTargetKind::ClearButton);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorObjectField(
|
||||
layout,
|
||||
UIPoint(layout.pickerButtonRect.x + 2.0f, layout.pickerButtonRect.y + 2.0f)).kind,
|
||||
UIEditorObjectFieldHitTargetKind::PickerButton);
|
||||
}
|
||||
|
||||
TEST(UIEditorObjectFieldTest, LayoutWithoutValueUsesEmptyDisplayAndOmitsClearButton) {
|
||||
UIEditorObjectFieldSpec spec = {};
|
||||
spec.fieldId = "target";
|
||||
spec.label = "Target";
|
||||
spec.hasValue = false;
|
||||
spec.emptyText = "None (GameObject)";
|
||||
|
||||
const auto layout = BuildUIEditorObjectFieldLayout(
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
spec);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.clearButtonRect.width, 0.0f);
|
||||
EXPECT_GT(layout.pickerButtonRect.width, 0.0f);
|
||||
EXPECT_FLOAT_EQ(layout.typeRect.width, 0.0f);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorObjectField(
|
||||
layout,
|
||||
UIPoint(layout.valueRect.x + 4.0f, layout.valueRect.y + 4.0f)).kind,
|
||||
UIEditorObjectFieldHitTargetKind::ValueBox);
|
||||
}
|
||||
|
||||
TEST(UIEditorObjectFieldTest, DrawEmitsLabelValueTypeAndButtonGlyphs) {
|
||||
UIEditorObjectFieldSpec spec = {};
|
||||
spec.fieldId = "target";
|
||||
spec.label = "Target";
|
||||
spec.hasValue = true;
|
||||
spec.objectName = "Main Camera";
|
||||
spec.objectTypeName = "Camera";
|
||||
|
||||
UIEditorObjectFieldState state = {};
|
||||
XCEngine::UI::UIDrawData drawData = {};
|
||||
auto& drawList = drawData.EmplaceDrawList("ObjectField");
|
||||
AppendUIEditorObjectField(
|
||||
drawList,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
spec,
|
||||
state);
|
||||
|
||||
bool hasLabel = false;
|
||||
bool hasValue = false;
|
||||
bool hasType = false;
|
||||
bool hasClearGlyph = false;
|
||||
bool hasPickerGlyph = false;
|
||||
bool hasOutline = false;
|
||||
for (const auto& command : drawList.GetCommands()) {
|
||||
hasLabel = hasLabel || command.text == "Target";
|
||||
hasValue = hasValue || command.text == "Main Camera";
|
||||
hasType = hasType || command.text == "Camera";
|
||||
hasClearGlyph = hasClearGlyph || command.text == "X";
|
||||
hasPickerGlyph = hasPickerGlyph || command.text == "o";
|
||||
hasOutline = hasOutline || command.type == UIDrawCommandType::RectOutline;
|
||||
}
|
||||
|
||||
EXPECT_TRUE(hasLabel);
|
||||
EXPECT_TRUE(hasValue);
|
||||
EXPECT_TRUE(hasType);
|
||||
EXPECT_TRUE(hasClearGlyph);
|
||||
EXPECT_TRUE(hasPickerGlyph);
|
||||
EXPECT_TRUE(hasOutline);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,157 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorObjectFieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::UIEditorObjectFieldInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorObjectFieldInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorObjectFieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorObjectFieldSpec;
|
||||
|
||||
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKey(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIEditorObjectFieldSpec BuildSpec() {
|
||||
UIEditorObjectFieldSpec spec = {};
|
||||
spec.fieldId = "target";
|
||||
spec.label = "Target";
|
||||
spec.hasValue = true;
|
||||
spec.objectName = "Main Camera";
|
||||
spec.objectTypeName = "Camera";
|
||||
return spec;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorObjectFieldInteractionTest, ClickValueBoxRequestsActivateAndFocus) {
|
||||
UIEditorObjectFieldSpec spec = BuildSpec();
|
||||
UIEditorObjectFieldInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorObjectFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
{});
|
||||
|
||||
frame = UpdateUIEditorObjectFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
{
|
||||
MakePointer(UIInputEventType::PointerButtonDown, frame.layout.valueRect.x + 4.0f, frame.layout.valueRect.y + 4.0f, UIPointerButton::Left),
|
||||
MakePointer(UIInputEventType::PointerButtonUp, frame.layout.valueRect.x + 4.0f, frame.layout.valueRect.y + 4.0f, UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.activateRequested);
|
||||
EXPECT_TRUE(state.fieldState.focused);
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorObjectFieldHitTargetKind::ValueBox);
|
||||
}
|
||||
|
||||
TEST(UIEditorObjectFieldInteractionTest, ClickClearButtonRequestsClear) {
|
||||
UIEditorObjectFieldSpec spec = BuildSpec();
|
||||
UIEditorObjectFieldInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorObjectFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
{});
|
||||
|
||||
frame = UpdateUIEditorObjectFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
{
|
||||
MakePointer(UIInputEventType::PointerButtonDown, frame.layout.clearButtonRect.x + 2.0f, frame.layout.clearButtonRect.y + 2.0f, UIPointerButton::Left),
|
||||
MakePointer(UIInputEventType::PointerButtonUp, frame.layout.clearButtonRect.x + 2.0f, frame.layout.clearButtonRect.y + 2.0f, UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.clearRequested);
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorObjectFieldHitTargetKind::ClearButton);
|
||||
}
|
||||
|
||||
TEST(UIEditorObjectFieldInteractionTest, KeyboardActivateAndClearFollowFocus) {
|
||||
UIEditorObjectFieldSpec spec = BuildSpec();
|
||||
UIEditorObjectFieldInteractionState state = {};
|
||||
state.fieldState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorObjectFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
{ MakeKey(KeyCode::Enter) });
|
||||
EXPECT_TRUE(frame.result.activateRequested);
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
|
||||
frame = UpdateUIEditorObjectFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
{ MakeKey(KeyCode::Delete) });
|
||||
EXPECT_TRUE(frame.result.clearRequested);
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
}
|
||||
|
||||
TEST(UIEditorObjectFieldInteractionTest, ReadOnlyFieldSuppressesActivateAndClear) {
|
||||
UIEditorObjectFieldSpec spec = BuildSpec();
|
||||
spec.readOnly = true;
|
||||
UIEditorObjectFieldInteractionState state = {};
|
||||
state.fieldState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorObjectFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
{ MakeKey(KeyCode::Space), MakeKey(KeyCode::Delete) });
|
||||
|
||||
EXPECT_FALSE(frame.result.activateRequested);
|
||||
EXPECT_FALSE(frame.result.clearRequested);
|
||||
}
|
||||
|
||||
TEST(UIEditorObjectFieldInteractionTest, FocusLostClearsHoverAndActiveState) {
|
||||
UIEditorObjectFieldSpec spec = BuildSpec();
|
||||
UIEditorObjectFieldInteractionState state = {};
|
||||
state.fieldState.focused = true;
|
||||
state.fieldState.activeTarget = UIEditorObjectFieldHitTargetKind::PickerButton;
|
||||
state.fieldState.hoveredTarget = UIEditorObjectFieldHitTargetKind::ValueBox;
|
||||
state.hasPointerPosition = true;
|
||||
state.pointerPosition = UIPoint(250.0f, 10.0f);
|
||||
|
||||
UIInputEvent focusLost = {};
|
||||
focusLost.type = UIInputEventType::FocusLost;
|
||||
|
||||
const auto frame = UpdateUIEditorObjectFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
|
||||
{ focusLost });
|
||||
|
||||
EXPECT_TRUE(frame.result.focusChanged);
|
||||
EXPECT_FALSE(state.fieldState.focused);
|
||||
EXPECT_EQ(state.fieldState.activeTarget, UIEditorObjectFieldHitTargetKind::None);
|
||||
EXPECT_EQ(state.fieldState.hoveredTarget, UIEditorObjectFieldHitTargetKind::None);
|
||||
EXPECT_FALSE(state.hasPointerPosition);
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Panels/UIEditorPanelContentHost.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceSession.h>
|
||||
#include <XCEditor/Docking/UIEditorDockHost.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::CollectMountedUIEditorPanelContentHostPanelIds;
|
||||
using XCEngine::UI::Editor::FindUIEditorPanelContentHostMountRequest;
|
||||
using XCEngine::UI::Editor::FindUIEditorPanelContentHostPanelState;
|
||||
using XCEngine::UI::Editor::GetUIEditorPanelContentHostEventKindName;
|
||||
using XCEngine::UI::Editor::ResolveUIEditorPanelContentHostRequest;
|
||||
using XCEngine::UI::Editor::UIEditorPanelContentHostState;
|
||||
using XCEngine::UI::Editor::UIEditorPanelPresentationKind;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorPanelContentHost;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorDockHostLayout;
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", UIEditorPanelPresentationKind::HostedContent, false, true, true },
|
||||
{ "doc-b", "Document B", UIEditorPanelPresentationKind::HostedContent, false, true, true },
|
||||
{ "console", "Console", UIEditorPanelPresentationKind::Placeholder, true, true, true },
|
||||
{ "inspector", "Inspector", UIEditorPanelPresentationKind::HostedContent, false, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.68f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"documents",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A"),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B"),
|
||||
BuildUIEditorWorkspacePanel("console-node", "console", "Console", true)
|
||||
},
|
||||
1u),
|
||||
BuildUIEditorWorkspaceSingleTabStack("inspector-node", "inspector", "Inspector"));
|
||||
workspace.activePanelId = "doc-b";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
std::vector<std::string> FormatEvents(
|
||||
const std::vector<XCEngine::UI::Editor::UIEditorPanelContentHostEvent>& events) {
|
||||
std::vector<std::string> formatted = {};
|
||||
formatted.reserve(events.size());
|
||||
for (const auto& event : events) {
|
||||
formatted.push_back(
|
||||
std::string(GetUIEditorPanelContentHostEventKindName(event.kind)) + ":" + event.panelId);
|
||||
}
|
||||
return formatted;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorPanelContentHostTest, ResolveRequestMountsSelectedHostedTabAndStandaloneHostedPanel) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
const auto layout = BuildUIEditorDockHostLayout(
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session);
|
||||
|
||||
const auto request = ResolveUIEditorPanelContentHostRequest(
|
||||
layout,
|
||||
registry);
|
||||
|
||||
ASSERT_EQ(request.mountRequests.size(), 2u);
|
||||
EXPECT_NE(FindUIEditorPanelContentHostMountRequest(request, "doc-b"), nullptr);
|
||||
EXPECT_NE(FindUIEditorPanelContentHostMountRequest(request, "inspector"), nullptr);
|
||||
EXPECT_EQ(FindUIEditorPanelContentHostMountRequest(request, "doc-a"), nullptr);
|
||||
EXPECT_EQ(FindUIEditorPanelContentHostMountRequest(request, "console"), nullptr);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelContentHostTest, UpdateEmitsMountedAndUnmountedWhenHostedTabSelectionChanges) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
UIEditorPanelContentHostState state = {};
|
||||
auto layout = BuildUIEditorDockHostLayout(
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session);
|
||||
auto request = ResolveUIEditorPanelContentHostRequest(layout, registry);
|
||||
const auto initialFrame = UpdateUIEditorPanelContentHost(
|
||||
state,
|
||||
request,
|
||||
registry);
|
||||
EXPECT_EQ(
|
||||
FormatEvents(initialFrame.events),
|
||||
std::vector<std::string>({ "Mounted:doc-b", "Mounted:inspector" }));
|
||||
EXPECT_EQ(
|
||||
CollectMountedUIEditorPanelContentHostPanelIds(initialFrame),
|
||||
std::vector<std::string>({ "doc-b", "inspector" }));
|
||||
|
||||
workspace.root.children[0].selectedTabIndex = 0u;
|
||||
workspace.activePanelId = "doc-a";
|
||||
layout = BuildUIEditorDockHostLayout(
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session);
|
||||
request = ResolveUIEditorPanelContentHostRequest(layout, registry);
|
||||
const auto switchedFrame = UpdateUIEditorPanelContentHost(
|
||||
state,
|
||||
request,
|
||||
registry);
|
||||
|
||||
EXPECT_EQ(
|
||||
FormatEvents(switchedFrame.events),
|
||||
std::vector<std::string>({ "Mounted:doc-a", "Unmounted:doc-b" }));
|
||||
|
||||
const auto* docAState = FindUIEditorPanelContentHostPanelState(switchedFrame, "doc-a");
|
||||
const auto* docBState = FindUIEditorPanelContentHostPanelState(switchedFrame, "doc-b");
|
||||
ASSERT_NE(docAState, nullptr);
|
||||
ASSERT_NE(docBState, nullptr);
|
||||
EXPECT_TRUE(docAState->mounted);
|
||||
EXPECT_FALSE(docBState->mounted);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelContentHostTest, UpdateEmitsBoundsChangedForMountedHostedPanelsWhenLayoutChanges) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
UIEditorPanelContentHostState state = {};
|
||||
auto layout = BuildUIEditorDockHostLayout(
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session);
|
||||
auto request = ResolveUIEditorPanelContentHostRequest(layout, registry);
|
||||
UpdateUIEditorPanelContentHost(state, request, registry);
|
||||
|
||||
layout = BuildUIEditorDockHostLayout(
|
||||
UIRect(0.0f, 0.0f, 1440.0f, 800.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session);
|
||||
request = ResolveUIEditorPanelContentHostRequest(layout, registry);
|
||||
const auto resizedFrame = UpdateUIEditorPanelContentHost(
|
||||
state,
|
||||
request,
|
||||
registry);
|
||||
|
||||
EXPECT_EQ(
|
||||
FormatEvents(resizedFrame.events),
|
||||
std::vector<std::string>({ "BoundsChanged:doc-b", "BoundsChanged:inspector" }));
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEditor/Panels/UIEditorPanelFrame.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIColor;
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorPanelFrameBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorPanelFrameForeground;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorPanelFrameLayout;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorPanelFrame;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorPanelFrameAction;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorPanelFrameBorderColor;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorPanelFrameBorderThickness;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPanelFrameAction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPanelFrameHitTarget;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPanelFrameLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPanelFrameMetrics;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPanelFramePalette;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPanelFrameState;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPanelFrameText;
|
||||
|
||||
void ExpectColorEq(
|
||||
const UIColor& actual,
|
||||
const UIColor& expected) {
|
||||
EXPECT_FLOAT_EQ(actual.r, expected.r);
|
||||
EXPECT_FLOAT_EQ(actual.g, expected.g);
|
||||
EXPECT_FLOAT_EQ(actual.b, expected.b);
|
||||
EXPECT_FLOAT_EQ(actual.a, expected.a);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelFrameTest, LayoutBuildsHeaderBodyFooterAndActionRectsFromState) {
|
||||
UIEditorPanelFrameState state = {};
|
||||
state.active = true;
|
||||
state.pinned = true;
|
||||
state.closable = true;
|
||||
state.pinnable = true;
|
||||
state.showFooter = true;
|
||||
|
||||
const UIEditorPanelFrameMetrics metrics = {};
|
||||
const UIEditorPanelFrameLayout layout =
|
||||
BuildUIEditorPanelFrameLayout(UIRect(10.0f, 20.0f, 300.0f, 200.0f), state, metrics);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.headerRect.x, 10.0f);
|
||||
EXPECT_FLOAT_EQ(layout.headerRect.y, 20.0f);
|
||||
EXPECT_FLOAT_EQ(layout.headerRect.width, 300.0f);
|
||||
EXPECT_FLOAT_EQ(layout.headerRect.height, metrics.headerHeight);
|
||||
|
||||
EXPECT_TRUE(layout.hasFooter);
|
||||
EXPECT_FLOAT_EQ(layout.footerRect.y, 20.0f + 200.0f - metrics.footerHeight);
|
||||
EXPECT_FLOAT_EQ(layout.footerRect.height, metrics.footerHeight);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.bodyRect.x, 10.0f + metrics.contentPadding);
|
||||
EXPECT_FLOAT_EQ(layout.bodyRect.y, 20.0f + metrics.headerHeight + metrics.contentPadding);
|
||||
EXPECT_FLOAT_EQ(layout.bodyRect.width, 300.0f - metrics.contentPadding * 2.0f);
|
||||
EXPECT_FLOAT_EQ(
|
||||
layout.bodyRect.height,
|
||||
200.0f - metrics.headerHeight - metrics.footerHeight - metrics.contentPadding * 2.0f);
|
||||
|
||||
EXPECT_TRUE(layout.showPinButton);
|
||||
EXPECT_TRUE(layout.showCloseButton);
|
||||
const float buttonExtent =
|
||||
(std::min)(metrics.actionButtonExtent, metrics.headerHeight - metrics.actionInsetX * 2.0f);
|
||||
const float buttonY = 20.0f + (metrics.headerHeight - buttonExtent) * 0.5f;
|
||||
EXPECT_FLOAT_EQ(layout.closeButtonRect.width, buttonExtent);
|
||||
EXPECT_FLOAT_EQ(layout.closeButtonRect.x, 10.0f + 300.0f - metrics.actionInsetX - buttonExtent);
|
||||
EXPECT_FLOAT_EQ(layout.closeButtonRect.y, buttonY);
|
||||
EXPECT_FLOAT_EQ(
|
||||
layout.pinButtonRect.x,
|
||||
layout.closeButtonRect.x - metrics.actionGap - buttonExtent);
|
||||
EXPECT_FLOAT_EQ(layout.pinButtonRect.y, buttonY);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelFrameTest, BorderPolicyUsesFocusBeforeActiveBeforeHover) {
|
||||
const UIEditorPanelFramePalette palette = {};
|
||||
const UIEditorPanelFrameMetrics metrics = {};
|
||||
|
||||
UIEditorPanelFrameState hoveredOnly = {};
|
||||
hoveredOnly.hovered = true;
|
||||
UIEditorPanelFrameState activeOnly = hoveredOnly;
|
||||
activeOnly.active = true;
|
||||
UIEditorPanelFrameState focusedState = activeOnly;
|
||||
focusedState.focused = true;
|
||||
|
||||
ExpectColorEq(
|
||||
ResolveUIEditorPanelFrameBorderColor(hoveredOnly, palette),
|
||||
palette.hoveredBorderColor);
|
||||
ExpectColorEq(
|
||||
ResolveUIEditorPanelFrameBorderColor(activeOnly, palette),
|
||||
palette.activeBorderColor);
|
||||
ExpectColorEq(
|
||||
ResolveUIEditorPanelFrameBorderColor(focusedState, palette),
|
||||
palette.focusedBorderColor);
|
||||
|
||||
EXPECT_FLOAT_EQ(
|
||||
ResolveUIEditorPanelFrameBorderThickness(UIEditorPanelFrameState{}, metrics),
|
||||
metrics.baseBorderThickness);
|
||||
EXPECT_FLOAT_EQ(
|
||||
ResolveUIEditorPanelFrameBorderThickness(hoveredOnly, metrics),
|
||||
metrics.hoveredBorderThickness);
|
||||
EXPECT_FLOAT_EQ(
|
||||
ResolveUIEditorPanelFrameBorderThickness(activeOnly, metrics),
|
||||
metrics.activeBorderThickness);
|
||||
EXPECT_FLOAT_EQ(
|
||||
ResolveUIEditorPanelFrameBorderThickness(focusedState, metrics),
|
||||
metrics.focusedBorderThickness);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelFrameTest, HitTestReturnsActionAndRegionTargetsFromLayout) {
|
||||
UIEditorPanelFrameState state = {};
|
||||
state.closable = true;
|
||||
state.pinnable = true;
|
||||
state.showFooter = true;
|
||||
|
||||
const UIEditorPanelFrameLayout layout =
|
||||
BuildUIEditorPanelFrameLayout(UIRect(10.0f, 20.0f, 300.0f, 200.0f), state);
|
||||
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorPanelFrameAction(
|
||||
layout,
|
||||
state,
|
||||
UIPoint(
|
||||
layout.pinButtonRect.x + layout.pinButtonRect.width * 0.5f,
|
||||
layout.pinButtonRect.y + layout.pinButtonRect.height * 0.5f)),
|
||||
UIEditorPanelFrameAction::Pin);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorPanelFrameAction(
|
||||
layout,
|
||||
state,
|
||||
UIPoint(
|
||||
layout.closeButtonRect.x + layout.closeButtonRect.width * 0.5f,
|
||||
layout.closeButtonRect.y + layout.closeButtonRect.height * 0.5f)),
|
||||
UIEditorPanelFrameAction::Close);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorPanelFrame(layout, state, UIPoint(40.0f, 35.0f)),
|
||||
UIEditorPanelFrameHitTarget::Header);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorPanelFrame(layout, state, UIPoint(40.0f, 90.0f)),
|
||||
UIEditorPanelFrameHitTarget::Body);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorPanelFrame(layout, state, UIPoint(40.0f, 205.0f)),
|
||||
UIEditorPanelFrameHitTarget::Footer);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelFrameTest, BackgroundAndForegroundEmitExpectedChromeAndActionCommands) {
|
||||
UIEditorPanelFrameState state = {};
|
||||
state.active = true;
|
||||
state.focused = true;
|
||||
state.pinned = true;
|
||||
state.closable = true;
|
||||
state.pinnable = true;
|
||||
state.showFooter = true;
|
||||
state.pinHovered = true;
|
||||
|
||||
const UIEditorPanelFramePalette palette = {};
|
||||
const UIEditorPanelFrameText text{
|
||||
"Inspector",
|
||||
"PanelFrame chrome contract",
|
||||
"focused | pinned | footer visible"
|
||||
};
|
||||
const UIEditorPanelFrameLayout layout =
|
||||
BuildUIEditorPanelFrameLayout(UIRect(20.0f, 30.0f, 320.0f, 220.0f), state);
|
||||
|
||||
UIDrawList background("PanelFrameBackground");
|
||||
AppendUIEditorPanelFrameBackground(background, layout, state, palette);
|
||||
|
||||
ASSERT_EQ(background.GetCommandCount(), 6u);
|
||||
const auto& backgroundCommands = background.GetCommands();
|
||||
EXPECT_EQ(backgroundCommands[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[1].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(backgroundCommands[2].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[3].type, UIDrawCommandType::Line);
|
||||
EXPECT_EQ(backgroundCommands[4].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[5].type, UIDrawCommandType::Line);
|
||||
ExpectColorEq(backgroundCommands[1].color, palette.focusedBorderColor);
|
||||
|
||||
UIDrawList foreground("PanelFrameForeground");
|
||||
AppendUIEditorPanelFrameForeground(foreground, layout, state, text, palette);
|
||||
|
||||
ASSERT_EQ(foreground.GetCommandCount(), 9u);
|
||||
const auto& foregroundCommands = foreground.GetCommands();
|
||||
EXPECT_EQ(foregroundCommands[0].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[0].text, "Inspector");
|
||||
EXPECT_EQ(foregroundCommands[1].text, "PanelFrame chrome contract");
|
||||
EXPECT_EQ(foregroundCommands[2].text, "focused | pinned | footer visible");
|
||||
EXPECT_EQ(foregroundCommands[5].text, "P");
|
||||
EXPECT_EQ(foregroundCommands[8].text, "X");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,246 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Panels/UIEditorPanelHostLifecycle.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::FindUIEditorPanelHostState;
|
||||
using XCEngine::UI::Editor::GetUIEditorPanelHostLifecycleEventKindName;
|
||||
using XCEngine::UI::Editor::TryCloseUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::TryHideUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::TryOpenUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::UIEditorPanelHostLifecycleRequest;
|
||||
using XCEngine::UI::Editor::UIEditorPanelHostLifecycleState;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorPanelHostLifecycle;
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.66f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
std::vector<std::string> FormatEvents(
|
||||
const std::vector<XCEngine::UI::Editor::UIEditorPanelHostLifecycleEvent>& events) {
|
||||
std::vector<std::string> formatted = {};
|
||||
formatted.reserve(events.size());
|
||||
for (const auto& event : events) {
|
||||
formatted.push_back(
|
||||
std::string(GetUIEditorPanelHostLifecycleEventKindName(event.kind)) + ":" + event.panelId);
|
||||
}
|
||||
return formatted;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorPanelHostLifecycleTest, InitialFrameResolvesAttachedVisibleActiveAndFocusedPanels) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
const UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
UIEditorPanelHostLifecycleState lifecycleState = {};
|
||||
const auto frame = UpdateUIEditorPanelHostLifecycle(
|
||||
lifecycleState,
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
UIEditorPanelHostLifecycleRequest{ "doc-a" });
|
||||
|
||||
ASSERT_EQ(frame.panelStates.size(), 3u);
|
||||
const auto* docA = FindUIEditorPanelHostState(frame, "doc-a");
|
||||
const auto* docB = FindUIEditorPanelHostState(frame, "doc-b");
|
||||
const auto* details = FindUIEditorPanelHostState(frame, "details");
|
||||
ASSERT_NE(docA, nullptr);
|
||||
ASSERT_NE(docB, nullptr);
|
||||
ASSERT_NE(details, nullptr);
|
||||
|
||||
EXPECT_TRUE(docA->attached);
|
||||
EXPECT_TRUE(docA->visible);
|
||||
EXPECT_TRUE(docA->active);
|
||||
EXPECT_TRUE(docA->focused);
|
||||
|
||||
EXPECT_TRUE(docB->attached);
|
||||
EXPECT_FALSE(docB->visible);
|
||||
EXPECT_FALSE(docB->active);
|
||||
EXPECT_FALSE(docB->focused);
|
||||
|
||||
EXPECT_TRUE(details->attached);
|
||||
EXPECT_TRUE(details->visible);
|
||||
EXPECT_FALSE(details->active);
|
||||
EXPECT_FALSE(details->focused);
|
||||
|
||||
const std::vector<std::string> expectedEvents = {
|
||||
"Attached:doc-a",
|
||||
"Attached:doc-b",
|
||||
"Attached:details",
|
||||
"Shown:doc-a",
|
||||
"Shown:details",
|
||||
"Activated:doc-a",
|
||||
"FocusGained:doc-a"
|
||||
};
|
||||
EXPECT_EQ(FormatEvents(frame.events), expectedEvents);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelHostLifecycleTest, HidingActiveTabKeepsPanelAttachedButEmitsVisibilityAndActivationTransitions) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
UIEditorPanelHostLifecycleState lifecycleState = {};
|
||||
UpdateUIEditorPanelHostLifecycle(
|
||||
lifecycleState,
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
UIEditorPanelHostLifecycleRequest{ "doc-a" });
|
||||
|
||||
ASSERT_TRUE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
|
||||
const auto frame = UpdateUIEditorPanelHostLifecycle(
|
||||
lifecycleState,
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
UIEditorPanelHostLifecycleRequest{ "doc-b" });
|
||||
|
||||
const auto* docA = FindUIEditorPanelHostState(frame, "doc-a");
|
||||
const auto* docB = FindUIEditorPanelHostState(frame, "doc-b");
|
||||
ASSERT_NE(docA, nullptr);
|
||||
ASSERT_NE(docB, nullptr);
|
||||
|
||||
EXPECT_TRUE(docA->attached);
|
||||
EXPECT_FALSE(docA->visible);
|
||||
EXPECT_FALSE(docA->active);
|
||||
EXPECT_FALSE(docA->focused);
|
||||
|
||||
EXPECT_TRUE(docB->attached);
|
||||
EXPECT_TRUE(docB->visible);
|
||||
EXPECT_TRUE(docB->active);
|
||||
EXPECT_TRUE(docB->focused);
|
||||
|
||||
const std::vector<std::string> expectedEvents = {
|
||||
"FocusLost:doc-a",
|
||||
"Deactivated:doc-a",
|
||||
"Hidden:doc-a",
|
||||
"Shown:doc-b",
|
||||
"Activated:doc-b",
|
||||
"FocusGained:doc-b"
|
||||
};
|
||||
EXPECT_EQ(FormatEvents(frame.events), expectedEvents);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelHostLifecycleTest, ClosingHiddenPanelEmitsDetachWithoutVisibilityTransitions) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
UIEditorPanelHostLifecycleState lifecycleState = {};
|
||||
UpdateUIEditorPanelHostLifecycle(
|
||||
lifecycleState,
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
UIEditorPanelHostLifecycleRequest{ "doc-a" });
|
||||
|
||||
ASSERT_TRUE(TryCloseUIEditorWorkspacePanel(registry, workspace, session, "doc-b"));
|
||||
const auto frame = UpdateUIEditorPanelHostLifecycle(
|
||||
lifecycleState,
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
UIEditorPanelHostLifecycleRequest{ "doc-a" });
|
||||
|
||||
const auto* docB = FindUIEditorPanelHostState(frame, "doc-b");
|
||||
ASSERT_NE(docB, nullptr);
|
||||
EXPECT_FALSE(docB->attached);
|
||||
EXPECT_FALSE(docB->visible);
|
||||
EXPECT_FALSE(docB->active);
|
||||
EXPECT_FALSE(docB->focused);
|
||||
|
||||
const std::vector<std::string> expectedEvents = {
|
||||
"Detached:doc-b"
|
||||
};
|
||||
EXPECT_EQ(FormatEvents(frame.events), expectedEvents);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelHostLifecycleTest, ClearingFocusOnlyEmitsFocusLostAndReopeningPanelReattachesIt) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
UIEditorPanelHostLifecycleState lifecycleState = {};
|
||||
UpdateUIEditorPanelHostLifecycle(
|
||||
lifecycleState,
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
UIEditorPanelHostLifecycleRequest{ "doc-a" });
|
||||
|
||||
const auto focusLostFrame = UpdateUIEditorPanelHostLifecycle(
|
||||
lifecycleState,
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
UIEditorPanelHostLifecycleRequest{});
|
||||
EXPECT_EQ(
|
||||
FormatEvents(focusLostFrame.events),
|
||||
std::vector<std::string>({ "FocusLost:doc-a" }));
|
||||
|
||||
ASSERT_TRUE(TryCloseUIEditorWorkspacePanel(registry, workspace, session, "doc-b"));
|
||||
UpdateUIEditorPanelHostLifecycle(
|
||||
lifecycleState,
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
UIEditorPanelHostLifecycleRequest{});
|
||||
|
||||
ASSERT_TRUE(TryOpenUIEditorWorkspacePanel(registry, workspace, session, "doc-b"));
|
||||
const auto reopenFrame = UpdateUIEditorPanelHostLifecycle(
|
||||
lifecycleState,
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
UIEditorPanelHostLifecycleRequest{ "doc-b" });
|
||||
|
||||
const std::vector<std::string> expectedEvents = {
|
||||
"Deactivated:doc-a",
|
||||
"Hidden:doc-a",
|
||||
"Attached:doc-b",
|
||||
"Shown:doc-b",
|
||||
"Activated:doc-b",
|
||||
"FocusGained:doc-b"
|
||||
};
|
||||
EXPECT_EQ(FormatEvents(reopenFrame.events), expectedEvents);
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Foundation/UIEditorPanelInputFilter.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Editor::FilterUIEditorPanelInputEvents;
|
||||
using XCEngine::UI::Editor::BuildUIEditorPanelInputEvents;
|
||||
using XCEngine::UI::Editor::UIEditorPanelInputFilterOptions;
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIPointerButton;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
|
||||
UIInputEvent MakePointerMove(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerMove;
|
||||
event.position = UIPoint(x, y);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerButtonDown(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonDown;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = UIPointerButton::Left;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKeyDown() {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeFocusLost() {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::FocusLost;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerLeave() {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerLeave;
|
||||
return event;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorPanelInputFilterTests, FiltersPointerByBoundsAndPreservesKeyboardAndFocusChannels) {
|
||||
const std::vector<UIInputEvent> filtered =
|
||||
FilterUIEditorPanelInputEvents(
|
||||
UIRect(0.0f, 0.0f, 100.0f, 100.0f),
|
||||
{
|
||||
MakePointerMove(20.0f, 20.0f),
|
||||
MakePointerButtonDown(140.0f, 20.0f),
|
||||
MakeKeyDown(),
|
||||
MakeFocusLost(),
|
||||
MakePointerLeave()
|
||||
},
|
||||
UIEditorPanelInputFilterOptions{
|
||||
.allowPointerInBounds = true,
|
||||
.allowPointerWhileCaptured = false,
|
||||
.allowKeyboardInput = true,
|
||||
.allowFocusEvents = true,
|
||||
.includePointerLeave = true
|
||||
});
|
||||
|
||||
ASSERT_EQ(filtered.size(), 4u);
|
||||
EXPECT_EQ(filtered[0].type, UIInputEventType::PointerMove);
|
||||
EXPECT_EQ(filtered[1].type, UIInputEventType::KeyDown);
|
||||
EXPECT_EQ(filtered[2].type, UIInputEventType::FocusLost);
|
||||
EXPECT_EQ(filtered[3].type, UIInputEventType::PointerLeave);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelInputFilterTests, CapturedPointerEventsBypassBoundsWhenEnabled) {
|
||||
const std::vector<UIInputEvent> filtered =
|
||||
FilterUIEditorPanelInputEvents(
|
||||
UIRect(0.0f, 0.0f, 100.0f, 100.0f),
|
||||
{
|
||||
MakePointerMove(140.0f, 20.0f),
|
||||
MakePointerButtonDown(180.0f, 32.0f)
|
||||
},
|
||||
UIEditorPanelInputFilterOptions{
|
||||
.allowPointerInBounds = false,
|
||||
.allowPointerWhileCaptured = true,
|
||||
.allowKeyboardInput = false,
|
||||
.allowFocusEvents = false,
|
||||
.includePointerLeave = false
|
||||
});
|
||||
|
||||
ASSERT_EQ(filtered.size(), 2u);
|
||||
EXPECT_EQ(filtered[0].type, UIInputEventType::PointerMove);
|
||||
EXPECT_EQ(filtered[1].type, UIInputEventType::PointerButtonDown);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelInputFilterTests, SyntheticFocusTransitionsArePrependedToFilteredEvents) {
|
||||
const std::vector<UIInputEvent> filtered =
|
||||
BuildUIEditorPanelInputEvents(
|
||||
UIRect(0.0f, 0.0f, 100.0f, 100.0f),
|
||||
{
|
||||
MakePointerMove(20.0f, 20.0f),
|
||||
MakeKeyDown()
|
||||
},
|
||||
UIEditorPanelInputFilterOptions{
|
||||
.allowPointerInBounds = true,
|
||||
.allowPointerWhileCaptured = false,
|
||||
.allowKeyboardInput = true,
|
||||
.allowFocusEvents = false,
|
||||
.includePointerLeave = false
|
||||
},
|
||||
true,
|
||||
true);
|
||||
|
||||
ASSERT_EQ(filtered.size(), 4u);
|
||||
EXPECT_EQ(filtered[0].type, UIInputEventType::FocusLost);
|
||||
EXPECT_EQ(filtered[1].type, UIInputEventType::FocusGained);
|
||||
EXPECT_EQ(filtered[2].type, UIInputEventType::PointerMove);
|
||||
EXPECT_EQ(filtered[3].type, UIInputEventType::KeyDown);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Composition/EditorShellAssetBuilder.h"
|
||||
|
||||
#include <XCEditor/Panels/UIEditorPanelRegistry.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Editor::App::BuildEditorApplicationShellAsset;
|
||||
using XCEngine::UI::Editor::FindUIEditorPanelDescriptor;
|
||||
using XCEngine::UI::Editor::UIEditorPanelDescriptor;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistryValidationCode;
|
||||
using XCEngine::UI::Editor::ValidateUIEditorPanelRegistry;
|
||||
|
||||
TEST(UIEditorPanelRegistryTest, DefaultRegistryContainsShellDescriptors) {
|
||||
const UIEditorPanelRegistry registry =
|
||||
BuildEditorApplicationShellAsset(".").panelRegistry;
|
||||
|
||||
ASSERT_EQ(registry.panels.size(), 6u);
|
||||
const UIEditorPanelDescriptor* descriptor =
|
||||
FindUIEditorPanelDescriptor(registry, "hierarchy");
|
||||
ASSERT_NE(descriptor, nullptr);
|
||||
EXPECT_FALSE(descriptor->canHide);
|
||||
EXPECT_FALSE(descriptor->canClose);
|
||||
EXPECT_NE(FindUIEditorPanelDescriptor(registry, "scene"), nullptr);
|
||||
EXPECT_NE(FindUIEditorPanelDescriptor(registry, "project"), nullptr);
|
||||
EXPECT_EQ(FindUIEditorPanelDescriptor(registry, "missing-panel"), nullptr);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelRegistryTest, ValidationRejectsEmptyPanelIdAndTitle) {
|
||||
UIEditorPanelRegistry emptyIdRegistry = {};
|
||||
emptyIdRegistry.panels.push_back(UIEditorPanelDescriptor{ "", "Panel", {}, true });
|
||||
EXPECT_EQ(
|
||||
ValidateUIEditorPanelRegistry(emptyIdRegistry).code,
|
||||
UIEditorPanelRegistryValidationCode::EmptyPanelId);
|
||||
|
||||
UIEditorPanelRegistry emptyTitleRegistry = {};
|
||||
emptyTitleRegistry.panels.push_back(UIEditorPanelDescriptor{ "panel-a", "", {}, true });
|
||||
EXPECT_EQ(
|
||||
ValidateUIEditorPanelRegistry(emptyTitleRegistry).code,
|
||||
UIEditorPanelRegistryValidationCode::EmptyDefaultTitle);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelRegistryTest, ValidationRejectsDuplicatePanelId) {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels.push_back(UIEditorPanelDescriptor{ "panel-a", "Panel A", {}, true });
|
||||
registry.panels.push_back(UIEditorPanelDescriptor{ "panel-a", "Panel A Duplicate", {}, true });
|
||||
|
||||
const auto validation = ValidateUIEditorPanelRegistry(registry);
|
||||
EXPECT_EQ(validation.code, UIEditorPanelRegistryValidationCode::DuplicatePanelId);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,412 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorPropertyGrid.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Widgets::UIExpansionModel;
|
||||
using XCEngine::UI::Widgets::UIPropertyEditModel;
|
||||
using XCEngine::UI::Widgets::UISelectionModel;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorPropertyGridBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorPropertyGridForeground;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorPropertyGridLayout;
|
||||
using XCEngine::UI::Editor::Widgets::FindUIEditorPropertyGridFieldLocation;
|
||||
using XCEngine::UI::Editor::Widgets::FindUIEditorPropertyGridSectionIndex;
|
||||
using XCEngine::UI::Editor::Widgets::FindUIEditorPropertyGridVisibleFieldIndex;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorPropertyGrid;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorPropertyGridFieldValueText;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridField;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridFieldKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridSection;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridState;
|
||||
|
||||
bool ContainsTextCommand(
|
||||
const XCEngine::UI::UIDrawData& drawData,
|
||||
std::string_view text) {
|
||||
for (const auto& drawList : drawData.GetDrawLists()) {
|
||||
for (const auto& command : drawList.GetCommands()) {
|
||||
if (command.type == XCEngine::UI::UIDrawCommandType::Text &&
|
||||
command.text == text) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeTextField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
std::string value,
|
||||
bool readOnly = false) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.valueText = std::move(value);
|
||||
field.readOnly = readOnly;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeBoolField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
bool value) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Bool;
|
||||
field.boolValue = value;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeNumberField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
double value,
|
||||
bool integerMode = true) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Number;
|
||||
field.numberValue.value = value;
|
||||
field.numberValue.step = 1.0;
|
||||
field.numberValue.minValue = 0.0;
|
||||
field.numberValue.maxValue = 5000.0;
|
||||
field.numberValue.integerMode = integerMode;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeEnumField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
std::vector<std::string> options,
|
||||
std::size_t selectedIndex) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Enum;
|
||||
field.enumValue.options = std::move(options);
|
||||
field.enumValue.selectedIndex = selectedIndex;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeColorField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
XCEngine::UI::UIColor value,
|
||||
bool showAlpha = true) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Color;
|
||||
field.colorValue.value = value;
|
||||
field.colorValue.showAlpha = showAlpha;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeVector4Field(
|
||||
std::string id,
|
||||
std::string label,
|
||||
std::array<double, 4u> values) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Vector4;
|
||||
field.vector4Value.values = values;
|
||||
return field;
|
||||
}
|
||||
|
||||
std::vector<UIEditorPropertyGridSection> BuildSections() {
|
||||
return {
|
||||
{
|
||||
"inspector",
|
||||
"Inspector",
|
||||
{
|
||||
MakeBoolField("enabled", "Enabled", true),
|
||||
MakeNumberField("render_queue", "Render Queue", 2000.0),
|
||||
MakeEnumField("render_mode", "Render Mode", { "Opaque", "Cutout", "Fade" }, 0u),
|
||||
MakeTextField("tag", "Tag", "Player")
|
||||
},
|
||||
0.0f
|
||||
},
|
||||
{
|
||||
"metadata",
|
||||
"Metadata",
|
||||
{
|
||||
MakeTextField("guid", "GUID", "asset-guid-001", true)
|
||||
},
|
||||
0.0f
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
UIPoint RectCenter(const XCEngine::UI::UIRect& rect) {
|
||||
return UIPoint(rect.x + rect.width * 0.5f, rect.y + rect.height * 0.5f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorPropertyGridTest, FindSectionAndFieldLocationReturnStableIndices) {
|
||||
const auto sections = BuildSections();
|
||||
|
||||
EXPECT_EQ(FindUIEditorPropertyGridSectionIndex(sections, "inspector"), 0u);
|
||||
EXPECT_EQ(FindUIEditorPropertyGridSectionIndex(sections, "metadata"), 1u);
|
||||
EXPECT_EQ(
|
||||
FindUIEditorPropertyGridSectionIndex(sections, "missing"),
|
||||
static_cast<std::size_t>(-1));
|
||||
|
||||
const auto renderModeLocation =
|
||||
FindUIEditorPropertyGridFieldLocation(sections, "render_mode");
|
||||
EXPECT_TRUE(renderModeLocation.IsValid());
|
||||
EXPECT_EQ(renderModeLocation.sectionIndex, 0u);
|
||||
EXPECT_EQ(renderModeLocation.fieldIndex, 2u);
|
||||
|
||||
const auto missingLocation =
|
||||
FindUIEditorPropertyGridFieldLocation(sections, "unknown");
|
||||
EXPECT_FALSE(missingLocation.IsValid());
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridTest, LayoutBuildsTypedFieldRectsWithStableColumns) {
|
||||
const auto sections = BuildSections();
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("inspector");
|
||||
|
||||
const auto layout = BuildUIEditorPropertyGridLayout(
|
||||
UIRect(10.0f, 20.0f, 420.0f, 260.0f),
|
||||
sections,
|
||||
expansionModel);
|
||||
|
||||
ASSERT_EQ(layout.sectionHeaderRects.size(), sections.size());
|
||||
ASSERT_EQ(layout.visibleFieldIndices.size(), 4u);
|
||||
EXPECT_EQ(layout.visibleFieldSectionIndices[0], 0u);
|
||||
EXPECT_FLOAT_EQ(layout.sectionHeaderRects[0].x, 18.0f);
|
||||
EXPECT_FLOAT_EQ(layout.sectionHeaderRects[0].y, 28.0f);
|
||||
|
||||
EXPECT_EQ(
|
||||
FindUIEditorPropertyGridVisibleFieldIndex(layout, "enabled", sections),
|
||||
0u);
|
||||
EXPECT_EQ(
|
||||
FindUIEditorPropertyGridVisibleFieldIndex(layout, "guid", sections),
|
||||
static_cast<std::size_t>(-1));
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.fieldLabelRects[0].x, layout.fieldRowRects[0].x + 12.0f);
|
||||
EXPECT_FLOAT_EQ(layout.fieldValueRects[0].x, layout.fieldRowRects[0].x + 236.0f);
|
||||
EXPECT_FLOAT_EQ(layout.fieldValueRects[0].y, layout.fieldRowRects[0].y);
|
||||
EXPECT_FLOAT_EQ(layout.fieldValueRects[1].x, layout.fieldRowRects[1].x + 236.0f);
|
||||
EXPECT_FLOAT_EQ(layout.fieldValueRects[1].y, layout.fieldRowRects[1].y + 4.0f);
|
||||
EXPECT_FLOAT_EQ(layout.fieldValueRects[2].x, layout.fieldRowRects[2].x + 236.0f);
|
||||
EXPECT_FLOAT_EQ(layout.fieldValueRects[2].y, layout.fieldRowRects[2].y + 4.0f);
|
||||
EXPECT_FLOAT_EQ(layout.fieldValueRects[3].x, layout.fieldRowRects[3].x + 236.0f);
|
||||
EXPECT_FLOAT_EQ(layout.fieldValueRects[3].y, layout.fieldRowRects[3].y + 4.0f);
|
||||
EXPECT_GT(layout.fieldValueRects[2].width, 0.0f);
|
||||
EXPECT_GT(layout.fieldValueRects[3].width, 0.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridTest, HitTestResolvesHeaderRowAndTypedValueHosts) {
|
||||
const auto sections = BuildSections();
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("inspector");
|
||||
expansionModel.Expand("metadata");
|
||||
|
||||
const auto layout = BuildUIEditorPropertyGridLayout(
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
sections,
|
||||
expansionModel);
|
||||
|
||||
const auto headerHit =
|
||||
HitTestUIEditorPropertyGrid(layout, RectCenter(layout.sectionHeaderRects[0]));
|
||||
EXPECT_EQ(headerHit.kind, UIEditorPropertyGridHitTargetKind::SectionHeader);
|
||||
EXPECT_EQ(headerHit.sectionIndex, 0u);
|
||||
|
||||
const auto rowHit = HitTestUIEditorPropertyGrid(
|
||||
layout,
|
||||
UIPoint(
|
||||
layout.fieldRowRects[1].x + 16.0f,
|
||||
layout.fieldRowRects[1].y + layout.fieldRowRects[1].height * 0.5f));
|
||||
EXPECT_EQ(rowHit.kind, UIEditorPropertyGridHitTargetKind::FieldRow);
|
||||
EXPECT_EQ(rowHit.fieldIndex, 1u);
|
||||
|
||||
const auto boolValueHit =
|
||||
HitTestUIEditorPropertyGrid(layout, RectCenter(layout.fieldValueRects[0]));
|
||||
EXPECT_EQ(boolValueHit.kind, UIEditorPropertyGridHitTargetKind::ValueBox);
|
||||
EXPECT_EQ(boolValueHit.fieldIndex, 0u);
|
||||
|
||||
const auto boolTrailingHit = HitTestUIEditorPropertyGrid(
|
||||
layout,
|
||||
UIPoint(
|
||||
layout.fieldValueRects[0].x + layout.fieldValueRects[0].width - 6.0f,
|
||||
layout.fieldValueRects[0].y + layout.fieldValueRects[0].height * 0.5f));
|
||||
EXPECT_EQ(boolTrailingHit.kind, UIEditorPropertyGridHitTargetKind::ValueBox);
|
||||
EXPECT_EQ(boolTrailingHit.fieldIndex, 0u);
|
||||
|
||||
const auto enumValueHit =
|
||||
HitTestUIEditorPropertyGrid(layout, RectCenter(layout.fieldValueRects[2]));
|
||||
EXPECT_EQ(enumValueHit.kind, UIEditorPropertyGridHitTargetKind::ValueBox);
|
||||
EXPECT_EQ(enumValueHit.fieldIndex, 2u);
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridTest, BackgroundAndForegroundEmitTypedCommandsAndPopupOverlay) {
|
||||
const auto sections = BuildSections();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("render_mode");
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("inspector");
|
||||
expansionModel.Expand("metadata");
|
||||
UIPropertyEditModel propertyEditModel = {};
|
||||
propertyEditModel.BeginEdit("tag", "Player");
|
||||
propertyEditModel.UpdateStagedValue("Hero");
|
||||
UIEditorPropertyGridState state = {};
|
||||
state.focused = true;
|
||||
state.hoveredFieldId = "enabled";
|
||||
state.hoveredHitTarget = UIEditorPropertyGridHitTargetKind::ValueBox;
|
||||
state.popupFieldId = "render_mode";
|
||||
state.popupHighlightedIndex = 1u;
|
||||
|
||||
const auto layout = BuildUIEditorPropertyGridLayout(
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
expansionModel);
|
||||
|
||||
XCEngine::UI::UIDrawData drawData = {};
|
||||
auto& drawList = drawData.EmplaceDrawList("PropertyGrid");
|
||||
AppendUIEditorPropertyGridBackground(
|
||||
drawList,
|
||||
layout,
|
||||
sections,
|
||||
selectionModel,
|
||||
propertyEditModel,
|
||||
state);
|
||||
AppendUIEditorPropertyGridForeground(
|
||||
drawList,
|
||||
layout,
|
||||
sections,
|
||||
state,
|
||||
propertyEditModel);
|
||||
|
||||
const auto& commands = drawList.GetCommands();
|
||||
ASSERT_GE(commands.size(), 16u);
|
||||
EXPECT_EQ(commands[0].type, XCEngine::UI::UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(commands[1].type, XCEngine::UI::UIDrawCommandType::RectOutline);
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Inspector"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Enabled"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Render Queue"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Opaque"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Hero"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "EDIT"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Cutout"));
|
||||
EXPECT_EQ(ResolveUIEditorPropertyGridFieldValueText(sections[0].fields[1]), "2000");
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridTest, Vector4FieldUsesHostedLayoutAndForegroundText) {
|
||||
std::vector<UIEditorPropertyGridSection> sections = {
|
||||
{
|
||||
"transform",
|
||||
"Transform",
|
||||
{
|
||||
MakeVector4Field("rotation", "Rotation", { 1.0, 2.0, 3.0, 4.0 })
|
||||
},
|
||||
0.0f
|
||||
}
|
||||
};
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("transform");
|
||||
UIPropertyEditModel propertyEditModel = {};
|
||||
UIEditorPropertyGridState state = {};
|
||||
|
||||
const auto layout = BuildUIEditorPropertyGridLayout(
|
||||
UIRect(0.0f, 0.0f, 520.0f, 220.0f),
|
||||
sections,
|
||||
expansionModel);
|
||||
|
||||
ASSERT_EQ(layout.visibleFieldIndices.size(), 1u);
|
||||
EXPECT_GT(layout.fieldValueRects[0].x, layout.fieldLabelRects[0].x + layout.fieldLabelRects[0].width);
|
||||
EXPECT_GT(layout.fieldValueRects[0].width, 200.0f);
|
||||
|
||||
XCEngine::UI::UIDrawData drawData = {};
|
||||
auto& drawList = drawData.EmplaceDrawList("PropertyGridVector4");
|
||||
AppendUIEditorPropertyGridBackground(
|
||||
drawList,
|
||||
layout,
|
||||
sections,
|
||||
selectionModel,
|
||||
propertyEditModel,
|
||||
state);
|
||||
AppendUIEditorPropertyGridForeground(
|
||||
drawList,
|
||||
layout,
|
||||
sections,
|
||||
state,
|
||||
propertyEditModel);
|
||||
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Rotation"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "X"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Y"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Z"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "W"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "4"));
|
||||
EXPECT_EQ(ResolveUIEditorPropertyGridFieldValueText(sections[0].fields[0]), "1, 2, 3, 4");
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridTest, ColorFieldUsesHostedLayoutAndPopupAwareForeground) {
|
||||
std::vector<UIEditorPropertyGridSection> sections = {
|
||||
{
|
||||
"material",
|
||||
"Material",
|
||||
{
|
||||
MakeColorField("tint", "Tint", XCEngine::UI::UIColor(0.8f, 0.4f, 0.2f, 0.5f))
|
||||
},
|
||||
0.0f
|
||||
}
|
||||
};
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("tint");
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("material");
|
||||
UIPropertyEditModel propertyEditModel = {};
|
||||
UIEditorPropertyGridState state = {};
|
||||
state.focused = true;
|
||||
state.hoveredFieldId = "tint";
|
||||
state.hoveredHitTarget = UIEditorPropertyGridHitTargetKind::ValueBox;
|
||||
XCEngine::UI::Editor::Widgets::UIEditorPropertyGridColorFieldVisualState colorState = {};
|
||||
colorState.fieldId = "tint";
|
||||
colorState.state.hoveredTarget =
|
||||
XCEngine::UI::Editor::Widgets::UIEditorColorFieldHitTargetKind::Swatch;
|
||||
colorState.state.focused = true;
|
||||
colorState.state.popupOpen = true;
|
||||
state.colorFieldStates.push_back(colorState);
|
||||
|
||||
const auto layout = BuildUIEditorPropertyGridLayout(
|
||||
UIRect(0.0f, 0.0f, 520.0f, 320.0f),
|
||||
sections,
|
||||
expansionModel);
|
||||
|
||||
ASSERT_EQ(layout.visibleFieldIndices.size(), 1u);
|
||||
EXPECT_GT(layout.fieldValueRects[0].x, layout.fieldLabelRects[0].x + layout.fieldLabelRects[0].width);
|
||||
EXPECT_GT(layout.fieldValueRects[0].width, 200.0f);
|
||||
|
||||
XCEngine::UI::UIDrawData drawData = {};
|
||||
auto& drawList = drawData.EmplaceDrawList("PropertyGridColor");
|
||||
AppendUIEditorPropertyGridBackground(
|
||||
drawList,
|
||||
layout,
|
||||
sections,
|
||||
selectionModel,
|
||||
propertyEditModel,
|
||||
state);
|
||||
AppendUIEditorPropertyGridForeground(
|
||||
drawList,
|
||||
layout,
|
||||
sections,
|
||||
state,
|
||||
propertyEditModel);
|
||||
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Tint"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Color"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Hexadecimal"));
|
||||
EXPECT_EQ(ResolveUIEditorPropertyGridFieldValueText(sections[0].fields[0]), "#CC663380");
|
||||
}
|
||||
@@ -1,811 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorFieldStyle.h>
|
||||
#include <XCEditor/Fields/UIEditorAssetField.h>
|
||||
#include <XCEditor/Fields/UIEditorPropertyGridInteraction.h>
|
||||
#include <XCEditor/Fields/UIEditorColorField.h>
|
||||
#include <XCEditor/Fields/UIEditorVector3Field.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Widgets::UIExpansionModel;
|
||||
using XCEngine::UI::Widgets::UIPropertyEditModel;
|
||||
using XCEngine::UI::Widgets::UISelectionModel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorPropertyGridColorFieldMetrics;
|
||||
using XCEngine::UI::Editor::BuildUIEditorPropertyGridAssetFieldMetrics;
|
||||
using XCEngine::UI::Editor::UIEditorPropertyGridInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorPropertyGridInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorAssetFieldLayout;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorColorFieldLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridField;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridFieldKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridSection;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorVector3FieldLayout;
|
||||
|
||||
UIEditorPropertyGridField MakeTextField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
std::string value,
|
||||
bool readOnly = false) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.valueText = std::move(value);
|
||||
field.readOnly = readOnly;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeBoolField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
bool value) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Bool;
|
||||
field.boolValue = value;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeNumberField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
double value) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Number;
|
||||
field.numberValue.value = value;
|
||||
field.numberValue.step = 1.0;
|
||||
field.numberValue.minValue = 0.0;
|
||||
field.numberValue.maxValue = 5000.0;
|
||||
field.numberValue.integerMode = true;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeEnumField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
std::vector<std::string> options,
|
||||
std::size_t selectedIndex) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Enum;
|
||||
field.enumValue.options = std::move(options);
|
||||
field.enumValue.selectedIndex = selectedIndex;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeAssetField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
std::string assetId,
|
||||
std::string displayName,
|
||||
std::string statusText = "MAT") {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Asset;
|
||||
field.assetValue.assetId = std::move(assetId);
|
||||
field.assetValue.displayName = std::move(displayName);
|
||||
field.assetValue.statusText = std::move(statusText);
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeColorField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
XCEngine::UI::UIColor value,
|
||||
bool showAlpha = true) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Color;
|
||||
field.colorValue.value = value;
|
||||
field.colorValue.showAlpha = showAlpha;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeVector3Field(
|
||||
std::string id,
|
||||
std::string label,
|
||||
const std::array<double, 3u>& values,
|
||||
bool integerMode = false) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Vector3;
|
||||
field.vector3Value.values = values;
|
||||
field.vector3Value.step = integerMode ? 1.0 : 0.1;
|
||||
field.vector3Value.minValue = -1000000.0;
|
||||
field.vector3Value.maxValue = 1000000.0;
|
||||
field.vector3Value.integerMode = integerMode;
|
||||
return field;
|
||||
}
|
||||
|
||||
std::vector<UIEditorPropertyGridSection> BuildSections() {
|
||||
return {
|
||||
{
|
||||
"inspector",
|
||||
"Inspector",
|
||||
{
|
||||
MakeBoolField("enabled", "Enabled", true),
|
||||
MakeNumberField("render_queue", "Render Queue", 2000.0),
|
||||
MakeEnumField("render_mode", "Render Mode", { "Opaque", "Cutout", "Fade" }, 0u),
|
||||
MakeTextField("tag", "Tag", "Player")
|
||||
},
|
||||
0.0f
|
||||
},
|
||||
{
|
||||
"metadata",
|
||||
"Metadata",
|
||||
{
|
||||
MakeTextField("guid", "GUID", "asset-guid-001", true)
|
||||
},
|
||||
0.0f
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerMove(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerMove;
|
||||
event.position = UIPoint(x, y);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerDown(float x, float y, UIPointerButton button = UIPointerButton::Left) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonDown;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerUp(float x, float y, UIPointerButton button = UIPointerButton::Left) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonUp;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKeyDown(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeCharacter(char character) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::Character;
|
||||
event.character = static_cast<std::uint32_t>(character);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIPoint RectCenter(const XCEngine::UI::UIRect& rect) {
|
||||
return UIPoint(rect.x + rect.width * 0.5f, rect.y + rect.height * 0.5f);
|
||||
}
|
||||
|
||||
void ExpandAll(UIExpansionModel& expansionModel) {
|
||||
expansionModel.Expand("inspector");
|
||||
expansionModel.Expand("metadata");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorPropertyGridInteractionTest, PointerMoveUpdatesHoveredSectionAndField) {
|
||||
auto sections = BuildSections();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
ExpandAll(expansionModel);
|
||||
UIPropertyEditModel propertyEditModel = {};
|
||||
UIEditorPropertyGridInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{});
|
||||
|
||||
auto frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{ MakePointerMove(
|
||||
initialFrame.layout.sectionHeaderRects[0].x + 24.0f,
|
||||
initialFrame.layout.sectionHeaderRects[0].y + 16.0f) });
|
||||
EXPECT_EQ(state.propertyGridState.hoveredSectionId, "inspector");
|
||||
EXPECT_TRUE(state.propertyGridState.hoveredFieldId.empty());
|
||||
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{ MakePointerMove(
|
||||
initialFrame.layout.fieldRowRects[1].x + 16.0f,
|
||||
initialFrame.layout.fieldRowRects[1].y + 16.0f) });
|
||||
EXPECT_EQ(frame.result.hitTarget.fieldIndex, 1u);
|
||||
EXPECT_EQ(state.propertyGridState.hoveredFieldId, "render_queue");
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridInteractionTest, LeftClickSectionHeaderTogglesExpansion) {
|
||||
auto sections = BuildSections();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("inspector");
|
||||
UIPropertyEditModel propertyEditModel = {};
|
||||
UIEditorPropertyGridInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{});
|
||||
const UIPoint metadataHeaderCenter = RectCenter(initialFrame.layout.sectionHeaderRects[1]);
|
||||
|
||||
const auto frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakePointerDown(metadataHeaderCenter.x, metadataHeaderCenter.y),
|
||||
MakePointerUp(metadataHeaderCenter.x, metadataHeaderCenter.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.sectionToggled);
|
||||
EXPECT_EQ(frame.result.toggledSectionId, "metadata");
|
||||
EXPECT_TRUE(expansionModel.IsExpanded("metadata"));
|
||||
EXPECT_TRUE(state.propertyGridState.focused);
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridInteractionTest, LeftClickBoolValueHostTogglesValueAndSelectsField) {
|
||||
auto sections = BuildSections();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
ExpandAll(expansionModel);
|
||||
UIPropertyEditModel propertyEditModel = {};
|
||||
UIEditorPropertyGridInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{});
|
||||
const UIPoint boolValueCenter = RectCenter(initialFrame.layout.fieldValueRects[0]);
|
||||
|
||||
const auto frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakePointerDown(boolValueCenter.x, boolValueCenter.y),
|
||||
MakePointerUp(boolValueCenter.x, boolValueCenter.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_TRUE(frame.result.fieldValueChanged);
|
||||
EXPECT_EQ(frame.result.selectedFieldId, "enabled");
|
||||
EXPECT_EQ(frame.result.changedFieldId, "enabled");
|
||||
EXPECT_EQ(frame.result.changedValue, "false");
|
||||
EXPECT_FALSE(sections[0].fields[0].boolValue);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("enabled"));
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridInteractionTest, NumberValueBoxEditCanCommitWithEnter) {
|
||||
auto sections = BuildSections();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
ExpandAll(expansionModel);
|
||||
UIPropertyEditModel propertyEditModel = {};
|
||||
UIEditorPropertyGridInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{});
|
||||
const UIPoint numberValueCenter = RectCenter(initialFrame.layout.fieldValueRects[1]);
|
||||
|
||||
auto frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakePointerDown(numberValueCenter.x, numberValueCenter.y),
|
||||
MakePointerUp(numberValueCenter.x, numberValueCenter.y)
|
||||
});
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_EQ(frame.result.selectedFieldId, "render_queue");
|
||||
EXPECT_EQ(frame.result.activeFieldId, "render_queue");
|
||||
EXPECT_TRUE(propertyEditModel.HasActiveEdit());
|
||||
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakeKeyDown(KeyCode::Backspace),
|
||||
MakeKeyDown(KeyCode::Backspace),
|
||||
MakeKeyDown(KeyCode::Backspace),
|
||||
MakeKeyDown(KeyCode::Backspace),
|
||||
MakeCharacter('2'),
|
||||
MakeCharacter('5'),
|
||||
MakeCharacter('0'),
|
||||
MakeCharacter('0'),
|
||||
MakeKeyDown(KeyCode::Enter)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.editCommitted);
|
||||
EXPECT_TRUE(frame.result.fieldValueChanged);
|
||||
EXPECT_EQ(frame.result.committedFieldId, "render_queue");
|
||||
EXPECT_EQ(frame.result.committedValue, "2500");
|
||||
EXPECT_EQ(frame.result.changedFieldId, "render_queue");
|
||||
EXPECT_EQ(frame.result.changedValue, "2500");
|
||||
EXPECT_FALSE(propertyEditModel.HasActiveEdit());
|
||||
EXPECT_DOUBLE_EQ(sections[0].fields[1].numberValue.value, 2500.0);
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridInteractionTest, EnumPopupCanOpenNavigateSelectAndClose) {
|
||||
auto sections = BuildSections();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
ExpandAll(expansionModel);
|
||||
UIPropertyEditModel propertyEditModel = {};
|
||||
UIEditorPropertyGridInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{});
|
||||
const UIPoint enumValueCenter = RectCenter(initialFrame.layout.fieldValueRects[2]);
|
||||
|
||||
auto frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakePointerDown(enumValueCenter.x, enumValueCenter.y),
|
||||
MakePointerUp(enumValueCenter.x, enumValueCenter.y)
|
||||
});
|
||||
EXPECT_TRUE(frame.result.popupOpened);
|
||||
EXPECT_EQ(state.propertyGridState.popupFieldId, "render_mode");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("render_mode"));
|
||||
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakeKeyDown(KeyCode::Down),
|
||||
MakeKeyDown(KeyCode::Enter)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.popupClosed);
|
||||
EXPECT_TRUE(frame.result.fieldValueChanged);
|
||||
EXPECT_EQ(frame.result.changedFieldId, "render_mode");
|
||||
EXPECT_EQ(frame.result.changedValue, "Cutout");
|
||||
EXPECT_TRUE(state.propertyGridState.popupFieldId.empty());
|
||||
EXPECT_EQ(sections[0].fields[2].enumValue.selectedIndex, 1u);
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClearsFocus) {
|
||||
auto sections = BuildSections();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
ExpandAll(expansionModel);
|
||||
UIPropertyEditModel propertyEditModel = {};
|
||||
UIEditorPropertyGridInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{});
|
||||
const UIPoint tagValueCenter = RectCenter(initialFrame.layout.fieldValueRects[3]);
|
||||
|
||||
UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakePointerDown(tagValueCenter.x, tagValueCenter.y),
|
||||
MakePointerUp(tagValueCenter.x, tagValueCenter.y)
|
||||
});
|
||||
|
||||
auto frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakeCharacter('A'),
|
||||
MakeKeyDown(KeyCode::Escape)
|
||||
});
|
||||
EXPECT_TRUE(frame.result.editCanceled);
|
||||
EXPECT_FALSE(propertyEditModel.HasActiveEdit());
|
||||
EXPECT_EQ(sections[0].fields[3].valueText, "Player");
|
||||
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakePointerDown(520.0f, 360.0f),
|
||||
MakePointerUp(520.0f, 360.0f)
|
||||
});
|
||||
EXPECT_FALSE(state.propertyGridState.focused);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("tag"));
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridInteractionTest, ArrowAndHomeEndKeysNavigateVisibleFields) {
|
||||
auto sections = BuildSections();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("render_queue");
|
||||
UIExpansionModel expansionModel = {};
|
||||
ExpandAll(expansionModel);
|
||||
UIPropertyEditModel propertyEditModel = {};
|
||||
UIEditorPropertyGridInteractionState state = {};
|
||||
state.propertyGridState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{ MakeKeyDown(KeyCode::Down) });
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_EQ(frame.result.selectedFieldId, "render_mode");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("render_mode"));
|
||||
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{ MakeKeyDown(KeyCode::Home) });
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_EQ(frame.result.selectedFieldId, "enabled");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("enabled"));
|
||||
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{ MakeKeyDown(KeyCode::End) });
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_EQ(frame.result.selectedFieldId, "guid");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("guid"));
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridInteractionTest, ColorFieldPopupCanOpenAndDragAlphaThroughHostedInteraction) {
|
||||
std::vector<UIEditorPropertyGridSection> sections = {
|
||||
{
|
||||
"material",
|
||||
"Material",
|
||||
{
|
||||
MakeColorField("tint", "Tint", XCEngine::UI::UIColor(0.8f, 0.4f, 0.2f, 1.0f))
|
||||
},
|
||||
0.0f
|
||||
}
|
||||
};
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("material");
|
||||
UIPropertyEditModel propertyEditModel = {};
|
||||
UIEditorPropertyGridInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 360.0f),
|
||||
sections,
|
||||
{});
|
||||
|
||||
XCEngine::UI::Editor::Widgets::UIEditorColorFieldSpec initialColorSpec = {};
|
||||
initialColorSpec.fieldId = "tint";
|
||||
initialColorSpec.label = "Tint";
|
||||
initialColorSpec.value = sections[0].fields[0].colorValue.value;
|
||||
initialColorSpec.showAlpha = sections[0].fields[0].colorValue.showAlpha;
|
||||
const auto initialColorLayout = BuildUIEditorColorFieldLayout(
|
||||
frame.layout.fieldRowRects[0],
|
||||
initialColorSpec,
|
||||
BuildUIEditorPropertyGridColorFieldMetrics({}),
|
||||
UIRect(-4096.0f, -4096.0f, 8192.0f, 8192.0f));
|
||||
const UIPoint swatchCenter = RectCenter(initialColorLayout.swatchRect);
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 360.0f),
|
||||
sections,
|
||||
{
|
||||
MakePointerDown(swatchCenter.x, swatchCenter.y),
|
||||
MakePointerUp(swatchCenter.x, swatchCenter.y)
|
||||
});
|
||||
|
||||
ASSERT_TRUE(frame.result.popupOpened);
|
||||
ASSERT_EQ(state.propertyGridState.colorFieldStates.size(), 1u);
|
||||
EXPECT_TRUE(state.propertyGridState.colorFieldStates[0].state.popupOpen);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("tint"));
|
||||
|
||||
const auto popupFrame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 360.0f),
|
||||
sections,
|
||||
{});
|
||||
const auto& colorState = state.propertyGridState.colorFieldStates[0].state;
|
||||
EXPECT_TRUE(colorState.popupOpen);
|
||||
|
||||
XCEngine::UI::Editor::Widgets::UIEditorColorFieldSpec colorSpec = {};
|
||||
colorSpec.fieldId = "tint";
|
||||
colorSpec.label = "Tint";
|
||||
colorSpec.value = sections[0].fields[0].colorValue.value;
|
||||
colorSpec.showAlpha = sections[0].fields[0].colorValue.showAlpha;
|
||||
const auto colorLayout = BuildUIEditorColorFieldLayout(
|
||||
popupFrame.layout.fieldRowRects[0],
|
||||
colorSpec,
|
||||
BuildUIEditorPropertyGridColorFieldMetrics({}),
|
||||
UIRect(-4096.0f, -4096.0f, 8192.0f, 8192.0f));
|
||||
|
||||
const float alphaX =
|
||||
colorLayout.alphaSliderRect.x + colorLayout.alphaSliderRect.width * 0.25f;
|
||||
const float alphaY =
|
||||
colorLayout.alphaSliderRect.y + colorLayout.alphaSliderRect.height * 0.5f;
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 360.0f),
|
||||
sections,
|
||||
{
|
||||
MakePointerDown(alphaX, alphaY),
|
||||
MakePointerMove(alphaX, alphaY),
|
||||
MakePointerUp(alphaX, alphaY)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.fieldValueChanged);
|
||||
EXPECT_EQ(frame.result.changedFieldId, "tint");
|
||||
EXPECT_LT(sections[0].fields[0].colorValue.value.a, 0.5f);
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridInteractionTest, Vector3FieldCanStartEditAndCommitInsidePropertyGridHost) {
|
||||
std::vector<UIEditorPropertyGridSection> sections = {
|
||||
{
|
||||
"transform",
|
||||
"Transform",
|
||||
{
|
||||
MakeVector3Field("position", "Position", { 1.0, 2.0, 3.0 }, true)
|
||||
},
|
||||
0.0f
|
||||
}
|
||||
};
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("transform");
|
||||
UIPropertyEditModel propertyEditModel = {};
|
||||
UIEditorPropertyGridInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 360.0f),
|
||||
sections,
|
||||
{});
|
||||
|
||||
XCEngine::UI::Editor::Widgets::UIEditorVector3FieldSpec vectorSpec = {};
|
||||
vectorSpec.fieldId = "position";
|
||||
vectorSpec.label = "Position";
|
||||
vectorSpec.values = sections[0].fields[0].vector3Value.values;
|
||||
vectorSpec.componentLabels = sections[0].fields[0].vector3Value.componentLabels;
|
||||
vectorSpec.step = sections[0].fields[0].vector3Value.step;
|
||||
vectorSpec.minValue = sections[0].fields[0].vector3Value.minValue;
|
||||
vectorSpec.maxValue = sections[0].fields[0].vector3Value.maxValue;
|
||||
vectorSpec.integerMode = sections[0].fields[0].vector3Value.integerMode;
|
||||
const auto vectorLayout = BuildUIEditorVector3FieldLayout(
|
||||
frame.layout.fieldRowRects[0],
|
||||
vectorSpec,
|
||||
XCEngine::UI::Editor::BuildUIEditorPropertyGridVector3FieldMetrics({}));
|
||||
const UIPoint componentCenter = RectCenter(vectorLayout.componentRects[2]);
|
||||
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 360.0f),
|
||||
sections,
|
||||
{
|
||||
MakePointerDown(componentCenter.x, componentCenter.y),
|
||||
MakePointerUp(componentCenter.x, componentCenter.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_EQ(frame.result.selectedFieldId, "position");
|
||||
ASSERT_EQ(state.vector3FieldInteractionStates.size(), 1u);
|
||||
EXPECT_EQ(state.vector3FieldInteractionStates[0].fieldId, "position");
|
||||
EXPECT_TRUE(state.vector3FieldInteractionStates[0].state.vector3FieldState.editing);
|
||||
EXPECT_EQ(
|
||||
state.vector3FieldInteractionStates[0].state.vector3FieldState.selectedComponentIndex,
|
||||
2u);
|
||||
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 360.0f),
|
||||
sections,
|
||||
{
|
||||
MakeKeyDown(KeyCode::Backspace),
|
||||
MakeCharacter('9'),
|
||||
MakeKeyDown(KeyCode::Enter)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.editCommitted);
|
||||
EXPECT_TRUE(frame.result.fieldValueChanged);
|
||||
EXPECT_EQ(frame.result.changedFieldId, "position");
|
||||
EXPECT_EQ(frame.result.changedValue, "1, 2, 9");
|
||||
EXPECT_DOUBLE_EQ(sections[0].fields[0].vector3Value.values[0], 1.0);
|
||||
EXPECT_DOUBLE_EQ(sections[0].fields[0].vector3Value.values[1], 2.0);
|
||||
EXPECT_DOUBLE_EQ(sections[0].fields[0].vector3Value.values[2], 9.0);
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridInteractionTest, AssetFieldCanRequestPickerAndClearInsidePropertyGridHost) {
|
||||
std::vector<UIEditorPropertyGridSection> sections = {
|
||||
{
|
||||
"renderer",
|
||||
"Renderer",
|
||||
{
|
||||
MakeAssetField(
|
||||
"material",
|
||||
"Material",
|
||||
"Assets/Materials/Stone.mat",
|
||||
"Stone.mat")
|
||||
},
|
||||
0.0f
|
||||
}
|
||||
};
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("renderer");
|
||||
UIPropertyEditModel propertyEditModel = {};
|
||||
UIEditorPropertyGridInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 360.0f),
|
||||
sections,
|
||||
{});
|
||||
|
||||
XCEngine::UI::Editor::Widgets::UIEditorAssetFieldSpec assetSpec = {};
|
||||
assetSpec.fieldId = "material";
|
||||
assetSpec.label = "Material";
|
||||
assetSpec.assetId = sections[0].fields[0].assetValue.assetId;
|
||||
assetSpec.displayName = sections[0].fields[0].assetValue.displayName;
|
||||
assetSpec.statusText = sections[0].fields[0].assetValue.statusText;
|
||||
assetSpec.emptyText = sections[0].fields[0].assetValue.emptyText;
|
||||
assetSpec.tint = sections[0].fields[0].assetValue.tint;
|
||||
assetSpec.showPickerButton = sections[0].fields[0].assetValue.showPickerButton;
|
||||
assetSpec.allowClear = sections[0].fields[0].assetValue.allowClear;
|
||||
assetSpec.showStatusBadge = sections[0].fields[0].assetValue.showStatusBadge;
|
||||
const auto assetLayout = BuildUIEditorAssetFieldLayout(
|
||||
frame.layout.fieldRowRects[0],
|
||||
assetSpec,
|
||||
BuildUIEditorPropertyGridAssetFieldMetrics({}));
|
||||
const UIPoint pickerCenter = RectCenter(assetLayout.pickerRect);
|
||||
const UIPoint clearCenter = RectCenter(assetLayout.clearRect);
|
||||
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 360.0f),
|
||||
sections,
|
||||
{
|
||||
MakePointerDown(pickerCenter.x, pickerCenter.y),
|
||||
MakePointerUp(pickerCenter.x, pickerCenter.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.pickerRequested);
|
||||
EXPECT_EQ(frame.result.requestedFieldId, "material");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("material"));
|
||||
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 360.0f),
|
||||
sections,
|
||||
{
|
||||
MakePointerDown(clearCenter.x, clearCenter.y),
|
||||
MakePointerUp(clearCenter.x, clearCenter.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.fieldValueChanged);
|
||||
EXPECT_EQ(frame.result.changedFieldId, "material");
|
||||
EXPECT_EQ(frame.result.changedValue, "None");
|
||||
EXPECT_TRUE(sections[0].fields[0].assetValue.assetId.empty());
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEditor/Collections/UIEditorScrollView.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorScrollViewBackground;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorScrollViewLayout;
|
||||
using XCEngine::UI::Editor::Widgets::ClampUIEditorScrollViewOffset;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorScrollView;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorScrollViewContentOrigin;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorScrollViewHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorScrollViewLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorScrollViewMetrics;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorScrollViewState;
|
||||
|
||||
TEST(UIEditorScrollViewTest, LayoutReservesScrollbarTrackAndThumbFromOverflow) {
|
||||
UIEditorScrollViewMetrics metrics = {};
|
||||
metrics.scrollbarWidth = 12.0f;
|
||||
metrics.scrollbarInset = 4.0f;
|
||||
metrics.minThumbHeight = 24.0f;
|
||||
|
||||
const UIEditorScrollViewLayout layout =
|
||||
BuildUIEditorScrollViewLayout(UIRect(10.0f, 20.0f, 240.0f, 120.0f), 360.0f, 60.0f, metrics);
|
||||
|
||||
EXPECT_TRUE(layout.hasScrollbar);
|
||||
EXPECT_FLOAT_EQ(layout.maxOffset, 240.0f);
|
||||
EXPECT_FLOAT_EQ(layout.verticalOffset, 60.0f);
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.width, 220.0f);
|
||||
EXPECT_FLOAT_EQ(layout.scrollbarTrackRect.x, 234.0f);
|
||||
EXPECT_FLOAT_EQ(layout.scrollbarTrackRect.height, 112.0f);
|
||||
EXPECT_NEAR(layout.scrollbarThumbRect.y, 42.666f, 0.01f);
|
||||
EXPECT_NEAR(layout.scrollbarThumbRect.height, 37.333f, 0.01f);
|
||||
|
||||
const UIPoint contentOrigin = ResolveUIEditorScrollViewContentOrigin(layout);
|
||||
EXPECT_FLOAT_EQ(contentOrigin.x, layout.contentRect.x);
|
||||
EXPECT_FLOAT_EQ(contentOrigin.y, layout.contentRect.y - 60.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorScrollViewTest, ClampAndNoScrollbarWhenContentFitsViewport) {
|
||||
const UIEditorScrollViewLayout layout =
|
||||
BuildUIEditorScrollViewLayout(UIRect(0.0f, 0.0f, 200.0f, 100.0f), 80.0f, 42.0f);
|
||||
|
||||
EXPECT_FALSE(layout.hasScrollbar);
|
||||
EXPECT_FLOAT_EQ(layout.maxOffset, 0.0f);
|
||||
EXPECT_FLOAT_EQ(layout.verticalOffset, 0.0f);
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.width, 200.0f);
|
||||
EXPECT_FLOAT_EQ(
|
||||
ClampUIEditorScrollViewOffset(UIRect(0.0f, 0.0f, 200.0f, 100.0f), 320.0f, 400.0f),
|
||||
220.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorScrollViewTest, HitTestPrioritizesThumbThenTrackThenContent) {
|
||||
const UIEditorScrollViewLayout layout =
|
||||
BuildUIEditorScrollViewLayout(UIRect(10.0f, 20.0f, 240.0f, 120.0f), 360.0f, 60.0f);
|
||||
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorScrollView(
|
||||
layout,
|
||||
UIPoint(
|
||||
layout.scrollbarThumbRect.x + 4.0f,
|
||||
layout.scrollbarThumbRect.y + 4.0f)).kind,
|
||||
UIEditorScrollViewHitTargetKind::ScrollbarThumb);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorScrollView(
|
||||
layout,
|
||||
UIPoint(
|
||||
layout.scrollbarTrackRect.x + 4.0f,
|
||||
layout.scrollbarTrackRect.y + layout.scrollbarTrackRect.height - 4.0f)).kind,
|
||||
UIEditorScrollViewHitTargetKind::ScrollbarTrack);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorScrollView(layout, UIPoint(layout.contentRect.x + 12.0f, layout.contentRect.y + 12.0f)).kind,
|
||||
UIEditorScrollViewHitTargetKind::Content);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorScrollView(layout, UIPoint(4.0f, 8.0f)).kind,
|
||||
UIEditorScrollViewHitTargetKind::None);
|
||||
}
|
||||
|
||||
TEST(UIEditorScrollViewTest, BackgroundCommandsEmitStableChrome) {
|
||||
const UIEditorScrollViewLayout layout =
|
||||
BuildUIEditorScrollViewLayout(UIRect(0.0f, 0.0f, 220.0f, 120.0f), 320.0f, 80.0f);
|
||||
UIEditorScrollViewState state = {};
|
||||
state.focused = true;
|
||||
state.scrollbarHovered = true;
|
||||
|
||||
UIDrawList drawList("ScrollView");
|
||||
AppendUIEditorScrollViewBackground(drawList, layout, state);
|
||||
|
||||
ASSERT_EQ(drawList.GetCommandCount(), 4u);
|
||||
EXPECT_EQ(drawList.GetCommands()[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(drawList.GetCommands()[1].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(drawList.GetCommands()[2].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(drawList.GetCommands()[3].type, UIDrawCommandType::FilledRect);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,176 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Collections/UIEditorScrollViewInteraction.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::UIEditorScrollViewInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorScrollViewInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorScrollViewHitTargetKind;
|
||||
|
||||
UIInputEvent MakePointerMove(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerMove;
|
||||
event.position = UIPoint(x, y);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerDown(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonDown;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = UIPointerButton::Left;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerUp(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonUp;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = UIPointerButton::Left;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerWheel(float x, float y, float wheelDelta) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerWheel;
|
||||
event.position = UIPoint(x, y);
|
||||
event.wheelDelta = wheelDelta;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeFocusLost() {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::FocusLost;
|
||||
return event;
|
||||
}
|
||||
|
||||
TEST(UIEditorScrollViewInteractionTest, WheelInsideViewportUpdatesOffsetAndClamps) {
|
||||
UIEditorScrollViewInteractionState state = {};
|
||||
float verticalOffset = 0.0f;
|
||||
|
||||
auto frame = UpdateUIEditorScrollViewInteraction(
|
||||
state,
|
||||
verticalOffset,
|
||||
UIRect(0.0f, 0.0f, 200.0f, 100.0f),
|
||||
360.0f,
|
||||
{ MakePointerWheel(24.0f, 32.0f, -1.0f) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.offsetChanged);
|
||||
EXPECT_FLOAT_EQ(verticalOffset, 48.0f);
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorScrollViewHitTargetKind::Content);
|
||||
|
||||
frame = UpdateUIEditorScrollViewInteraction(
|
||||
state,
|
||||
verticalOffset,
|
||||
UIRect(0.0f, 0.0f, 200.0f, 100.0f),
|
||||
360.0f,
|
||||
{ MakePointerWheel(24.0f, 32.0f, -20.0f) });
|
||||
EXPECT_TRUE(frame.result.offsetChanged);
|
||||
EXPECT_FLOAT_EQ(verticalOffset, 260.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorScrollViewInteractionTest, ThumbDragUpdatesOffsetAcrossPointerMoves) {
|
||||
UIEditorScrollViewInteractionState state = {};
|
||||
float verticalOffset = 0.0f;
|
||||
|
||||
const auto initialFrame = UpdateUIEditorScrollViewInteraction(
|
||||
state,
|
||||
verticalOffset,
|
||||
UIRect(0.0f, 0.0f, 220.0f, 120.0f),
|
||||
420.0f,
|
||||
{});
|
||||
const float thumbX = initialFrame.layout.scrollbarThumbRect.x + 4.0f;
|
||||
const float thumbY = initialFrame.layout.scrollbarThumbRect.y + 8.0f;
|
||||
|
||||
auto frame = UpdateUIEditorScrollViewInteraction(
|
||||
state,
|
||||
verticalOffset,
|
||||
UIRect(0.0f, 0.0f, 220.0f, 120.0f),
|
||||
420.0f,
|
||||
{ MakePointerDown(thumbX, thumbY) });
|
||||
EXPECT_TRUE(frame.result.startedThumbDrag);
|
||||
EXPECT_TRUE(state.scrollViewState.draggingScrollbarThumb);
|
||||
|
||||
frame = UpdateUIEditorScrollViewInteraction(
|
||||
state,
|
||||
verticalOffset,
|
||||
UIRect(0.0f, 0.0f, 220.0f, 120.0f),
|
||||
420.0f,
|
||||
{ MakePointerMove(thumbX, thumbY + 40.0f) });
|
||||
EXPECT_TRUE(frame.result.offsetChanged);
|
||||
EXPECT_GT(verticalOffset, 0.0f);
|
||||
|
||||
frame = UpdateUIEditorScrollViewInteraction(
|
||||
state,
|
||||
verticalOffset,
|
||||
UIRect(0.0f, 0.0f, 220.0f, 120.0f),
|
||||
420.0f,
|
||||
{ MakePointerUp(thumbX, thumbY + 40.0f) });
|
||||
EXPECT_TRUE(frame.result.endedThumbDrag);
|
||||
EXPECT_FALSE(state.scrollViewState.draggingScrollbarThumb);
|
||||
}
|
||||
|
||||
TEST(UIEditorScrollViewInteractionTest, ContentClickFocusesAndOutsideClickClearsFocus) {
|
||||
UIEditorScrollViewInteractionState state = {};
|
||||
float verticalOffset = 0.0f;
|
||||
|
||||
auto frame = UpdateUIEditorScrollViewInteraction(
|
||||
state,
|
||||
verticalOffset,
|
||||
UIRect(10.0f, 20.0f, 220.0f, 120.0f),
|
||||
420.0f,
|
||||
{ MakePointerDown(36.0f, 48.0f) });
|
||||
EXPECT_TRUE(frame.result.focusChanged);
|
||||
EXPECT_TRUE(state.scrollViewState.focused);
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorScrollViewHitTargetKind::Content);
|
||||
|
||||
frame = UpdateUIEditorScrollViewInteraction(
|
||||
state,
|
||||
verticalOffset,
|
||||
UIRect(10.0f, 20.0f, 220.0f, 120.0f),
|
||||
420.0f,
|
||||
{ MakePointerDown(300.0f, 220.0f) });
|
||||
EXPECT_TRUE(frame.result.focusChanged);
|
||||
EXPECT_FALSE(state.scrollViewState.focused);
|
||||
}
|
||||
|
||||
TEST(UIEditorScrollViewInteractionTest, FocusLostClearsHoverAndThumbDragState) {
|
||||
UIEditorScrollViewInteractionState state = {};
|
||||
float verticalOffset = 0.0f;
|
||||
|
||||
const auto initialFrame = UpdateUIEditorScrollViewInteraction(
|
||||
state,
|
||||
verticalOffset,
|
||||
UIRect(0.0f, 0.0f, 220.0f, 120.0f),
|
||||
420.0f,
|
||||
{});
|
||||
const float thumbX = initialFrame.layout.scrollbarThumbRect.x + 4.0f;
|
||||
const float thumbY = initialFrame.layout.scrollbarThumbRect.y + 8.0f;
|
||||
|
||||
UpdateUIEditorScrollViewInteraction(
|
||||
state,
|
||||
verticalOffset,
|
||||
UIRect(0.0f, 0.0f, 220.0f, 120.0f),
|
||||
420.0f,
|
||||
{ MakePointerDown(thumbX, thumbY) });
|
||||
ASSERT_TRUE(state.scrollViewState.draggingScrollbarThumb);
|
||||
|
||||
const auto frame = UpdateUIEditorScrollViewInteraction(
|
||||
state,
|
||||
verticalOffset,
|
||||
UIRect(0.0f, 0.0f, 220.0f, 120.0f),
|
||||
420.0f,
|
||||
{ MakeFocusLost() });
|
||||
EXPECT_TRUE(frame.result.focusChanged);
|
||||
EXPECT_FALSE(state.scrollViewState.focused);
|
||||
EXPECT_FALSE(state.scrollViewState.draggingScrollbarThumb);
|
||||
EXPECT_FALSE(state.hasPointerPosition);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,164 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEditor/Shell/UIEditorShellCompose.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceModel.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceSession.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::BuildUIEditorShellComposeLayout;
|
||||
using XCEngine::UI::Editor::ResolveUIEditorShellComposeRequest;
|
||||
using XCEngine::UI::Editor::UIEditorPanelPresentationKind;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorShellComposeFrame;
|
||||
using XCEngine::UI::Editor::UIEditorShellComposeModel;
|
||||
using XCEngine::UI::Editor::UIEditorShellComposeState;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorShellCompose;
|
||||
using XCEngine::UI::Editor::AppendUIEditorShellCompose;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorMenuBarItem;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSegment;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot;
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "hierarchy", "Hierarchy", UIEditorPanelPresentationKind::Placeholder, true, true, false },
|
||||
{ "scene", "Scene", UIEditorPanelPresentationKind::ViewportShell, false, true, false },
|
||||
{ "document", "Document", UIEditorPanelPresentationKind::Placeholder, true, true, true },
|
||||
{ "inspector", "Inspector", UIEditorPanelPresentationKind::Placeholder, true, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.24f,
|
||||
BuildUIEditorWorkspacePanel("hierarchy-node", "hierarchy", "Hierarchy", true),
|
||||
BuildUIEditorWorkspaceSplit(
|
||||
"main",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.72f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"center-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("scene-node", "scene", "Scene"),
|
||||
BuildUIEditorWorkspacePanel("document-node", "document", "Document", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspacePanel("inspector-node", "inspector", "Inspector", true)));
|
||||
workspace.activePanelId = "scene";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
UIEditorShellComposeModel BuildShellModel() {
|
||||
UIEditorShellComposeModel model = {};
|
||||
model.menuBarItems = {
|
||||
UIEditorMenuBarItem{ "file", "File", true, 0.0f },
|
||||
UIEditorMenuBarItem{ "window", "Window", true, 0.0f },
|
||||
UIEditorMenuBarItem{ "layout", "Layout", true, 0.0f }
|
||||
};
|
||||
model.statusSegments = {
|
||||
UIEditorStatusBarSegment{ "scene", "Scene", UIEditorStatusBarSlot::Leading, {}, true, true, 72.0f },
|
||||
UIEditorStatusBarSegment{ "frame", "16.7 ms", UIEditorStatusBarSlot::Trailing, {}, true, false, 72.0f }
|
||||
};
|
||||
|
||||
UIEditorWorkspacePanelPresentationModel presentation = {};
|
||||
presentation.panelId = "scene";
|
||||
presentation.kind = UIEditorPanelPresentationKind::ViewportShell;
|
||||
presentation.viewportShellModel.spec.chrome.title = "Scene";
|
||||
presentation.viewportShellModel.spec.chrome.showTopBar = true;
|
||||
presentation.viewportShellModel.spec.chrome.showBottomBar = true;
|
||||
presentation.viewportShellModel.frame.hasTexture = false;
|
||||
presentation.viewportShellModel.frame.statusText = "Viewport frame";
|
||||
model.workspacePresentations = { presentation };
|
||||
return model;
|
||||
}
|
||||
|
||||
bool ContainsTextCommand(const UIDrawList& drawList, std::string_view text) {
|
||||
for (const auto& command : drawList.GetCommands()) {
|
||||
if (command.type == UIDrawCommandType::Text && command.text == text) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorShellComposeTest, LayoutAllocatesMenuWorkspaceAndStatusBandsWithoutOverlap) {
|
||||
const UIEditorShellComposeModel model = BuildShellModel();
|
||||
|
||||
const auto layout = BuildUIEditorShellComposeLayout(
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
model.menuBarItems,
|
||||
model.statusSegments);
|
||||
|
||||
EXPECT_GT(layout.menuBarRect.height, 0.0f);
|
||||
EXPECT_GT(layout.workspaceRect.height, 0.0f);
|
||||
EXPECT_GT(layout.statusBarRect.height, 0.0f);
|
||||
EXPECT_LE(layout.menuBarRect.y + layout.menuBarRect.height, layout.workspaceRect.y);
|
||||
EXPECT_LE(layout.workspaceRect.y + layout.workspaceRect.height, layout.statusBarRect.y);
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.y, 0.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorShellComposeTest, ResolveRequestRoutesWorkspaceComposeThroughWorkspaceBand) {
|
||||
const auto registry = BuildPanelRegistry();
|
||||
const auto workspace = BuildWorkspace();
|
||||
const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
const UIEditorShellComposeModel model = BuildShellModel();
|
||||
|
||||
const auto request = ResolveUIEditorShellComposeRequest(
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
model);
|
||||
|
||||
EXPECT_FLOAT_EQ(request.workspaceRequest.dockHostLayout.bounds.x, request.layout.workspaceRect.x);
|
||||
EXPECT_FLOAT_EQ(request.workspaceRequest.dockHostLayout.bounds.y, request.layout.workspaceRect.y);
|
||||
EXPECT_FLOAT_EQ(request.workspaceRequest.dockHostLayout.bounds.width, request.layout.workspaceRect.width);
|
||||
EXPECT_FLOAT_EQ(request.workspaceRequest.dockHostLayout.bounds.height, request.layout.workspaceRect.height);
|
||||
ASSERT_EQ(request.workspaceRequest.viewportRequests.size(), 1u);
|
||||
EXPECT_EQ(request.workspaceRequest.viewportRequests.front().panelId, "scene");
|
||||
}
|
||||
|
||||
TEST(UIEditorShellComposeTest, AppendComposeEmitsMenuStatusAndWorkspaceCommandsTogether) {
|
||||
const auto registry = BuildPanelRegistry();
|
||||
const auto workspace = BuildWorkspace();
|
||||
const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
const UIEditorShellComposeModel model = BuildShellModel();
|
||||
|
||||
UIEditorShellComposeState state = {};
|
||||
const UIEditorShellComposeFrame frame = UpdateUIEditorShellCompose(
|
||||
state,
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
model,
|
||||
{});
|
||||
|
||||
UIDrawList drawList("EditorShellCompose");
|
||||
AppendUIEditorShellCompose(drawList, frame, model, state);
|
||||
|
||||
EXPECT_GT(drawList.GetCommandCount(), 20u);
|
||||
EXPECT_TRUE(ContainsTextCommand(drawList, "File"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawList, "Scene"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawList, "16.7 ms"));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,194 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Foundation/UIEditorShortcutManager.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::UIEditorCommandPanelSource;
|
||||
using XCEngine::UI::Editor::UIEditorCommandRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorShortcutDispatchStatus;
|
||||
using XCEngine::UI::Editor::UIEditorShortcutManager;
|
||||
using XCEngine::UI::Editor::UIEditorShortcutManagerValidationCode;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIShortcutBinding;
|
||||
using XCEngine::UI::UIShortcutContext;
|
||||
using XCEngine::UI::UIShortcutScope;
|
||||
|
||||
UIEditorCommandRegistry BuildCommandRegistry() {
|
||||
UIEditorCommandRegistry registry = {};
|
||||
registry.commands = {
|
||||
{
|
||||
"workspace.hide_active",
|
||||
"Hide Active",
|
||||
{ UIEditorWorkspaceCommandKind::HidePanel, UIEditorCommandPanelSource::ActivePanel, {} }
|
||||
},
|
||||
{
|
||||
"workspace.activate_details",
|
||||
"Activate Details",
|
||||
{ UIEditorWorkspaceCommandKind::ActivatePanel, UIEditorCommandPanelSource::FixedPanelId, "details" }
|
||||
},
|
||||
{
|
||||
"workspace.reset",
|
||||
"Reset Workspace",
|
||||
{ UIEditorWorkspaceCommandKind::ResetWorkspace, UIEditorCommandPanelSource::None, {} }
|
||||
}
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.66f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
UIShortcutBinding MakeBinding(
|
||||
std::string commandId,
|
||||
UIShortcutScope scope,
|
||||
XCEngine::UI::UIElementId ownerId,
|
||||
KeyCode keyCode,
|
||||
bool control = true) {
|
||||
UIShortcutBinding binding = {};
|
||||
binding.commandId = std::move(commandId);
|
||||
binding.scope = scope;
|
||||
binding.ownerId = ownerId;
|
||||
binding.triggerEventType = UIInputEventType::KeyDown;
|
||||
binding.chord.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
binding.chord.modifiers.control = control;
|
||||
return binding;
|
||||
}
|
||||
|
||||
UIInputEvent MakeCtrlKeyEvent(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
event.modifiers.control = true;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIShortcutContext BuildPanelShortcutContext(XCEngine::UI::UIElementId panelOwnerId) {
|
||||
UIShortcutContext context = {};
|
||||
context.commandScope.path = { 10u, panelOwnerId, 30u };
|
||||
context.commandScope.windowId = 10u;
|
||||
context.commandScope.panelId = panelOwnerId;
|
||||
context.commandScope.widgetId = 30u;
|
||||
return context;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorShortcutManagerTest, ValidationRejectsUnknownCommandAndConflictingChord) {
|
||||
UIEditorShortcutManager manager(BuildCommandRegistry());
|
||||
manager.RegisterBinding(MakeBinding("missing.command", UIShortcutScope::Global, 0u, KeyCode::H));
|
||||
|
||||
auto validation = manager.ValidateConfiguration();
|
||||
EXPECT_EQ(validation.code, UIEditorShortcutManagerValidationCode::UnknownCommandId);
|
||||
|
||||
manager = UIEditorShortcutManager(BuildCommandRegistry());
|
||||
manager.RegisterBinding(MakeBinding("workspace.hide_active", UIShortcutScope::Global, 0u, KeyCode::H));
|
||||
manager.RegisterBinding(MakeBinding("workspace.reset", UIShortcutScope::Global, 0u, KeyCode::H));
|
||||
validation = manager.ValidateConfiguration();
|
||||
EXPECT_EQ(validation.code, UIEditorShortcutManagerValidationCode::ConflictingBinding);
|
||||
}
|
||||
|
||||
TEST(UIEditorShortcutManagerTest, DispatchUsesActivePanelSourceForWorkspaceCommand) {
|
||||
UIEditorShortcutManager manager(BuildCommandRegistry());
|
||||
manager.RegisterBinding(MakeBinding("workspace.hide_active", UIShortcutScope::Global, 0u, KeyCode::H));
|
||||
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(
|
||||
XCEngine::UI::Editor::UIEditorPanelRegistry{
|
||||
{
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true }
|
||||
}
|
||||
},
|
||||
BuildWorkspace());
|
||||
|
||||
const auto result =
|
||||
manager.Dispatch(MakeCtrlKeyEvent(KeyCode::H), {}, controller);
|
||||
|
||||
EXPECT_EQ(result.status, UIEditorShortcutDispatchStatus::Dispatched);
|
||||
EXPECT_TRUE(result.commandExecuted);
|
||||
EXPECT_EQ(result.commandId, "workspace.hide_active");
|
||||
EXPECT_EQ(result.commandResult.status, UIEditorWorkspaceCommandStatus::Changed);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorShortcutManagerTest, DispatchPrefersPanelScopeBindingOverGlobalBinding) {
|
||||
UIEditorShortcutManager manager(BuildCommandRegistry());
|
||||
manager.RegisterBinding(MakeBinding("workspace.reset", UIShortcutScope::Global, 0u, KeyCode::P));
|
||||
manager.RegisterBinding(MakeBinding("workspace.activate_details", UIShortcutScope::Panel, 20u, KeyCode::P));
|
||||
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(
|
||||
XCEngine::UI::Editor::UIEditorPanelRegistry{
|
||||
{
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true }
|
||||
}
|
||||
},
|
||||
BuildWorkspace());
|
||||
|
||||
const auto result = manager.Dispatch(
|
||||
MakeCtrlKeyEvent(KeyCode::P),
|
||||
BuildPanelShortcutContext(20u),
|
||||
controller);
|
||||
|
||||
EXPECT_EQ(result.status, UIEditorShortcutDispatchStatus::Dispatched);
|
||||
EXPECT_EQ(result.commandId, "workspace.activate_details");
|
||||
EXPECT_EQ(result.commandResult.status, UIEditorWorkspaceCommandStatus::Changed);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "details");
|
||||
}
|
||||
|
||||
TEST(UIEditorShortcutManagerTest, DispatchSuppressesMatchedShortcutWhenTextInputIsActive) {
|
||||
UIEditorShortcutManager manager(BuildCommandRegistry());
|
||||
manager.RegisterBinding(MakeBinding("workspace.hide_active", UIShortcutScope::Global, 0u, KeyCode::H));
|
||||
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(
|
||||
XCEngine::UI::Editor::UIEditorPanelRegistry{
|
||||
{
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true }
|
||||
}
|
||||
},
|
||||
BuildWorkspace());
|
||||
|
||||
UIShortcutContext context = {};
|
||||
context.textInputActive = true;
|
||||
|
||||
const auto result =
|
||||
manager.Dispatch(MakeCtrlKeyEvent(KeyCode::H), context, controller);
|
||||
|
||||
EXPECT_EQ(result.status, UIEditorShortcutDispatchStatus::Suppressed);
|
||||
EXPECT_FALSE(result.commandExecuted);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a");
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEditor/Shell/UIEditorStatusBar.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIColor;
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorStatusBarBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorStatusBarForeground;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorStatusBarLayout;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorStatusBar;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorStatusBarDesiredSegmentWidth;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorStatusBarTextColor;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarInvalidIndex;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarPalette;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSegment;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarState;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarTextTone;
|
||||
|
||||
void ExpectColorEq(const UIColor& actual, const UIColor& expected) {
|
||||
EXPECT_FLOAT_EQ(actual.r, expected.r);
|
||||
EXPECT_FLOAT_EQ(actual.g, expected.g);
|
||||
EXPECT_FLOAT_EQ(actual.b, expected.b);
|
||||
EXPECT_FLOAT_EQ(actual.a, expected.a);
|
||||
}
|
||||
|
||||
std::vector<UIEditorStatusBarSegment> BuildSegments() {
|
||||
return {
|
||||
{ "scene", "Scene: Main", UIEditorStatusBarSlot::Leading, UIEditorStatusBarTextTone::Primary, true, true, 92.0f },
|
||||
{ "selection", "Selection: Camera", UIEditorStatusBarSlot::Leading, UIEditorStatusBarTextTone::Accent, true, false, 138.0f },
|
||||
{ "frame", "16.7 ms", UIEditorStatusBarSlot::Trailing, UIEditorStatusBarTextTone::Muted, true, true, 64.0f },
|
||||
{ "gpu", "GPU Ready", UIEditorStatusBarSlot::Trailing, UIEditorStatusBarTextTone::Primary, true, false, 86.0f }
|
||||
};
|
||||
}
|
||||
|
||||
TEST(UIEditorStatusBarTest, DesiredWidthUsesExplicitValueBeforeLabelEstimate) {
|
||||
UIEditorStatusBarSegment explicitWidth = {};
|
||||
explicitWidth.label = "Scene";
|
||||
explicitWidth.desiredWidth = 84.0f;
|
||||
|
||||
UIEditorStatusBarSegment inferredWidth = {};
|
||||
inferredWidth.label = "Scene";
|
||||
const XCEngine::UI::Editor::Widgets::UIEditorStatusBarMetrics metrics = {};
|
||||
|
||||
EXPECT_FLOAT_EQ(ResolveUIEditorStatusBarDesiredSegmentWidth(explicitWidth, metrics), 84.0f);
|
||||
EXPECT_FLOAT_EQ(
|
||||
ResolveUIEditorStatusBarDesiredSegmentWidth(inferredWidth, metrics),
|
||||
metrics.segmentPaddingX * 2.0f +
|
||||
static_cast<float>(inferredWidth.label.size()) * metrics.estimatedGlyphWidth);
|
||||
}
|
||||
|
||||
TEST(UIEditorStatusBarTest, LayoutBuildsLeadingAndTrailingSlotsWithSeparators) {
|
||||
const XCEngine::UI::Editor::Widgets::UIEditorStatusBarMetrics metrics = {};
|
||||
const UIEditorStatusBarLayout layout =
|
||||
BuildUIEditorStatusBarLayout(UIRect(20.0f, 40.0f, 520.0f, 28.0f), BuildSegments(), metrics);
|
||||
const float leftStart = 20.0f + metrics.outerPaddingX;
|
||||
const float rightLimit = 20.0f + 520.0f - metrics.outerPaddingX;
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.leadingSlotRect.x, leftStart);
|
||||
EXPECT_FLOAT_EQ(layout.segmentRects[0].x, leftStart);
|
||||
EXPECT_FLOAT_EQ(
|
||||
layout.separatorRects[0].x,
|
||||
layout.segmentRects[0].x + layout.segmentRects[0].width);
|
||||
EXPECT_FLOAT_EQ(
|
||||
layout.segmentRects[1].x,
|
||||
layout.separatorRects[0].x + layout.separatorRects[0].width + metrics.segmentGap);
|
||||
EXPECT_FLOAT_EQ(
|
||||
layout.leadingSlotRect.width,
|
||||
layout.segmentRects[1].x + layout.segmentRects[1].width - layout.leadingSlotRect.x);
|
||||
|
||||
EXPECT_FLOAT_EQ(
|
||||
layout.segmentRects[3].x + layout.segmentRects[3].width,
|
||||
rightLimit);
|
||||
EXPECT_FLOAT_EQ(
|
||||
layout.separatorRects[2].x,
|
||||
layout.segmentRects[2].x + layout.segmentRects[2].width + metrics.segmentGap);
|
||||
EXPECT_FLOAT_EQ(
|
||||
layout.segmentRects[3].x,
|
||||
layout.separatorRects[2].x + layout.separatorRects[2].width + metrics.segmentGap);
|
||||
EXPECT_FLOAT_EQ(layout.trailingSlotRect.x, layout.segmentRects[2].x);
|
||||
EXPECT_FLOAT_EQ(
|
||||
layout.trailingSlotRect.width,
|
||||
rightLimit - layout.trailingSlotRect.x);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.separatorRects[1].width, 0.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorStatusBarTest, HitTestReturnsSeparatorThenSegmentThenBackground) {
|
||||
const UIEditorStatusBarLayout layout =
|
||||
BuildUIEditorStatusBarLayout(UIRect(20.0f, 40.0f, 520.0f, 28.0f), BuildSegments());
|
||||
|
||||
auto hit = HitTestUIEditorStatusBar(
|
||||
layout,
|
||||
UIPoint(
|
||||
layout.separatorRects[0].x + layout.separatorRects[0].width * 0.5f,
|
||||
layout.separatorRects[0].y + layout.separatorRects[0].height * 0.5f));
|
||||
EXPECT_EQ(hit.kind, UIEditorStatusBarHitTargetKind::Separator);
|
||||
EXPECT_EQ(hit.index, 0u);
|
||||
|
||||
hit = HitTestUIEditorStatusBar(
|
||||
layout,
|
||||
UIPoint(
|
||||
layout.segmentRects[1].x + layout.segmentRects[1].width * 0.5f,
|
||||
layout.segmentRects[1].y + layout.segmentRects[1].height * 0.5f));
|
||||
EXPECT_EQ(hit.kind, UIEditorStatusBarHitTargetKind::Segment);
|
||||
EXPECT_EQ(hit.index, 1u);
|
||||
|
||||
hit = HitTestUIEditorStatusBar(layout, UIPoint(300.0f, 54.0f));
|
||||
EXPECT_EQ(hit.kind, UIEditorStatusBarHitTargetKind::Background);
|
||||
EXPECT_EQ(hit.index, UIEditorStatusBarInvalidIndex);
|
||||
}
|
||||
|
||||
TEST(UIEditorStatusBarTest, BackgroundAndForegroundEmitStableChromeAndTextCommands) {
|
||||
const auto segments = BuildSegments();
|
||||
UIEditorStatusBarState state = {};
|
||||
state.hoveredIndex = 0u;
|
||||
state.activeIndex = 1u;
|
||||
state.focused = true;
|
||||
|
||||
const UIEditorStatusBarPalette palette = {};
|
||||
const UIEditorStatusBarLayout layout =
|
||||
BuildUIEditorStatusBarLayout(UIRect(12.0f, 16.0f, 520.0f, 28.0f), segments);
|
||||
|
||||
UIDrawList background("StatusBarBackground");
|
||||
AppendUIEditorStatusBarBackground(background, layout, segments, state, palette);
|
||||
ASSERT_EQ(background.GetCommandCount(), 8u);
|
||||
EXPECT_EQ(background.GetCommands()[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(background.GetCommands()[1].type, UIDrawCommandType::RectOutline);
|
||||
ExpectColorEq(background.GetCommands()[1].color, palette.focusedBorderColor);
|
||||
|
||||
UIDrawList foreground("StatusBarForeground");
|
||||
AppendUIEditorStatusBarForeground(foreground, layout, segments, state, palette);
|
||||
ASSERT_EQ(foreground.GetCommandCount(), 4u);
|
||||
EXPECT_EQ(foreground.GetCommands()[0].text, "Scene: Main");
|
||||
EXPECT_EQ(foreground.GetCommands()[1].text, "Selection: Camera");
|
||||
ExpectColorEq(
|
||||
foreground.GetCommands()[1].color,
|
||||
ResolveUIEditorStatusBarTextColor(UIEditorStatusBarTextTone::Accent, palette));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,203 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEditor/Collections/UIEditorTabStrip.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorTabStripBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorTabStripForeground;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorTabStripLayout;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorTabStrip;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorTabStripDesiredHeaderLabelWidth;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorTabStripSelectedIndex;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripItem;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripMetrics;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripState;
|
||||
|
||||
TEST(UIEditorTabStripTest, DesiredHeaderWidthUsesLabelWidthAndLeftInsetOnly) {
|
||||
UIEditorTabStripMetrics metrics = {};
|
||||
metrics.layoutMetrics.tabHorizontalPadding = 10.0f;
|
||||
metrics.estimatedGlyphWidth = 8.0f;
|
||||
metrics.labelInsetX = 12.0f;
|
||||
|
||||
const float measuredWidth = ResolveUIEditorTabStripDesiredHeaderLabelWidth(
|
||||
UIEditorTabStripItem{ "doc-a", "ABCD", 0.0f },
|
||||
metrics);
|
||||
const float fixedWidth = ResolveUIEditorTabStripDesiredHeaderLabelWidth(
|
||||
UIEditorTabStripItem{ "doc-b", "Ignored", 42.0f },
|
||||
metrics);
|
||||
|
||||
EXPECT_FLOAT_EQ(measuredWidth, 34.0f);
|
||||
EXPECT_FLOAT_EQ(fixedWidth, 44.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripTest, SelectedIndexResolvesByTabIdAndFallsBackToValidRange) {
|
||||
const std::vector<UIEditorTabStripItem> items = {
|
||||
{ "doc-a", "Document A", 0.0f },
|
||||
{ "doc-b", "Document B", 0.0f },
|
||||
{ "doc-c", "Document C", 0.0f }
|
||||
};
|
||||
|
||||
EXPECT_EQ(ResolveUIEditorTabStripSelectedIndex(items, "doc-b"), 1u);
|
||||
EXPECT_EQ(ResolveUIEditorTabStripSelectedIndex(items, "missing", 2u), 2u);
|
||||
EXPECT_EQ(ResolveUIEditorTabStripSelectedIndex(items, "missing"), 0u);
|
||||
EXPECT_EQ(
|
||||
ResolveUIEditorTabStripSelectedIndex({}, "missing"),
|
||||
UIEditorTabStripInvalidIndex);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripTest, LayoutUsesCoreTabArrangementWithoutCloseButtons) {
|
||||
UIEditorTabStripMetrics metrics = {};
|
||||
metrics.layoutMetrics.headerHeight = 30.0f;
|
||||
metrics.layoutMetrics.tabMinWidth = 80.0f;
|
||||
metrics.layoutMetrics.tabHorizontalPadding = 12.0f;
|
||||
metrics.layoutMetrics.tabGap = 4.0f;
|
||||
metrics.labelInsetX = 12.0f;
|
||||
|
||||
const std::vector<UIEditorTabStripItem> items = {
|
||||
{ "doc-a", "Document A", 48.0f },
|
||||
{ "doc-b", "Document B", 40.0f }
|
||||
};
|
||||
|
||||
UIEditorTabStripState state = {};
|
||||
state.selectedIndex = 0u;
|
||||
|
||||
const UIEditorTabStripLayout layout =
|
||||
BuildUIEditorTabStripLayout(UIRect(10.0f, 20.0f, 260.0f, 180.0f), items, state, metrics);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.headerRect.x, 10.0f);
|
||||
EXPECT_FLOAT_EQ(layout.headerRect.y, 20.0f);
|
||||
EXPECT_FLOAT_EQ(layout.headerRect.width, 260.0f);
|
||||
EXPECT_FLOAT_EQ(layout.headerRect.height, 30.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.x, 10.0f);
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.y, 50.0f);
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.width, 260.0f);
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.height, 150.0f);
|
||||
|
||||
ASSERT_EQ(layout.tabHeaderRects.size(), 2u);
|
||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[0].x, 10.0f);
|
||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[0].width, 80.0f);
|
||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[1].x, 94.0f);
|
||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[1].width, 80.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripTest, HitTestPrioritizesTabThenHeaderThenContent) {
|
||||
const std::vector<UIEditorTabStripItem> items = {
|
||||
{ "doc-a", "Document A", 48.0f },
|
||||
{ "doc-b", "Document B", 40.0f }
|
||||
};
|
||||
|
||||
UIEditorTabStripState state = {};
|
||||
state.selectedIndex = 0u;
|
||||
const UIEditorTabStripLayout layout =
|
||||
BuildUIEditorTabStripLayout(UIRect(10.0f, 20.0f, 260.0f, 180.0f), items, state);
|
||||
|
||||
const auto rightSideTabHit = HitTestUIEditorTabStrip(
|
||||
layout,
|
||||
state,
|
||||
UIPoint(
|
||||
layout.tabHeaderRects[0].x + layout.tabHeaderRects[0].width - 2.0f,
|
||||
layout.tabHeaderRects[0].y + layout.tabHeaderRects[0].height * 0.5f));
|
||||
EXPECT_EQ(rightSideTabHit.kind, UIEditorTabStripHitTargetKind::Tab);
|
||||
EXPECT_EQ(rightSideTabHit.index, 0u);
|
||||
|
||||
const auto tabHit = HitTestUIEditorTabStrip(layout, state, UIPoint(40.0f, 34.0f));
|
||||
EXPECT_EQ(tabHit.kind, UIEditorTabStripHitTargetKind::Tab);
|
||||
EXPECT_EQ(tabHit.index, 0u);
|
||||
|
||||
const auto headerHit = HitTestUIEditorTabStrip(layout, state, UIPoint(200.0f, 34.0f));
|
||||
EXPECT_EQ(headerHit.kind, UIEditorTabStripHitTargetKind::HeaderBackground);
|
||||
EXPECT_EQ(headerHit.index, UIEditorTabStripInvalidIndex);
|
||||
|
||||
const auto contentHit = HitTestUIEditorTabStrip(layout, state, UIPoint(40.0f, 70.0f));
|
||||
EXPECT_EQ(contentHit.kind, UIEditorTabStripHitTargetKind::Content);
|
||||
EXPECT_EQ(contentHit.index, UIEditorTabStripInvalidIndex);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripTest, BackgroundAndForegroundEmitStableChromeCommands) {
|
||||
const std::vector<UIEditorTabStripItem> items = {
|
||||
{ "doc-a", "Document A", 48.0f },
|
||||
{ "doc-b", "Document B", 40.0f }
|
||||
};
|
||||
|
||||
UIEditorTabStripState state = {};
|
||||
state.selectedIndex = 0u;
|
||||
state.hoveredIndex = 1u;
|
||||
state.focused = true;
|
||||
|
||||
const UIEditorTabStripLayout layout =
|
||||
BuildUIEditorTabStripLayout(UIRect(10.0f, 20.0f, 260.0f, 180.0f), items, state);
|
||||
|
||||
UIDrawList background("TabStripBackground");
|
||||
AppendUIEditorTabStripBackground(background, layout, state);
|
||||
ASSERT_EQ(background.GetCommandCount(), 8u);
|
||||
const auto& backgroundCommands = background.GetCommands();
|
||||
EXPECT_EQ(backgroundCommands[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[1].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[2].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[3].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[4].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(backgroundCommands[5].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[6].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[7].type, UIDrawCommandType::RectOutline);
|
||||
|
||||
UIDrawList foreground("TabStripForeground");
|
||||
AppendUIEditorTabStripForeground(foreground, layout, items, state);
|
||||
ASSERT_EQ(foreground.GetCommandCount(), 8u);
|
||||
const auto& foregroundCommands = foreground.GetCommands();
|
||||
EXPECT_EQ(foregroundCommands[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(foregroundCommands[1].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(foregroundCommands[2].type, UIDrawCommandType::PushClipRect);
|
||||
EXPECT_EQ(foregroundCommands[3].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[3].text, "Document A");
|
||||
EXPECT_EQ(foregroundCommands[5].type, UIDrawCommandType::PushClipRect);
|
||||
EXPECT_EQ(foregroundCommands[6].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[6].text, "Document B");
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripTest, ForegroundCentersTabLabelsWithinHeaderContentArea) {
|
||||
UIEditorTabStripMetrics metrics = {};
|
||||
metrics.layoutMetrics.headerHeight = 22.0f;
|
||||
metrics.layoutMetrics.tabMinWidth = 68.0f;
|
||||
metrics.layoutMetrics.tabHorizontalPadding = 8.0f;
|
||||
metrics.layoutMetrics.tabGap = 1.0f;
|
||||
metrics.labelInsetX = 8.0f;
|
||||
|
||||
const std::vector<UIEditorTabStripItem> items = {
|
||||
{ "doc-a", "Scene", 30.0f },
|
||||
{ "doc-b", "Game", 24.0f }
|
||||
};
|
||||
|
||||
UIEditorTabStripState state = {};
|
||||
state.selectedIndex = 0u;
|
||||
const UIEditorTabStripLayout layout =
|
||||
BuildUIEditorTabStripLayout(UIRect(10.0f, 20.0f, 220.0f, 120.0f), items, state, metrics);
|
||||
|
||||
UIDrawList foreground("TabStripForeground");
|
||||
AppendUIEditorTabStripForeground(foreground, layout, items, state, {}, metrics);
|
||||
|
||||
const auto& commands = foreground.GetCommands();
|
||||
ASSERT_EQ(commands[3].type, UIDrawCommandType::Text);
|
||||
ASSERT_EQ(commands[6].type, UIDrawCommandType::Text);
|
||||
|
||||
const float firstExpectedX =
|
||||
layout.tabHeaderRects[0].x +
|
||||
std::floor((layout.tabHeaderRects[0].width - items[0].desiredHeaderLabelWidth) * 0.5f);
|
||||
const float secondExpectedX =
|
||||
layout.tabHeaderRects[1].x +
|
||||
std::floor((layout.tabHeaderRects[1].width - items[1].desiredHeaderLabelWidth) * 0.5f);
|
||||
|
||||
EXPECT_FLOAT_EQ(commands[3].position.x, firstExpectedX);
|
||||
EXPECT_FLOAT_EQ(commands[6].position.x, secondExpectedX);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,354 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Collections/UIEditorTabStripInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::UIEditorTabStripInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorTabStripInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripItem;
|
||||
|
||||
std::vector<UIEditorTabStripItem> BuildTabItems() {
|
||||
return {
|
||||
{ "doc-a", "Document A", 48.0f },
|
||||
{ "doc-b", "Document B", 46.0f },
|
||||
{ "doc-c", "Document C", 44.0f }
|
||||
};
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerMove(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerMove;
|
||||
event.position = UIPoint(x, y);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerDown(float x, float y, UIPointerButton button = UIPointerButton::Left) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonDown;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerUp(float x, float y, UIPointerButton button = UIPointerButton::Left) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonUp;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKeyDown(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerLeave() {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerLeave;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeFocusLost() {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::FocusLost;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIPoint RectCenter(const XCEngine::UI::UIRect& rect) {
|
||||
return UIPoint(rect.x + rect.width * 0.5f, rect.y + rect.height * 0.5f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, PointerMoveUpdatesHoveredTabOnly) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-a";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{});
|
||||
|
||||
const auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{ MakePointerMove(
|
||||
initialFrame.layout.tabHeaderRects[1].x + 12.0f,
|
||||
initialFrame.layout.tabHeaderRects[1].y + 12.0f) });
|
||||
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorTabStripHitTargetKind::Tab);
|
||||
EXPECT_EQ(state.tabStripState.hoveredIndex, 1u);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, LeftClickTabSelectsAndFocusesStrip) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-a";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint tabCenter = RectCenter(initialFrame.layout.tabHeaderRects[1]);
|
||||
|
||||
const auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(tabCenter.x, tabCenter.y),
|
||||
MakePointerUp(tabCenter.x, tabCenter.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_EQ(frame.result.selectedTabId, "doc-b");
|
||||
EXPECT_EQ(frame.result.selectedIndex, 1u);
|
||||
EXPECT_EQ(selectedTabId, "doc-b");
|
||||
EXPECT_TRUE(state.tabStripState.focused);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, KeyboardNavigationMovesSelectionWhenFocused) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-b";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
state.tabStripState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::Right) });
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_EQ(frame.result.selectedTabId, "doc-c");
|
||||
EXPECT_EQ(selectedTabId, "doc-c");
|
||||
|
||||
frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::Home) });
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_EQ(frame.result.selectedTabId, "doc-a");
|
||||
EXPECT_EQ(selectedTabId, "doc-a");
|
||||
|
||||
frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::End) });
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_EQ(frame.result.selectedTabId, "doc-c");
|
||||
EXPECT_EQ(selectedTabId, "doc-c");
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, OutsideClickAndFocusLostClearFocusAndHover) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-b";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
state.tabStripState.focused = true;
|
||||
state.tabStripState.hoveredIndex = 1u;
|
||||
|
||||
auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(420.0f, 240.0f),
|
||||
MakePointerUp(420.0f, 240.0f)
|
||||
});
|
||||
EXPECT_FALSE(state.tabStripState.focused);
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorTabStripHitTargetKind::None);
|
||||
EXPECT_EQ(selectedTabId, "doc-b");
|
||||
|
||||
frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{ MakePointerLeave(), MakeFocusLost() });
|
||||
EXPECT_FALSE(state.tabStripState.focused);
|
||||
EXPECT_EQ(state.tabStripState.hoveredIndex, UIEditorTabStripInvalidIndex);
|
||||
EXPECT_FALSE(state.hasPointerPosition);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, DraggingTabRequestsPointerCaptureAndReportsDraggedTab) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-a";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint sourceCenter = RectCenter(initialFrame.layout.tabHeaderRects[0]);
|
||||
const UIPoint dragPoint(
|
||||
initialFrame.layout.tabHeaderRects[2].x + 8.0f,
|
||||
sourceCenter.y);
|
||||
|
||||
const auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(sourceCenter.x, sourceCenter.y),
|
||||
MakePointerMove(dragPoint.x, dragPoint.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
EXPECT_TRUE(frame.result.dragStarted);
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_EQ(frame.result.draggedTabId, "doc-a");
|
||||
EXPECT_EQ(frame.result.dragSourceIndex, 0u);
|
||||
EXPECT_TRUE(state.dragCaptureActive);
|
||||
EXPECT_EQ(state.dragSourceIndex, 0u);
|
||||
EXPECT_TRUE(state.dragState.active);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, ReleasingActiveDragReleasesCaptureAndClearsDragState) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-a";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint sourceCenter = RectCenter(initialFrame.layout.tabHeaderRects[0]);
|
||||
const UIPoint dragPoint(
|
||||
initialFrame.layout.tabHeaderRects[2].x + 8.0f,
|
||||
sourceCenter.y);
|
||||
|
||||
const auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(sourceCenter.x, sourceCenter.y),
|
||||
MakePointerMove(dragPoint.x, dragPoint.y),
|
||||
MakePointerUp(dragPoint.x, dragPoint.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(frame.result.dragEnded);
|
||||
EXPECT_TRUE(frame.result.dragCanceled);
|
||||
EXPECT_EQ(frame.result.draggedTabId, "doc-a");
|
||||
EXPECT_EQ(frame.result.dragSourceIndex, 0u);
|
||||
EXPECT_FALSE(state.dragCaptureActive);
|
||||
EXPECT_EQ(state.dragSourceIndex, UIEditorTabStripInvalidIndex);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, EscapeCancelsActiveTabDragAndReleasesCapture) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-a";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint sourceCenter = RectCenter(initialFrame.layout.tabHeaderRects[1]);
|
||||
const UIPoint dragPoint(
|
||||
initialFrame.layout.tabHeaderRects[2].x + 8.0f,
|
||||
sourceCenter.y);
|
||||
|
||||
auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(sourceCenter.x, sourceCenter.y),
|
||||
MakePointerMove(dragPoint.x, dragPoint.y)
|
||||
});
|
||||
ASSERT_TRUE(frame.result.dragStarted);
|
||||
ASSERT_TRUE(state.dragCaptureActive);
|
||||
|
||||
frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::Escape) });
|
||||
|
||||
EXPECT_TRUE(frame.result.dragCanceled);
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_EQ(frame.result.draggedTabId, "doc-b");
|
||||
EXPECT_FALSE(state.dragCaptureActive);
|
||||
EXPECT_EQ(state.dragSourceIndex, UIEditorTabStripInvalidIndex);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, FocusLostCancelsActiveTabDrag) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-a";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint sourceCenter = RectCenter(initialFrame.layout.tabHeaderRects[0]);
|
||||
const UIPoint dragPoint(
|
||||
initialFrame.layout.tabHeaderRects[2].x + 8.0f,
|
||||
sourceCenter.y);
|
||||
|
||||
auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(sourceCenter.x, sourceCenter.y),
|
||||
MakePointerMove(dragPoint.x, dragPoint.y)
|
||||
});
|
||||
ASSERT_TRUE(frame.result.dragStarted);
|
||||
|
||||
frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{ MakeFocusLost() });
|
||||
|
||||
EXPECT_TRUE(frame.result.dragCanceled);
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_FALSE(state.tabStripState.focused);
|
||||
EXPECT_FALSE(state.dragCaptureActive);
|
||||
EXPECT_EQ(state.dragSourceIndex, UIEditorTabStripInvalidIndex);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorTextField.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorTextFieldLayout;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorTextField;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldSpec;
|
||||
|
||||
TEST(UIEditorTextFieldTest, LayoutBuildsValueRect) {
|
||||
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
|
||||
const auto layout = BuildUIEditorTextFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
|
||||
|
||||
EXPECT_GT(layout.labelRect.width, 0.0f);
|
||||
EXPECT_GT(layout.controlRect.width, 0.0f);
|
||||
EXPECT_GT(layout.valueRect.width, 0.0f);
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.width, layout.valueRect.width);
|
||||
}
|
||||
|
||||
TEST(UIEditorTextFieldTest, HitTestResolvesValueBoxAndRow) {
|
||||
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
|
||||
const auto layout = BuildUIEditorTextFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
|
||||
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorTextField(layout, UIPoint(layout.valueRect.x + 4.0f, layout.valueRect.y + 4.0f)).kind,
|
||||
UIEditorTextFieldHitTargetKind::ValueBox);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorTextField(layout, UIPoint(layout.labelRect.x + 4.0f, layout.labelRect.y + 4.0f)).kind,
|
||||
UIEditorTextFieldHitTargetKind::Row);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,152 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorTextFieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::UIEditorTextFieldInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorTextFieldInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldSpec;
|
||||
|
||||
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKey(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeCharacter(char character) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::Character;
|
||||
event.character = static_cast<std::uint32_t>(character);
|
||||
return event;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorTextFieldInteractionTest, ClickValueBoxStartsEditing) {
|
||||
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
|
||||
UIEditorTextFieldInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorTextFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{});
|
||||
|
||||
frame = UpdateUIEditorTextFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
frame.layout.valueRect.x + 2.0f,
|
||||
frame.layout.valueRect.y + 2.0f,
|
||||
UIPointerButton::Left),
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonUp,
|
||||
frame.layout.valueRect.x + 2.0f,
|
||||
frame.layout.valueRect.y + 2.0f,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.textFieldState.editing);
|
||||
EXPECT_EQ(spec.value, "Player");
|
||||
}
|
||||
|
||||
TEST(UIEditorTextFieldInteractionTest, EnterStartsEditingAndCommitUpdatesValue) {
|
||||
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
|
||||
UIEditorTextFieldInteractionState state = {};
|
||||
state.textFieldState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorTextFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Enter) });
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.textFieldState.editing);
|
||||
|
||||
frame = UpdateUIEditorTextFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeCharacter('X'), MakeKey(KeyCode::Enter) });
|
||||
|
||||
EXPECT_TRUE(frame.result.editCommitted);
|
||||
EXPECT_TRUE(frame.result.valueChanged);
|
||||
EXPECT_FALSE(state.textFieldState.editing);
|
||||
EXPECT_EQ(spec.value, "PlayerX");
|
||||
EXPECT_EQ(frame.result.committedText, "PlayerX");
|
||||
}
|
||||
|
||||
TEST(UIEditorTextFieldInteractionTest, CharacterInputCanStartEditingAndEscapeCancels) {
|
||||
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
|
||||
UIEditorTextFieldInteractionState state = {};
|
||||
state.textFieldState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorTextFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeCharacter('N') });
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.textFieldState.editing);
|
||||
EXPECT_EQ(state.textFieldState.displayText, "N");
|
||||
|
||||
frame = UpdateUIEditorTextFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Escape) });
|
||||
EXPECT_TRUE(frame.result.editCanceled);
|
||||
EXPECT_FALSE(state.textFieldState.editing);
|
||||
EXPECT_EQ(spec.value, "Player");
|
||||
EXPECT_EQ(state.textFieldState.displayText, "Player");
|
||||
}
|
||||
|
||||
TEST(UIEditorTextFieldInteractionTest, FocusLostCommitsEdit) {
|
||||
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
|
||||
UIEditorTextFieldInteractionState state = {};
|
||||
state.textFieldState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorTextFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Enter), MakeCharacter('1') });
|
||||
EXPECT_TRUE(state.textFieldState.editing);
|
||||
|
||||
frame = UpdateUIEditorTextFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{
|
||||
UIInputEvent {
|
||||
.type = UIInputEventType::FocusLost
|
||||
}
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.editCommitted);
|
||||
EXPECT_EQ(spec.value, "Player1");
|
||||
EXPECT_FALSE(state.textFieldState.editing);
|
||||
EXPECT_FALSE(state.textFieldState.focused);
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Collections/UIEditorTreePanelBehavior.h>
|
||||
|
||||
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
|
||||
namespace TreeDrag = XCEngine::UI::Editor::Collections::TreeDragDrop;
|
||||
namespace Widgets = XCEngine::UI::Editor::Widgets;
|
||||
|
||||
using XCEngine::UI::Editor::BuildUIEditorTreePanelInlineRenameBounds;
|
||||
using XCEngine::UI::Editor::BuildUIEditorTreePanelInputEvents;
|
||||
using XCEngine::UI::Editor::BuildUIEditorTreePanelInteractionInputEvents;
|
||||
using XCEngine::UI::Editor::FilterUIEditorTreePanelPointerInputEvents;
|
||||
using XCEngine::UI::Editor::FindUIEditorTreePanelVisibleItemIndex;
|
||||
using XCEngine::UI::Editor::UIEditorTreePanelInputFilterOptions;
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIPointerButton;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
|
||||
UIInputEvent MakePointerMove(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerMove;
|
||||
event.position = UIPoint(x, y);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerButtonDown(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonDown;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = UIPointerButton::Left;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerButtonUp(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonUp;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = UIPointerButton::Left;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKeyDown() {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeFocusLost() {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::FocusLost;
|
||||
return event;
|
||||
}
|
||||
|
||||
struct TreeFixture {
|
||||
std::vector<Widgets::UIEditorTreeViewItem> items = {};
|
||||
::XCEngine::UI::Widgets::UIExpansionModel expansion = {};
|
||||
Widgets::UIEditorTreeViewLayout layout = {};
|
||||
};
|
||||
|
||||
TreeFixture BuildTreeFixture() {
|
||||
TreeFixture fixture = {};
|
||||
fixture.items = {
|
||||
Widgets::UIEditorTreeViewItem{ .itemId = "root", .label = "Root" },
|
||||
Widgets::UIEditorTreeViewItem{ .itemId = "child", .label = "Child", .depth = 1u }
|
||||
};
|
||||
fixture.expansion.Expand("root");
|
||||
fixture.layout = Widgets::BuildUIEditorTreeViewLayout(
|
||||
UIRect(0.0f, 0.0f, 240.0f, 120.0f),
|
||||
fixture.items,
|
||||
fixture.expansion);
|
||||
return fixture;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorTreePanelBehaviorTests, FilterPanelInputHonorsBoundsAndInputFocus) {
|
||||
const std::vector<UIInputEvent> filtered =
|
||||
BuildUIEditorTreePanelInputEvents(
|
||||
UIRect(0.0f, 0.0f, 100.0f, 100.0f),
|
||||
{
|
||||
MakePointerMove(40.0f, 40.0f),
|
||||
MakePointerMove(140.0f, 40.0f),
|
||||
MakeKeyDown(),
|
||||
MakeFocusLost()
|
||||
},
|
||||
UIEditorTreePanelInputFilterOptions{
|
||||
.allowInteraction = true,
|
||||
.hasInputFocus = true,
|
||||
.captureActive = false
|
||||
});
|
||||
|
||||
ASSERT_EQ(filtered.size(), 3u);
|
||||
EXPECT_EQ(filtered[0].type, UIInputEventType::PointerMove);
|
||||
EXPECT_EQ(filtered[1].type, UIInputEventType::KeyDown);
|
||||
EXPECT_EQ(filtered[2].type, UIInputEventType::FocusLost);
|
||||
}
|
||||
|
||||
TEST(UIEditorTreePanelBehaviorTests, FilterPointerInputSuppressesPointerOnlyEvents) {
|
||||
const std::vector<UIInputEvent> filtered =
|
||||
FilterUIEditorTreePanelPointerInputEvents(
|
||||
{
|
||||
MakePointerMove(10.0f, 10.0f),
|
||||
MakePointerButtonDown(10.0f, 10.0f),
|
||||
MakeKeyDown(),
|
||||
MakeFocusLost()
|
||||
},
|
||||
true);
|
||||
|
||||
ASSERT_EQ(filtered.size(), 2u);
|
||||
EXPECT_EQ(filtered[0].type, UIInputEventType::KeyDown);
|
||||
EXPECT_EQ(filtered[1].type, UIInputEventType::FocusLost);
|
||||
}
|
||||
|
||||
TEST(UIEditorTreePanelBehaviorTests, BuildInteractionInputEventsSuppressesDragGesturePreview) {
|
||||
const TreeFixture fixture = BuildTreeFixture();
|
||||
TreeDrag::State dragState = {};
|
||||
ASSERT_GE(fixture.layout.rowRects.size(), 2u);
|
||||
const UIRect sourceRow = fixture.layout.rowRects[1];
|
||||
const UIRect targetRow = fixture.layout.rowRects[0];
|
||||
|
||||
const std::vector<UIInputEvent> filtered =
|
||||
BuildUIEditorTreePanelInteractionInputEvents(
|
||||
dragState,
|
||||
fixture.layout,
|
||||
fixture.items,
|
||||
{
|
||||
MakePointerButtonDown(
|
||||
sourceRow.x + 12.0f,
|
||||
sourceRow.y + sourceRow.height * 0.5f),
|
||||
MakePointerMove(
|
||||
sourceRow.x + 24.0f,
|
||||
sourceRow.y + sourceRow.height * 0.5f),
|
||||
MakePointerMove(
|
||||
targetRow.x + 12.0f,
|
||||
targetRow.y + targetRow.height * 0.5f),
|
||||
MakePointerButtonUp(
|
||||
targetRow.x + 12.0f,
|
||||
targetRow.y + targetRow.height * 0.5f),
|
||||
MakeFocusLost()
|
||||
});
|
||||
|
||||
ASSERT_EQ(filtered.size(), 2u);
|
||||
EXPECT_EQ(filtered[0].type, UIInputEventType::PointerButtonDown);
|
||||
EXPECT_EQ(filtered[1].type, UIInputEventType::FocusLost);
|
||||
}
|
||||
|
||||
TEST(UIEditorTreePanelBehaviorTests, VisibleIndexAndRenameBoundsFollowCurrentLayout) {
|
||||
const TreeFixture fixture = BuildTreeFixture();
|
||||
const Widgets::UIEditorTextFieldMetrics hostedMetrics = {
|
||||
.valueTextInsetX = 8.0f
|
||||
};
|
||||
|
||||
const std::size_t visibleIndex =
|
||||
FindUIEditorTreePanelVisibleItemIndex(fixture.layout, fixture.items, "child");
|
||||
ASSERT_EQ(visibleIndex, 1u);
|
||||
|
||||
const UIRect bounds =
|
||||
BuildUIEditorTreePanelInlineRenameBounds(
|
||||
fixture.layout,
|
||||
fixture.items,
|
||||
"child",
|
||||
hostedMetrics);
|
||||
const UIRect& rowRect = fixture.layout.rowRects[visibleIndex];
|
||||
const UIRect& labelRect = fixture.layout.labelRects[visibleIndex];
|
||||
|
||||
EXPECT_FLOAT_EQ(bounds.x, (std::max)(rowRect.x, labelRect.x - hostedMetrics.valueTextInsetX));
|
||||
EXPECT_FLOAT_EQ(bounds.y, rowRect.y);
|
||||
EXPECT_FLOAT_EQ(bounds.height, rowRect.height);
|
||||
EXPECT_FLOAT_EQ(bounds.width, (std::max)(120.0f, rowRect.x + rowRect.width - 8.0f - bounds.x));
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
|
||||
#include <XCEngine/UI/Widgets/UISelectionModel.h>
|
||||
#include <XCEditor/Collections/UIEditorTreeView.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Widgets::UIExpansionModel;
|
||||
using XCEngine::UI::Widgets::UISelectionModel;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorTreeViewBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorTreeViewForeground;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorTreeViewLayout;
|
||||
using XCEngine::UI::Editor::Widgets::CollectUIEditorTreeViewVisibleItemIndices;
|
||||
using XCEngine::UI::Editor::Widgets::DoesUIEditorTreeViewItemHaveChildren;
|
||||
using XCEngine::UI::Editor::Widgets::FindUIEditorTreeViewFirstVisibleChildItemIndex;
|
||||
using XCEngine::UI::Editor::Widgets::FindUIEditorTreeViewItemIndex;
|
||||
using XCEngine::UI::Editor::Widgets::FindUIEditorTreeViewParentItemIndex;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorTreeView;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewItem;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewInvalidIndex;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewMetrics;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewState;
|
||||
|
||||
bool ContainsTextCommand(const UIDrawList& drawList, std::string_view text) {
|
||||
for (const auto& command : drawList.GetCommands()) {
|
||||
if (command.type == UIDrawCommandType::Text && command.text == text) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ContainsCommandType(const UIDrawList& drawList, UIDrawCommandType type) {
|
||||
for (const auto& command : drawList.GetCommands()) {
|
||||
if (command.type == type) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<UIEditorTreeViewItem> BuildTreeItems() {
|
||||
return {
|
||||
{ "scene", "Scene", 0u, false, 0.0f },
|
||||
{ "camera", "Camera", 1u, true, 0.0f },
|
||||
{ "lights", "Lights", 1u, false, 0.0f },
|
||||
{ "sun", "Directional Light", 2u, true, 0.0f },
|
||||
{ "ui", "UI Root", 0u, false, 0.0f },
|
||||
{ "canvas", "Canvas", 1u, true, 0.0f }
|
||||
};
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewTest, ChildDetectionUsesFlatHierarchyDepthRules) {
|
||||
const std::vector<UIEditorTreeViewItem> items = BuildTreeItems();
|
||||
|
||||
EXPECT_TRUE(DoesUIEditorTreeViewItemHaveChildren(items, 0u));
|
||||
EXPECT_FALSE(DoesUIEditorTreeViewItemHaveChildren(items, 1u));
|
||||
EXPECT_TRUE(DoesUIEditorTreeViewItemHaveChildren(items, 2u));
|
||||
EXPECT_TRUE(DoesUIEditorTreeViewItemHaveChildren(items, 4u));
|
||||
|
||||
EXPECT_EQ(FindUIEditorTreeViewItemIndex(items, "lights"), 2u);
|
||||
EXPECT_EQ(FindUIEditorTreeViewParentItemIndex(items, 0u), UIEditorTreeViewInvalidIndex);
|
||||
EXPECT_EQ(FindUIEditorTreeViewParentItemIndex(items, 1u), 0u);
|
||||
EXPECT_EQ(FindUIEditorTreeViewParentItemIndex(items, 3u), 2u);
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewTest, VisibleItemsFollowExpansionModel) {
|
||||
const std::vector<UIEditorTreeViewItem> items = BuildTreeItems();
|
||||
|
||||
UIExpansionModel expansionModel = {};
|
||||
EXPECT_EQ(
|
||||
FindUIEditorTreeViewFirstVisibleChildItemIndex(items, expansionModel, 0u),
|
||||
UIEditorTreeViewInvalidIndex);
|
||||
EXPECT_EQ(
|
||||
CollectUIEditorTreeViewVisibleItemIndices(items, expansionModel),
|
||||
std::vector<std::size_t>({ 0u, 4u }));
|
||||
|
||||
expansionModel.Expand("scene");
|
||||
EXPECT_EQ(
|
||||
FindUIEditorTreeViewFirstVisibleChildItemIndex(items, expansionModel, 0u),
|
||||
1u);
|
||||
EXPECT_EQ(
|
||||
CollectUIEditorTreeViewVisibleItemIndices(items, expansionModel),
|
||||
std::vector<std::size_t>({ 0u, 1u, 2u, 4u }));
|
||||
|
||||
expansionModel.Expand("lights");
|
||||
expansionModel.Expand("ui");
|
||||
EXPECT_EQ(
|
||||
FindUIEditorTreeViewFirstVisibleChildItemIndex(items, expansionModel, 2u),
|
||||
3u);
|
||||
EXPECT_EQ(
|
||||
CollectUIEditorTreeViewVisibleItemIndices(items, expansionModel),
|
||||
std::vector<std::size_t>({ 0u, 1u, 2u, 3u, 4u, 5u }));
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewTest, LayoutBuildsIndentedDisclosureAndLabelRects) {
|
||||
const std::vector<UIEditorTreeViewItem> items = BuildTreeItems();
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("scene");
|
||||
|
||||
UIEditorTreeViewMetrics metrics = {};
|
||||
metrics.rowHeight = 24.0f;
|
||||
metrics.rowGap = 4.0f;
|
||||
metrics.horizontalPadding = 10.0f;
|
||||
metrics.indentWidth = 20.0f;
|
||||
metrics.disclosureExtent = 10.0f;
|
||||
metrics.disclosureLabelGap = 6.0f;
|
||||
|
||||
const UIEditorTreeViewLayout layout =
|
||||
BuildUIEditorTreeViewLayout(UIRect(20.0f, 30.0f, 280.0f, 240.0f), items, expansionModel, metrics);
|
||||
|
||||
ASSERT_EQ(layout.visibleItemIndices.size(), 4u);
|
||||
EXPECT_EQ(layout.visibleItemIndices[0], 0u);
|
||||
EXPECT_EQ(layout.visibleItemIndices[1], 1u);
|
||||
EXPECT_EQ(layout.visibleItemIndices[2], 2u);
|
||||
EXPECT_EQ(layout.visibleItemIndices[3], 4u);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.rowRects[0].x, 20.0f);
|
||||
EXPECT_FLOAT_EQ(layout.rowRects[0].y, 30.0f);
|
||||
EXPECT_FLOAT_EQ(layout.rowRects[0].height, 24.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.disclosureRects[0].x, 30.0f);
|
||||
EXPECT_FLOAT_EQ(layout.disclosureRects[1].x, 50.0f);
|
||||
EXPECT_FLOAT_EQ(layout.disclosureRects[2].x, 50.0f);
|
||||
EXPECT_FLOAT_EQ(layout.labelRects[0].x, 46.0f);
|
||||
EXPECT_FLOAT_EQ(layout.labelRects[1].x, 66.0f);
|
||||
EXPECT_TRUE(layout.itemHasChildren[0]);
|
||||
EXPECT_FALSE(layout.itemHasChildren[1]);
|
||||
EXPECT_TRUE(layout.itemExpanded[0]);
|
||||
EXPECT_FALSE(layout.itemExpanded[2]);
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewTest, HitTestPrioritizesDisclosureBeforeRow) {
|
||||
const std::vector<UIEditorTreeViewItem> items = BuildTreeItems();
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("scene");
|
||||
|
||||
const UIEditorTreeViewLayout layout =
|
||||
BuildUIEditorTreeViewLayout(UIRect(20.0f, 30.0f, 280.0f, 240.0f), items, expansionModel);
|
||||
|
||||
const auto disclosureHit = HitTestUIEditorTreeView(layout, UIPoint(34.0f, 44.0f));
|
||||
EXPECT_EQ(disclosureHit.kind, UIEditorTreeViewHitTargetKind::Disclosure);
|
||||
EXPECT_EQ(disclosureHit.itemIndex, 0u);
|
||||
|
||||
const auto rowHit = HitTestUIEditorTreeView(layout, UIPoint(88.0f, 44.0f));
|
||||
EXPECT_EQ(rowHit.kind, UIEditorTreeViewHitTargetKind::Row);
|
||||
EXPECT_EQ(rowHit.itemIndex, 0u);
|
||||
|
||||
const auto emptyHit = HitTestUIEditorTreeView(layout, UIPoint(12.0f, 18.0f));
|
||||
EXPECT_EQ(emptyHit.kind, UIEditorTreeViewHitTargetKind::None);
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewTest, BackgroundAndForegroundEmitStableCommands) {
|
||||
const std::vector<UIEditorTreeViewItem> items = BuildTreeItems();
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("scene");
|
||||
expansionModel.Expand("ui");
|
||||
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("camera");
|
||||
|
||||
UIEditorTreeViewState state = {};
|
||||
state.hoveredItemId = "lights";
|
||||
state.focused = true;
|
||||
|
||||
const UIEditorTreeViewLayout layout =
|
||||
BuildUIEditorTreeViewLayout(UIRect(20.0f, 30.0f, 280.0f, 240.0f), items, expansionModel);
|
||||
|
||||
UIDrawList background("TreeViewBackground");
|
||||
AppendUIEditorTreeViewBackground(background, layout, items, selectionModel, state);
|
||||
ASSERT_EQ(background.GetCommandCount(), 4u);
|
||||
EXPECT_EQ(background.GetCommands()[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(background.GetCommands()[1].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(background.GetCommands()[2].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(background.GetCommands()[3].type, UIDrawCommandType::FilledRect);
|
||||
|
||||
UIDrawList foreground("TreeViewForeground");
|
||||
AppendUIEditorTreeViewForeground(foreground, layout, items);
|
||||
ASSERT_EQ(foreground.GetCommandCount(), 20u);
|
||||
EXPECT_EQ(foreground.GetCommands()[0].type, UIDrawCommandType::PushClipRect);
|
||||
EXPECT_TRUE(ContainsCommandType(foreground, UIDrawCommandType::FilledTriangle));
|
||||
EXPECT_TRUE(ContainsTextCommand(foreground, "Scene"));
|
||||
EXPECT_TRUE(ContainsTextCommand(foreground, "Camera"));
|
||||
EXPECT_EQ(foreground.GetCommands()[19].type, UIDrawCommandType::PopClipRect);
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewTest, LeadingIconAddsImageCommandAndReservesIconRect) {
|
||||
std::vector<UIEditorTreeViewItem> items = {
|
||||
{ "folder", "Assets", 0u, true, 0.0f, XCEngine::UI::UITextureHandle { 1u, 18u, 18u } }
|
||||
};
|
||||
UIExpansionModel expansionModel = {};
|
||||
|
||||
UIEditorTreeViewMetrics metrics = {};
|
||||
metrics.rowHeight = 20.0f;
|
||||
metrics.horizontalPadding = 6.0f;
|
||||
metrics.disclosureExtent = 18.0f;
|
||||
metrics.disclosureLabelGap = 2.0f;
|
||||
metrics.iconExtent = 18.0f;
|
||||
metrics.iconLabelGap = 2.0f;
|
||||
|
||||
const UIEditorTreeViewLayout layout =
|
||||
BuildUIEditorTreeViewLayout(UIRect(10.0f, 12.0f, 240.0f, 80.0f), items, expansionModel, metrics);
|
||||
|
||||
ASSERT_EQ(layout.iconRects.size(), 1u);
|
||||
EXPECT_FLOAT_EQ(layout.iconRects[0].x, 36.0f);
|
||||
EXPECT_FLOAT_EQ(layout.iconRects[0].y, 13.0f);
|
||||
EXPECT_FLOAT_EQ(layout.labelRects[0].x, 56.0f);
|
||||
|
||||
UIDrawList foreground("TreeViewForegroundWithIcon");
|
||||
AppendUIEditorTreeViewForeground(foreground, layout, items);
|
||||
EXPECT_TRUE(ContainsCommandType(foreground, UIDrawCommandType::Image));
|
||||
EXPECT_TRUE(ContainsTextCommand(foreground, "Assets"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,619 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Collections/UIEditorTreeViewInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIInputModifiers;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Widgets::UIExpansionModel;
|
||||
using XCEngine::UI::Widgets::UISelectionModel;
|
||||
using XCEngine::UI::Editor::UIEditorTreeViewInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorTreeViewInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorTreeViewLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewItem;
|
||||
|
||||
std::vector<UIEditorTreeViewItem> BuildTreeItems() {
|
||||
return {
|
||||
{ "scene", "Scene", 0u, false, 0.0f },
|
||||
{ "camera", "Camera", 1u, true, 0.0f },
|
||||
{ "lights", "Lights", 1u, false, 0.0f },
|
||||
{ "sun", "Directional Light", 2u, true, 0.0f },
|
||||
{ "ui", "UI Root", 0u, false, 0.0f },
|
||||
{ "canvas", "Canvas", 1u, true, 0.0f }
|
||||
};
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerMove(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerMove;
|
||||
event.position = UIPoint(x, y);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputModifiers MakeShiftModifiers() {
|
||||
UIInputModifiers modifiers = {};
|
||||
modifiers.shift = true;
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
UIInputModifiers MakeControlModifiers() {
|
||||
UIInputModifiers modifiers = {};
|
||||
modifiers.control = true;
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerDown(
|
||||
float x,
|
||||
float y,
|
||||
UIPointerButton button = UIPointerButton::Left,
|
||||
UIInputModifiers modifiers = {}) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonDown;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
event.modifiers = modifiers;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerUp(
|
||||
float x,
|
||||
float y,
|
||||
UIPointerButton button = UIPointerButton::Left,
|
||||
UIInputModifiers modifiers = {}) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonUp;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
event.modifiers = modifiers;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerLeave() {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerLeave;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeFocusLost() {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::FocusLost;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKeyDown(KeyCode keyCode, UIInputModifiers modifiers = {}) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
event.modifiers = modifiers;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIPoint RectCenter(const XCEngine::UI::UIRect& rect) {
|
||||
return UIPoint(rect.x + rect.width * 0.5f, rect.y + rect.height * 0.5f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorTreeViewInteractionTest, PointerMoveUpdatesHoveredItemAndHitTarget) {
|
||||
const auto items = BuildTreeItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("scene");
|
||||
UIEditorTreeViewInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint lightsCenter = RectCenter(initialFrame.layout.rowRects[2]);
|
||||
|
||||
const auto frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakePointerMove(lightsCenter.x, lightsCenter.y) });
|
||||
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorTreeViewHitTargetKind::Row);
|
||||
EXPECT_EQ(frame.result.hitTarget.itemIndex, 2u);
|
||||
EXPECT_EQ(state.treeViewState.hoveredItemId, "lights");
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewInteractionTest, LeftClickRowSelectsItemAndFocusesTree) {
|
||||
const auto items = BuildTreeItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("scene");
|
||||
UIEditorTreeViewInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint cameraCenter = RectCenter(initialFrame.layout.rowRects[1]);
|
||||
|
||||
const auto frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(cameraCenter.x, cameraCenter.y),
|
||||
MakePointerUp(cameraCenter.x, cameraCenter.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_EQ(frame.result.selectedItemId, "camera");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("camera"));
|
||||
EXPECT_TRUE(state.treeViewState.focused);
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewInteractionTest, LeftClickDisclosureTogglesExpansionAndRebuildsLayout) {
|
||||
const auto items = BuildTreeItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("scene");
|
||||
UIEditorTreeViewInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint lightsDisclosureCenter = RectCenter(initialFrame.layout.disclosureRects[2]);
|
||||
|
||||
const auto frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(lightsDisclosureCenter.x, lightsDisclosureCenter.y),
|
||||
MakePointerUp(lightsDisclosureCenter.x, lightsDisclosureCenter.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.expansionChanged);
|
||||
EXPECT_EQ(frame.result.toggledItemId, "lights");
|
||||
EXPECT_TRUE(expansionModel.IsExpanded("lights"));
|
||||
ASSERT_EQ(frame.layout.visibleItemIndices.size(), 5u);
|
||||
EXPECT_EQ(frame.layout.visibleItemIndices[3], 3u);
|
||||
EXPECT_EQ(frame.layout.visibleItemIndices[4], 4u);
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewInteractionTest, DisclosureClickKeepsKeyboardCurrentAnchoredToSelection) {
|
||||
const auto items = BuildTreeItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("camera");
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("scene");
|
||||
UIEditorTreeViewInteractionState state = {};
|
||||
state.treeViewState.focused = true;
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint lightsDisclosureCenter = RectCenter(initialFrame.layout.disclosureRects[2]);
|
||||
|
||||
const auto frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(lightsDisclosureCenter.x, lightsDisclosureCenter.y),
|
||||
MakePointerUp(lightsDisclosureCenter.x, lightsDisclosureCenter.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.expansionChanged);
|
||||
EXPECT_FALSE(frame.result.selectionChanged);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("camera"));
|
||||
ASSERT_TRUE(state.keyboardNavigation.HasCurrentIndex());
|
||||
EXPECT_EQ(state.keyboardNavigation.GetCurrentIndex(), 1u);
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewInteractionTest, RightClickRowSelectsItemAndMarksSecondaryClick) {
|
||||
const auto items = BuildTreeItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("scene");
|
||||
UIEditorTreeViewInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint lightsCenter = RectCenter(initialFrame.layout.rowRects[2]);
|
||||
|
||||
const auto frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(lightsCenter.x, lightsCenter.y, UIPointerButton::Right),
|
||||
MakePointerUp(lightsCenter.x, lightsCenter.y, UIPointerButton::Right)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.secondaryClicked);
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_EQ(frame.result.selectedItemId, "lights");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("lights"));
|
||||
EXPECT_TRUE(state.treeViewState.focused);
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewInteractionTest, ControlClickRowTogglesMembershipWithoutDroppingExistingSelection) {
|
||||
const auto items = BuildTreeItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelections({ "camera", "lights" }, "lights");
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("scene");
|
||||
UIEditorTreeViewInteractionState state = {};
|
||||
state.treeViewState.focused = true;
|
||||
state.selectionAnchorId = "lights";
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint sceneCenter = RectCenter(initialFrame.layout.rowRects[0]);
|
||||
auto frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(sceneCenter.x, sceneCenter.y, UIPointerButton::Left, MakeControlModifiers()),
|
||||
MakePointerUp(sceneCenter.x, sceneCenter.y, UIPointerButton::Left, MakeControlModifiers())
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("camera"));
|
||||
EXPECT_TRUE(selectionModel.IsSelected("lights"));
|
||||
EXPECT_TRUE(selectionModel.IsSelected("scene"));
|
||||
EXPECT_EQ(selectionModel.GetSelectedId(), "scene");
|
||||
|
||||
const UIPoint lightsCenter = RectCenter(initialFrame.layout.rowRects[2]);
|
||||
frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(lightsCenter.x, lightsCenter.y, UIPointerButton::Left, MakeControlModifiers()),
|
||||
MakePointerUp(lightsCenter.x, lightsCenter.y, UIPointerButton::Left, MakeControlModifiers())
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("camera"));
|
||||
EXPECT_FALSE(selectionModel.IsSelected("lights"));
|
||||
EXPECT_TRUE(selectionModel.IsSelected("scene"));
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewInteractionTest, ShiftClickRowSelectsVisibleRangeFromAnchor) {
|
||||
const auto items = BuildTreeItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("camera");
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("scene");
|
||||
UIEditorTreeViewInteractionState state = {};
|
||||
state.treeViewState.focused = true;
|
||||
state.selectionAnchorId = "camera";
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint uiCenter = RectCenter(initialFrame.layout.rowRects[3]);
|
||||
|
||||
const auto frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(uiCenter.x, uiCenter.y, UIPointerButton::Left, MakeShiftModifiers()),
|
||||
MakePointerUp(uiCenter.x, uiCenter.y, UIPointerButton::Left, MakeShiftModifiers())
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_EQ(frame.result.selectedItemId, "ui");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("camera"));
|
||||
EXPECT_TRUE(selectionModel.IsSelected("lights"));
|
||||
EXPECT_TRUE(selectionModel.IsSelected("ui"));
|
||||
EXPECT_EQ(selectionModel.GetSelectionCount(), 3u);
|
||||
EXPECT_EQ(state.selectionAnchorId, "camera");
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewInteractionTest, RightClickSelectedRowKeepsExistingMultiSelection) {
|
||||
const auto items = BuildTreeItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelections({ "camera", "lights", "ui" }, "camera");
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("scene");
|
||||
UIEditorTreeViewInteractionState state = {};
|
||||
state.treeViewState.focused = true;
|
||||
state.selectionAnchorId = "camera";
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint lightsCenter = RectCenter(initialFrame.layout.rowRects[2]);
|
||||
|
||||
const auto frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(lightsCenter.x, lightsCenter.y, UIPointerButton::Right),
|
||||
MakePointerUp(lightsCenter.x, lightsCenter.y, UIPointerButton::Right)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.secondaryClicked);
|
||||
EXPECT_FALSE(frame.result.selectionChanged);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("camera"));
|
||||
EXPECT_TRUE(selectionModel.IsSelected("lights"));
|
||||
EXPECT_TRUE(selectionModel.IsSelected("ui"));
|
||||
EXPECT_EQ(selectionModel.GetSelectionCount(), 3u);
|
||||
EXPECT_EQ(selectionModel.GetSelectedId(), "lights");
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewInteractionTest, OutsideClickAndFocusLostClearFocusAndHover) {
|
||||
const auto items = BuildTreeItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("scene");
|
||||
UIEditorTreeViewInteractionState state = {};
|
||||
state.treeViewState.focused = true;
|
||||
state.treeViewState.hoveredItemId = "camera";
|
||||
|
||||
auto frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakePointerDown(400.0f, 260.0f), MakePointerUp(400.0f, 260.0f) });
|
||||
EXPECT_FALSE(state.treeViewState.focused);
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorTreeViewHitTargetKind::None);
|
||||
|
||||
frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakePointerLeave(), MakeFocusLost() });
|
||||
EXPECT_FALSE(state.treeViewState.focused);
|
||||
EXPECT_TRUE(state.treeViewState.hoveredItemId.empty());
|
||||
EXPECT_FALSE(state.hasPointerPosition);
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewInteractionTest, KeyboardNavigationMovesSelectionAcrossVisibleRows) {
|
||||
const auto items = BuildTreeItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("camera");
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("scene");
|
||||
UIEditorTreeViewInteractionState state = {};
|
||||
state.treeViewState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::Down) });
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("lights"));
|
||||
EXPECT_EQ(state.keyboardNavigation.GetCurrentIndex(), 2u);
|
||||
|
||||
frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::Home) });
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("scene"));
|
||||
|
||||
frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::End) });
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("ui"));
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewInteractionTest, ShiftArrowExtendsVisibleRangeFromAnchor) {
|
||||
const auto items = BuildTreeItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("camera");
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("scene");
|
||||
UIEditorTreeViewInteractionState state = {};
|
||||
state.treeViewState.focused = true;
|
||||
state.selectionAnchorId = "camera";
|
||||
|
||||
const auto frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::Down, MakeShiftModifiers()) });
|
||||
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("camera"));
|
||||
EXPECT_TRUE(selectionModel.IsSelected("lights"));
|
||||
EXPECT_EQ(selectionModel.GetSelectionCount(), 2u);
|
||||
EXPECT_EQ(frame.result.selectedItemId, "lights");
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewInteractionTest, F2RequestsRenameForPrimarySelectionWhenFocused) {
|
||||
const auto items = BuildTreeItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("lights");
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("scene");
|
||||
UIEditorTreeViewInteractionState state = {};
|
||||
state.treeViewState.focused = true;
|
||||
|
||||
const auto frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::F2) });
|
||||
|
||||
EXPECT_TRUE(frame.result.renameRequested);
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_EQ(frame.result.renameItemId, "lights");
|
||||
EXPECT_EQ(frame.result.selectedItemId, "lights");
|
||||
EXPECT_EQ(frame.result.selectedVisibleIndex, 2u);
|
||||
EXPECT_FALSE(frame.result.selectionChanged);
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewInteractionTest, RightAndLeftKeysExpandCollapseAndMoveHierarchySelection) {
|
||||
const auto items = BuildTreeItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("lights");
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("scene");
|
||||
UIEditorTreeViewInteractionState state = {};
|
||||
state.treeViewState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::Right) });
|
||||
EXPECT_TRUE(frame.result.expansionChanged);
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_TRUE(expansionModel.IsExpanded("lights"));
|
||||
EXPECT_EQ(frame.result.toggledItemId, "lights");
|
||||
EXPECT_EQ(frame.result.selectedItemId, "lights");
|
||||
EXPECT_EQ(frame.result.selectedVisibleIndex, 2u);
|
||||
|
||||
frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::Right) });
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("sun"));
|
||||
|
||||
frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::Left) });
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("lights"));
|
||||
|
||||
frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::Left) });
|
||||
EXPECT_TRUE(frame.result.expansionChanged);
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_FALSE(expansionModel.IsExpanded("lights"));
|
||||
EXPECT_EQ(frame.result.selectedItemId, "lights");
|
||||
EXPECT_EQ(frame.result.selectedVisibleIndex, 2u);
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewInteractionTest, CollapsingAncestorRehomesHiddenSelectionToCollapsedItem) {
|
||||
const auto items = BuildTreeItems();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("sun");
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("scene");
|
||||
expansionModel.Expand("lights");
|
||||
UIEditorTreeViewInteractionState state = {};
|
||||
state.treeViewState.focused = true;
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint lightsDisclosureCenter = RectCenter(initialFrame.layout.disclosureRects[2]);
|
||||
|
||||
const auto frame = UpdateUIEditorTreeViewInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 240.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(lightsDisclosureCenter.x, lightsDisclosureCenter.y),
|
||||
MakePointerUp(lightsDisclosureCenter.x, lightsDisclosureCenter.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.expansionChanged);
|
||||
EXPECT_FALSE(expansionModel.IsExpanded("lights"));
|
||||
EXPECT_TRUE(selectionModel.IsSelected("lights"));
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorVector2Field.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorVector2FieldLayout;
|
||||
using XCEngine::UI::Editor::Widgets::FormatUIEditorVector2FieldComponentValue;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector2Field;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldSpec;
|
||||
|
||||
UIRect MakeInspectorBounds() {
|
||||
return UIRect(0.0f, 0.0f, 392.0f, 22.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector2FieldTest, FormatSupportsPerComponentDisplay) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.25, -3.5 };
|
||||
|
||||
EXPECT_EQ(FormatUIEditorVector2FieldComponentValue(spec, 0u), "1.25");
|
||||
EXPECT_EQ(FormatUIEditorVector2FieldComponentValue(spec, 1u), "-3.5");
|
||||
}
|
||||
|
||||
TEST(UIEditorVector2FieldTest, LayoutBuildsTwoComponentRects) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
const auto layout = BuildUIEditorVector2FieldLayout(MakeInspectorBounds(), spec);
|
||||
|
||||
EXPECT_GT(layout.labelRect.width, 0.0f);
|
||||
EXPECT_GT(layout.controlRect.width, 0.0f);
|
||||
EXPECT_GT(layout.componentRects[0].width, 0.0f);
|
||||
EXPECT_GT(layout.componentRects[1].width, 0.0f);
|
||||
EXPECT_LT(layout.componentRects[0].x + layout.componentRects[0].width, layout.componentRects[1].x);
|
||||
EXPECT_GT(layout.componentPrefixRects[0].width, 0.0f);
|
||||
EXPECT_GT(layout.componentValueRects[0].width, 0.0f);
|
||||
EXPECT_GT(layout.componentValueRects[0].x, layout.componentPrefixRects[0].x + layout.componentPrefixRects[0].width);
|
||||
EXPECT_GT(layout.componentValueRects[1].x, layout.componentPrefixRects[1].x + layout.componentPrefixRects[1].width);
|
||||
EXPECT_EQ(layout.componentValueRects[0].height, layout.componentRects[0].height);
|
||||
EXPECT_EQ(layout.componentValueRects[1].height, layout.componentRects[1].height);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector2FieldTest, HitTestTreatsAxisLabelAreaAsComponentHost) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
const auto layout = BuildUIEditorVector2FieldLayout(MakeInspectorBounds(), spec);
|
||||
|
||||
const auto prefixHit = HitTestUIEditorVector2Field(
|
||||
layout,
|
||||
UIPoint(
|
||||
layout.componentPrefixRects[0].x + layout.componentPrefixRects[0].width * 0.5f,
|
||||
layout.componentPrefixRects[0].y + layout.componentPrefixRects[0].height * 0.5f));
|
||||
EXPECT_EQ(prefixHit.kind, UIEditorVector2FieldHitTargetKind::Component);
|
||||
EXPECT_EQ(prefixHit.componentIndex, 0u);
|
||||
|
||||
const auto valueHit = HitTestUIEditorVector2Field(
|
||||
layout,
|
||||
UIPoint(
|
||||
layout.componentValueRects[1].x + 4.0f,
|
||||
layout.componentValueRects[1].y + 4.0f));
|
||||
EXPECT_EQ(valueHit.kind, UIEditorVector2FieldHitTargetKind::Component);
|
||||
EXPECT_EQ(valueHit.componentIndex, 1u);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector2FieldTest, HitTestResolvesComponentAndRow) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
const auto layout = BuildUIEditorVector2FieldLayout(MakeInspectorBounds(), spec);
|
||||
|
||||
const auto firstHit = HitTestUIEditorVector2Field(
|
||||
layout,
|
||||
UIPoint(layout.componentRects[0].x + 4.0f, layout.componentRects[0].y + 4.0f));
|
||||
EXPECT_EQ(firstHit.kind, UIEditorVector2FieldHitTargetKind::Component);
|
||||
EXPECT_EQ(firstHit.componentIndex, 0u);
|
||||
|
||||
const auto rowHit = HitTestUIEditorVector2Field(
|
||||
layout,
|
||||
UIPoint(layout.labelRect.x + 4.0f, layout.labelRect.y + 4.0f));
|
||||
EXPECT_EQ(rowHit.kind, UIEditorVector2FieldHitTargetKind::Row);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,199 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorVector2FieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::UIEditorVector2FieldInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorVector2FieldInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldSpec;
|
||||
|
||||
UIRect MakeInspectorBounds() {
|
||||
return UIRect(0.0f, 0.0f, 392.0f, 22.0f);
|
||||
}
|
||||
|
||||
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKey(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeCharacter(char character) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::Character;
|
||||
event.character = static_cast<std::uint32_t>(character);
|
||||
return event;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorVector2FieldInteractionTest, ClickSecondComponentStartsEditing) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0 };
|
||||
UIEditorVector2FieldInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{});
|
||||
|
||||
frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
frame.layout.componentRects[1].x + 4.0f,
|
||||
frame.layout.componentRects[1].y + 4.0f,
|
||||
UIPointerButton::Left),
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonUp,
|
||||
frame.layout.componentRects[1].x + 4.0f,
|
||||
frame.layout.componentRects[1].y + 4.0f,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.vector2FieldState.editing);
|
||||
EXPECT_EQ(state.vector2FieldState.selectedComponentIndex, 1u);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector2FieldInteractionTest, ClickAxisLabelAreaAlsoStartsEditing) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0 };
|
||||
UIEditorVector2FieldInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{});
|
||||
|
||||
const float clickX =
|
||||
frame.layout.componentPrefixRects[0].x + frame.layout.componentPrefixRects[0].width * 0.5f;
|
||||
const float clickY =
|
||||
frame.layout.componentPrefixRects[0].y + frame.layout.componentPrefixRects[0].height * 0.5f;
|
||||
|
||||
frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{
|
||||
MakePointer(UIInputEventType::PointerButtonDown, clickX, clickY, UIPointerButton::Left),
|
||||
MakePointer(UIInputEventType::PointerButtonUp, clickX, clickY, UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.vector2FieldState.editing);
|
||||
EXPECT_EQ(state.vector2FieldState.selectedComponentIndex, 0u);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector2FieldInteractionTest, TabSelectsNextComponentAndArrowAppliesStep) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0 };
|
||||
spec.step = 0.5;
|
||||
UIEditorVector2FieldInteractionState state = {};
|
||||
state.vector2FieldState.focused = true;
|
||||
state.vector2FieldState.selectedComponentIndex = 0u;
|
||||
|
||||
auto frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{ MakeKey(KeyCode::Tab) });
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_EQ(state.vector2FieldState.selectedComponentIndex, 1u);
|
||||
|
||||
frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{ MakeKey(KeyCode::Up) });
|
||||
EXPECT_TRUE(frame.result.stepApplied);
|
||||
EXPECT_EQ(frame.result.changedComponentIndex, 1u);
|
||||
EXPECT_DOUBLE_EQ(spec.values[1], 2.5);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector2FieldInteractionTest, EnterStartsEditingAndCommitUpdatesSelectedComponent) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0 };
|
||||
spec.integerMode = true;
|
||||
UIEditorVector2FieldInteractionState state = {};
|
||||
state.vector2FieldState.focused = true;
|
||||
state.vector2FieldState.selectedComponentIndex = 0u;
|
||||
|
||||
auto frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{ MakeKey(KeyCode::Enter) });
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.vector2FieldState.editing);
|
||||
|
||||
frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{ MakeCharacter('4'), MakeKey(KeyCode::Enter) });
|
||||
|
||||
EXPECT_TRUE(frame.result.editCommitted);
|
||||
EXPECT_TRUE(frame.result.valueChanged);
|
||||
EXPECT_EQ(frame.result.changedComponentIndex, 0u);
|
||||
EXPECT_DOUBLE_EQ(spec.values[0], 14.0);
|
||||
EXPECT_DOUBLE_EQ(spec.values[1], 2.0);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector2FieldInteractionTest, CharacterInputCanStartEditingAndEscapeCancels) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0 };
|
||||
UIEditorVector2FieldInteractionState state = {};
|
||||
state.vector2FieldState.focused = true;
|
||||
state.vector2FieldState.selectedComponentIndex = 1u;
|
||||
|
||||
auto frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{ MakeCharacter('9') });
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.vector2FieldState.editing);
|
||||
EXPECT_EQ(state.vector2FieldState.displayTexts[1], "9");
|
||||
|
||||
frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{ MakeKey(KeyCode::Escape) });
|
||||
EXPECT_TRUE(frame.result.editCanceled);
|
||||
EXPECT_FALSE(state.vector2FieldState.editing);
|
||||
EXPECT_DOUBLE_EQ(spec.values[1], 2.0);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorVector3Field.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorVector3FieldLayout;
|
||||
using XCEngine::UI::Editor::Widgets::FormatUIEditorVector3FieldComponentValue;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector3Field;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldSpec;
|
||||
|
||||
TEST(UIEditorVector3FieldTest, FormatSupportsPerComponentDisplay) {
|
||||
UIEditorVector3FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.25, -3.5, 8.0 };
|
||||
|
||||
EXPECT_EQ(FormatUIEditorVector3FieldComponentValue(spec, 0u), "1.25");
|
||||
EXPECT_EQ(FormatUIEditorVector3FieldComponentValue(spec, 1u), "-3.5");
|
||||
EXPECT_EQ(FormatUIEditorVector3FieldComponentValue(spec, 2u), "8");
|
||||
}
|
||||
|
||||
TEST(UIEditorVector3FieldTest, LayoutBuildsThreeComponentRects) {
|
||||
UIEditorVector3FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
const auto layout = BuildUIEditorVector3FieldLayout(UIRect(0.0f, 0.0f, 520.0f, 32.0f), spec);
|
||||
|
||||
EXPECT_GT(layout.labelRect.width, 0.0f);
|
||||
EXPECT_GT(layout.componentRects[0].width, 0.0f);
|
||||
EXPECT_GT(layout.componentRects[1].width, 0.0f);
|
||||
EXPECT_GT(layout.componentRects[2].width, 0.0f);
|
||||
EXPECT_LT(layout.componentRects[0].x + layout.componentRects[0].width, layout.componentRects[1].x);
|
||||
EXPECT_LT(layout.componentRects[1].x + layout.componentRects[1].width, layout.componentRects[2].x);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector3FieldTest, HitTestResolvesComponentAndRow) {
|
||||
UIEditorVector3FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
const auto layout = BuildUIEditorVector3FieldLayout(UIRect(0.0f, 0.0f, 520.0f, 32.0f), spec);
|
||||
|
||||
const auto thirdHit = HitTestUIEditorVector3Field(
|
||||
layout,
|
||||
UIPoint(layout.componentRects[2].x + 4.0f, layout.componentRects[2].y + 4.0f));
|
||||
EXPECT_EQ(thirdHit.kind, UIEditorVector3FieldHitTargetKind::Component);
|
||||
EXPECT_EQ(thirdHit.componentIndex, 2u);
|
||||
|
||||
const auto rowHit = HitTestUIEditorVector3Field(
|
||||
layout,
|
||||
UIPoint(layout.labelRect.x + 4.0f, layout.labelRect.y + 4.0f));
|
||||
EXPECT_EQ(rowHit.kind, UIEditorVector3FieldHitTargetKind::Row);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,164 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorVector3FieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::UIEditorVector3FieldInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorVector3FieldInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldSpec;
|
||||
|
||||
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKey(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeCharacter(char character) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::Character;
|
||||
event.character = static_cast<std::uint32_t>(character);
|
||||
return event;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorVector3FieldInteractionTest, ClickThirdComponentStartsEditing) {
|
||||
UIEditorVector3FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0, 3.0 };
|
||||
UIEditorVector3FieldInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorVector3FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
|
||||
{});
|
||||
|
||||
frame = UpdateUIEditorVector3FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
|
||||
{
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
frame.layout.componentRects[2].x + 4.0f,
|
||||
frame.layout.componentRects[2].y + 4.0f,
|
||||
UIPointerButton::Left),
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonUp,
|
||||
frame.layout.componentRects[2].x + 4.0f,
|
||||
frame.layout.componentRects[2].y + 4.0f,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.vector3FieldState.editing);
|
||||
EXPECT_EQ(state.vector3FieldState.selectedComponentIndex, 2u);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector3FieldInteractionTest, TabSelectsNextComponentAndArrowAppliesStep) {
|
||||
UIEditorVector3FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0, 3.0 };
|
||||
spec.step = 0.5;
|
||||
UIEditorVector3FieldInteractionState state = {};
|
||||
state.vector3FieldState.focused = true;
|
||||
state.vector3FieldState.selectedComponentIndex = 1u;
|
||||
|
||||
auto frame = UpdateUIEditorVector3FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Tab) });
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_EQ(state.vector3FieldState.selectedComponentIndex, 2u);
|
||||
|
||||
frame = UpdateUIEditorVector3FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Up) });
|
||||
EXPECT_TRUE(frame.result.stepApplied);
|
||||
EXPECT_EQ(frame.result.changedComponentIndex, 2u);
|
||||
EXPECT_DOUBLE_EQ(spec.values[2], 3.5);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector3FieldInteractionTest, EnterStartsEditingAndCommitUpdatesSelectedComponent) {
|
||||
UIEditorVector3FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0, 3.0 };
|
||||
spec.integerMode = true;
|
||||
UIEditorVector3FieldInteractionState state = {};
|
||||
state.vector3FieldState.focused = true;
|
||||
state.vector3FieldState.selectedComponentIndex = 1u;
|
||||
|
||||
auto frame = UpdateUIEditorVector3FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Enter) });
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.vector3FieldState.editing);
|
||||
|
||||
frame = UpdateUIEditorVector3FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
|
||||
{ MakeCharacter('4'), MakeKey(KeyCode::Enter) });
|
||||
|
||||
EXPECT_TRUE(frame.result.editCommitted);
|
||||
EXPECT_TRUE(frame.result.valueChanged);
|
||||
EXPECT_EQ(frame.result.changedComponentIndex, 1u);
|
||||
EXPECT_DOUBLE_EQ(spec.values[0], 1.0);
|
||||
EXPECT_DOUBLE_EQ(spec.values[1], 24.0);
|
||||
EXPECT_DOUBLE_EQ(spec.values[2], 3.0);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector3FieldInteractionTest, CharacterInputCanStartEditingAndEscapeCancels) {
|
||||
UIEditorVector3FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0, 3.0 };
|
||||
UIEditorVector3FieldInteractionState state = {};
|
||||
state.vector3FieldState.focused = true;
|
||||
state.vector3FieldState.selectedComponentIndex = 0u;
|
||||
|
||||
auto frame = UpdateUIEditorVector3FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
|
||||
{ MakeCharacter('9') });
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.vector3FieldState.editing);
|
||||
EXPECT_EQ(state.vector3FieldState.displayTexts[0], "9");
|
||||
|
||||
frame = UpdateUIEditorVector3FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Escape) });
|
||||
EXPECT_TRUE(frame.result.editCanceled);
|
||||
EXPECT_FALSE(state.vector3FieldState.editing);
|
||||
EXPECT_DOUBLE_EQ(spec.values[0], 1.0);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorVector4Field.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorVector4FieldLayout;
|
||||
using XCEngine::UI::Editor::Widgets::FormatUIEditorVector4FieldComponentValue;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector4Field;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector4FieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector4FieldSpec;
|
||||
|
||||
TEST(UIEditorVector4FieldTest, FormatSupportsPerComponentDisplay) {
|
||||
UIEditorVector4FieldSpec spec = {};
|
||||
spec.fieldId = "rotation";
|
||||
spec.label = "Rotation";
|
||||
spec.values = { 1.25, -3.5, 8.0, 0.125 };
|
||||
|
||||
EXPECT_EQ(FormatUIEditorVector4FieldComponentValue(spec, 0u), "1.25");
|
||||
EXPECT_EQ(FormatUIEditorVector4FieldComponentValue(spec, 1u), "-3.5");
|
||||
EXPECT_EQ(FormatUIEditorVector4FieldComponentValue(spec, 2u), "8");
|
||||
EXPECT_EQ(FormatUIEditorVector4FieldComponentValue(spec, 3u), "0.125");
|
||||
}
|
||||
|
||||
TEST(UIEditorVector4FieldTest, LayoutBuildsFourComponentRects) {
|
||||
UIEditorVector4FieldSpec spec = {};
|
||||
spec.fieldId = "rotation";
|
||||
spec.label = "Rotation";
|
||||
const auto layout = BuildUIEditorVector4FieldLayout(UIRect(0.0f, 0.0f, 620.0f, 32.0f), spec);
|
||||
|
||||
EXPECT_GT(layout.labelRect.width, 0.0f);
|
||||
EXPECT_GT(layout.componentRects[0].width, 0.0f);
|
||||
EXPECT_GT(layout.componentRects[1].width, 0.0f);
|
||||
EXPECT_GT(layout.componentRects[2].width, 0.0f);
|
||||
EXPECT_GT(layout.componentRects[3].width, 0.0f);
|
||||
EXPECT_LT(layout.componentRects[0].x + layout.componentRects[0].width, layout.componentRects[1].x);
|
||||
EXPECT_LT(layout.componentRects[1].x + layout.componentRects[1].width, layout.componentRects[2].x);
|
||||
EXPECT_LT(layout.componentRects[2].x + layout.componentRects[2].width, layout.componentRects[3].x);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector4FieldTest, HitTestResolvesComponentAndRow) {
|
||||
UIEditorVector4FieldSpec spec = {};
|
||||
spec.fieldId = "rotation";
|
||||
spec.label = "Rotation";
|
||||
const auto layout = BuildUIEditorVector4FieldLayout(UIRect(0.0f, 0.0f, 620.0f, 32.0f), spec);
|
||||
|
||||
const auto fourthHit = HitTestUIEditorVector4Field(
|
||||
layout,
|
||||
UIPoint(layout.componentRects[3].x + 4.0f, layout.componentRects[3].y + 4.0f));
|
||||
EXPECT_EQ(fourthHit.kind, UIEditorVector4FieldHitTargetKind::Component);
|
||||
EXPECT_EQ(fourthHit.componentIndex, 3u);
|
||||
|
||||
const auto rowHit = HitTestUIEditorVector4Field(
|
||||
layout,
|
||||
UIPoint(layout.labelRect.x + 4.0f, layout.labelRect.y + 4.0f));
|
||||
EXPECT_EQ(rowHit.kind, UIEditorVector4FieldHitTargetKind::Row);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,165 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Fields/UIEditorVector4FieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::UIEditorVector4FieldInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorVector4FieldInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector4FieldSpec;
|
||||
|
||||
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKey(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeCharacter(char character) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::Character;
|
||||
event.character = static_cast<std::uint32_t>(character);
|
||||
return event;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorVector4FieldInteractionTest, ClickFourthComponentStartsEditing) {
|
||||
UIEditorVector4FieldSpec spec = {};
|
||||
spec.fieldId = "rotation";
|
||||
spec.label = "Rotation";
|
||||
spec.values = { 1.0, 2.0, 3.0, 4.0 };
|
||||
UIEditorVector4FieldInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorVector4FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 620.0f, 32.0f),
|
||||
{});
|
||||
|
||||
frame = UpdateUIEditorVector4FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 620.0f, 32.0f),
|
||||
{
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
frame.layout.componentRects[3].x + 4.0f,
|
||||
frame.layout.componentRects[3].y + 4.0f,
|
||||
UIPointerButton::Left),
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonUp,
|
||||
frame.layout.componentRects[3].x + 4.0f,
|
||||
frame.layout.componentRects[3].y + 4.0f,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.vector4FieldState.editing);
|
||||
EXPECT_EQ(state.vector4FieldState.selectedComponentIndex, 3u);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector4FieldInteractionTest, TabSelectsNextComponentAndArrowAppliesStep) {
|
||||
UIEditorVector4FieldSpec spec = {};
|
||||
spec.fieldId = "rotation";
|
||||
spec.label = "Rotation";
|
||||
spec.values = { 1.0, 2.0, 3.0, 4.0 };
|
||||
spec.step = 0.5;
|
||||
UIEditorVector4FieldInteractionState state = {};
|
||||
state.vector4FieldState.focused = true;
|
||||
state.vector4FieldState.selectedComponentIndex = 2u;
|
||||
|
||||
auto frame = UpdateUIEditorVector4FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 620.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Tab) });
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_EQ(state.vector4FieldState.selectedComponentIndex, 3u);
|
||||
|
||||
frame = UpdateUIEditorVector4FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 620.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Up) });
|
||||
EXPECT_TRUE(frame.result.stepApplied);
|
||||
EXPECT_EQ(frame.result.changedComponentIndex, 3u);
|
||||
EXPECT_DOUBLE_EQ(spec.values[3], 4.5);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector4FieldInteractionTest, EnterStartsEditingAndCommitUpdatesSelectedComponent) {
|
||||
UIEditorVector4FieldSpec spec = {};
|
||||
spec.fieldId = "rotation";
|
||||
spec.label = "Rotation";
|
||||
spec.values = { 1.0, 2.0, 3.0, 4.0 };
|
||||
spec.integerMode = true;
|
||||
UIEditorVector4FieldInteractionState state = {};
|
||||
state.vector4FieldState.focused = true;
|
||||
state.vector4FieldState.selectedComponentIndex = 2u;
|
||||
|
||||
auto frame = UpdateUIEditorVector4FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 620.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Enter) });
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.vector4FieldState.editing);
|
||||
|
||||
frame = UpdateUIEditorVector4FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 620.0f, 32.0f),
|
||||
{ MakeCharacter('7'), MakeKey(KeyCode::Enter) });
|
||||
|
||||
EXPECT_TRUE(frame.result.editCommitted);
|
||||
EXPECT_TRUE(frame.result.valueChanged);
|
||||
EXPECT_EQ(frame.result.changedComponentIndex, 2u);
|
||||
EXPECT_DOUBLE_EQ(spec.values[0], 1.0);
|
||||
EXPECT_DOUBLE_EQ(spec.values[1], 2.0);
|
||||
EXPECT_DOUBLE_EQ(spec.values[2], 37.0);
|
||||
EXPECT_DOUBLE_EQ(spec.values[3], 4.0);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector4FieldInteractionTest, CharacterInputCanStartEditingAndEscapeCancels) {
|
||||
UIEditorVector4FieldSpec spec = {};
|
||||
spec.fieldId = "rotation";
|
||||
spec.label = "Rotation";
|
||||
spec.values = { 1.0, 2.0, 3.0, 4.0 };
|
||||
UIEditorVector4FieldInteractionState state = {};
|
||||
state.vector4FieldState.focused = true;
|
||||
state.vector4FieldState.selectedComponentIndex = 0u;
|
||||
|
||||
auto frame = UpdateUIEditorVector4FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 620.0f, 32.0f),
|
||||
{ MakeCharacter('9') });
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.vector4FieldState.editing);
|
||||
EXPECT_EQ(state.vector4FieldState.displayTexts[0], "9");
|
||||
|
||||
frame = UpdateUIEditorVector4FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 620.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Escape) });
|
||||
EXPECT_TRUE(frame.result.editCanceled);
|
||||
EXPECT_FALSE(state.vector4FieldState.editing);
|
||||
EXPECT_DOUBLE_EQ(spec.values[0], 1.0);
|
||||
}
|
||||
@@ -1,426 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Viewport/UIEditorViewportInputBridge.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIInputModifiers;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::IsUIEditorViewportInputBridgeKeyDown;
|
||||
using XCEngine::UI::Editor::IsUIEditorViewportInputBridgePointerButtonDown;
|
||||
using XCEngine::UI::Editor::UIEditorViewportInputBridgeFocusMode;
|
||||
using XCEngine::UI::Editor::UIEditorViewportInputBridgeRequest;
|
||||
using XCEngine::UI::Editor::UIEditorViewportInputBridgeState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorViewportInputBridge;
|
||||
|
||||
UIInputEvent MakePointerEvent(
|
||||
UIInputEventType type,
|
||||
float x,
|
||||
float y,
|
||||
UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerEventWithModifiers(
|
||||
UIInputEventType type,
|
||||
float x,
|
||||
float y,
|
||||
const XCEngine::UI::UIInputModifiers& modifiers,
|
||||
UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
event.modifiers = modifiers;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputModifiers MakePointerModifiers(UIPointerButton button) {
|
||||
UIInputModifiers modifiers = {};
|
||||
switch (button) {
|
||||
case UIPointerButton::Left:
|
||||
modifiers.leftMouse = true;
|
||||
break;
|
||||
case UIPointerButton::Right:
|
||||
modifiers.rightMouse = true;
|
||||
break;
|
||||
case UIPointerButton::Middle:
|
||||
modifiers.middleMouse = true;
|
||||
break;
|
||||
case UIPointerButton::X1:
|
||||
modifiers.x1Mouse = true;
|
||||
break;
|
||||
case UIPointerButton::X2:
|
||||
modifiers.x2Mouse = true;
|
||||
break;
|
||||
case UIPointerButton::None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKeyEvent(
|
||||
UIInputEventType type,
|
||||
std::int32_t keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.keyCode = keyCode;
|
||||
return event;
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportInputBridgeTest, PointerDownInsideStartsFocusAndCaptureAndTracksLocalPosition) {
|
||||
UIEditorViewportInputBridgeState state = {};
|
||||
|
||||
const auto frame = UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
UIRect(100.0f, 200.0f, 640.0f, 360.0f),
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 280.0f),
|
||||
MakePointerEvent(UIInputEventType::PointerButtonDown, 220.0f, 280.0f, UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.hovered);
|
||||
EXPECT_TRUE(frame.focused);
|
||||
EXPECT_TRUE(frame.captured);
|
||||
EXPECT_TRUE(frame.focusGained);
|
||||
EXPECT_TRUE(frame.captureStarted);
|
||||
EXPECT_TRUE(frame.pointerPressedInside);
|
||||
EXPECT_FLOAT_EQ(frame.localPointerPosition.x, 120.0f);
|
||||
EXPECT_FLOAT_EQ(frame.localPointerPosition.y, 80.0f);
|
||||
EXPECT_TRUE(IsUIEditorViewportInputBridgePointerButtonDown(state, UIPointerButton::Left));
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportInputBridgeTest, FirstPointerMoveDoesNotCreateSyntheticDeltaFromOrigin) {
|
||||
UIEditorViewportInputBridgeState state = {};
|
||||
|
||||
const auto frame = UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
UIRect(100.0f, 200.0f, 640.0f, 360.0f),
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 280.0f)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.hovered);
|
||||
EXPECT_FALSE(frame.pointerMoved);
|
||||
EXPECT_FLOAT_EQ(frame.pointerDelta.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(frame.pointerDelta.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(frame.localPointerPosition.x, 120.0f);
|
||||
EXPECT_FLOAT_EQ(frame.localPointerPosition.y, 80.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportInputBridgeTest, PointerUpEndsCaptureAndOutsidePointerDownClearsFocus) {
|
||||
UIEditorViewportInputBridgeState state = {};
|
||||
UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
UIRect(100.0f, 200.0f, 640.0f, 360.0f),
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerButtonDown, 220.0f, 280.0f, UIPointerButton::Left)
|
||||
});
|
||||
|
||||
auto frame = UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
UIRect(100.0f, 200.0f, 640.0f, 360.0f),
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerButtonUp, 230.0f, 290.0f, UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.pointerReleasedInside);
|
||||
EXPECT_TRUE(frame.captureEnded);
|
||||
EXPECT_FALSE(frame.captured);
|
||||
|
||||
frame = UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
UIRect(100.0f, 200.0f, 640.0f, 360.0f),
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerButtonDown, 40.0f, 60.0f, UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.focusLost);
|
||||
EXPECT_FALSE(frame.focused);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportInputBridgeTest, CoalescedPointerClickPreservesBothButtonTransitions) {
|
||||
UIEditorViewportInputBridgeState state = {};
|
||||
|
||||
const auto frame = UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
UIRect(100.0f, 200.0f, 640.0f, 360.0f),
|
||||
{
|
||||
MakePointerEvent(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
220.0f,
|
||||
280.0f,
|
||||
UIPointerButton::Left),
|
||||
MakePointerEvent(
|
||||
UIInputEventType::PointerButtonUp,
|
||||
220.0f,
|
||||
280.0f,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
ASSERT_EQ(frame.pointerButtonTransitions.size(), 2u);
|
||||
EXPECT_TRUE(frame.pointerButtonTransitions[0].pressed);
|
||||
EXPECT_EQ(frame.pointerButtonTransitions[0].button, UIPointerButton::Left);
|
||||
EXPECT_TRUE(frame.pointerButtonTransitions[0].inside);
|
||||
EXPECT_FALSE(frame.pointerButtonTransitions[1].pressed);
|
||||
EXPECT_EQ(frame.pointerButtonTransitions[1].button, UIPointerButton::Left);
|
||||
EXPECT_TRUE(frame.pointerButtonTransitions[1].inside);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportInputBridgeTest, InteractionRectCanIncludeTopBarWhileLocalCoordinatesStayBoundToSurface) {
|
||||
UIEditorViewportInputBridgeState state = {};
|
||||
const UIRect interactionRect(100.0f, 80.0f, 640.0f, 406.0f);
|
||||
const UIRect localRect(100.0f, 104.0f, 640.0f, 382.0f);
|
||||
|
||||
const auto frame = UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
interactionRect,
|
||||
localRect,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 92.0f),
|
||||
MakePointerEvent(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
220.0f,
|
||||
92.0f,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.hasPointerPosition);
|
||||
EXPECT_TRUE(frame.hovered);
|
||||
EXPECT_TRUE(frame.focused);
|
||||
EXPECT_TRUE(frame.captured);
|
||||
EXPECT_TRUE(frame.pointerPressedInside);
|
||||
EXPECT_FLOAT_EQ(frame.localPointerPosition.x, 120.0f);
|
||||
EXPECT_FLOAT_EQ(frame.localPointerPosition.y, -12.0f);
|
||||
ASSERT_EQ(frame.pointerButtonTransitions.size(), 1u);
|
||||
EXPECT_TRUE(frame.pointerButtonTransitions.front().inside);
|
||||
EXPECT_FLOAT_EQ(frame.pointerButtonTransitions.front().localPointerPosition.y, -12.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportInputBridgeTest, PointerMoveWhileCapturedKeepsDeltaEvenOutsideSurface) {
|
||||
UIEditorViewportInputBridgeState state = {};
|
||||
UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
UIRect(100.0f, 200.0f, 640.0f, 360.0f),
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerButtonDown, 220.0f, 280.0f, UIPointerButton::Left)
|
||||
});
|
||||
|
||||
const auto frame = UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
UIRect(100.0f, 200.0f, 640.0f, 360.0f),
|
||||
{
|
||||
MakePointerEventWithModifiers(
|
||||
UIInputEventType::PointerMove,
|
||||
60.0f,
|
||||
120.0f,
|
||||
MakePointerModifiers(UIPointerButton::Left))
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.pointerMoved);
|
||||
EXPECT_FLOAT_EQ(frame.pointerDelta.x, -160.0f);
|
||||
EXPECT_FLOAT_EQ(frame.pointerDelta.y, -160.0f);
|
||||
EXPECT_FALSE(frame.hovered);
|
||||
EXPECT_TRUE(frame.captured);
|
||||
EXPECT_FLOAT_EQ(frame.localPointerPosition.x, -40.0f);
|
||||
EXPECT_FLOAT_EQ(frame.localPointerPosition.y, -80.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportInputBridgeTest, RightPointerCaptureTracksHeldButtonAcrossDragFrames) {
|
||||
UIEditorViewportInputBridgeState state = {};
|
||||
const UIRect inputRect(100.0f, 200.0f, 640.0f, 360.0f);
|
||||
|
||||
auto frame = UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
inputRect,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 280.0f),
|
||||
MakePointerEventWithModifiers(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
220.0f,
|
||||
280.0f,
|
||||
MakePointerModifiers(UIPointerButton::Right),
|
||||
UIPointerButton::Right)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.focused);
|
||||
EXPECT_TRUE(frame.captured);
|
||||
EXPECT_TRUE(frame.captureStarted);
|
||||
EXPECT_TRUE(IsUIEditorViewportInputBridgePointerButtonDown(state, UIPointerButton::Right));
|
||||
|
||||
frame = UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
inputRect,
|
||||
{
|
||||
MakePointerEventWithModifiers(
|
||||
UIInputEventType::PointerMove,
|
||||
260.0f,
|
||||
320.0f,
|
||||
MakePointerModifiers(UIPointerButton::Right))
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.captured);
|
||||
EXPECT_TRUE(frame.pointerMoved);
|
||||
EXPECT_FLOAT_EQ(frame.pointerDelta.x, 40.0f);
|
||||
EXPECT_FLOAT_EQ(frame.pointerDelta.y, 40.0f);
|
||||
EXPECT_TRUE(IsUIEditorViewportInputBridgePointerButtonDown(state, UIPointerButton::Right));
|
||||
|
||||
frame = UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
inputRect,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerButtonUp, 260.0f, 320.0f, UIPointerButton::Right)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.captureEnded);
|
||||
EXPECT_FALSE(frame.captured);
|
||||
EXPECT_FALSE(IsUIEditorViewportInputBridgePointerButtonDown(state, UIPointerButton::Right));
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportInputBridgeTest, PointerMoveReconcilesReleasedMouseButtonWhenUpEventWasMissed) {
|
||||
UIEditorViewportInputBridgeState state = {};
|
||||
const UIRect inputRect(100.0f, 200.0f, 640.0f, 360.0f);
|
||||
|
||||
XCEngine::UI::UIInputModifiers leftDownModifiers = {};
|
||||
leftDownModifiers.leftMouse = true;
|
||||
|
||||
auto frame = UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
inputRect,
|
||||
{
|
||||
MakePointerEventWithModifiers(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
220.0f,
|
||||
280.0f,
|
||||
leftDownModifiers,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.captured);
|
||||
EXPECT_TRUE(IsUIEditorViewportInputBridgePointerButtonDown(state, UIPointerButton::Left));
|
||||
|
||||
frame = UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
inputRect,
|
||||
{
|
||||
MakePointerEventWithModifiers(
|
||||
UIInputEventType::PointerMove,
|
||||
260.0f,
|
||||
320.0f,
|
||||
{})
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.pointerMoved);
|
||||
EXPECT_TRUE(frame.captureEnded);
|
||||
EXPECT_FALSE(frame.captured);
|
||||
EXPECT_FALSE(IsUIEditorViewportInputBridgePointerButtonDown(state, UIPointerButton::Left));
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportInputBridgeTest, WheelAndKeyboardAreAcceptedOnlyWhileFocused) {
|
||||
UIEditorViewportInputBridgeState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
UIRect(100.0f, 200.0f, 640.0f, 360.0f),
|
||||
{
|
||||
[] {
|
||||
UIInputEvent wheel = {};
|
||||
wheel.type = UIInputEventType::PointerWheel;
|
||||
wheel.position = UIPoint(220.0f, 280.0f);
|
||||
wheel.wheelDelta = 120.0f;
|
||||
return wheel;
|
||||
}(),
|
||||
MakeKeyEvent(UIInputEventType::KeyDown, 87)
|
||||
});
|
||||
|
||||
EXPECT_FLOAT_EQ(frame.wheelDelta, 120.0f);
|
||||
EXPECT_TRUE(frame.pressedKeyCodes.empty());
|
||||
|
||||
frame = UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
UIRect(100.0f, 200.0f, 640.0f, 360.0f),
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerButtonDown, 220.0f, 280.0f, UIPointerButton::Left),
|
||||
MakeKeyEvent(UIInputEventType::KeyDown, 87),
|
||||
MakeKeyEvent(UIInputEventType::KeyUp, 87)
|
||||
});
|
||||
|
||||
ASSERT_EQ(frame.pressedKeyCodes.size(), 1u);
|
||||
ASSERT_EQ(frame.releasedKeyCodes.size(), 1u);
|
||||
EXPECT_EQ(frame.pressedKeyCodes[0], 87);
|
||||
EXPECT_EQ(frame.releasedKeyCodes[0], 87);
|
||||
EXPECT_FALSE(IsUIEditorViewportInputBridgeKeyDown(state, 87));
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportInputBridgeTest, FocusLostClearsCapturedStateAndHeldKeys) {
|
||||
UIEditorViewportInputBridgeState state = {};
|
||||
UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
UIRect(100.0f, 200.0f, 640.0f, 360.0f),
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerButtonDown, 220.0f, 280.0f, UIPointerButton::Left),
|
||||
MakeKeyEvent(UIInputEventType::KeyDown, 70)
|
||||
});
|
||||
|
||||
UIInputEvent focusLost = {};
|
||||
focusLost.type = UIInputEventType::FocusLost;
|
||||
const auto frame =
|
||||
UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
UIRect(100.0f, 200.0f, 640.0f, 360.0f),
|
||||
{ focusLost });
|
||||
|
||||
EXPECT_TRUE(frame.focusLost);
|
||||
EXPECT_TRUE(frame.captureEnded);
|
||||
EXPECT_FALSE(state.focused);
|
||||
EXPECT_FALSE(state.captured);
|
||||
EXPECT_FALSE(IsUIEditorViewportInputBridgeKeyDown(state, 70));
|
||||
EXPECT_FALSE(IsUIEditorViewportInputBridgePointerButtonDown(state, UIPointerButton::Left));
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportInputBridgeTest, ExternalFocusModeTransfersFocusAndReleasesCaptureFromOwnerState) {
|
||||
UIEditorViewportInputBridgeState state = {};
|
||||
UIEditorViewportInputBridgeRequest focusedRequest = {};
|
||||
focusedRequest.focusMode = UIEditorViewportInputBridgeFocusMode::External;
|
||||
focusedRequest.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
UIRect(100.0f, 200.0f, 640.0f, 360.0f),
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerButtonDown, 220.0f, 280.0f, UIPointerButton::Left)
|
||||
},
|
||||
{},
|
||||
focusedRequest);
|
||||
|
||||
EXPECT_TRUE(frame.focusGained);
|
||||
EXPECT_TRUE(frame.focused);
|
||||
EXPECT_TRUE(frame.captured);
|
||||
|
||||
UIEditorViewportInputBridgeRequest unfocusedRequest = focusedRequest;
|
||||
unfocusedRequest.focused = false;
|
||||
frame = UpdateUIEditorViewportInputBridge(
|
||||
state,
|
||||
UIRect(100.0f, 200.0f, 640.0f, 360.0f),
|
||||
{},
|
||||
{},
|
||||
unfocusedRequest);
|
||||
|
||||
EXPECT_TRUE(frame.focusLost);
|
||||
EXPECT_TRUE(frame.captureEnded);
|
||||
EXPECT_FALSE(frame.focused);
|
||||
EXPECT_FALSE(frame.captured);
|
||||
EXPECT_FALSE(state.focused);
|
||||
EXPECT_FALSE(state.captured);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,176 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Viewport/UIEditorViewportShell.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::ResolveUIEditorViewportShellRequest;
|
||||
using XCEngine::UI::Editor::UIEditorViewportShellModel;
|
||||
using XCEngine::UI::Editor::UIEditorViewportShellSpec;
|
||||
using XCEngine::UI::Editor::UIEditorViewportShellState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorViewportShell;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarInvalidIndex;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarState;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotInvalidIndex;
|
||||
|
||||
UIInputEvent MakePointerEvent(
|
||||
UIInputEventType type,
|
||||
float x,
|
||||
float y,
|
||||
UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportShellTest, ResolveRequestUsesViewportInputRectSize) {
|
||||
UIEditorViewportShellSpec spec = {};
|
||||
const auto request = ResolveUIEditorViewportShellRequest(
|
||||
UIRect(10.0f, 20.0f, 800.0f, 600.0f),
|
||||
spec);
|
||||
|
||||
EXPECT_FLOAT_EQ(request.slotLayout.inputRect.width, 800.0f);
|
||||
EXPECT_FLOAT_EQ(request.slotLayout.inputRect.height, 554.0f);
|
||||
EXPECT_FLOAT_EQ(request.requestedViewportSize.width, 800.0f);
|
||||
EXPECT_FLOAT_EQ(request.requestedViewportSize.height, 554.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportShellTest, ResolveRequestTracksChromeBarVisibility) {
|
||||
UIEditorViewportShellSpec spec = {};
|
||||
spec.chrome.showTopBar = false;
|
||||
spec.chrome.showBottomBar = false;
|
||||
|
||||
const auto request = ResolveUIEditorViewportShellRequest(
|
||||
UIRect(10.0f, 20.0f, 800.0f, 600.0f),
|
||||
spec);
|
||||
|
||||
EXPECT_FLOAT_EQ(request.slotLayout.inputRect.height, 600.0f);
|
||||
EXPECT_FLOAT_EQ(request.requestedViewportSize.height, 600.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportShellTest, UpdateShellMapsInputBridgeStateToViewportSlotState) {
|
||||
UIEditorViewportShellModel model = {};
|
||||
UIEditorViewportShellState state = {};
|
||||
const auto request = ResolveUIEditorViewportShellRequest(
|
||||
UIRect(10.0f, 20.0f, 800.0f, 600.0f),
|
||||
model.spec);
|
||||
|
||||
const float insideX = request.slotLayout.inputRect.x + 80.0f;
|
||||
const float insideY = request.slotLayout.inputRect.y + 60.0f;
|
||||
const auto frame = UpdateUIEditorViewportShell(
|
||||
state,
|
||||
UIRect(10.0f, 20.0f, 800.0f, 600.0f),
|
||||
model,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, insideX, insideY),
|
||||
MakePointerEvent(UIInputEventType::PointerButtonDown, insideX, insideY, UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.inputFrame.focused);
|
||||
EXPECT_TRUE(frame.inputFrame.captured);
|
||||
EXPECT_TRUE(frame.slotState.focused);
|
||||
EXPECT_TRUE(frame.slotState.surfaceHovered);
|
||||
EXPECT_TRUE(frame.slotState.surfaceActive);
|
||||
EXPECT_TRUE(frame.slotState.inputCaptured);
|
||||
EXPECT_TRUE(frame.slotState.statusBarState.focused);
|
||||
EXPECT_FLOAT_EQ(frame.requestedViewportSize.width, request.requestedViewportSize.width);
|
||||
EXPECT_FLOAT_EQ(frame.requestedViewportSize.height, request.requestedViewportSize.height);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportShellTest, UpdateShellPreservesVisualOverrides) {
|
||||
UIEditorViewportShellModel model = {};
|
||||
model.spec.visualState.hoveredToolIndex = 1u;
|
||||
model.spec.visualState.activeToolIndex = 2u;
|
||||
model.spec.visualState.statusBarState = UIEditorStatusBarState{
|
||||
3u,
|
||||
4u,
|
||||
true
|
||||
};
|
||||
|
||||
UIEditorViewportShellState state = {};
|
||||
const auto frame = UpdateUIEditorViewportShell(
|
||||
state,
|
||||
UIRect(10.0f, 20.0f, 800.0f, 600.0f),
|
||||
model,
|
||||
{});
|
||||
|
||||
EXPECT_EQ(frame.slotState.hoveredToolIndex, 1u);
|
||||
EXPECT_EQ(frame.slotState.activeToolIndex, 2u);
|
||||
EXPECT_EQ(frame.slotState.statusBarState.hoveredIndex, 3u);
|
||||
EXPECT_EQ(frame.slotState.statusBarState.activeIndex, 4u);
|
||||
EXPECT_TRUE(frame.slotState.statusBarState.focused);
|
||||
EXPECT_FALSE(frame.slotState.focused);
|
||||
EXPECT_FALSE(frame.slotState.surfaceHovered);
|
||||
EXPECT_FALSE(frame.slotState.surfaceActive);
|
||||
EXPECT_FALSE(frame.slotState.inputCaptured);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportShellTest, UpdateShellDoesNotIntroduceInvalidIndicesByDefault) {
|
||||
UIEditorViewportShellModel model = {};
|
||||
UIEditorViewportShellState state = {};
|
||||
|
||||
const auto frame = UpdateUIEditorViewportShell(
|
||||
state,
|
||||
UIRect(10.0f, 20.0f, 800.0f, 600.0f),
|
||||
model,
|
||||
{});
|
||||
|
||||
EXPECT_EQ(frame.slotState.hoveredToolIndex, UIEditorViewportSlotInvalidIndex);
|
||||
EXPECT_EQ(frame.slotState.activeToolIndex, UIEditorViewportSlotInvalidIndex);
|
||||
EXPECT_EQ(frame.slotState.statusBarState.hoveredIndex, UIEditorStatusBarInvalidIndex);
|
||||
EXPECT_EQ(frame.slotState.statusBarState.activeIndex, UIEditorStatusBarInvalidIndex);
|
||||
EXPECT_FALSE(frame.slotState.statusBarState.focused);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportShellTest, TopBarPointerDownUsesShellBoundsButKeepsSurfaceHoverSeparate) {
|
||||
UIEditorViewportShellModel model = {};
|
||||
model.spec.chrome.showTopBar = true;
|
||||
model.spec.toolItems = {
|
||||
XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolItem{
|
||||
"scene.pivot.toggle",
|
||||
"Pivot",
|
||||
XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolSlot::Leading,
|
||||
true,
|
||||
false,
|
||||
68.0f
|
||||
}
|
||||
};
|
||||
UIEditorViewportShellState state = {};
|
||||
|
||||
const auto request = ResolveUIEditorViewportShellRequest(
|
||||
UIRect(10.0f, 20.0f, 800.0f, 600.0f),
|
||||
model.spec);
|
||||
const UIRect topBarRect = request.slotLayout.topBarRect;
|
||||
const UIPoint clickPoint(
|
||||
topBarRect.x + 20.0f,
|
||||
topBarRect.y + topBarRect.height * 0.5f);
|
||||
|
||||
const auto frame = UpdateUIEditorViewportShell(
|
||||
state,
|
||||
UIRect(10.0f, 20.0f, 800.0f, 600.0f),
|
||||
model,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, clickPoint.x, clickPoint.y),
|
||||
MakePointerEvent(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
clickPoint.x,
|
||||
clickPoint.y,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.inputFrame.pointerPressedInside);
|
||||
EXPECT_TRUE(frame.inputFrame.captureStarted);
|
||||
EXPECT_TRUE(frame.inputFrame.focused);
|
||||
EXPECT_FALSE(frame.slotState.surfaceHovered);
|
||||
EXPECT_TRUE(frame.slotState.inputCaptured);
|
||||
EXPECT_LT(frame.inputFrame.localPointerPosition.y, 0.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,283 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEditor/Viewport/UIEditorViewportSlot.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIColor;
|
||||
using XCEngine::UI::UIDrawCommand;
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::UISize;
|
||||
using XCEngine::UI::UITextureHandle;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorViewportSlotBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorViewportSlotForeground;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorViewportSlotLayout;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorViewportSlot;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorViewportSlotDesiredToolWidth;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSegment;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotChrome;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotFrame;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotInvalidIndex;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotPalette;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotState;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolItem;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolSlot;
|
||||
|
||||
void ExpectColorEq(const UIColor& actual, const UIColor& expected) {
|
||||
EXPECT_FLOAT_EQ(actual.r, expected.r);
|
||||
EXPECT_FLOAT_EQ(actual.g, expected.g);
|
||||
EXPECT_FLOAT_EQ(actual.b, expected.b);
|
||||
EXPECT_FLOAT_EQ(actual.a, expected.a);
|
||||
}
|
||||
|
||||
bool RectEq(const UIRect& actual, const UIRect& expected) {
|
||||
return actual.x == expected.x &&
|
||||
actual.y == expected.y &&
|
||||
actual.width == expected.width &&
|
||||
actual.height == expected.height;
|
||||
}
|
||||
|
||||
const UIDrawCommand* FindCommand(
|
||||
const UIDrawList& drawList,
|
||||
UIDrawCommandType type,
|
||||
const UIRect& rect) {
|
||||
for (const UIDrawCommand& command : drawList.GetCommands()) {
|
||||
if (command.type == type && RectEq(command.rect, rect)) {
|
||||
return &command;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ContainsText(const UIDrawList& drawList, std::string_view text) {
|
||||
for (const UIDrawCommand& command : drawList.GetCommands()) {
|
||||
if (command.type == UIDrawCommandType::Text && command.text == text) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<UIEditorViewportSlotToolItem> BuildToolItems() {
|
||||
return {
|
||||
{ "mode", "Perspective", UIEditorViewportSlotToolSlot::Leading, true, true, 96.0f },
|
||||
{ "lit", "Lit", UIEditorViewportSlotToolSlot::Trailing, true, false, 48.0f },
|
||||
{ "gizmos", "Gizmos", UIEditorViewportSlotToolSlot::Trailing, true, false, 72.0f }
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<UIEditorStatusBarSegment> BuildStatusSegments() {
|
||||
return {
|
||||
{ "scene", "Scene", UIEditorStatusBarSlot::Leading, {}, true, true, 72.0f },
|
||||
{ "resolution", "1280x720", UIEditorStatusBarSlot::Leading, {}, true, false, 92.0f },
|
||||
{ "frame", "16.7 ms", UIEditorStatusBarSlot::Trailing, {}, true, false, 72.0f }
|
||||
};
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportSlotTest, DesiredToolWidthUsesExplicitValueBeforeEstimatedLabelWidth) {
|
||||
UIEditorViewportSlotToolItem explicitWidth = {};
|
||||
explicitWidth.label = "Scene";
|
||||
explicitWidth.desiredWidth = 88.0f;
|
||||
|
||||
UIEditorViewportSlotToolItem inferredWidth = {};
|
||||
inferredWidth.label = "Scene";
|
||||
|
||||
EXPECT_FLOAT_EQ(ResolveUIEditorViewportSlotDesiredToolWidth(explicitWidth), 88.0f);
|
||||
EXPECT_FLOAT_EQ(ResolveUIEditorViewportSlotDesiredToolWidth(inferredWidth), 48.5f);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportSlotTest, LayoutBuildsTopBarSurfaceBottomBarAndAspectFittedTexture) {
|
||||
UIEditorViewportSlotChrome chrome = {};
|
||||
chrome.topBarHeight = 40.0f;
|
||||
chrome.bottomBarHeight = 28.0f;
|
||||
|
||||
UIEditorViewportSlotFrame frame = {};
|
||||
frame.hasTexture = true;
|
||||
frame.presentedSize = UISize(1600.0f, 900.0f);
|
||||
|
||||
const UIEditorViewportSlotLayout layout =
|
||||
BuildUIEditorViewportSlotLayout(
|
||||
UIRect(10.0f, 20.0f, 800.0f, 600.0f),
|
||||
chrome,
|
||||
frame,
|
||||
BuildToolItems(),
|
||||
BuildStatusSegments());
|
||||
|
||||
EXPECT_TRUE(layout.hasTopBar);
|
||||
EXPECT_TRUE(layout.hasBottomBar);
|
||||
EXPECT_FLOAT_EQ(layout.topBarRect.y, 20.0f);
|
||||
EXPECT_FLOAT_EQ(layout.topBarRect.height, 40.0f);
|
||||
EXPECT_FLOAT_EQ(layout.bottomBarRect.y, 592.0f);
|
||||
EXPECT_FLOAT_EQ(layout.bottomBarRect.height, 28.0f);
|
||||
EXPECT_FLOAT_EQ(layout.surfaceRect.y, 60.0f);
|
||||
EXPECT_FLOAT_EQ(layout.surfaceRect.height, 532.0f);
|
||||
EXPECT_FLOAT_EQ(layout.requestedSurfaceSize.width, 800.0f);
|
||||
EXPECT_FLOAT_EQ(layout.requestedSurfaceSize.height, 532.0f);
|
||||
EXPECT_FLOAT_EQ(layout.textureRect.x, 10.0f);
|
||||
EXPECT_FLOAT_EQ(layout.textureRect.y, layout.inputRect.y);
|
||||
EXPECT_FLOAT_EQ(layout.textureRect.width, 800.0f);
|
||||
EXPECT_FLOAT_EQ(layout.textureRect.height, layout.inputRect.height);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportSlotTest, ToolItemsAlignToEdgesAndTitleRectClampsBetweenToolBands) {
|
||||
UIEditorViewportSlotChrome chrome = {};
|
||||
chrome.topBarHeight = 40.0f;
|
||||
chrome.bottomBarHeight = 0.0f;
|
||||
chrome.showBottomBar = false;
|
||||
|
||||
const UIEditorViewportSlotLayout layout =
|
||||
BuildUIEditorViewportSlotLayout(
|
||||
UIRect(0.0f, 0.0f, 900.0f, 520.0f),
|
||||
chrome,
|
||||
UIEditorViewportSlotFrame{},
|
||||
BuildToolItems(),
|
||||
{});
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.toolItemRects[0].x, 8.0f);
|
||||
EXPECT_FLOAT_EQ(layout.toolItemRects[0].y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(layout.toolItemRects[0].width, 96.0f);
|
||||
EXPECT_FLOAT_EQ(layout.toolItemRects[0].height, 40.0f);
|
||||
EXPECT_FLOAT_EQ(layout.toolItemRects[1].x, 768.0f);
|
||||
EXPECT_FLOAT_EQ(layout.toolItemRects[2].x, 820.0f);
|
||||
EXPECT_FLOAT_EQ(layout.titleRect.x, 110.0f);
|
||||
EXPECT_FLOAT_EQ(layout.titleRect.width, 652.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportSlotTest, HitTestPrioritizesToolThenStatusThenSurface) {
|
||||
UIEditorViewportSlotChrome chrome = {};
|
||||
chrome.topBarHeight = 40.0f;
|
||||
chrome.bottomBarHeight = 28.0f;
|
||||
|
||||
const UIEditorViewportSlotLayout layout =
|
||||
BuildUIEditorViewportSlotLayout(
|
||||
UIRect(0.0f, 0.0f, 900.0f, 520.0f),
|
||||
chrome,
|
||||
UIEditorViewportSlotFrame{},
|
||||
BuildToolItems(),
|
||||
BuildStatusSegments());
|
||||
|
||||
auto hit = HitTestUIEditorViewportSlot(layout, UIPoint(30.0f, 16.0f));
|
||||
EXPECT_EQ(hit.kind, UIEditorViewportSlotHitTargetKind::ToolItem);
|
||||
EXPECT_EQ(hit.index, 0u);
|
||||
|
||||
hit = HitTestUIEditorViewportSlot(layout, UIPoint(22.0f, 505.0f));
|
||||
EXPECT_EQ(hit.kind, UIEditorViewportSlotHitTargetKind::StatusSegment);
|
||||
EXPECT_EQ(hit.index, 0u);
|
||||
|
||||
hit = HitTestUIEditorViewportSlot(layout, UIPoint(450.0f, 240.0f));
|
||||
EXPECT_EQ(hit.kind, UIEditorViewportSlotHitTargetKind::Surface);
|
||||
EXPECT_EQ(hit.index, UIEditorViewportSlotInvalidIndex);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportSlotTest, BackgroundAndForegroundEmitChromeAndImageBranchCommands) {
|
||||
UIEditorViewportSlotChrome chrome = {};
|
||||
chrome.title = "Scene View";
|
||||
chrome.subtitle = "ViewportSlot shell";
|
||||
|
||||
UIEditorViewportSlotFrame frame = {};
|
||||
frame.hasTexture = true;
|
||||
frame.texture = UITextureHandle{ 1u, 1280u, 720u };
|
||||
frame.presentedSize = UISize(1280.0f, 720.0f);
|
||||
|
||||
UIEditorViewportSlotState state = {};
|
||||
state.focused = true;
|
||||
state.surfaceHovered = true;
|
||||
state.surfaceActive = true;
|
||||
state.inputCaptured = true;
|
||||
state.hoveredToolIndex = 0u;
|
||||
state.activeToolIndex = 1u;
|
||||
state.statusBarState.focused = true;
|
||||
|
||||
const auto toolItems = BuildToolItems();
|
||||
const auto statusSegments = BuildStatusSegments();
|
||||
const UIEditorViewportSlotLayout layout =
|
||||
BuildUIEditorViewportSlotLayout(
|
||||
UIRect(12.0f, 16.0f, 900.0f, 520.0f),
|
||||
chrome,
|
||||
frame,
|
||||
toolItems,
|
||||
statusSegments);
|
||||
|
||||
const UIEditorViewportSlotPalette palette = {};
|
||||
|
||||
UIDrawList background("ViewportSlotBackground");
|
||||
AppendUIEditorViewportSlotBackground(
|
||||
background,
|
||||
layout,
|
||||
toolItems,
|
||||
statusSegments,
|
||||
state,
|
||||
palette);
|
||||
|
||||
const UIDrawCommand* surfaceBorder =
|
||||
FindCommand(background, UIDrawCommandType::RectOutline, layout.inputRect);
|
||||
ASSERT_NE(surfaceBorder, nullptr);
|
||||
ExpectColorEq(surfaceBorder->color, palette.surfaceCapturedBorderColor);
|
||||
|
||||
UIDrawList foreground("ViewportSlotForeground");
|
||||
AppendUIEditorViewportSlotForeground(
|
||||
foreground,
|
||||
layout,
|
||||
chrome,
|
||||
frame,
|
||||
toolItems,
|
||||
statusSegments,
|
||||
state,
|
||||
palette);
|
||||
|
||||
EXPECT_TRUE(ContainsText(foreground, "Scene View"));
|
||||
EXPECT_TRUE(ContainsText(foreground, "Perspective"));
|
||||
EXPECT_FALSE(ContainsText(foreground, "Texture 1280x720"));
|
||||
|
||||
bool foundImage = false;
|
||||
for (const UIDrawCommand& command : foreground.GetCommands()) {
|
||||
if (command.type == UIDrawCommandType::Image &&
|
||||
RectEq(command.rect, layout.textureRect)) {
|
||||
foundImage = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(foundImage);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportSlotTest, ForegroundFallsBackToStatusTextWhenTextureIsUnavailable) {
|
||||
UIEditorViewportSlotChrome chrome = {};
|
||||
chrome.title = "Game View";
|
||||
|
||||
UIEditorViewportSlotFrame frame = {};
|
||||
frame.statusText = "Viewport is waiting for frame";
|
||||
|
||||
const auto toolItems = BuildToolItems();
|
||||
const auto statusSegments = BuildStatusSegments();
|
||||
const UIEditorViewportSlotLayout layout =
|
||||
BuildUIEditorViewportSlotLayout(
|
||||
UIRect(0.0f, 0.0f, 720.0f, 420.0f),
|
||||
chrome,
|
||||
frame,
|
||||
toolItems,
|
||||
statusSegments);
|
||||
|
||||
UIDrawList foreground("ViewportSlotForegroundFallback");
|
||||
AppendUIEditorViewportSlotForeground(
|
||||
foreground,
|
||||
layout,
|
||||
chrome,
|
||||
frame,
|
||||
toolItems,
|
||||
statusSegments,
|
||||
UIEditorViewportSlotState{});
|
||||
|
||||
EXPECT_FALSE(ContainsText(foreground, "Viewport is waiting for frame"));
|
||||
EXPECT_FALSE(ContainsText(foreground, "Texture 1280x720"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,546 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Workspace/UIEditorWindowWorkspaceController.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceQueries.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Editor::AreUIEditorWorkspaceModelsEquivalent;
|
||||
using XCEngine::UI::Editor::AreUIEditorWorkspaceSessionsEquivalent;
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWindowWorkspaceController;
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWindowWorkspaceSet;
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::CollectUIEditorWorkspaceVisiblePanels;
|
||||
using XCEngine::UI::Editor::ContainsUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::FindUIEditorPanelSessionState;
|
||||
using XCEngine::UI::Editor::FindMutableUIEditorWindowWorkspaceState;
|
||||
using XCEngine::UI::Editor::FindUIEditorWindowWorkspaceState;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorWindowWorkspaceController;
|
||||
using XCEngine::UI::Editor::UIEditorWindowWorkspaceOperationStatus;
|
||||
using XCEngine::UI::Editor::UIEditorWindowWorkspaceSet;
|
||||
using XCEngine::UI::Editor::UIEditorWindowWorkspaceState;
|
||||
using XCEngine::UI::Editor::UIEditorWindowWorkspaceValidationCode;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceDockPlacement;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceNodeKind;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
using XCEngine::UI::Editor::ValidateUIEditorWindowWorkspaceSet;
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "inspector", "Inspector", {}, true, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.7f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspaceSingleTabStack(
|
||||
"inspector-panel",
|
||||
"inspector",
|
||||
"Inspector",
|
||||
true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
std::vector<std::string> CollectVisiblePanelIds(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const XCEngine::UI::Editor::UIEditorWorkspaceSession& session) {
|
||||
const auto panels = CollectUIEditorWorkspaceVisiblePanels(workspace, session);
|
||||
std::vector<std::string> ids = {};
|
||||
ids.reserve(panels.size());
|
||||
for (const auto& panel : panels) {
|
||||
ids.push_back(panel.panelId);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
bool AreWindowSetsEquivalent(
|
||||
const UIEditorWindowWorkspaceSet& lhs,
|
||||
const UIEditorWindowWorkspaceSet& rhs) {
|
||||
if (lhs.primaryWindowId != rhs.primaryWindowId ||
|
||||
lhs.activeWindowId != rhs.activeWindowId ||
|
||||
lhs.windows.size() != rhs.windows.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < lhs.windows.size(); ++index) {
|
||||
const auto& lhsWindow = lhs.windows[index];
|
||||
const auto& rhsWindow = rhs.windows[index];
|
||||
if (lhsWindow.windowId != rhsWindow.windowId ||
|
||||
!AreUIEditorWorkspaceModelsEquivalent(lhsWindow.workspace, rhsWindow.workspace) ||
|
||||
!AreUIEditorWorkspaceSessionsEquivalent(lhsWindow.session, rhsWindow.session)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorWindowWorkspaceControllerTest, DetachPanelCreatesNewDetachedWindowAndMovesSessionState) {
|
||||
UIEditorWindowWorkspaceController controller =
|
||||
BuildDefaultUIEditorWindowWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
|
||||
const auto result = controller.DetachPanelToNewWindow(
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
"doc-b-window");
|
||||
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
EXPECT_EQ(result.targetWindowId, "doc-b-window");
|
||||
EXPECT_EQ(result.activeWindowId, "doc-b-window");
|
||||
ASSERT_EQ(result.windowIds.size(), 2u);
|
||||
|
||||
const auto* mainWindow =
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "main-window");
|
||||
ASSERT_NE(mainWindow, nullptr);
|
||||
EXPECT_FALSE(ContainsUIEditorWorkspacePanel(mainWindow->workspace, "doc-b"));
|
||||
EXPECT_EQ(FindUIEditorPanelSessionState(mainWindow->session, "doc-b"), nullptr);
|
||||
|
||||
const auto* detachedWindow =
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "doc-b-window");
|
||||
ASSERT_NE(detachedWindow, nullptr);
|
||||
ASSERT_TRUE(ContainsUIEditorWorkspacePanel(detachedWindow->workspace, "doc-b"));
|
||||
ASSERT_NE(FindUIEditorPanelSessionState(detachedWindow->session, "doc-b"), nullptr);
|
||||
EXPECT_EQ(detachedWindow->workspace.activePanelId, "doc-b");
|
||||
|
||||
const auto mainVisibleIds =
|
||||
CollectVisiblePanelIds(mainWindow->workspace, mainWindow->session);
|
||||
ASSERT_EQ(mainVisibleIds.size(), 2u);
|
||||
EXPECT_EQ(mainVisibleIds[0], "doc-a");
|
||||
EXPECT_EQ(mainVisibleIds[1], "inspector");
|
||||
}
|
||||
|
||||
TEST(UIEditorWindowWorkspaceControllerTest, MovingSinglePanelDetachedWindowBackToMainClosesSourceWindow) {
|
||||
UIEditorWindowWorkspaceController controller =
|
||||
BuildDefaultUIEditorWindowWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
ASSERT_EQ(
|
||||
controller.DetachPanelToNewWindow(
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
"doc-b-window").status,
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
|
||||
const auto result = controller.MovePanelToStack(
|
||||
"doc-b-window",
|
||||
"doc-b-window-root",
|
||||
"doc-b",
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
1u);
|
||||
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
EXPECT_EQ(result.activeWindowId, "main-window");
|
||||
ASSERT_EQ(result.windowIds.size(), 1u);
|
||||
EXPECT_EQ(result.windowIds[0], "main-window");
|
||||
|
||||
EXPECT_EQ(
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "doc-b-window"),
|
||||
nullptr);
|
||||
|
||||
const auto* mainWindow =
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "main-window");
|
||||
ASSERT_NE(mainWindow, nullptr);
|
||||
ASSERT_TRUE(ContainsUIEditorWorkspacePanel(mainWindow->workspace, "doc-b"));
|
||||
ASSERT_NE(FindUIEditorPanelSessionState(mainWindow->session, "doc-b"), nullptr);
|
||||
EXPECT_EQ(mainWindow->workspace.activePanelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorWindowWorkspaceControllerTest, DockingDetachedPanelIntoMainWindowAlsoClosesSourceWindow) {
|
||||
UIEditorWindowWorkspaceController controller =
|
||||
BuildDefaultUIEditorWindowWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
ASSERT_EQ(
|
||||
controller.DetachPanelToNewWindow(
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
"doc-b-window").status,
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
|
||||
const auto result = controller.DockPanelRelative(
|
||||
"doc-b-window",
|
||||
"doc-b-window-root",
|
||||
"doc-b",
|
||||
"main-window",
|
||||
"inspector-panel",
|
||||
UIEditorWorkspaceDockPlacement::Left,
|
||||
0.4f);
|
||||
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
EXPECT_EQ(result.activeWindowId, "main-window");
|
||||
ASSERT_EQ(result.windowIds.size(), 1u);
|
||||
|
||||
const auto* mainWindow =
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "main-window");
|
||||
ASSERT_NE(mainWindow, nullptr);
|
||||
ASSERT_TRUE(ContainsUIEditorWorkspacePanel(mainWindow->workspace, "doc-b"));
|
||||
ASSERT_NE(FindUIEditorPanelSessionState(mainWindow->session, "doc-b"), nullptr);
|
||||
|
||||
const auto visibleIds =
|
||||
CollectVisiblePanelIds(mainWindow->workspace, mainWindow->session);
|
||||
ASSERT_EQ(visibleIds.size(), 3u);
|
||||
EXPECT_EQ(visibleIds[0], "doc-a");
|
||||
EXPECT_EQ(visibleIds[1], "doc-b");
|
||||
EXPECT_EQ(visibleIds[2], "inspector");
|
||||
}
|
||||
|
||||
TEST(UIEditorWindowWorkspaceControllerTest, DetachWithDuplicatePreferredWindowIdCreatesUniqueWindowId) {
|
||||
UIEditorWindowWorkspaceController controller =
|
||||
BuildDefaultUIEditorWindowWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
|
||||
const auto result = controller.DetachPanelToNewWindow(
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
"main-window");
|
||||
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
EXPECT_EQ(result.targetWindowId, "main-window-1");
|
||||
EXPECT_EQ(result.activeWindowId, "main-window-1");
|
||||
ASSERT_EQ(result.windowIds.size(), 2u);
|
||||
|
||||
EXPECT_NE(
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "main-window-1"),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
TEST(UIEditorWindowWorkspaceControllerTest, DetachingSinglePanelDetachedWindowIsNoOp) {
|
||||
UIEditorWindowWorkspaceController controller =
|
||||
BuildDefaultUIEditorWindowWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
ASSERT_EQ(
|
||||
controller.DetachPanelToNewWindow(
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
"doc-b-window").status,
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
|
||||
const auto result = controller.DetachPanelToNewWindow(
|
||||
"doc-b-window",
|
||||
"doc-b-window-root",
|
||||
"doc-b",
|
||||
"doc-b-window-again");
|
||||
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::NoOp);
|
||||
EXPECT_EQ(result.targetWindowId, "doc-b-window");
|
||||
EXPECT_EQ(result.activeWindowId, "doc-b-window");
|
||||
ASSERT_EQ(result.windowIds.size(), 2u);
|
||||
|
||||
EXPECT_EQ(
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "doc-b-window-again"),
|
||||
nullptr);
|
||||
EXPECT_NE(
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "doc-b-window"),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
TEST(UIEditorWindowWorkspaceControllerTest, MovingPanelBetweenDetachedWindowsClosesEmptiedSourceWindow) {
|
||||
UIEditorWindowWorkspaceController controller =
|
||||
BuildDefaultUIEditorWindowWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
ASSERT_EQ(
|
||||
controller.DetachPanelToNewWindow(
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
"doc-b-window").status,
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
ASSERT_EQ(
|
||||
controller.DetachPanelToNewWindow(
|
||||
"main-window",
|
||||
"inspector-panel",
|
||||
"inspector",
|
||||
"inspector-window").status,
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
|
||||
const auto result = controller.MovePanelToStack(
|
||||
"doc-b-window",
|
||||
"doc-b-window-root",
|
||||
"doc-b",
|
||||
"inspector-window",
|
||||
"inspector-window-root",
|
||||
1u);
|
||||
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
EXPECT_EQ(result.activeWindowId, "inspector-window");
|
||||
ASSERT_EQ(result.windowIds.size(), 2u);
|
||||
|
||||
EXPECT_EQ(
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "doc-b-window"),
|
||||
nullptr);
|
||||
|
||||
const auto* mainWindow =
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "main-window");
|
||||
ASSERT_NE(mainWindow, nullptr);
|
||||
const auto mainVisibleIds =
|
||||
CollectVisiblePanelIds(mainWindow->workspace, mainWindow->session);
|
||||
ASSERT_EQ(mainVisibleIds.size(), 1u);
|
||||
EXPECT_EQ(mainVisibleIds[0], "doc-a");
|
||||
|
||||
const auto* inspectorWindow =
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "inspector-window");
|
||||
ASSERT_NE(inspectorWindow, nullptr);
|
||||
ASSERT_TRUE(ContainsUIEditorWorkspacePanel(inspectorWindow->workspace, "doc-b"));
|
||||
ASSERT_TRUE(ContainsUIEditorWorkspacePanel(inspectorWindow->workspace, "inspector"));
|
||||
ASSERT_NE(FindUIEditorPanelSessionState(inspectorWindow->session, "doc-b"), nullptr);
|
||||
ASSERT_NE(FindUIEditorPanelSessionState(inspectorWindow->session, "inspector"), nullptr);
|
||||
EXPECT_EQ(inspectorWindow->workspace.activePanelId, "doc-b");
|
||||
ASSERT_EQ(inspectorWindow->workspace.root.children.size(), 2u);
|
||||
EXPECT_EQ(inspectorWindow->workspace.root.children[0].panel.panelId, "inspector");
|
||||
EXPECT_EQ(inspectorWindow->workspace.root.children[1].panel.panelId, "doc-b");
|
||||
EXPECT_EQ(inspectorWindow->workspace.root.selectedTabIndex, 1u);
|
||||
ASSERT_EQ(inspectorWindow->session.panelStates.size(), 2u);
|
||||
EXPECT_EQ(inspectorWindow->session.panelStates[0].panelId, "inspector");
|
||||
EXPECT_EQ(inspectorWindow->session.panelStates[1].panelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorWindowWorkspaceControllerTest, RejectedCrossWindowMoveDoesNotMutateWindowSet) {
|
||||
UIEditorWindowWorkspaceController controller =
|
||||
BuildDefaultUIEditorWindowWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
ASSERT_EQ(
|
||||
controller.DetachPanelToNewWindow(
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
"doc-b-window").status,
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
|
||||
const auto result = controller.MovePanelToStack(
|
||||
"doc-b-window",
|
||||
"doc-b-window-root",
|
||||
"doc-b",
|
||||
"main-window",
|
||||
"missing-target-node",
|
||||
0u);
|
||||
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::Rejected);
|
||||
EXPECT_EQ(result.activeWindowId, "doc-b-window");
|
||||
ASSERT_EQ(result.windowIds.size(), 2u);
|
||||
|
||||
const auto* mainWindow =
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "main-window");
|
||||
ASSERT_NE(mainWindow, nullptr);
|
||||
EXPECT_FALSE(ContainsUIEditorWorkspacePanel(mainWindow->workspace, "doc-b"));
|
||||
EXPECT_EQ(FindUIEditorPanelSessionState(mainWindow->session, "doc-b"), nullptr);
|
||||
|
||||
const auto* detachedWindow =
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "doc-b-window");
|
||||
ASSERT_NE(detachedWindow, nullptr);
|
||||
EXPECT_TRUE(ContainsUIEditorWorkspacePanel(detachedWindow->workspace, "doc-b"));
|
||||
EXPECT_EQ(detachedWindow->workspace.activePanelId, "doc-b");
|
||||
EXPECT_NE(FindUIEditorPanelSessionState(detachedWindow->session, "doc-b"), nullptr);
|
||||
}
|
||||
|
||||
TEST(UIEditorWindowWorkspaceControllerTest, DockingPanelBetweenDetachedWindowsClosesEmptiedSourceWindow) {
|
||||
UIEditorWindowWorkspaceController controller =
|
||||
BuildDefaultUIEditorWindowWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
ASSERT_EQ(
|
||||
controller.DetachPanelToNewWindow(
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
"doc-b-window").status,
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
ASSERT_EQ(
|
||||
controller.DetachPanelToNewWindow(
|
||||
"main-window",
|
||||
"inspector-panel",
|
||||
"inspector",
|
||||
"inspector-window").status,
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
|
||||
const auto result = controller.DockPanelRelative(
|
||||
"doc-b-window",
|
||||
"doc-b-window-root",
|
||||
"doc-b",
|
||||
"inspector-window",
|
||||
"inspector-window-root",
|
||||
UIEditorWorkspaceDockPlacement::Bottom,
|
||||
0.35f);
|
||||
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
EXPECT_EQ(result.activeWindowId, "inspector-window");
|
||||
ASSERT_EQ(result.windowIds.size(), 2u);
|
||||
|
||||
const UIEditorWindowWorkspaceSet& windowSet = controller.GetWindowSet();
|
||||
EXPECT_EQ(windowSet.primaryWindowId, "main-window");
|
||||
EXPECT_EQ(windowSet.activeWindowId, "inspector-window");
|
||||
EXPECT_EQ(
|
||||
FindUIEditorWindowWorkspaceState(windowSet, "doc-b-window"),
|
||||
nullptr);
|
||||
|
||||
const auto* mainWindow =
|
||||
FindUIEditorWindowWorkspaceState(windowSet, "main-window");
|
||||
ASSERT_NE(mainWindow, nullptr);
|
||||
const auto mainVisibleIds =
|
||||
CollectVisiblePanelIds(mainWindow->workspace, mainWindow->session);
|
||||
ASSERT_EQ(mainVisibleIds.size(), 1u);
|
||||
EXPECT_EQ(mainVisibleIds[0], "doc-a");
|
||||
|
||||
const auto* inspectorWindow =
|
||||
FindUIEditorWindowWorkspaceState(windowSet, "inspector-window");
|
||||
ASSERT_NE(inspectorWindow, nullptr);
|
||||
EXPECT_TRUE(ContainsUIEditorWorkspacePanel(inspectorWindow->workspace, "inspector"));
|
||||
EXPECT_TRUE(ContainsUIEditorWorkspacePanel(inspectorWindow->workspace, "doc-b"));
|
||||
EXPECT_EQ(inspectorWindow->workspace.activePanelId, "doc-b");
|
||||
EXPECT_EQ(inspectorWindow->workspace.root.kind, UIEditorWorkspaceNodeKind::Split);
|
||||
ASSERT_EQ(inspectorWindow->session.panelStates.size(), 2u);
|
||||
EXPECT_EQ(inspectorWindow->session.panelStates[0].panelId, "inspector");
|
||||
EXPECT_EQ(inspectorWindow->session.panelStates[1].panelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorWindowWorkspaceControllerTest, RejectedCrossWindowDockDoesNotMutateWindowSet) {
|
||||
UIEditorWindowWorkspaceController controller =
|
||||
BuildDefaultUIEditorWindowWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
ASSERT_EQ(
|
||||
controller.DetachPanelToNewWindow(
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
"doc-b-window").status,
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
|
||||
const UIEditorWindowWorkspaceSet windowSetBefore = controller.GetWindowSet();
|
||||
const auto result = controller.DockPanelRelative(
|
||||
"doc-b-window",
|
||||
"doc-b-window-root",
|
||||
"doc-b",
|
||||
"missing-window",
|
||||
"inspector-panel",
|
||||
UIEditorWorkspaceDockPlacement::Left,
|
||||
0.4f);
|
||||
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::Rejected);
|
||||
EXPECT_EQ(result.activeWindowId, "doc-b-window");
|
||||
EXPECT_EQ(controller.GetWindowSet().primaryWindowId, "main-window");
|
||||
EXPECT_TRUE(AreWindowSetsEquivalent(controller.GetWindowSet(), windowSetBefore));
|
||||
}
|
||||
|
||||
TEST(UIEditorWindowWorkspaceControllerTest, RejectedCrossWindowMoveFromMissingSourceDoesNotMutateWindowSet) {
|
||||
UIEditorWindowWorkspaceController controller =
|
||||
BuildDefaultUIEditorWindowWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
ASSERT_EQ(
|
||||
controller.DetachPanelToNewWindow(
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
"doc-b-window").status,
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
|
||||
const UIEditorWindowWorkspaceSet windowSetBefore = controller.GetWindowSet();
|
||||
const auto result = controller.MovePanelToStack(
|
||||
"missing-window",
|
||||
"doc-b-window-root",
|
||||
"doc-b",
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
0u);
|
||||
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::Rejected);
|
||||
EXPECT_EQ(result.activeWindowId, "doc-b-window");
|
||||
EXPECT_EQ(controller.GetWindowSet().primaryWindowId, "main-window");
|
||||
EXPECT_TRUE(AreWindowSetsEquivalent(controller.GetWindowSet(), windowSetBefore));
|
||||
}
|
||||
|
||||
TEST(UIEditorWindowWorkspaceControllerTest, RejectedCrossWindowMoveFromDetachedWindowWithWrongSourceNodeDoesNotMutateWindowSet) {
|
||||
UIEditorWindowWorkspaceController controller =
|
||||
BuildDefaultUIEditorWindowWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
ASSERT_EQ(
|
||||
controller.DetachPanelToNewWindow(
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
"doc-b-window").status,
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
|
||||
const UIEditorWindowWorkspaceSet windowSetBefore = controller.GetWindowSet();
|
||||
const auto result = controller.MovePanelToStack(
|
||||
"doc-b-window",
|
||||
"wrong-root",
|
||||
"doc-b",
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
1u);
|
||||
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::Rejected);
|
||||
EXPECT_EQ(result.activeWindowId, "doc-b-window");
|
||||
EXPECT_TRUE(AreWindowSetsEquivalent(controller.GetWindowSet(), windowSetBefore));
|
||||
}
|
||||
|
||||
TEST(UIEditorWindowWorkspaceControllerTest, RejectedCrossWindowMoveFromHiddenDetachedWindowDoesNotMutateWindowSet) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
|
||||
UIEditorWindowWorkspaceController seedController =
|
||||
BuildDefaultUIEditorWindowWorkspaceController(registry, BuildWorkspace());
|
||||
ASSERT_EQ(
|
||||
seedController.DetachPanelToNewWindow(
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
"doc-b-window").status,
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
|
||||
UIEditorWindowWorkspaceSet windowSet = seedController.GetWindowSet();
|
||||
UIEditorWindowWorkspaceState* detachedWindow =
|
||||
FindMutableUIEditorWindowWorkspaceState(windowSet, "doc-b-window");
|
||||
ASSERT_NE(detachedWindow, nullptr);
|
||||
ASSERT_EQ(detachedWindow->session.panelStates.size(), 1u);
|
||||
detachedWindow->session.panelStates.front().open = false;
|
||||
detachedWindow->session.panelStates.front().visible = false;
|
||||
detachedWindow->workspace.activePanelId.clear();
|
||||
|
||||
UIEditorWindowWorkspaceController controller(registry, windowSet);
|
||||
EXPECT_TRUE(controller.ValidateState().IsValid());
|
||||
|
||||
const UIEditorWindowWorkspaceSet windowSetBefore = controller.GetWindowSet();
|
||||
const auto result = controller.MovePanelToStack(
|
||||
"doc-b-window",
|
||||
"doc-b-window-root",
|
||||
"doc-b",
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
1u);
|
||||
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::Rejected);
|
||||
EXPECT_EQ(result.activeWindowId, "doc-b-window");
|
||||
EXPECT_TRUE(AreWindowSetsEquivalent(controller.GetWindowSet(), windowSetBefore));
|
||||
}
|
||||
|
||||
TEST(UIEditorWindowWorkspaceControllerTest, ValidationRejectsDuplicatePanelAcrossWindows) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
|
||||
UIEditorWindowWorkspaceSet windowSet =
|
||||
BuildDefaultUIEditorWindowWorkspaceSet(registry, BuildWorkspace());
|
||||
windowSet.activeWindowId = "doc-a-window";
|
||||
|
||||
UIEditorWindowWorkspaceState duplicateWindow = {};
|
||||
duplicateWindow.windowId = "doc-a-window";
|
||||
duplicateWindow.workspace.root = BuildUIEditorWorkspaceSingleTabStack(
|
||||
"doc-a-window-root",
|
||||
"doc-a",
|
||||
"Document A",
|
||||
true);
|
||||
duplicateWindow.workspace.activePanelId = "doc-a";
|
||||
duplicateWindow.session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, duplicateWindow.workspace);
|
||||
windowSet.windows.push_back(std::move(duplicateWindow));
|
||||
|
||||
const auto validation = ValidateUIEditorWindowWorkspaceSet(registry, windowSet);
|
||||
EXPECT_EQ(
|
||||
validation.code,
|
||||
UIEditorWindowWorkspaceValidationCode::DuplicatePanelAcrossWindows);
|
||||
EXPECT_NE(validation.message.find("doc-a"), std::string::npos);
|
||||
EXPECT_NE(validation.message.find("main-window"), std::string::npos);
|
||||
EXPECT_NE(validation.message.find("doc-a-window"), std::string::npos);
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceCompose.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::CollectUIEditorWorkspaceComposeExternalBodyPanelIds;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::FindUIEditorWorkspacePanelPresentationState;
|
||||
using XCEngine::UI::Editor::FindUIEditorWorkspaceViewportPresentationFrame;
|
||||
using XCEngine::UI::Editor::FindUIEditorWorkspaceViewportPresentationRequest;
|
||||
using XCEngine::UI::Editor::ResolveUIEditorViewportShellRequest;
|
||||
using XCEngine::UI::Editor::ResolveUIEditorWorkspaceComposeRequest;
|
||||
using XCEngine::UI::Editor::UIEditorPanelPresentationKind;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceComposeFrame;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceComposeState;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorWorkspaceCompose;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostTabStackLayout;
|
||||
|
||||
UIEditorPanelRegistry BuildRegistryWithViewportPanels() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "viewport", "Viewport", UIEditorPanelPresentationKind::ViewportShell, false, true, true },
|
||||
{ "doc", "Document", UIEditorPanelPresentationKind::Placeholder, true, true, true },
|
||||
{ "details", "Details", UIEditorPanelPresentationKind::Placeholder, true, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorPanelRegistry BuildRegistryWithTwoViewportTabs() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "viewport-a", "Viewport A", UIEditorPanelPresentationKind::ViewportShell, false, true, true },
|
||||
{ "viewport-b", "Viewport B", UIEditorPanelPresentationKind::ViewportShell, false, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildViewportWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.7f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"tab-stack",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("viewport-node", "viewport", "Viewport"),
|
||||
BuildUIEditorWorkspacePanel("doc-node", "doc", "Document", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true));
|
||||
workspace.activePanelId = "viewport";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildTwoViewportTabWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceTabStack(
|
||||
"tab-stack",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("viewport-a-node", "viewport-a", "Viewport A"),
|
||||
BuildUIEditorWorkspacePanel("viewport-b-node", "viewport-b", "Viewport B")
|
||||
},
|
||||
1u);
|
||||
workspace.activePanelId = "viewport-b";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
XCEngine::UI::Editor::UIEditorViewportShellModel BuildViewportShellModel(std::string title) {
|
||||
XCEngine::UI::Editor::UIEditorViewportShellModel model = {};
|
||||
model.spec.chrome.title = std::move(title);
|
||||
model.spec.chrome.subtitle = "Compose";
|
||||
model.spec.chrome.showTopBar = true;
|
||||
model.spec.chrome.showBottomBar = true;
|
||||
model.frame.hasTexture = false;
|
||||
model.frame.statusText = "Viewport shell";
|
||||
return model;
|
||||
}
|
||||
|
||||
UIEditorPanelRegistry BuildRegistryWithHostedContentPanels() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", UIEditorPanelPresentationKind::HostedContent, false, true, true },
|
||||
{ "doc-b", "Document B", UIEditorPanelPresentationKind::HostedContent, false, true, true },
|
||||
{ "inspector", "Inspector", UIEditorPanelPresentationKind::HostedContent, false, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildHostedContentWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.68f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"documents",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A"),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B")
|
||||
},
|
||||
1u),
|
||||
BuildUIEditorWorkspacePanel("inspector-node", "inspector", "Inspector"));
|
||||
workspace.activePanelId = "doc-b";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
UIEditorWorkspacePanelPresentationModel BuildHostedContentPresentationModel(
|
||||
std::string panelId) {
|
||||
UIEditorWorkspacePanelPresentationModel model = {};
|
||||
model.panelId = std::move(panelId);
|
||||
model.kind = UIEditorPanelPresentationKind::HostedContent;
|
||||
return model;
|
||||
}
|
||||
|
||||
UIEditorWorkspacePanelPresentationModel BuildViewportPresentationModel(
|
||||
std::string panelId,
|
||||
std::string title) {
|
||||
UIEditorWorkspacePanelPresentationModel model = {};
|
||||
model.panelId = std::move(panelId);
|
||||
model.kind = UIEditorPanelPresentationKind::ViewportShell;
|
||||
model.viewportShellModel = BuildViewportShellModel(std::move(title));
|
||||
return model;
|
||||
}
|
||||
|
||||
const UIEditorDockHostTabStackLayout* FindTabStackByNodeId(
|
||||
const UIEditorDockHostLayout& layout,
|
||||
std::string_view nodeId) {
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
if (tabStack.nodeId == nodeId) {
|
||||
return &tabStack;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorWorkspaceComposeTest, ResolveRequestMapsViewportPresentationToVisiblePanelBodyRect) {
|
||||
const auto registry = BuildRegistryWithViewportPanels();
|
||||
const UIEditorWorkspaceModel workspace = BuildViewportWorkspace();
|
||||
const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
const std::vector<UIEditorWorkspacePanelPresentationModel> presentationModels = {
|
||||
BuildViewportPresentationModel("viewport", "Viewport")
|
||||
};
|
||||
|
||||
const auto request = ResolveUIEditorWorkspaceComposeRequest(
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
presentationModels);
|
||||
|
||||
ASSERT_EQ(request.viewportRequests.size(), 1u);
|
||||
const auto* viewportRequest =
|
||||
FindUIEditorWorkspaceViewportPresentationRequest(request, "viewport");
|
||||
ASSERT_NE(viewportRequest, nullptr);
|
||||
|
||||
ASSERT_EQ(request.dockHostLayout.tabStacks.size(), 2u);
|
||||
const auto* documentStack = FindTabStackByNodeId(request.dockHostLayout, "tab-stack");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
const UIRect expectedBodyRect = documentStack->contentFrameLayout.bodyRect;
|
||||
EXPECT_FLOAT_EQ(viewportRequest->bounds.x, expectedBodyRect.x);
|
||||
EXPECT_FLOAT_EQ(viewportRequest->bounds.y, expectedBodyRect.y);
|
||||
EXPECT_FLOAT_EQ(viewportRequest->bounds.width, expectedBodyRect.width);
|
||||
EXPECT_FLOAT_EQ(viewportRequest->bounds.height, expectedBodyRect.height);
|
||||
|
||||
const auto expectedShellRequest = ResolveUIEditorViewportShellRequest(
|
||||
expectedBodyRect,
|
||||
presentationModels.front().viewportShellModel.spec);
|
||||
EXPECT_FLOAT_EQ(
|
||||
viewportRequest->viewportShellRequest.requestedViewportSize.width,
|
||||
expectedShellRequest.requestedViewportSize.width);
|
||||
EXPECT_FLOAT_EQ(
|
||||
viewportRequest->viewportShellRequest.requestedViewportSize.height,
|
||||
expectedShellRequest.requestedViewportSize.height);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceComposeTest, UpdateComposeOnlyBuildsFrameForSelectedViewportTab) {
|
||||
const auto registry = BuildRegistryWithTwoViewportTabs();
|
||||
const UIEditorWorkspaceModel workspace = BuildTwoViewportTabWorkspace();
|
||||
const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
const std::vector<UIEditorWorkspacePanelPresentationModel> presentationModels = {
|
||||
BuildViewportPresentationModel("viewport-a", "Viewport A"),
|
||||
BuildViewportPresentationModel("viewport-b", "Viewport B")
|
||||
};
|
||||
|
||||
UIEditorWorkspaceComposeState state = {};
|
||||
const UIEditorWorkspaceComposeFrame frame = UpdateUIEditorWorkspaceCompose(
|
||||
state,
|
||||
UIRect(0.0f, 0.0f, 960.0f, 640.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
presentationModels,
|
||||
{});
|
||||
|
||||
ASSERT_EQ(frame.viewportFrames.size(), 1u);
|
||||
EXPECT_EQ(frame.viewportFrames.front().panelId, "viewport-b");
|
||||
EXPECT_EQ(
|
||||
FindUIEditorWorkspaceViewportPresentationFrame(frame, "viewport-a"),
|
||||
nullptr);
|
||||
EXPECT_NE(
|
||||
FindUIEditorWorkspaceViewportPresentationFrame(frame, "viewport-b"),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceComposeTest, PlaceholderPanelsDoNotGenerateExternalViewportFrames) {
|
||||
const auto registry = BuildRegistryWithViewportPanels();
|
||||
const UIEditorWorkspaceModel workspace = BuildViewportWorkspace();
|
||||
const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
std::vector<UIEditorWorkspacePanelPresentationModel> presentationModels = {
|
||||
BuildViewportPresentationModel("details", "Details")
|
||||
};
|
||||
|
||||
UIEditorWorkspaceComposeState state = {};
|
||||
const auto request = ResolveUIEditorWorkspaceComposeRequest(
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
presentationModels);
|
||||
EXPECT_TRUE(request.viewportRequests.empty());
|
||||
|
||||
const UIEditorWorkspaceComposeFrame frame = UpdateUIEditorWorkspaceCompose(
|
||||
state,
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
presentationModels,
|
||||
{});
|
||||
EXPECT_TRUE(frame.viewportFrames.empty());
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceComposeTest, HiddenViewportTabResetsCapturedAndFocusedState) {
|
||||
const auto registry = BuildRegistryWithTwoViewportTabs();
|
||||
UIEditorWorkspaceModel workspace = BuildTwoViewportTabWorkspace();
|
||||
const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
const std::vector<UIEditorWorkspacePanelPresentationModel> presentationModels = {
|
||||
BuildViewportPresentationModel("viewport-a", "Viewport A"),
|
||||
BuildViewportPresentationModel("viewport-b", "Viewport B")
|
||||
};
|
||||
|
||||
const auto initialRequest = ResolveUIEditorWorkspaceComposeRequest(
|
||||
UIRect(0.0f, 0.0f, 960.0f, 640.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
presentationModels);
|
||||
ASSERT_EQ(initialRequest.viewportRequests.size(), 1u);
|
||||
const auto* selectedViewportRequest =
|
||||
FindUIEditorWorkspaceViewportPresentationRequest(initialRequest, "viewport-b");
|
||||
ASSERT_NE(selectedViewportRequest, nullptr);
|
||||
|
||||
const UIRect bounds = selectedViewportRequest->bounds;
|
||||
const UIPoint center(
|
||||
bounds.x + bounds.width * 0.5f,
|
||||
bounds.y + bounds.height * 0.5f);
|
||||
const std::vector<UIInputEvent> inputEvents = {
|
||||
[] (const UIPoint& point) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonDown;
|
||||
event.pointerButton = UIPointerButton::Left;
|
||||
event.position = point;
|
||||
return event;
|
||||
}(center)
|
||||
};
|
||||
|
||||
UIEditorWorkspaceComposeState state = {};
|
||||
UpdateUIEditorWorkspaceCompose(
|
||||
state,
|
||||
UIRect(0.0f, 0.0f, 960.0f, 640.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
presentationModels,
|
||||
inputEvents);
|
||||
|
||||
const auto* viewportBStateBeforeHide =
|
||||
FindUIEditorWorkspacePanelPresentationState(state, "viewport-b");
|
||||
ASSERT_NE(viewportBStateBeforeHide, nullptr);
|
||||
EXPECT_TRUE(viewportBStateBeforeHide->viewportShellState.inputBridgeState.focused);
|
||||
EXPECT_TRUE(viewportBStateBeforeHide->viewportShellState.inputBridgeState.captured);
|
||||
|
||||
workspace.root.selectedTabIndex = 0u;
|
||||
workspace.activePanelId = "viewport-a";
|
||||
|
||||
const UIEditorWorkspaceComposeFrame frame = UpdateUIEditorWorkspaceCompose(
|
||||
state,
|
||||
UIRect(0.0f, 0.0f, 960.0f, 640.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
presentationModels,
|
||||
{});
|
||||
|
||||
ASSERT_EQ(frame.viewportFrames.size(), 1u);
|
||||
EXPECT_EQ(frame.viewportFrames.front().panelId, "viewport-a");
|
||||
|
||||
const auto* viewportBStateAfterHide =
|
||||
FindUIEditorWorkspacePanelPresentationState(state, "viewport-b");
|
||||
ASSERT_NE(viewportBStateAfterHide, nullptr);
|
||||
EXPECT_FALSE(viewportBStateAfterHide->viewportShellState.inputBridgeState.focused);
|
||||
EXPECT_FALSE(viewportBStateAfterHide->viewportShellState.inputBridgeState.captured);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceComposeTest, HostedContentPanelsFlowThroughContentHostAndSuppressDockPlaceholder) {
|
||||
const auto registry = BuildRegistryWithHostedContentPanels();
|
||||
const UIEditorWorkspaceModel workspace = BuildHostedContentWorkspace();
|
||||
const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
const std::vector<UIEditorWorkspacePanelPresentationModel> presentationModels = {
|
||||
BuildHostedContentPresentationModel("doc-a"),
|
||||
BuildHostedContentPresentationModel("doc-b"),
|
||||
BuildHostedContentPresentationModel("inspector")
|
||||
};
|
||||
|
||||
UIEditorWorkspaceComposeState state = {};
|
||||
const UIEditorWorkspaceComposeFrame frame = UpdateUIEditorWorkspaceCompose(
|
||||
state,
|
||||
UIRect(0.0f, 0.0f, 1200.0f, 760.0f),
|
||||
registry,
|
||||
workspace,
|
||||
session,
|
||||
presentationModels,
|
||||
{});
|
||||
|
||||
EXPECT_TRUE(frame.viewportFrames.empty());
|
||||
EXPECT_EQ(
|
||||
CollectUIEditorWorkspaceComposeExternalBodyPanelIds(frame),
|
||||
std::vector<std::string>({ "doc-b", "inspector" }));
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceController.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceQueries.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceSession.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::AreUIEditorWorkspaceModelsEquivalent;
|
||||
using XCEngine::UI::Editor::FindUIEditorPanelSessionState;
|
||||
using XCEngine::UI::Editor::FindUIEditorWorkspaceNode;
|
||||
using XCEngine::UI::Editor::GetUIEditorWorkspaceCommandKindName;
|
||||
using XCEngine::UI::Editor::GetUIEditorWorkspaceCommandStatusName;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommand;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceControllerValidationCode;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceController;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceDockPlacement;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceLayoutOperationStatus;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceNode;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "doc-c", "Document C", {}, true, true, true },
|
||||
{ "hidden-a", "Hidden A", {}, true, true, true },
|
||||
{ "hidden-b", "Hidden B", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true },
|
||||
{ "root", "Root", {}, true, false, false }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.66f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorWorkspaceControllerTest, CommandNameHelpersExposeStableDebugNames) {
|
||||
EXPECT_EQ(GetUIEditorWorkspaceCommandKindName(UIEditorWorkspaceCommandKind::HidePanel), "HidePanel");
|
||||
EXPECT_EQ(GetUIEditorWorkspaceCommandStatusName(UIEditorWorkspaceCommandStatus::Changed), "Changed");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceControllerTest, ValidateStateUsesControllerLevelErrorClassification) {
|
||||
UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
workspace.activePanelId = "missing-panel";
|
||||
|
||||
UIEditorWorkspaceController controller(
|
||||
registry,
|
||||
workspace,
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace));
|
||||
|
||||
const auto validation = controller.ValidateState();
|
||||
EXPECT_EQ(validation.code, UIEditorWorkspaceControllerValidationCode::InvalidWorkspace);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceControllerTest, HideCommandChangesStateAndRepeatedHideBecomesNoOp) {
|
||||
UIEditorWorkspaceController controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
|
||||
const auto first = controller.Dispatch({ UIEditorWorkspaceCommandKind::HidePanel, "doc-a" });
|
||||
EXPECT_EQ(first.status, UIEditorWorkspaceCommandStatus::Changed);
|
||||
EXPECT_EQ(first.activePanelId, "doc-b");
|
||||
ASSERT_EQ(first.visiblePanelIds.size(), 2u);
|
||||
EXPECT_EQ(first.visiblePanelIds[0], "doc-b");
|
||||
EXPECT_EQ(first.visiblePanelIds[1], "details");
|
||||
|
||||
const auto second = controller.Dispatch({ UIEditorWorkspaceCommandKind::HidePanel, "doc-a" });
|
||||
EXPECT_EQ(second.status, UIEditorWorkspaceCommandStatus::NoOp);
|
||||
EXPECT_EQ(second.message, "Panel is already hidden.");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceControllerTest, ClosedPanelCannotBeActivatedUntilOpenedAgain) {
|
||||
UIEditorWorkspaceController controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
|
||||
EXPECT_EQ(
|
||||
controller.Dispatch({ UIEditorWorkspaceCommandKind::ClosePanel, "doc-b" }).status,
|
||||
UIEditorWorkspaceCommandStatus::Changed);
|
||||
|
||||
const auto activateClosed =
|
||||
controller.Dispatch({ UIEditorWorkspaceCommandKind::ActivatePanel, "doc-b" });
|
||||
EXPECT_EQ(activateClosed.status, UIEditorWorkspaceCommandStatus::Rejected);
|
||||
EXPECT_EQ(activateClosed.activePanelId, "doc-a");
|
||||
|
||||
const auto reopen =
|
||||
controller.Dispatch({ UIEditorWorkspaceCommandKind::OpenPanel, "doc-b" });
|
||||
EXPECT_EQ(reopen.status, UIEditorWorkspaceCommandStatus::Changed);
|
||||
EXPECT_EQ(reopen.activePanelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceControllerTest, ResetCommandRestoresBaselineState) {
|
||||
UIEditorWorkspaceController controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
|
||||
EXPECT_EQ(
|
||||
controller.Dispatch({ UIEditorWorkspaceCommandKind::HidePanel, "doc-a" }).status,
|
||||
UIEditorWorkspaceCommandStatus::Changed);
|
||||
EXPECT_EQ(
|
||||
controller.Dispatch({ UIEditorWorkspaceCommandKind::ClosePanel, "doc-b" }).status,
|
||||
UIEditorWorkspaceCommandStatus::Changed);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "details");
|
||||
|
||||
const auto reset = controller.Dispatch({ UIEditorWorkspaceCommandKind::ResetWorkspace, {} });
|
||||
EXPECT_EQ(reset.status, UIEditorWorkspaceCommandStatus::Changed);
|
||||
EXPECT_EQ(reset.activePanelId, "doc-a");
|
||||
ASSERT_EQ(reset.visiblePanelIds.size(), 2u);
|
||||
EXPECT_EQ(reset.visiblePanelIds[0], "doc-a");
|
||||
EXPECT_EQ(reset.visiblePanelIds[1], "details");
|
||||
|
||||
const auto repeatReset = controller.Dispatch({ UIEditorWorkspaceCommandKind::ResetWorkspace, {} });
|
||||
EXPECT_EQ(repeatReset.status, UIEditorWorkspaceCommandStatus::NoOp);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceControllerTest, RejectsUnknownPanelAndNonCloseablePanelCommands) {
|
||||
UIEditorWorkspaceController controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
|
||||
const auto unknown = controller.Dispatch({ UIEditorWorkspaceCommandKind::ShowPanel, "missing" });
|
||||
EXPECT_EQ(unknown.status, UIEditorWorkspaceCommandStatus::Rejected);
|
||||
|
||||
UIEditorWorkspaceModel rootWorkspace = {};
|
||||
rootWorkspace.root = BuildUIEditorWorkspacePanel("root-node", "root", "Root", true);
|
||||
rootWorkspace.activePanelId = "root";
|
||||
UIEditorWorkspaceController rootController =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), rootWorkspace);
|
||||
|
||||
const auto nonCloseable =
|
||||
rootController.Dispatch({ UIEditorWorkspaceCommandKind::ClosePanel, "root" });
|
||||
EXPECT_EQ(nonCloseable.status, UIEditorWorkspaceCommandStatus::Rejected);
|
||||
EXPECT_EQ(rootController.GetWorkspace().activePanelId, "root");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceControllerTest, MoveTabToStackMovesVisibleTabAcrossStacks) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.55f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"left-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates = {
|
||||
{ "doc-a", true, true },
|
||||
{ "doc-b", true, true },
|
||||
{ "details", true, true }
|
||||
};
|
||||
|
||||
UIEditorWorkspaceController controller(BuildPanelRegistry(), workspace, session);
|
||||
|
||||
const auto result = controller.MoveTabToStack("left-tabs", "doc-b", "details-node", 1u);
|
||||
EXPECT_EQ(result.status, UIEditorWorkspaceLayoutOperationStatus::Changed);
|
||||
EXPECT_EQ(result.activePanelId, "doc-b");
|
||||
|
||||
const UIEditorWorkspaceNode* leftTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "left-tabs");
|
||||
const UIEditorWorkspaceNode* detailsTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "details-node");
|
||||
ASSERT_NE(leftTabs, nullptr);
|
||||
ASSERT_NE(detailsTabs, nullptr);
|
||||
ASSERT_EQ(leftTabs->children.size(), 1u);
|
||||
ASSERT_EQ(detailsTabs->children.size(), 2u);
|
||||
EXPECT_EQ(detailsTabs->children[1].panel.panelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceControllerTest, MoveTabToStackRejectsSameStackRequests) {
|
||||
UIEditorWorkspaceController controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
|
||||
const auto result = controller.MoveTabToStack("document-tabs", "doc-b", "document-tabs", 0u);
|
||||
EXPECT_EQ(result.status, UIEditorWorkspaceLayoutOperationStatus::Rejected);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceControllerTest, DockTabRelativeCreatesSplitAroundTargetStack) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.55f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"left-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates = {
|
||||
{ "doc-a", true, true },
|
||||
{ "doc-b", true, true },
|
||||
{ "details", true, true }
|
||||
};
|
||||
|
||||
UIEditorWorkspaceController controller(BuildPanelRegistry(), workspace, session);
|
||||
|
||||
const auto result = controller.DockTabRelative(
|
||||
"left-tabs",
|
||||
"doc-b",
|
||||
"details-node",
|
||||
UIEditorWorkspaceDockPlacement::Bottom,
|
||||
0.4f);
|
||||
EXPECT_EQ(result.status, UIEditorWorkspaceLayoutOperationStatus::Changed);
|
||||
EXPECT_EQ(result.activePanelId, "doc-b");
|
||||
|
||||
const UIEditorWorkspaceNode* detailsTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "details-node");
|
||||
ASSERT_NE(detailsTabs, nullptr);
|
||||
EXPECT_EQ(detailsTabs->kind, XCEngine::UI::Editor::UIEditorWorkspaceNodeKind::TabStack);
|
||||
|
||||
bool foundDockedTab = false;
|
||||
for (const std::string candidate : { "details-node__dock_doc-b_stack", "details-node__dock_doc-b_stack-1" }) {
|
||||
const UIEditorWorkspaceNode* docked =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), candidate);
|
||||
if (docked != nullptr) {
|
||||
foundDockedTab = docked->children.size() == 1u &&
|
||||
docked->children[0].panel.panelId == "doc-b";
|
||||
if (foundDockedTab) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(foundDockedTab);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceControllerTest, DockTabRelativeReturnsNoOpWhenRedockingAlreadyDockedSingleTabStack) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.55f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"left-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates = {
|
||||
{ "doc-a", true, true },
|
||||
{ "doc-b", true, true },
|
||||
{ "details", true, true }
|
||||
};
|
||||
|
||||
UIEditorWorkspaceController controller(BuildPanelRegistry(), workspace, session);
|
||||
|
||||
const auto first = controller.DockTabRelative(
|
||||
"left-tabs",
|
||||
"doc-b",
|
||||
"details-node",
|
||||
UIEditorWorkspaceDockPlacement::Bottom,
|
||||
0.4f);
|
||||
ASSERT_EQ(first.status, UIEditorWorkspaceLayoutOperationStatus::Changed);
|
||||
|
||||
std::string movedStackId = {};
|
||||
for (const std::string candidate : { "details-node__dock_doc-b_stack", "details-node__dock_doc-b_stack-1" }) {
|
||||
if (FindUIEditorWorkspaceNode(controller.GetWorkspace(), candidate) != nullptr) {
|
||||
movedStackId = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_FALSE(movedStackId.empty());
|
||||
|
||||
const UIEditorWorkspaceModel afterFirstDock = controller.GetWorkspace();
|
||||
|
||||
const auto second = controller.DockTabRelative(
|
||||
movedStackId,
|
||||
"doc-b",
|
||||
"details-node",
|
||||
UIEditorWorkspaceDockPlacement::Bottom,
|
||||
0.4f);
|
||||
EXPECT_EQ(second.status, UIEditorWorkspaceLayoutOperationStatus::NoOp);
|
||||
EXPECT_EQ(second.activePanelId, "doc-b");
|
||||
EXPECT_TRUE(
|
||||
AreUIEditorWorkspaceModelsEquivalent(
|
||||
controller.GetWorkspace(),
|
||||
afterFirstDock));
|
||||
}
|
||||
@@ -1,385 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Docking/UIEditorDockHost.h>
|
||||
#include <XCEditor/Panels/UIEditorPanelContentHost.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceInteraction.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::FindUIEditorPanelContentHostPanelState;
|
||||
using XCEngine::UI::Editor::FindUIEditorWorkspaceViewportPresentationFrame;
|
||||
using XCEngine::UI::Editor::IsUIEditorWorkspaceHostedPanelInputOwner;
|
||||
using XCEngine::UI::Editor::IsUIEditorWorkspaceViewportInputOwner;
|
||||
using XCEngine::UI::Editor::UIEditorPanelPresentationKind;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorViewportShellModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceInteractionModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceInteractionState;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorWorkspaceInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::FindUIEditorDockHostSplitterLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostTabStackLayout;
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "viewport", "Viewport", UIEditorPanelPresentationKind::ViewportShell, false, true, true },
|
||||
{ "doc", "Document", UIEditorPanelPresentationKind::Placeholder, true, true, true },
|
||||
{ "details", "Details", UIEditorPanelPresentationKind::HostedContent, true, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.7f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"tab-stack",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("viewport-node", "viewport", "Viewport"),
|
||||
BuildUIEditorWorkspacePanel("doc-node", "doc", "Document", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true));
|
||||
workspace.activePanelId = "viewport";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
UIEditorViewportShellModel BuildViewportShellModel() {
|
||||
UIEditorViewportShellModel model = {};
|
||||
model.spec.chrome.title = "Viewport";
|
||||
model.spec.chrome.subtitle = "Workspace Interaction";
|
||||
model.spec.chrome.showTopBar = true;
|
||||
model.spec.chrome.showBottomBar = true;
|
||||
model.frame.statusText = "Viewport shell";
|
||||
return model;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceInteractionModel BuildInteractionModel() {
|
||||
UIEditorWorkspaceInteractionModel model = {};
|
||||
UIEditorWorkspacePanelPresentationModel presentation = {};
|
||||
presentation.panelId = "viewport";
|
||||
presentation.kind = UIEditorPanelPresentationKind::ViewportShell;
|
||||
presentation.viewportShellModel = BuildViewportShellModel();
|
||||
model.workspacePresentations = { presentation };
|
||||
return model;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerEvent(
|
||||
UIInputEventType type,
|
||||
float x,
|
||||
float y,
|
||||
UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIPoint RectCenter(const UIRect& rect) {
|
||||
return UIPoint(rect.x + rect.width * 0.5f, rect.y + rect.height * 0.5f);
|
||||
}
|
||||
|
||||
const UIEditorDockHostTabStackLayout* FindTabStackByNodeId(
|
||||
const UIEditorDockHostLayout& layout,
|
||||
std::string_view nodeId) {
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
if (tabStack.nodeId == nodeId) {
|
||||
return &tabStack;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorWorkspaceInteractionTest, PointerDownInsideViewportBubblesPointerCaptureRequest) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorWorkspaceInteractionState state = {};
|
||||
const UIEditorWorkspaceInteractionModel model = BuildInteractionModel();
|
||||
|
||||
auto frame = UpdateUIEditorWorkspaceInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
model,
|
||||
{});
|
||||
const auto* viewportFrame =
|
||||
FindUIEditorWorkspaceViewportPresentationFrame(frame.composeFrame, "viewport");
|
||||
ASSERT_NE(viewportFrame, nullptr);
|
||||
const UIPoint center = RectCenter(viewportFrame->viewportShellFrame.slotLayout.inputRect);
|
||||
|
||||
frame = UpdateUIEditorWorkspaceInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
model,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, center.x, center.y),
|
||||
MakePointerEvent(UIInputEventType::PointerButtonDown, center.x, center.y, UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
EXPECT_EQ(frame.result.viewportPanelId, "viewport");
|
||||
EXPECT_TRUE(frame.result.viewportInputFrame.captureStarted);
|
||||
EXPECT_TRUE(frame.result.viewportInputFrame.focused);
|
||||
EXPECT_TRUE(frame.inputOwnerChanged);
|
||||
EXPECT_TRUE(IsUIEditorWorkspaceViewportInputOwner(frame.inputOwner, "viewport"));
|
||||
|
||||
viewportFrame = FindUIEditorWorkspaceViewportPresentationFrame(frame.composeFrame, "viewport");
|
||||
ASSERT_NE(viewportFrame, nullptr);
|
||||
EXPECT_TRUE(viewportFrame->viewportShellFrame.inputFrame.captured);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceInteractionTest, HostedPanelPointerDownTransfersOwnerAndClearsViewportFocus) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorWorkspaceInteractionState state = {};
|
||||
const UIEditorWorkspaceInteractionModel model = BuildInteractionModel();
|
||||
|
||||
auto frame = UpdateUIEditorWorkspaceInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
model,
|
||||
{});
|
||||
const auto* viewportFrame =
|
||||
FindUIEditorWorkspaceViewportPresentationFrame(frame.composeFrame, "viewport");
|
||||
ASSERT_NE(viewportFrame, nullptr);
|
||||
const UIPoint viewportCenter = RectCenter(viewportFrame->viewportShellFrame.slotLayout.inputRect);
|
||||
|
||||
frame = UpdateUIEditorWorkspaceInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
model,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, viewportCenter.x, viewportCenter.y),
|
||||
MakePointerEvent(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
viewportCenter.x,
|
||||
viewportCenter.y,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
ASSERT_TRUE(IsUIEditorWorkspaceViewportInputOwner(frame.inputOwner, "viewport"));
|
||||
|
||||
const auto* detailsPanel = FindUIEditorPanelContentHostPanelState(
|
||||
frame.composeFrame.contentHostFrame,
|
||||
"details");
|
||||
ASSERT_NE(detailsPanel, nullptr);
|
||||
const UIPoint detailsCenter = RectCenter(detailsPanel->bounds);
|
||||
|
||||
frame = UpdateUIEditorWorkspaceInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
model,
|
||||
{
|
||||
MakePointerEvent(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
detailsCenter.x,
|
||||
detailsCenter.y,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.inputOwnerChanged);
|
||||
EXPECT_TRUE(IsUIEditorWorkspaceHostedPanelInputOwner(frame.inputOwner, "details"));
|
||||
EXPECT_EQ(frame.result.viewportPanelId, "viewport");
|
||||
EXPECT_TRUE(frame.result.viewportInputFrame.focusLost);
|
||||
EXPECT_TRUE(frame.result.viewportInputFrame.captureEnded);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceInteractionTest, PointerUpInsideViewportBubblesPointerReleaseRequest) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorWorkspaceInteractionState state = {};
|
||||
const UIEditorWorkspaceInteractionModel model = BuildInteractionModel();
|
||||
|
||||
auto frame = UpdateUIEditorWorkspaceInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
model,
|
||||
{});
|
||||
const auto* viewportFrame =
|
||||
FindUIEditorWorkspaceViewportPresentationFrame(frame.composeFrame, "viewport");
|
||||
ASSERT_NE(viewportFrame, nullptr);
|
||||
const UIPoint center = RectCenter(viewportFrame->viewportShellFrame.slotLayout.inputRect);
|
||||
|
||||
UpdateUIEditorWorkspaceInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
model,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, center.x, center.y),
|
||||
MakePointerEvent(UIInputEventType::PointerButtonDown, center.x, center.y, UIPointerButton::Left)
|
||||
});
|
||||
|
||||
frame = UpdateUIEditorWorkspaceInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
model,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerButtonUp, center.x, center.y, UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_EQ(frame.result.viewportPanelId, "viewport");
|
||||
EXPECT_TRUE(frame.result.viewportInputFrame.captureEnded);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceInteractionTest, PointerDownOnViewportTopBarBubblesPointerCaptureRequest) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorWorkspaceInteractionState state = {};
|
||||
const UIEditorWorkspaceInteractionModel model = BuildInteractionModel();
|
||||
|
||||
auto frame = UpdateUIEditorWorkspaceInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
model,
|
||||
{});
|
||||
const auto* viewportFrame =
|
||||
FindUIEditorWorkspaceViewportPresentationFrame(frame.composeFrame, "viewport");
|
||||
ASSERT_NE(viewportFrame, nullptr);
|
||||
const UIPoint topBarCenter = RectCenter(viewportFrame->viewportShellFrame.slotLayout.topBarRect);
|
||||
|
||||
frame = UpdateUIEditorWorkspaceInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
model,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, topBarCenter.x, topBarCenter.y),
|
||||
MakePointerEvent(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
topBarCenter.x,
|
||||
topBarCenter.y,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
EXPECT_EQ(frame.result.viewportPanelId, "viewport");
|
||||
EXPECT_TRUE(frame.result.viewportInputFrame.captureStarted);
|
||||
EXPECT_TRUE(frame.result.viewportInputFrame.pointerPressedInside);
|
||||
EXPECT_LT(frame.result.viewportInputFrame.localPointerPosition.y, 0.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceInteractionTest, ActivatingDocumentTabRemovesViewportPresentationInSameFrame) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorWorkspaceInteractionState state = {};
|
||||
const UIEditorWorkspaceInteractionModel model = BuildInteractionModel();
|
||||
|
||||
auto frame = UpdateUIEditorWorkspaceInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
model,
|
||||
{});
|
||||
ASSERT_EQ(frame.composeFrame.dockHostLayout.tabStacks.size(), 2u);
|
||||
const auto* documentStack =
|
||||
FindTabStackByNodeId(frame.composeFrame.dockHostLayout, "tab-stack");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
const UIRect docTabRect = documentStack->tabStripLayout.tabHeaderRects[1];
|
||||
const UIPoint docTabCenter = RectCenter(docTabRect);
|
||||
|
||||
frame = UpdateUIEditorWorkspaceInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
model,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, docTabCenter.x, docTabCenter.y),
|
||||
MakePointerEvent(UIInputEventType::PointerButtonDown, docTabCenter.x, docTabCenter.y, UIPointerButton::Left),
|
||||
MakePointerEvent(UIInputEventType::PointerButtonUp, docTabCenter.x, docTabCenter.y, UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.dockHostResult.commandExecuted);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc");
|
||||
EXPECT_TRUE(frame.composeFrame.viewportFrames.empty());
|
||||
ASSERT_EQ(frame.composeFrame.dockHostLayout.tabStacks.size(), 2u);
|
||||
documentStack = FindTabStackByNodeId(frame.composeFrame.dockHostLayout, "tab-stack");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
EXPECT_EQ(documentStack->selectedPanelId, "doc");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceInteractionTest, SplitterDragKeepsViewportBoundsSyncedAfterLayoutChange) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorWorkspaceInteractionState state = {};
|
||||
const UIEditorWorkspaceInteractionModel model = BuildInteractionModel();
|
||||
|
||||
auto frame = UpdateUIEditorWorkspaceInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
model,
|
||||
{});
|
||||
const auto* viewportFrame =
|
||||
FindUIEditorWorkspaceViewportPresentationFrame(frame.composeFrame, "viewport");
|
||||
ASSERT_NE(viewportFrame, nullptr);
|
||||
const float initialWidth = viewportFrame->bounds.width;
|
||||
const auto* splitter =
|
||||
FindUIEditorDockHostSplitterLayout(frame.composeFrame.dockHostLayout, "root-split");
|
||||
ASSERT_NE(splitter, nullptr);
|
||||
const UIPoint splitterCenter = RectCenter(splitter->handleHitRect);
|
||||
|
||||
frame = UpdateUIEditorWorkspaceInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
model,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, splitterCenter.x, splitterCenter.y),
|
||||
MakePointerEvent(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
splitterCenter.x,
|
||||
splitterCenter.y,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
|
||||
splitter = FindUIEditorDockHostSplitterLayout(frame.composeFrame.dockHostLayout, "root-split");
|
||||
ASSERT_NE(splitter, nullptr);
|
||||
const UIPoint draggedCenter(
|
||||
splitter->handleHitRect.x + splitter->handleHitRect.width * 0.5f + 92.0f,
|
||||
splitter->handleHitRect.y + splitter->handleHitRect.height * 0.5f);
|
||||
frame = UpdateUIEditorWorkspaceInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||
model,
|
||||
{
|
||||
MakePointerEvent(UIInputEventType::PointerMove, draggedCenter.x, draggedCenter.y)
|
||||
});
|
||||
EXPECT_TRUE(frame.result.dockHostResult.layoutChanged);
|
||||
|
||||
viewportFrame = FindUIEditorWorkspaceViewportPresentationFrame(frame.composeFrame, "viewport");
|
||||
ASSERT_NE(viewportFrame, nullptr);
|
||||
EXPECT_GT(viewportFrame->bounds.width, initialWidth);
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceController.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceLayoutPersistence.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Editor::AreUIEditorWorkspaceLayoutSnapshotsEquivalent;
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceLayoutSnapshot;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::DeserializeUIEditorWorkspaceLayoutSnapshot;
|
||||
using XCEngine::UI::Editor::SerializeUIEditorWorkspaceLayoutSnapshot;
|
||||
using XCEngine::UI::Editor::TryCloseUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::TryHideUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceController;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceLayoutLoadCode;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceLayoutOperationStatus;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceNodeKind;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.66f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspaceSingleTabStack("details-node", "details", "Details", true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
std::string ReplaceFirst(
|
||||
std::string source,
|
||||
std::string_view from,
|
||||
std::string_view to) {
|
||||
const std::size_t index = source.find(from);
|
||||
EXPECT_NE(index, std::string::npos);
|
||||
if (index == std::string::npos) {
|
||||
return source;
|
||||
}
|
||||
|
||||
source.replace(index, from.size(), to);
|
||||
return source;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorWorkspaceLayoutPersistenceTest, SerializeAndDeserializeRoundTripPreservesWorkspaceAndSession) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
ASSERT_TRUE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
|
||||
ASSERT_TRUE(TryCloseUIEditorWorkspacePanel(registry, workspace, session, "doc-b"));
|
||||
|
||||
const auto snapshot = BuildUIEditorWorkspaceLayoutSnapshot(workspace, session);
|
||||
const std::string serialized = SerializeUIEditorWorkspaceLayoutSnapshot(snapshot);
|
||||
const auto loadResult =
|
||||
DeserializeUIEditorWorkspaceLayoutSnapshot(registry, serialized);
|
||||
|
||||
ASSERT_TRUE(loadResult.IsValid()) << loadResult.message;
|
||||
EXPECT_TRUE(
|
||||
AreUIEditorWorkspaceLayoutSnapshotsEquivalent(loadResult.snapshot, snapshot));
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceLayoutPersistenceTest, DeserializeRejectsInvalidSelectedTabIndex) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
const UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
const std::string invalidSerialized = ReplaceFirst(
|
||||
SerializeUIEditorWorkspaceLayoutSnapshot(
|
||||
BuildUIEditorWorkspaceLayoutSnapshot(workspace, session)),
|
||||
"node_tabstack \"document-tabs\" 0 2",
|
||||
"node_tabstack \"document-tabs\" 5 2");
|
||||
|
||||
const auto loadResult =
|
||||
DeserializeUIEditorWorkspaceLayoutSnapshot(registry, invalidSerialized);
|
||||
|
||||
EXPECT_EQ(loadResult.code, UIEditorWorkspaceLayoutLoadCode::InvalidWorkspace);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceLayoutPersistenceTest, DeserializeRejectsMissingSessionRecord) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
const UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
const std::string invalidSerialized = ReplaceFirst(
|
||||
SerializeUIEditorWorkspaceLayoutSnapshot(
|
||||
BuildUIEditorWorkspaceLayoutSnapshot(workspace, session)),
|
||||
"session \"details\" 1 1\n",
|
||||
"");
|
||||
|
||||
const auto loadResult =
|
||||
DeserializeUIEditorWorkspaceLayoutSnapshot(registry, invalidSerialized);
|
||||
|
||||
EXPECT_EQ(loadResult.code, UIEditorWorkspaceLayoutLoadCode::InvalidWorkspaceSession);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceLayoutPersistenceTest, DeserializeUpgradesLegacyStandalonePanelLeaves) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
const std::string legacySerialized =
|
||||
"XCUI_EDITOR_WORKSPACE_LAYOUT 1\n"
|
||||
"active \"doc-a\"\n"
|
||||
"node_split \"root-split\" \"horizontal\" 0.66\n"
|
||||
"node_tabstack \"document-tabs\" 0 2\n"
|
||||
"node_panel \"doc-a-node\" \"doc-a\" \"Document A\" 1\n"
|
||||
"node_panel \"doc-b-node\" \"doc-b\" \"Document B\" 1\n"
|
||||
"node_panel \"details-node\" \"details\" \"Details\" 1\n"
|
||||
"session \"doc-a\" 1 1\n"
|
||||
"session \"doc-b\" 1 1\n"
|
||||
"session \"details\" 1 1\n";
|
||||
|
||||
const auto loadResult =
|
||||
DeserializeUIEditorWorkspaceLayoutSnapshot(registry, legacySerialized);
|
||||
|
||||
ASSERT_TRUE(loadResult.IsValid()) << loadResult.message;
|
||||
ASSERT_EQ(loadResult.snapshot.workspace.root.kind, UIEditorWorkspaceNodeKind::Split);
|
||||
ASSERT_EQ(loadResult.snapshot.workspace.root.children.size(), 2u);
|
||||
EXPECT_EQ(
|
||||
loadResult.snapshot.workspace.root.children[1].kind,
|
||||
UIEditorWorkspaceNodeKind::TabStack);
|
||||
ASSERT_EQ(loadResult.snapshot.workspace.root.children[1].children.size(), 1u);
|
||||
EXPECT_EQ(
|
||||
loadResult.snapshot.workspace.root.children[1].children[0].panel.panelId,
|
||||
"details");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceLayoutPersistenceTest, RestoreSerializedLayoutRestoresSavedStateAfterFurtherMutations) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
UIEditorWorkspaceController controller =
|
||||
BuildDefaultUIEditorWorkspaceController(registry, BuildWorkspace());
|
||||
|
||||
EXPECT_EQ(
|
||||
controller.Dispatch({ UIEditorWorkspaceCommandKind::HidePanel, "doc-a" }).status,
|
||||
UIEditorWorkspaceCommandStatus::Changed);
|
||||
|
||||
const std::string savedLayout =
|
||||
SerializeUIEditorWorkspaceLayoutSnapshot(controller.CaptureLayoutSnapshot());
|
||||
|
||||
EXPECT_EQ(
|
||||
controller.Dispatch({ UIEditorWorkspaceCommandKind::ClosePanel, "doc-b" }).status,
|
||||
UIEditorWorkspaceCommandStatus::Changed);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "details");
|
||||
|
||||
const auto restoreResult = controller.RestoreSerializedLayout(savedLayout);
|
||||
EXPECT_EQ(restoreResult.status, UIEditorWorkspaceLayoutOperationStatus::Changed);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-b");
|
||||
ASSERT_EQ(restoreResult.visiblePanelIds.size(), 2u);
|
||||
EXPECT_EQ(restoreResult.visiblePanelIds[0], "doc-b");
|
||||
EXPECT_EQ(restoreResult.visiblePanelIds[1], "details");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceLayoutPersistenceTest, RestoreSerializedLayoutRejectsInvalidPayloadWithoutMutatingCurrentState) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
UIEditorWorkspaceController controller =
|
||||
BuildDefaultUIEditorWorkspaceController(registry, BuildWorkspace());
|
||||
|
||||
EXPECT_EQ(
|
||||
controller.Dispatch({ UIEditorWorkspaceCommandKind::ActivatePanel, "details" }).status,
|
||||
UIEditorWorkspaceCommandStatus::Changed);
|
||||
const auto before = controller.CaptureLayoutSnapshot();
|
||||
|
||||
const std::string invalidSerialized = ReplaceFirst(
|
||||
SerializeUIEditorWorkspaceLayoutSnapshot(before),
|
||||
"active \"details\"",
|
||||
"active \"missing\"");
|
||||
|
||||
const auto restoreResult = controller.RestoreSerializedLayout(invalidSerialized);
|
||||
EXPECT_EQ(restoreResult.status, UIEditorWorkspaceLayoutOperationStatus::Rejected);
|
||||
|
||||
const auto after = controller.CaptureLayoutSnapshot();
|
||||
EXPECT_TRUE(AreUIEditorWorkspaceLayoutSnapshotsEquivalent(after, before));
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceLayoutPersistenceTest, RestoreLayoutSnapshotReturnsNoOpWhenStateAlreadyMatchesSnapshot) {
|
||||
UIEditorWorkspaceController controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
|
||||
const auto result = controller.RestoreLayoutSnapshot(controller.CaptureLayoutSnapshot());
|
||||
|
||||
EXPECT_EQ(result.status, UIEditorWorkspaceLayoutOperationStatus::NoOp);
|
||||
}
|
||||
@@ -1,508 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceModel.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceMutation.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceQueries.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceSession.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceValidation.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::AreUIEditorWorkspaceModelsEquivalent;
|
||||
using XCEngine::UI::Editor::CanonicalizeUIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::CollectUIEditorWorkspaceVisiblePanels;
|
||||
using XCEngine::UI::Editor::ContainsUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::FindUIEditorWorkspaceActivePanel;
|
||||
using XCEngine::UI::Editor::FindUIEditorWorkspaceNode;
|
||||
using XCEngine::UI::Editor::TryActivateUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::TryDockUIEditorWorkspaceTabRelative;
|
||||
using XCEngine::UI::Editor::TryMoveUIEditorWorkspaceTabToStack;
|
||||
using XCEngine::UI::Editor::TrySetUIEditorWorkspaceSplitRatio;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceDockPlacement;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceNode;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceNodeKind;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceValidationCode;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceVisiblePanel;
|
||||
using XCEngine::UI::Editor::ValidateUIEditorWorkspace;
|
||||
|
||||
std::vector<std::string> CollectVisiblePanelIds(const UIEditorWorkspaceModel& workspace) {
|
||||
const auto panels = CollectUIEditorWorkspaceVisiblePanels(workspace);
|
||||
std::vector<std::string> ids = {};
|
||||
ids.reserve(panels.size());
|
||||
for (const auto& panel : panels) {
|
||||
ids.push_back(panel.panelId);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, ValidationRejectsSplitWithoutExactlyTwoChildren) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root.kind = UIEditorWorkspaceNodeKind::Split;
|
||||
workspace.root.nodeId = "root-split";
|
||||
workspace.root.splitAxis = UIEditorWorkspaceSplitAxis::Horizontal;
|
||||
workspace.root.splitRatio = 0.5f;
|
||||
workspace.root.children.push_back(
|
||||
BuildUIEditorWorkspacePanel("panel-a-node", "panel-a", "Panel A", true));
|
||||
|
||||
const auto result = ValidateUIEditorWorkspace(workspace);
|
||||
EXPECT_EQ(result.code, UIEditorWorkspaceValidationCode::InvalidSplitChildCount);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, ValidationRejectsTabStackWithNestedSplitChild) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceTabStack(
|
||||
"root-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("panel-a-node", "panel-a", "Panel A", true),
|
||||
BuildUIEditorWorkspaceSplit(
|
||||
"nested-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.5f,
|
||||
BuildUIEditorWorkspacePanel("panel-b-node", "panel-b", "Panel B", true),
|
||||
BuildUIEditorWorkspacePanel("panel-c-node", "panel-c", "Panel C", true))
|
||||
},
|
||||
0u);
|
||||
|
||||
const auto result = ValidateUIEditorWorkspace(workspace);
|
||||
EXPECT_EQ(result.code, UIEditorWorkspaceValidationCode::NonPanelTabChild);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, ValidationRejectsDuplicateNodeIds) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.5f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"left-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("duplicate-node", "panel-a", "Panel A", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"right-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("duplicate-node", "panel-b", "Panel B", true)
|
||||
},
|
||||
0u));
|
||||
|
||||
const auto result = ValidateUIEditorWorkspace(workspace);
|
||||
EXPECT_EQ(result.code, UIEditorWorkspaceValidationCode::DuplicateNodeId);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, VisiblePanelsOnlyIncludeSelectedTabsAcrossSplitTree) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.68f,
|
||||
BuildUIEditorWorkspaceSingleTabStack("left-panel-node", "left-panel", "Left Panel", true),
|
||||
BuildUIEditorWorkspaceSplit(
|
||||
"right-split",
|
||||
UIEditorWorkspaceSplitAxis::Vertical,
|
||||
0.74f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
1u),
|
||||
BuildUIEditorWorkspaceSingleTabStack(
|
||||
"bottom-panel-node",
|
||||
"bottom-panel",
|
||||
"Bottom Panel",
|
||||
true)));
|
||||
workspace.activePanelId = "doc-b";
|
||||
|
||||
const auto validation = ValidateUIEditorWorkspace(workspace);
|
||||
ASSERT_TRUE(validation.IsValid()) << validation.message;
|
||||
|
||||
const auto visibleIds = CollectVisiblePanelIds(workspace);
|
||||
ASSERT_EQ(visibleIds.size(), 3u);
|
||||
EXPECT_EQ(visibleIds[0], "left-panel");
|
||||
EXPECT_EQ(visibleIds[1], "doc-b");
|
||||
EXPECT_EQ(visibleIds[2], "bottom-panel");
|
||||
|
||||
const auto* activePanel = FindUIEditorWorkspaceActivePanel(workspace);
|
||||
ASSERT_NE(activePanel, nullptr);
|
||||
EXPECT_EQ(activePanel->panelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, ActivatingHiddenPanelSelectsContainingTabAndUpdatesActivePanel) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.62f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true),
|
||||
BuildUIEditorWorkspacePanel("doc-c-node", "doc-c", "Document C", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspaceSingleTabStack("details-node", "details", "Details", true));
|
||||
|
||||
ASSERT_TRUE(ContainsUIEditorWorkspacePanel(workspace, "doc-b"));
|
||||
ASSERT_TRUE(TryActivateUIEditorWorkspacePanel(workspace, "doc-b"));
|
||||
EXPECT_EQ(workspace.activePanelId, "doc-b");
|
||||
ASSERT_EQ(workspace.root.children.front().selectedTabIndex, 1u);
|
||||
|
||||
const auto visibleIds = CollectVisiblePanelIds(workspace);
|
||||
ASSERT_EQ(visibleIds.size(), 2u);
|
||||
EXPECT_EQ(visibleIds[0], "doc-b");
|
||||
EXPECT_EQ(visibleIds[1], "details");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, ValidationRejectsActivePanelHiddenByCurrentTabSelection) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u);
|
||||
workspace.activePanelId = "doc-b";
|
||||
|
||||
const auto result = ValidateUIEditorWorkspace(workspace);
|
||||
EXPECT_EQ(result.code, UIEditorWorkspaceValidationCode::InvalidActivePanelId);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, SplitRatioMutationTargetsSplitNodeAndRejectsInvalidValues) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.62f,
|
||||
BuildUIEditorWorkspaceSingleTabStack("left-node", "left", "Left", true),
|
||||
BuildUIEditorWorkspaceSingleTabStack("right-node", "right", "Right", true));
|
||||
|
||||
ASSERT_TRUE(TrySetUIEditorWorkspaceSplitRatio(workspace, "root-split", 0.35f));
|
||||
const auto* splitNode = FindUIEditorWorkspaceNode(workspace, "root-split");
|
||||
ASSERT_NE(splitNode, nullptr);
|
||||
EXPECT_FLOAT_EQ(splitNode->splitRatio, 0.35f);
|
||||
|
||||
EXPECT_FALSE(TrySetUIEditorWorkspaceSplitRatio(workspace, "root-split", 0.35f));
|
||||
EXPECT_FALSE(TrySetUIEditorWorkspaceSplitRatio(workspace, "left-node", 0.5f));
|
||||
EXPECT_FALSE(TrySetUIEditorWorkspaceSplitRatio(workspace, "missing", 0.5f));
|
||||
EXPECT_FALSE(TrySetUIEditorWorkspaceSplitRatio(workspace, "root-split", 1.0f));
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, CanonicalizeWrapsStandalonePanelsIntoSingleTabStacks) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.5f,
|
||||
BuildUIEditorWorkspacePanel("left-node", "left", "Left", true),
|
||||
BuildUIEditorWorkspacePanel("right-node", "right", "Right", true));
|
||||
|
||||
const UIEditorWorkspaceModel canonicalWorkspace =
|
||||
CanonicalizeUIEditorWorkspaceModel(workspace);
|
||||
|
||||
ASSERT_EQ(canonicalWorkspace.root.kind, UIEditorWorkspaceNodeKind::Split);
|
||||
ASSERT_EQ(canonicalWorkspace.root.children.size(), 2u);
|
||||
EXPECT_EQ(canonicalWorkspace.root.children[0].kind, UIEditorWorkspaceNodeKind::TabStack);
|
||||
EXPECT_EQ(canonicalWorkspace.root.children[0].nodeId, "left-node");
|
||||
ASSERT_EQ(canonicalWorkspace.root.children[0].children.size(), 1u);
|
||||
EXPECT_EQ(
|
||||
canonicalWorkspace.root.children[0].children[0].kind,
|
||||
UIEditorWorkspaceNodeKind::Panel);
|
||||
EXPECT_EQ(
|
||||
canonicalWorkspace.root.children[0].children[0].panel.panelId,
|
||||
"left");
|
||||
EXPECT_EQ(canonicalWorkspace.root.children[1].kind, UIEditorWorkspaceNodeKind::TabStack);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, MoveTabToStackMergesIntoTargetStackAndActivatesMovedPanel) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.58f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"left-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspaceSingleTabStack(
|
||||
"right-tabs",
|
||||
"details",
|
||||
"Details",
|
||||
true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates = {
|
||||
{ "doc-a", true, true },
|
||||
{ "doc-b", true, true },
|
||||
{ "details", true, true }
|
||||
};
|
||||
|
||||
ASSERT_TRUE(TryMoveUIEditorWorkspaceTabToStack(
|
||||
workspace,
|
||||
session,
|
||||
"left-tabs",
|
||||
"doc-b",
|
||||
"right-tabs",
|
||||
1u));
|
||||
|
||||
const UIEditorWorkspaceNode* leftTabs =
|
||||
FindUIEditorWorkspaceNode(workspace, "left-tabs");
|
||||
const UIEditorWorkspaceNode* rightTabs =
|
||||
FindUIEditorWorkspaceNode(workspace, "right-tabs");
|
||||
ASSERT_NE(leftTabs, nullptr);
|
||||
ASSERT_NE(rightTabs, nullptr);
|
||||
ASSERT_EQ(leftTabs->children.size(), 1u);
|
||||
ASSERT_EQ(rightTabs->children.size(), 2u);
|
||||
EXPECT_EQ(leftTabs->children[0].panel.panelId, "doc-a");
|
||||
EXPECT_EQ(rightTabs->children[0].panel.panelId, "details");
|
||||
EXPECT_EQ(rightTabs->children[1].panel.panelId, "doc-b");
|
||||
EXPECT_EQ(rightTabs->selectedTabIndex, 1u);
|
||||
EXPECT_EQ(workspace.activePanelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, MoveTabToStackRejectsSameStackRequests) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceTabStack(
|
||||
"left-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u);
|
||||
workspace.activePanelId = "doc-a";
|
||||
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates = {
|
||||
{ "doc-a", true, true },
|
||||
{ "doc-b", true, true }
|
||||
};
|
||||
|
||||
EXPECT_FALSE(TryMoveUIEditorWorkspaceTabToStack(
|
||||
workspace,
|
||||
session,
|
||||
"left-tabs",
|
||||
"doc-b",
|
||||
"left-tabs",
|
||||
1u));
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, DockTabRelativeSplitsTargetStackAndCreatesNewLeaf) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.58f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"left-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspaceSingleTabStack(
|
||||
"right-tabs",
|
||||
"details",
|
||||
"Details",
|
||||
true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates = {
|
||||
{ "doc-a", true, true },
|
||||
{ "doc-b", true, true },
|
||||
{ "details", true, true }
|
||||
};
|
||||
|
||||
ASSERT_TRUE(TryDockUIEditorWorkspaceTabRelative(
|
||||
workspace,
|
||||
session,
|
||||
"left-tabs",
|
||||
"doc-b",
|
||||
"right-tabs",
|
||||
UIEditorWorkspaceDockPlacement::Bottom,
|
||||
0.4f));
|
||||
|
||||
const UIEditorWorkspaceNode* rightTabs =
|
||||
FindUIEditorWorkspaceNode(workspace, "right-tabs");
|
||||
ASSERT_NE(rightTabs, nullptr);
|
||||
ASSERT_EQ(rightTabs->kind, UIEditorWorkspaceNodeKind::TabStack);
|
||||
ASSERT_EQ(rightTabs->children.size(), 1u);
|
||||
EXPECT_EQ(rightTabs->children[0].panel.panelId, "details");
|
||||
|
||||
const UIEditorWorkspaceNode* movedTabStack = nullptr;
|
||||
if (const UIEditorWorkspaceNode* rootSplit =
|
||||
FindUIEditorWorkspaceNode(workspace, "root-split");
|
||||
rootSplit != nullptr) {
|
||||
const auto visibleIds = CollectVisiblePanelIds(workspace);
|
||||
ASSERT_EQ(visibleIds.size(), 3u);
|
||||
EXPECT_EQ(visibleIds[0], "doc-a");
|
||||
EXPECT_EQ(visibleIds[1], "details");
|
||||
EXPECT_EQ(visibleIds[2], "doc-b");
|
||||
}
|
||||
|
||||
const std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels =
|
||||
CollectUIEditorWorkspaceVisiblePanels(workspace);
|
||||
ASSERT_EQ(visiblePanels.size(), 3u);
|
||||
EXPECT_EQ(visiblePanels[2].panelId, "doc-b");
|
||||
|
||||
for (const std::string candidate : { "right-tabs__dock_doc-b_stack", "right-tabs__dock_doc-b_stack-1" }) {
|
||||
movedTabStack = FindUIEditorWorkspaceNode(workspace, candidate);
|
||||
if (movedTabStack != nullptr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_NE(movedTabStack, nullptr);
|
||||
ASSERT_EQ(movedTabStack->kind, UIEditorWorkspaceNodeKind::TabStack);
|
||||
ASSERT_EQ(movedTabStack->children.size(), 1u);
|
||||
EXPECT_EQ(movedTabStack->children[0].panel.panelId, "doc-b");
|
||||
EXPECT_EQ(workspace.activePanelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, DockTabRelativeCanRedockSingleTabBranchBackOntoSiblingStack) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceTabStack(
|
||||
"center-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("scene-node", "scene", "Scene", true),
|
||||
BuildUIEditorWorkspacePanel("game-node", "game", "Game", true)
|
||||
},
|
||||
0u);
|
||||
workspace.activePanelId = "scene";
|
||||
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates = {
|
||||
{ "scene", true, true },
|
||||
{ "game", true, true }
|
||||
};
|
||||
|
||||
ASSERT_TRUE(TryDockUIEditorWorkspaceTabRelative(
|
||||
workspace,
|
||||
session,
|
||||
"center-tabs",
|
||||
"scene",
|
||||
"center-tabs",
|
||||
UIEditorWorkspaceDockPlacement::Top,
|
||||
0.5f));
|
||||
|
||||
const UIEditorWorkspaceNode* firstDockStack =
|
||||
FindUIEditorWorkspaceNode(workspace, "center-tabs__dock_scene_stack");
|
||||
ASSERT_NE(firstDockStack, nullptr);
|
||||
ASSERT_EQ(firstDockStack->kind, UIEditorWorkspaceNodeKind::TabStack);
|
||||
ASSERT_EQ(firstDockStack->children.size(), 1u);
|
||||
EXPECT_EQ(firstDockStack->children[0].panel.panelId, "scene");
|
||||
|
||||
ASSERT_TRUE(TryDockUIEditorWorkspaceTabRelative(
|
||||
workspace,
|
||||
session,
|
||||
"center-tabs__dock_scene_stack",
|
||||
"scene",
|
||||
"center-tabs",
|
||||
UIEditorWorkspaceDockPlacement::Top,
|
||||
0.5f));
|
||||
|
||||
const auto validation = ValidateUIEditorWorkspace(workspace);
|
||||
ASSERT_TRUE(validation.IsValid()) << validation.message;
|
||||
|
||||
const std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels =
|
||||
CollectUIEditorWorkspaceVisiblePanels(workspace);
|
||||
ASSERT_EQ(visiblePanels.size(), 2u);
|
||||
EXPECT_EQ(visiblePanels[0].panelId, "scene");
|
||||
EXPECT_EQ(visiblePanels[1].panelId, "game");
|
||||
EXPECT_EQ(workspace.activePanelId, "scene");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, DockTabRelativeCanRedockGeneratedSingleTabStackWithoutChangingLayout) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.58f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"left-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspaceSingleTabStack(
|
||||
"right-tabs",
|
||||
"details",
|
||||
"Details",
|
||||
true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates = {
|
||||
{ "doc-a", true, true },
|
||||
{ "doc-b", true, true },
|
||||
{ "details", true, true }
|
||||
};
|
||||
|
||||
ASSERT_TRUE(TryDockUIEditorWorkspaceTabRelative(
|
||||
workspace,
|
||||
session,
|
||||
"left-tabs",
|
||||
"doc-b",
|
||||
"right-tabs",
|
||||
UIEditorWorkspaceDockPlacement::Bottom,
|
||||
0.4f));
|
||||
|
||||
std::string movedStackId = {};
|
||||
for (const std::string candidate : { "right-tabs__dock_doc-b_stack", "right-tabs__dock_doc-b_stack-1" }) {
|
||||
if (FindUIEditorWorkspaceNode(workspace, candidate) != nullptr) {
|
||||
movedStackId = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_FALSE(movedStackId.empty());
|
||||
|
||||
const UIEditorWorkspaceModel afterFirstDock = workspace;
|
||||
|
||||
ASSERT_TRUE(TryDockUIEditorWorkspaceTabRelative(
|
||||
workspace,
|
||||
session,
|
||||
movedStackId,
|
||||
"doc-b",
|
||||
"right-tabs",
|
||||
UIEditorWorkspaceDockPlacement::Bottom,
|
||||
0.4f));
|
||||
|
||||
const auto validation = ValidateUIEditorWorkspace(workspace);
|
||||
ASSERT_TRUE(validation.IsValid()) << validation.message;
|
||||
EXPECT_TRUE(AreUIEditorWorkspaceModelsEquivalent(workspace, afterFirstDock));
|
||||
EXPECT_EQ(workspace.activePanelId, "doc-b");
|
||||
|
||||
const UIEditorWorkspaceNode* rightTabs =
|
||||
FindUIEditorWorkspaceNode(workspace, "right-tabs");
|
||||
ASSERT_NE(rightTabs, nullptr);
|
||||
EXPECT_EQ(rightTabs->kind, UIEditorWorkspaceNodeKind::TabStack);
|
||||
ASSERT_EQ(rightTabs->children.size(), 1u);
|
||||
EXPECT_EQ(rightTabs->children[0].panel.panelId, "details");
|
||||
|
||||
const UIEditorWorkspaceNode* movedTabStack =
|
||||
FindUIEditorWorkspaceNode(workspace, movedStackId);
|
||||
ASSERT_NE(movedTabStack, nullptr);
|
||||
ASSERT_EQ(movedTabStack->kind, UIEditorWorkspaceNodeKind::TabStack);
|
||||
ASSERT_EQ(movedTabStack->children.size(), 1u);
|
||||
EXPECT_EQ(movedTabStack->children[0].panel.panelId, "doc-b");
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Panels/UIEditorPanelRegistry.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceModel.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceSession.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::CollectUIEditorWorkspaceVisiblePanels;
|
||||
using XCEngine::UI::Editor::FindUIEditorPanelDescriptor;
|
||||
using XCEngine::UI::Editor::FindUIEditorPanelSessionState;
|
||||
using XCEngine::UI::Editor::TryActivateUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::TryCloseUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::TryHideUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::TryOpenUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::TryShowUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSessionValidationCode;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
using XCEngine::UI::Editor::ValidateUIEditorWorkspaceSession;
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true },
|
||||
{ "root", "Root", {}, true, false, false }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.66f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
std::vector<std::string> CollectVisiblePanelIds(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
const auto panels = CollectUIEditorWorkspaceVisiblePanels(workspace, session);
|
||||
std::vector<std::string> ids = {};
|
||||
ids.reserve(panels.size());
|
||||
for (const auto& panel : panels) {
|
||||
ids.push_back(panel.panelId);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorWorkspaceSessionTest, DefaultSessionTracksEveryWorkspacePanelAsOpenAndVisible) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
|
||||
const UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
const auto validation = ValidateUIEditorWorkspaceSession(registry, workspace, session);
|
||||
ASSERT_TRUE(validation.IsValid()) << validation.message;
|
||||
|
||||
ASSERT_EQ(session.panelStates.size(), 3u);
|
||||
ASSERT_NE(FindUIEditorPanelSessionState(session, "doc-a"), nullptr);
|
||||
ASSERT_NE(FindUIEditorPanelSessionState(session, "doc-b"), nullptr);
|
||||
ASSERT_NE(FindUIEditorPanelSessionState(session, "details"), nullptr);
|
||||
|
||||
const auto visibleIds = CollectVisiblePanelIds(workspace, session);
|
||||
ASSERT_EQ(visibleIds.size(), 2u);
|
||||
EXPECT_EQ(visibleIds[0], "doc-a");
|
||||
EXPECT_EQ(visibleIds[1], "details");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceSessionTest, HidingActiveDocumentSelectsNextVisiblePanelInTabStack) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
ASSERT_TRUE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
|
||||
|
||||
const auto* hiddenState = FindUIEditorPanelSessionState(session, "doc-a");
|
||||
ASSERT_NE(hiddenState, nullptr);
|
||||
EXPECT_TRUE(hiddenState->open);
|
||||
EXPECT_FALSE(hiddenState->visible);
|
||||
EXPECT_EQ(workspace.activePanelId, "doc-b");
|
||||
EXPECT_EQ(workspace.root.children.front().selectedTabIndex, 1u);
|
||||
|
||||
const auto visibleIds = CollectVisiblePanelIds(workspace, session);
|
||||
ASSERT_EQ(visibleIds.size(), 2u);
|
||||
EXPECT_EQ(visibleIds[0], "doc-b");
|
||||
EXPECT_EQ(visibleIds[1], "details");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceSessionTest, ShowingHiddenPanelRestoresVisibilityAndActivation) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
ASSERT_TRUE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
|
||||
ASSERT_TRUE(TryShowUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
|
||||
|
||||
EXPECT_EQ(workspace.activePanelId, "doc-a");
|
||||
EXPECT_EQ(workspace.root.children.front().selectedTabIndex, 0u);
|
||||
|
||||
const auto* restoredState = FindUIEditorPanelSessionState(session, "doc-a");
|
||||
ASSERT_NE(restoredState, nullptr);
|
||||
EXPECT_TRUE(restoredState->open);
|
||||
EXPECT_TRUE(restoredState->visible);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceSessionTest, ClosingPanelPreventsActivationUntilReopened) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
ASSERT_TRUE(TryCloseUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
|
||||
EXPECT_FALSE(TryActivateUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
|
||||
EXPECT_EQ(workspace.activePanelId, "doc-b");
|
||||
|
||||
ASSERT_TRUE(TryOpenUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
|
||||
EXPECT_EQ(workspace.activePanelId, "doc-a");
|
||||
EXPECT_EQ(workspace.root.children.front().selectedTabIndex, 0u);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceSessionTest, NonCloseablePanelCannotBeClosedOrHidden) {
|
||||
UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspacePanel("root-node", "root", "Root", true);
|
||||
workspace.activePanelId = "root";
|
||||
UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
ASSERT_NE(FindUIEditorPanelDescriptor(registry, "root"), nullptr);
|
||||
EXPECT_FALSE(TryCloseUIEditorWorkspacePanel(registry, workspace, session, "root"));
|
||||
EXPECT_FALSE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "root"));
|
||||
|
||||
const auto* rootState = FindUIEditorPanelSessionState(session, "root");
|
||||
ASSERT_NE(rootState, nullptr);
|
||||
EXPECT_TRUE(rootState->open);
|
||||
EXPECT_TRUE(rootState->visible);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceSessionTest, ValidationRejectsMissingPanelStateAndInvalidActivePanel) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
UIEditorWorkspaceSession session =
|
||||
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
|
||||
session.panelStates.pop_back();
|
||||
auto validation = ValidateUIEditorWorkspaceSession(registry, workspace, session);
|
||||
EXPECT_EQ(validation.code, UIEditorWorkspaceSessionValidationCode::MissingPanelState);
|
||||
|
||||
session = BuildDefaultUIEditorWorkspaceSession(registry, workspace);
|
||||
ASSERT_TRUE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
|
||||
ASSERT_TRUE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "doc-b"));
|
||||
workspace.activePanelId = "doc-a";
|
||||
|
||||
validation = ValidateUIEditorWorkspaceSession(registry, workspace, session);
|
||||
EXPECT_EQ(validation.code, UIEditorWorkspaceSessionValidationCode::InvalidActivePanelId);
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Docking/UIEditorDockHost.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceController.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceSplitterDragCorrection.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitterDragCorrectionState;
|
||||
using XCEngine::UI::Editor::BeginUIEditorWorkspaceSplitterDragCorrection;
|
||||
using XCEngine::UI::Editor::TryBuildUIEditorWorkspaceSplitterDragCorrectedSnapshot;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorDockHostLayout;
|
||||
using XCEngine::UI::Editor::Widgets::FindUIEditorDockHostSplitterLayout;
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true },
|
||||
{ "console", "Console", {}, true, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.5f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
1u),
|
||||
BuildUIEditorWorkspaceSplit(
|
||||
"right-split",
|
||||
UIEditorWorkspaceSplitAxis::Vertical,
|
||||
0.6f,
|
||||
BuildUIEditorWorkspaceSingleTabStack("details-node", "details", "Details", true),
|
||||
BuildUIEditorWorkspaceSingleTabStack("console-node", "console", "Console", true)));
|
||||
workspace.activePanelId = "doc-b";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorWorkspaceSplitterDragCorrectionTest, BuildsCorrectedSnapshotForActiveSplitterDrag) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
auto controller = BuildDefaultUIEditorWorkspaceController(registry, BuildWorkspace());
|
||||
const auto baselineSnapshot = controller.CaptureLayoutSnapshot();
|
||||
const auto baselineLayout = BuildUIEditorDockHostLayout(
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
registry,
|
||||
controller.GetWorkspace(),
|
||||
controller.GetSession());
|
||||
const auto* splitter = FindUIEditorDockHostSplitterLayout(baselineLayout, "root-split");
|
||||
ASSERT_NE(splitter, nullptr);
|
||||
|
||||
UIEditorWorkspaceSplitterDragCorrectionState correctionState = {};
|
||||
BeginUIEditorWorkspaceSplitterDragCorrection(
|
||||
correctionState,
|
||||
"root-split",
|
||||
baselineSnapshot,
|
||||
baselineLayout);
|
||||
|
||||
XCEngine::UI::Editor::UIEditorWorkspaceLayoutSnapshot correctedSnapshot = {};
|
||||
const UIPoint pointerPosition(
|
||||
splitter->splitterLayout.handleRect.x + splitter->splitterLayout.handleRect.width * 0.5f + 48.0f,
|
||||
splitter->splitterLayout.handleRect.y + splitter->splitterLayout.handleRect.height * 0.5f);
|
||||
|
||||
EXPECT_TRUE(TryBuildUIEditorWorkspaceSplitterDragCorrectedSnapshot(
|
||||
correctionState,
|
||||
pointerPosition,
|
||||
true,
|
||||
registry,
|
||||
{},
|
||||
correctedSnapshot));
|
||||
EXPECT_GT(correctedSnapshot.workspace.root.splitRatio, baselineSnapshot.workspace.root.splitRatio);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceSplitterDragCorrectionTest, RejectsMissingPointerPosition) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
auto controller = BuildDefaultUIEditorWorkspaceController(registry, BuildWorkspace());
|
||||
const auto baselineSnapshot = controller.CaptureLayoutSnapshot();
|
||||
const auto baselineLayout = BuildUIEditorDockHostLayout(
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
registry,
|
||||
controller.GetWorkspace(),
|
||||
controller.GetSession());
|
||||
|
||||
UIEditorWorkspaceSplitterDragCorrectionState correctionState = {};
|
||||
BeginUIEditorWorkspaceSplitterDragCorrection(
|
||||
correctionState,
|
||||
"root-split",
|
||||
baselineSnapshot,
|
||||
baselineLayout);
|
||||
|
||||
XCEngine::UI::Editor::UIEditorWorkspaceLayoutSnapshot correctedSnapshot = {};
|
||||
EXPECT_FALSE(TryBuildUIEditorWorkspaceSplitterDragCorrectedSnapshot(
|
||||
correctionState,
|
||||
UIPoint(0.0f, 0.0f),
|
||||
false,
|
||||
registry,
|
||||
{},
|
||||
correctedSnapshot));
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
#include "app/Rendering/Viewport/ViewportObjectIdPicker.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::RHI::ResourceStates;
|
||||
using XCEngine::Rendering::RenderObjectId;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UISize;
|
||||
using XCEngine::UI::Editor::App::BuildViewportObjectIdReadbackRequest;
|
||||
using XCEngine::UI::Editor::App::PickViewportObjectIdEntity;
|
||||
using XCEngine::UI::Editor::App::ResolveViewportObjectIdPixelCoordinate;
|
||||
using XCEngine::UI::Editor::App::ViewportObjectIdPickContext;
|
||||
using XCEngine::UI::Editor::App::ViewportObjectIdPickStatus;
|
||||
using XCEngine::UI::Editor::App::ViewportObjectIdReadbackRequest;
|
||||
|
||||
TEST(ViewportObjectIdPickerTest, ResolveViewportObjectIdPixelCoordinateScalesAndClampsToTextureExtent) {
|
||||
EXPECT_EQ(
|
||||
ResolveViewportObjectIdPixelCoordinate(100.0f, 200.0f, 800u),
|
||||
400u);
|
||||
EXPECT_EQ(
|
||||
ResolveViewportObjectIdPixelCoordinate(200.0f, 200.0f, 800u),
|
||||
799u);
|
||||
EXPECT_EQ(
|
||||
ResolveViewportObjectIdPixelCoordinate(-30.0f, 200.0f, 800u),
|
||||
0u);
|
||||
}
|
||||
|
||||
TEST(ViewportObjectIdPickerTest, BuildViewportObjectIdReadbackRequestRejectsUnavailableContext) {
|
||||
ViewportObjectIdReadbackRequest request = {};
|
||||
EXPECT_FALSE(BuildViewportObjectIdReadbackRequest({}, request));
|
||||
}
|
||||
|
||||
TEST(ViewportObjectIdPickerTest, BuildViewportObjectIdReadbackRequestMapsViewportCoordinatesIntoTextureSpace) {
|
||||
ViewportObjectIdPickContext context = {};
|
||||
context.commandQueue = reinterpret_cast<XCEngine::RHI::RHICommandQueue*>(static_cast<std::uintptr_t>(0x1));
|
||||
context.texture = reinterpret_cast<XCEngine::RHI::RHITexture*>(static_cast<std::uintptr_t>(0x2));
|
||||
context.textureState = ResourceStates::PixelShaderResource;
|
||||
context.textureWidth = 1024u;
|
||||
context.textureHeight = 512u;
|
||||
context.hasValidFrame = true;
|
||||
context.viewportSize = UISize(256.0f, 128.0f);
|
||||
context.viewportMousePosition = UIPoint(128.0f, 64.0f);
|
||||
|
||||
ViewportObjectIdReadbackRequest request = {};
|
||||
ASSERT_TRUE(BuildViewportObjectIdReadbackRequest(context, request));
|
||||
EXPECT_EQ(request.pixelX, 512u);
|
||||
EXPECT_EQ(request.pixelY, 256u);
|
||||
}
|
||||
|
||||
TEST(ViewportObjectIdPickerTest, PickViewportObjectIdEntityDecodesRuntimeObjectIdFromReadbackColor) {
|
||||
ViewportObjectIdPickContext context = {};
|
||||
context.commandQueue = reinterpret_cast<XCEngine::RHI::RHICommandQueue*>(static_cast<std::uintptr_t>(0x1));
|
||||
context.texture = reinterpret_cast<XCEngine::RHI::RHITexture*>(static_cast<std::uintptr_t>(0x2));
|
||||
context.textureState = ResourceStates::PixelShaderResource;
|
||||
context.textureWidth = 256u;
|
||||
context.textureHeight = 256u;
|
||||
context.hasValidFrame = true;
|
||||
context.viewportSize = UISize(256.0f, 256.0f);
|
||||
context.viewportMousePosition = UIPoint(32.0f, 48.0f);
|
||||
|
||||
constexpr RenderObjectId kExpectedObjectId = 0x00030201u;
|
||||
const auto result = PickViewportObjectIdEntity(
|
||||
context,
|
||||
[](const auto&, std::array<std::uint8_t, 4>& outRgba) {
|
||||
outRgba = { 0x01u, 0x02u, 0x03u, 0x00u };
|
||||
return true;
|
||||
});
|
||||
|
||||
EXPECT_EQ(result.status, ViewportObjectIdPickStatus::Success);
|
||||
EXPECT_EQ(result.renderObjectId, kExpectedObjectId);
|
||||
EXPECT_EQ(result.entityId, static_cast<std::uint64_t>(kExpectedObjectId));
|
||||
}
|
||||
|
||||
TEST(ViewportObjectIdPickerTest, PickViewportObjectIdEntityReportsReadbackFailure) {
|
||||
ViewportObjectIdPickContext context = {};
|
||||
context.commandQueue = reinterpret_cast<XCEngine::RHI::RHICommandQueue*>(static_cast<std::uintptr_t>(0x1));
|
||||
context.texture = reinterpret_cast<XCEngine::RHI::RHITexture*>(static_cast<std::uintptr_t>(0x2));
|
||||
context.textureState = ResourceStates::PixelShaderResource;
|
||||
context.textureWidth = 64u;
|
||||
context.textureHeight = 64u;
|
||||
context.hasValidFrame = true;
|
||||
context.viewportSize = UISize(64.0f, 64.0f);
|
||||
context.viewportMousePosition = UIPoint(4.0f, 8.0f);
|
||||
|
||||
const auto result = PickViewportObjectIdEntity(
|
||||
context,
|
||||
[](const auto&, std::array<std::uint8_t, 4>&) {
|
||||
return false;
|
||||
});
|
||||
|
||||
EXPECT_EQ(result.status, ViewportObjectIdPickStatus::ReadbackFailed);
|
||||
EXPECT_EQ(result.entityId, 0u);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user