tests: remove legacy test tree

This commit is contained in:
2026-04-22 00:22:32 +08:00
parent 8bfca5e8f2
commit bc47e6e5ac
754 changed files with 0 additions and 3517894 deletions

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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 &section;
}
}
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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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");
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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");
}

View File

@@ -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);
}

View File

@@ -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"));
}

View File

@@ -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");
}

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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 分钟前"));
}

View File

@@ -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"));
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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");
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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" }));
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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");
}

View File

@@ -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());
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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");
}

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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));
}

View File

@@ -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

View File

@@ -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"));
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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" }));
}

View File

@@ -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));
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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");
}

View File

@@ -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);
}

View File

@@ -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));
}

View File

@@ -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