Refine editor viewport and interaction workflow

This commit is contained in:
2026-03-29 15:12:38 +08:00
parent b0427b7091
commit 2651bad080
42 changed files with 3888 additions and 570 deletions

View File

@@ -16,6 +16,13 @@ namespace Editor {
namespace {
template <typename Fn>
void QueueDeferredAction(std::function<void()>& pendingAction, Fn&& fn) {
if (!pendingAction) {
pendingAction = std::forward<Fn>(fn);
}
}
void DrawProjectFolderTreePrefix(const UI::TreeNodePrefixContext& context) {
if (!context.drawList) {
return;
@@ -186,6 +193,7 @@ void ProjectPanel::Render() {
auto& manager = m_context->GetProjectManager();
BeginAssetDragDropFrame();
m_deferredContextAction = {};
RenderToolbar();
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserSurfaceColor());
@@ -215,6 +223,12 @@ void ProjectPanel::Render() {
FinalizeAssetDragDrop(manager);
ImGui::PopStyleColor();
if (m_deferredContextAction) {
auto deferredAction = std::move(m_deferredContextAction);
m_deferredContextAction = {};
deferredAction();
}
}
void ProjectPanel::RenderToolbar() {
@@ -246,6 +260,7 @@ void ProjectPanel::RenderToolbar() {
}
void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
auto* managerPtr = &manager;
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectNavigationPaneBackgroundColor());
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectNavigationPanePadding());
const bool open = ImGui::BeginChild("ProjectFolderTree", ImVec2(m_navigationWidth, 0.0f), false);
@@ -266,6 +281,17 @@ void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
UI::DrawEmptyState("No Assets Folder");
}
if (UI::BeginContextMenuForWindow("##ProjectFolderTreeContext")) {
Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() {
QueueDeferredAction(m_deferredContextAction, [this, managerPtr]() {
if (AssetItemPtr createdFolder = Commands::CreateFolder(*managerPtr, "New Folder")) {
BeginRename(createdFolder);
}
});
});
UI::EndContextMenu();
}
ImGui::EndChild();
}
@@ -298,9 +324,6 @@ void ProjectPanel::RenderFolderTreeNode(
manager.NavigateToFolder(folder);
}
if (node.secondaryClicked) {
Actions::HandleProjectItemContextRequest(manager, folder, m_itemContextMenu);
}
};
const UI::TreeNodeResult node = UI::DrawTreeNode(
@@ -323,6 +346,7 @@ void ProjectPanel::RenderFolderTreeNode(
}
void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
auto* managerPtr = &manager;
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserPaneBackgroundColor());
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
const bool open = ImGui::BeginChild("ProjectBrowser", ImVec2(0.0f, 0.0f), false);
@@ -361,7 +385,9 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
}
const float tileWidth = UI::AssetTileSize().x;
const float tileHeight = UI::AssetTileSize().y;
const float spacing = UI::AssetGridSpacing().x;
const float rowSpacing = UI::AssetGridSpacing().y;
const float panelWidth = ImGui::GetContentRegionAvail().x;
int columns = static_cast<int>((panelWidth + spacing) / (tileWidth + spacing));
if (columns < 1) {
@@ -369,26 +395,33 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
}
AssetItemPtr pendingSelection;
AssetItemPtr pendingContextTarget;
AssetItemPtr pendingOpenTarget;
const std::string selectedItemPath = manager.GetSelectedItemPath();
const ImVec2 gridOrigin = ImGui::GetCursorPos();
for (int visibleIndex = 0; visibleIndex < static_cast<int>(visibleItems.size()); ++visibleIndex) {
if (visibleIndex > 0 && visibleIndex % columns != 0) {
ImGui::SameLine();
}
const int column = visibleIndex % columns;
const int row = visibleIndex / columns;
ImGui::SetCursorPos(ImVec2(
gridOrigin.x + column * (tileWidth + spacing),
gridOrigin.y + row * (tileHeight + rowSpacing)));
const AssetItemPtr& item = visibleItems[visibleIndex];
const AssetItemInteraction interaction = RenderAssetItem(item, selectedItemPath == item->fullPath);
if (interaction.clicked) {
pendingSelection = item;
}
if (interaction.contextRequested) {
pendingContextTarget = item;
}
if (interaction.openRequested) {
pendingOpenTarget = item;
break;
}
if (m_deferredContextAction) {
break;
}
}
if (!visibleItems.empty()) {
const int rowCount = (static_cast<int>(visibleItems.size()) + columns - 1) / columns;
ImGui::SetCursorPosY(gridOrigin.y + rowCount * tileHeight + (rowCount - 1) * rowSpacing);
}
if (visibleItems.empty() && !search.empty()) {
@@ -397,29 +430,41 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
"No assets match the current search");
}
Actions::HandleProjectBackgroundPrimaryClick(manager, m_renameState);
if (pendingSelection) {
manager.SetSelectedItem(pendingSelection);
}
if (pendingContextTarget) {
Actions::HandleProjectItemContextRequest(manager, pendingContextTarget, m_itemContextMenu);
}
if (pendingOpenTarget) {
Actions::OpenProjectAsset(*m_context, pendingOpenTarget);
}
Actions::DrawProjectItemContextPopup(*m_context, m_itemContextMenu);
Actions::RequestProjectEmptyContextPopup(m_emptyContextMenu);
Actions::DrawProjectEmptyContextPopup(m_emptyContextMenu, [&]() {
if (AssetItemPtr createdFolder = Commands::CreateFolder(manager, "New Folder")) {
BeginRename(createdFolder);
if (!m_deferredContextAction) {
Actions::HandleProjectBackgroundPrimaryClick(manager, m_renameState);
if (pendingSelection) {
manager.SetSelectedItem(pendingSelection);
}
});
if (pendingOpenTarget) {
Actions::OpenProjectAsset(*m_context, pendingOpenTarget);
}
}
if (UI::BeginContextMenuForWindow("##ProjectBrowserContext")) {
Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() {
QueueDeferredAction(m_deferredContextAction, [this, managerPtr]() {
if (AssetItemPtr createdFolder = Commands::CreateFolder(*managerPtr, "New Folder")) {
BeginRename(createdFolder);
}
});
});
if (manager.CanNavigateBack()) {
Actions::DrawMenuSeparator();
Actions::DrawMenuAction(Actions::MakeNavigateBackAction(true), [&]() {
QueueDeferredAction(m_deferredContextAction, [managerPtr]() {
managerPtr->NavigateBack();
});
});
}
UI::EndContextMenu();
}
ImGui::EndChild();
ImGui::EndChild();
}
void ProjectPanel::RenderBrowserHeader(IProjectManager& manager) {
auto* managerPtr = &manager;
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserHeaderBackgroundColor());
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 5.0f));
const bool open = ImGui::BeginChild(
@@ -445,7 +490,11 @@ void ProjectPanel::RenderBrowserHeader(IProjectManager& manager) {
"Assets",
manager.GetPathDepth(),
[&](size_t index) { return manager.GetPathName(index); },
[&](size_t index) { manager.NavigateToIndex(index); });
[&](size_t index) {
QueueDeferredAction(m_deferredContextAction, [managerPtr, index]() {
managerPtr->NavigateToIndex(index);
});
});
ImDrawList* drawList = ImGui::GetWindowDrawList();
const ImVec2 windowMin = ImGui::GetWindowPos();
@@ -478,41 +527,54 @@ ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItem
UI::DrawAssetIcon(drawList, iconMin, iconMax, iconKind);
},
tileOptions);
const bool secondaryClicked = !isRenaming && ImGui::IsItemClicked(ImGuiMouseButton_Right);
if (isRenaming) {
const ImVec2 restoreCursor = ImGui::GetCursorPos();
ImGui::SetCursorScreenPos(tile.labelMin);
ImGui::SetNextItemWidth(tile.labelMax.x - tile.labelMin.x);
if (m_renameState.ConsumeFocusRequest()) {
ImGui::SetKeyboardFocusHere();
}
const bool submitted = ImGui::InputText(
const float renameWidth = tile.labelMax.x - tile.labelMin.x;
const float renameOffsetY = (std::max)(0.0f, (tile.labelMax.y - tile.labelMin.y - UI::InlineRenameFieldHeight()) * 0.5f);
const UI::InlineRenameFieldResult renameField = UI::DrawInlineRenameFieldAt(
"##Rename",
ImVec2(tile.labelMin.x, tile.labelMin.y + renameOffsetY),
m_renameState.Buffer(),
m_renameState.BufferSize(),
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll);
const bool cancelRequested = ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape);
const bool deactivated = ImGui::IsItemDeactivated();
ImGui::SetCursorPos(restoreCursor);
renameWidth,
m_renameState.ConsumeFocusRequest());
if (cancelRequested) {
if (renameField.cancelRequested) {
CancelRename();
} else if (submitted || deactivated) {
} else if (renameField.submitted || renameField.deactivated) {
CommitRename(m_context->GetProjectManager());
}
} else {
if (tile.clicked) {
interaction.clicked = true;
}
if (tile.contextRequested) {
interaction.contextRequested = true;
if (secondaryClicked && item) {
m_context->GetProjectManager().SetSelectedItem(item);
}
RegisterFolderDropTarget(m_context->GetProjectManager(), item);
Actions::BeginProjectAssetDrag(item, iconKind);
if (UI::BeginContextMenuForLastItem("##ProjectItemContext")) {
Actions::DrawMenuAction(Actions::MakeOpenAssetAction(Commands::CanOpenAsset(item)), [&]() {
QueueDeferredAction(m_deferredContextAction, [this, item]() {
Actions::OpenProjectAsset(*m_context, item);
});
});
Actions::DrawMenuAction(Actions::MakeAction("Rename", nullptr, false, item != nullptr), [&]() {
QueueDeferredAction(m_deferredContextAction, [this, item]() {
BeginRename(item);
});
});
Actions::DrawMenuAction(Actions::MakeDeleteAssetAction(item != nullptr), [&]() {
QueueDeferredAction(m_deferredContextAction, [this, item]() {
Commands::DeleteAsset(m_context->GetProjectManager(), item);
});
});
UI::EndContextMenu();
}
if (tile.openRequested) {
interaction.openRequested = true;
}