Extract hierarchy and project drag semantics

This commit is contained in:
2026-03-27 00:30:11 +08:00
parent 6ec5f05601
commit c97510ed5b
7 changed files with 119 additions and 72 deletions

View File

@@ -108,6 +108,7 @@
- `Inspector / Console` 的局部 action 组装也开始继续下沉到 shared router
- `Inspector` 的 component section header 菜单已开始改成 callback/router 驱动,而不是在 widget 层硬编码动作
- `MenuBar` 的 File / View / Help / global shortcut 也开始继续下沉到 shared main-menu router
- `Hierarchy / Project` 的 drag-drop payload、拖拽接收与部分选择语义也开始继续下沉到 shared action router
### 5. Dock / Layout 层
@@ -190,6 +191,7 @@
- 重命名状态已收成 `Begin / Commit / Cancel`
- 重命名交互已从 panel 局部字段收口到 shared inline edit state
- `Rename` 请求已能从 `MenuBar -> EventBus -> Hierarchy inline edit` 触发
- entity drag payload / 目标接收 / root drop / selection click 语义已开始继续从 panel 下沉到 shared hierarchy router
仍待完成:
@@ -205,6 +207,7 @@
- 资源图标绘制与图标配色已下沉到 shared UI token / widget
- 创建文件夹弹窗已改成 shared popup state 驱动
- `Back / Open / Delete` 已接 panel-focused keyboard action
- asset drag payload / folder drop / 拖拽预览高亮已开始继续从 panel 下沉到 shared project router
仍待完成:

View File

@@ -10,6 +10,10 @@ namespace XCEngine {
namespace Editor {
namespace Actions {
inline constexpr const char* HierarchyEntityPayloadType() {
return "ENTITY_PTR";
}
inline void RequestEntityRename(IEditorContext& context, const ::XCEngine::Components::GameObject* gameObject) {
if (!gameObject) {
return;
@@ -18,6 +22,63 @@ inline void RequestEntityRename(IEditorContext& context, const ::XCEngine::Compo
context.GetEventBus().Publish(EntityRenameRequestedEvent{ gameObject->GetID() });
}
inline void HandleHierarchySelectionClick(IEditorContext& context, uint64_t entityId, bool additive) {
auto& selectionManager = context.GetSelectionManager();
if (additive) {
if (!selectionManager.IsSelected(entityId)) {
selectionManager.AddToSelection(entityId);
}
return;
}
selectionManager.SetSelectedEntity(entityId);
}
inline bool BeginHierarchyEntityDrag(::XCEngine::Components::GameObject* gameObject) {
if (!gameObject || !ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
return false;
}
ImGui::SetDragDropPayload(HierarchyEntityPayloadType(), &gameObject, sizeof(::XCEngine::Components::GameObject*));
ImGui::Text("%s", gameObject->GetName().c_str());
ImGui::EndDragDropSource();
return true;
}
inline bool AcceptHierarchyEntityDrop(IEditorContext& context, ::XCEngine::Components::GameObject* target) {
if (!target || !ImGui::BeginDragDropTarget()) {
return false;
}
bool accepted = false;
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(HierarchyEntityPayloadType())) {
::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data;
if (sourceGameObject != target && Commands::CanReparentEntity(sourceGameObject, target)) {
Commands::ReparentEntityPreserveWorldTransform(context, sourceGameObject, target->GetID());
accepted = true;
}
}
ImGui::EndDragDropTarget();
return accepted;
}
inline bool AcceptHierarchyEntityDropToRoot(IEditorContext& context) {
if (!ImGui::BeginDragDropTarget()) {
return false;
}
bool accepted = false;
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(HierarchyEntityPayloadType())) {
::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data;
if (sourceGameObject && sourceGameObject->GetParent() != nullptr) {
Commands::ReparentEntityPreserveWorldTransform(context, sourceGameObject, 0);
accepted = true;
}
}
ImGui::EndDragDropTarget();
return accepted;
}
inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Components::GameObject* parent) {
DrawMenuAction(MakeCreateEmptyEntityAction(), [&]() {
Commands::CreateEmptyEntity(context, parent, "Create Entity", "GameObject");

View File

@@ -10,6 +10,10 @@ namespace XCEngine {
namespace Editor {
namespace Actions {
inline constexpr const char* ProjectAssetPayloadType() {
return "ASSET_ITEM";
}
inline int FindProjectItemIndex(IProjectManager& projectManager, const AssetItemPtr& item) {
if (!item) {
return -1;
@@ -29,6 +33,50 @@ inline int FindProjectItemIndex(IProjectManager& projectManager, const AssetItem
return -1;
}
inline const char* GetDraggedProjectAssetPath() {
const ImGuiPayload* payload = ImGui::GetDragDropPayload();
if (!payload || !payload->IsDataType(ProjectAssetPayloadType())) {
return nullptr;
}
return static_cast<const char*>(payload->Data);
}
inline bool IsProjectAssetBeingDragged(const AssetItemPtr& item) {
const char* draggedPath = GetDraggedProjectAssetPath();
return item != nullptr && draggedPath != nullptr && item->fullPath == draggedPath;
}
inline bool AcceptProjectAssetDrop(IProjectManager& projectManager, const AssetItemPtr& targetFolder) {
if (!targetFolder || !targetFolder->isFolder || !ImGui::BeginDragDropTarget()) {
return false;
}
bool accepted = false;
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(ProjectAssetPayloadType())) {
const char* draggedPath = static_cast<const char*>(payload->Data);
accepted = Commands::MoveAssetToFolder(projectManager, draggedPath, targetFolder);
}
ImGui::EndDragDropTarget();
return accepted;
}
inline bool BeginProjectAssetDrag(const AssetItemPtr& item, UI::AssetIconKind iconKind) {
if (!item || item->fullPath.empty() || !ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
return false;
}
ImGui::SetDragDropPayload(ProjectAssetPayloadType(), item->fullPath.c_str(), item->fullPath.length() + 1);
ImVec2 previewMin = ImGui::GetMousePos();
const ImVec2 previewSize = UI::AssetDragPreviewSize();
ImVec2 previewMax = ImVec2(previewMin.x + previewSize.x, previewMin.y + previewSize.y);
UI::DrawAssetIcon(ImGui::GetForegroundDrawList(), previewMin, previewMax, iconKind);
ImGui::EndDragDropSource();
return true;
}
inline void DrawProjectAssetContextActions(IEditorContext& context, const AssetItemPtr& item) {
auto& projectManager = context.GetProjectManager();
const int itemIndex = FindProjectItemIndex(projectManager, item);

View File

@@ -100,15 +100,7 @@ void HierarchyPanel::Render() {
}
ImGui::InvisibleButton("##DragTarget", ImVec2(-1, -1));
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ENTITY_PTR")) {
::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data;
if (sourceGameObject && sourceGameObject->GetParent() != nullptr) {
Commands::ReparentEntityPreserveWorldTransform(*m_context, sourceGameObject, 0);
}
}
ImGui::EndDragDropTarget();
}
Actions::AcceptHierarchyEntityDropToRoot(*m_context);
}
@@ -192,21 +184,15 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject
gameObject->GetChildCount() == 0);
if (node.clicked) {
ImGuiIO& io = ImGui::GetIO();
if (io.KeyCtrl) {
if (!m_context->GetSelectionManager().IsSelected(gameObject->GetID())) {
m_context->GetSelectionManager().AddToSelection(gameObject->GetID());
}
} else {
m_context->GetSelectionManager().SetSelectedEntity(gameObject->GetID());
}
Actions::HandleHierarchySelectionClick(*m_context, gameObject->GetID(), ImGui::GetIO().KeyCtrl);
}
if (node.doubleClicked) {
BeginRename(gameObject);
}
HandleDragDrop(gameObject);
Actions::BeginHierarchyEntityDrag(gameObject);
Actions::AcceptHierarchyEntityDrop(*m_context, gameObject);
if (UI::BeginPopupContextItem("EntityContextMenu")) {
Actions::DrawHierarchyContextActions(*m_context, gameObject);
@@ -250,24 +236,6 @@ void HierarchyPanel::CancelRename() {
m_renameState.Cancel();
}
void HierarchyPanel::HandleDragDrop(::XCEngine::Components::GameObject* gameObject) {
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
ImGui::SetDragDropPayload("ENTITY_PTR", &gameObject, sizeof(::XCEngine::Components::GameObject*));
ImGui::Text("%s", gameObject->GetName().c_str());
ImGui::EndDragDropSource();
}
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ENTITY_PTR")) {
::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data;
if (sourceGameObject != gameObject && Commands::CanReparentEntity(sourceGameObject, gameObject)) {
Commands::ReparentEntityPreserveWorldTransform(*m_context, sourceGameObject, gameObject->GetID());
}
}
ImGui::EndDragDropTarget();
}
}
bool HierarchyPanel::PassesFilter(::XCEngine::Components::GameObject* gameObject, const std::string& filter) {
if (!gameObject) return false;

View File

@@ -26,7 +26,6 @@ private:
void BeginRename(::XCEngine::Components::GameObject* gameObject);
void CommitRename();
void CancelRename();
void HandleDragDrop(::XCEngine::Components::GameObject* gameObject);
bool PassesFilter(::XCEngine::Components::GameObject* gameObject, const std::string& filter);
void SortEntities(std::vector<::XCEngine::Components::GameObject*>& entities);

View File

@@ -11,10 +11,6 @@
namespace XCEngine {
namespace Editor {
namespace {
constexpr const char* kAssetDragDropType = "ASSET_ITEM";
}
ProjectPanel::ProjectPanel() : Panel("Project") {
}
@@ -23,13 +19,6 @@ void ProjectPanel::Initialize(const std::string& projectPath) {
}
void ProjectPanel::Render() {
const ImGuiPayload* payload = ImGui::GetDragDropPayload();
if (payload && payload->IsDataType(kAssetDragDropType)) {
m_draggingPath = (const char*)payload->Data;
} else if (!ImGui::IsMouseDown(0)) {
m_draggingPath.clear();
}
UI::PanelWindowScope panel(m_name.c_str());
if (!panel.IsOpen()) {
return;
@@ -131,7 +120,7 @@ void ProjectPanel::RenderAssetItem(const AssetItemPtr& item, int index) {
bool isSelected = (manager.GetSelectedIndex() == index);
ImGui::PushID(index);
const bool isDraggingThisItem = !m_draggingPath.empty() && item->fullPath == m_draggingPath;
const bool isDraggingThisItem = Actions::IsProjectAssetBeingDragged(item);
const UI::AssetIconKind iconKind = item->isFolder ? UI::AssetIconKind::Folder : UI::AssetIconKind::File;
const UI::AssetTileResult tile = UI::DrawAssetTile(
@@ -151,28 +140,8 @@ void ProjectPanel::RenderAssetItem(const AssetItemPtr& item, int index) {
m_itemContextMenu.RequestOpen(item);
}
if (item->isFolder) {
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(kAssetDragDropType)) {
const char* draggedPath = (const char*)payload->Data;
Commands::MoveAssetToFolder(manager, draggedPath, item);
}
ImGui::EndDragDropTarget();
}
}
if (!item->fullPath.empty()) {
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
ImGui::SetDragDropPayload(kAssetDragDropType, item->fullPath.c_str(), item->fullPath.length() + 1);
ImVec2 previewMin = ImGui::GetMousePos();
const ImVec2 previewSize = UI::AssetDragPreviewSize();
ImVec2 previewMax = ImVec2(previewMin.x + previewSize.x, previewMin.y + previewSize.y);
UI::DrawAssetIcon(ImGui::GetForegroundDrawList(), previewMin, previewMax, iconKind);
ImGui::EndDragDropSource();
}
}
Actions::AcceptProjectAssetDrop(manager, item);
Actions::BeginProjectAssetDrag(item, iconKind);
if (tile.openRequested) {
Commands::OpenAsset(*m_context, item);

View File

@@ -19,7 +19,6 @@ private:
char m_searchBuffer[256] = "";
UI::TextInputPopupState<256> m_createFolderDialog;
UI::TargetedPopupState<AssetItemPtr> m_itemContextMenu;
std::string m_draggingPath;
};
}