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

123
editor/src/UI/ContextMenu.h Normal file
View File

@@ -0,0 +1,123 @@
#pragma once
#include "Core.h"
#include "StyleTokens.h"
#include "Widgets.h"
#include <XCEngine/Debug/Logger.h>
#include <imgui.h>
#include <string>
namespace XCEngine {
namespace Editor {
namespace UI {
inline void TraceContextMenuSubmenuIfNeeded(const char* label, const std::string& message) {
if (!label || std::string(label) != "Create") {
return;
}
XCEngine::Debug::Logger::Get().Info(
XCEngine::Debug::LogCategory::General,
XCEngine::Containers::String(message.c_str()));
}
inline constexpr int ContextMenuLayoutVarCount() {
return 3;
}
inline void PushContextMenuLayoutStyle() {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ContextMenuItemSpacing());
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ContextMenuFramePadding());
ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ContextMenuSelectableTextAlign());
}
inline void PopContextMenuLayoutStyle() {
ImGui::PopStyleVar(ContextMenuLayoutVarCount());
}
inline void PushContextMenuChrome() {
PushPopupChromeStyle();
PushContextMenuLayoutStyle();
}
inline void PopContextMenuChrome() {
PopContextMenuLayoutStyle();
PopPopupChromeStyle();
}
inline bool BeginContextMenu(
const char* id,
ImGuiWindowFlags flags = ImGuiWindowFlags_None) {
PushContextMenuChrome();
const bool open = ImGui::BeginPopup(id, flags);
if (!open) {
PopContextMenuChrome();
}
return open;
}
inline bool BeginContextMenuForLastItem(
const char* id = nullptr,
ImGuiPopupFlags flags = ImGuiPopupFlags_MouseButtonRight) {
PushContextMenuChrome();
const bool open = ImGui::BeginPopupContextItem(id, flags);
if (!open) {
PopContextMenuChrome();
}
return open;
}
inline bool BeginContextMenuForWindow(
const char* id = nullptr,
ImGuiPopupFlags flags =
ImGuiPopupFlags_MouseButtonRight |
ImGuiPopupFlags_NoOpenOverItems |
ImGuiPopupFlags_NoOpenOverExistingPopup) {
PushContextMenuChrome();
const bool open = ImGui::BeginPopupContextWindow(id, flags);
if (!open) {
PopContextMenuChrome();
}
return open;
}
inline void EndContextMenu() {
ImGui::EndPopup();
PopContextMenuChrome();
}
template <typename DrawContentFn>
inline bool DrawContextSubmenu(
const char* label,
DrawContentFn&& drawContent,
bool enabled = true) {
PushPopupWindowChrome();
const bool open = ImGui::BeginMenu(label, enabled);
PopPopupWindowChrome();
if (label && std::string(label) == "Create") {
static bool s_lastOpen = false;
if (open != s_lastOpen) {
TraceContextMenuSubmenuIfNeeded(
label,
std::string("Hierarchy create context submenu ") + (open ? "opened" : "closed"));
s_lastOpen = open;
}
}
if (!open) {
return false;
}
PushPopupContentChrome();
PushContextMenuLayoutStyle();
drawContent();
PopContextMenuLayoutStyle();
PopPopupContentChrome();
ImGui::EndMenu();
return true;
}
} // namespace UI
} // namespace Editor
} // namespace XCEngine

View File

@@ -3,6 +3,7 @@
#include "DividerChrome.h"
#include "StyleTokens.h"
#include <cmath>
#include <imgui.h>
namespace XCEngine {
@@ -38,27 +39,41 @@ inline void DrawDisclosureArrow(ImDrawList* drawList, const ImVec2& min, const I
return;
}
const ImVec2 center((min.x + max.x) * 0.5f, (min.y + max.y) * 0.5f);
const ImVec2 center(
static_cast<float>(std::floor((min.x + max.x) * 0.5f)) + 0.5f,
static_cast<float>(std::floor((min.y + max.y) * 0.5f)) + 0.5f);
const float width = max.x - min.x;
const float height = max.y - min.y;
const float size = (width < height ? width : height) * DisclosureArrowScale();
if (size <= 0.0f) {
const float radius = (std::max)(
3.0f,
static_cast<float>(std::floor((width < height ? width : height) * DisclosureArrowScale())));
const float baseHalfExtent = radius;
const float tipExtent = (std::max)(2.0f, static_cast<float>(std::floor(radius * 0.70f)));
if (baseHalfExtent < 1.0f || tipExtent < 1.0f) {
return;
}
if (open) {
drawList->AddTriangleFilled(
ImVec2(center.x - size, center.y - size * 0.45f),
ImVec2(center.x + size, center.y - size * 0.45f),
ImVec2(center.x, center.y + size),
color);
return;
ImVec2 points[3] = {
ImVec2(-baseHalfExtent, -tipExtent),
ImVec2(baseHalfExtent, -tipExtent),
ImVec2(0.0f, tipExtent)
};
if (!open) {
for (ImVec2& point : points) {
point = ImVec2(point.y, -point.x);
}
}
for (ImVec2& point : points) {
point.x += center.x;
point.y += center.y;
}
drawList->AddTriangleFilled(
ImVec2(center.x - size * 0.45f, center.y - size),
ImVec2(center.x - size * 0.45f, center.y + size),
ImVec2(center.x + size, center.y),
points[0],
points[1],
points[2],
color);
}

View File

@@ -120,7 +120,6 @@ private:
}
io.FontDefault = uiFont;
atlas->Build();
}
std::string m_iniPath;

View File

@@ -0,0 +1,34 @@
#pragma once
namespace XCEngine {
namespace Editor {
namespace UI {
enum class MenuCommandKind {
Action,
Separator
};
struct MenuCommand {
MenuCommandKind kind = MenuCommandKind::Action;
const char* label = nullptr;
const char* shortcut = nullptr;
bool selected = false;
bool enabled = true;
static MenuCommand Action(
const char* label,
const char* shortcut = nullptr,
bool selected = false,
bool enabled = true) {
return MenuCommand{ MenuCommandKind::Action, label, shortcut, selected, enabled };
}
static MenuCommand Separator() {
return MenuCommand{ MenuCommandKind::Separator, nullptr, nullptr, false, true };
}
};
} // namespace UI
} // namespace Editor
} // namespace XCEngine

View File

@@ -6,6 +6,8 @@ namespace XCEngine {
namespace Editor {
namespace UI {
inline float PopupWindowBorderSize();
inline ImVec2 DockHostFramePadding() {
return ImVec2(4.0f, 2.0f);
}
@@ -246,6 +248,10 @@ inline float CompactNavigationTreeIndentSpacing() {
return 14.0f;
}
inline float HierarchyTreeIndentSpacing() {
return 18.0f;
}
inline float NavigationTreeIconSize() {
return 18.0f;
}
@@ -263,7 +269,7 @@ inline float NavigationTreePrefixLabelGap() {
}
inline float DisclosureArrowScale() {
return 0.14f;
return 0.28f;
}
inline ImVec4 NavigationTreePrefixColor(bool selected = false, bool hovered = false) {
@@ -295,7 +301,9 @@ inline TreeViewStyle NavigationTreeStyle() {
}
inline TreeViewStyle HierarchyTreeStyle() {
return NavigationTreeStyle();
TreeViewStyle style = NavigationTreeStyle();
style.indentSpacing = HierarchyTreeIndentSpacing();
return style;
}
inline TreeViewStyle ProjectFolderTreeStyle() {
@@ -338,6 +346,18 @@ inline float SearchFieldFrameRounding() {
return 2.0f;
}
inline ImVec2 InlineRenameFieldFramePadding() {
return ImVec2(4.0f, 0.0f);
}
inline float InlineRenameFieldRounding() {
return 2.0f;
}
inline float InlineRenameFieldHeight() {
return ImGui::GetFontSize() + InlineRenameFieldFramePadding().y * 2.0f;
}
inline ImU32 PanelDividerColor() {
return ImGui::GetColorU32(PanelSplitterIdleColor());
}
@@ -476,6 +496,18 @@ inline float PopupFrameRounding() {
return 3.0f;
}
inline ImVec2 ContextMenuItemSpacing() {
return ImVec2(0.0f, 0.0f);
}
inline ImVec2 ContextMenuFramePadding() {
return ImVec2(8.0f, 5.0f);
}
inline ImVec2 ContextMenuSelectableTextAlign() {
return ImVec2(0.0f, 0.5f);
}
inline float PopupSubmenuArrowExtent() {
return 8.0f;
}

View File

@@ -5,10 +5,12 @@
#include "BuiltInIcons.h"
#include "ConsoleFilterState.h"
#include "ConsoleLogFormatter.h"
#include "ContextMenu.h"
#include "Core.h"
#include "DockHostStyle.h"
#include "DockTabBarChrome.h"
#include "DividerChrome.h"
#include "MenuCommand.h"
#include "PanelChrome.h"
#include "PopupState.h"
#include "PropertyLayout.h"

View File

@@ -1,8 +1,11 @@
#pragma once
#include "Core.h"
#include "MenuCommand.h"
#include "StyleTokens.h"
#include <XCEngine/Debug/Logger.h>
#include <imgui.h>
#include <string>
@@ -10,6 +13,16 @@ namespace XCEngine {
namespace Editor {
namespace UI {
inline void TracePopupSubmenuIfNeeded(const char* label, const std::string& message) {
if (!label || std::string(label) != "Create") {
return;
}
XCEngine::Debug::Logger::Get().Info(
XCEngine::Debug::LogCategory::General,
XCEngine::Containers::String(message.c_str()));
}
struct ComponentSectionResult {
bool open = false;
float contentIndent = 0.0f;
@@ -17,7 +30,6 @@ struct ComponentSectionResult {
struct AssetTileResult {
bool clicked = false;
bool contextRequested = false;
bool openRequested = false;
bool hovered = false;
ImVec2 min = ImVec2(0.0f, 0.0f);
@@ -41,29 +53,11 @@ enum class DialogActionResult {
Secondary
};
enum class MenuCommandKind {
Action,
Separator
};
struct MenuCommand {
MenuCommandKind kind = MenuCommandKind::Action;
const char* label = nullptr;
const char* shortcut = nullptr;
bool selected = false;
bool enabled = true;
static MenuCommand Action(
const char* label,
const char* shortcut = nullptr,
bool selected = false,
bool enabled = true) {
return MenuCommand{ MenuCommandKind::Action, label, shortcut, selected, enabled };
}
static MenuCommand Separator() {
return MenuCommand{ MenuCommandKind::Separator, nullptr, nullptr, false, true };
}
struct InlineRenameFieldResult {
bool submitted = false;
bool cancelRequested = false;
bool deactivated = false;
bool active = false;
};
template <typename DrawContentFn>
@@ -101,11 +95,13 @@ inline bool DrawPopupSubmenuScope(const char* label, DrawContentFn&& drawContent
popupOpen,
ImGuiSelectableFlags_NoAutoClosePopups,
ImVec2(rowWidth, rowHeight))) {
TracePopupSubmenuIfNeeded(label, "Hierarchy create submenu selectable clicked -> OpenPopup");
ImGui::OpenPopup(popupId);
}
const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
if (hovered && !popupOpen) {
TracePopupSubmenuIfNeeded(label, "Hierarchy create submenu hovered -> OpenPopup");
ImGui::OpenPopup(popupId);
}
@@ -138,6 +134,15 @@ inline bool DrawPopupSubmenuScope(const char* label, DrawContentFn&& drawContent
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoSavedSettings);
if (std::string(label) == "Create") {
static bool s_lastCreateOpen = false;
if (open != s_lastCreateOpen) {
TracePopupSubmenuIfNeeded(
label,
std::string("Hierarchy create submenu popup ") + (open ? "opened" : "closed"));
s_lastCreateOpen = open;
}
}
if (!open) {
ImGui::PopID();
return false;
@@ -146,6 +151,12 @@ inline bool DrawPopupSubmenuScope(const char* label, DrawContentFn&& drawContent
drawContent();
const bool popupHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
if (!hovered && !popupHovered && !ImGui::IsWindowAppearing()) {
TracePopupSubmenuIfNeeded(
label,
std::string("Hierarchy create submenu auto-close: rowHovered=") +
(hovered ? "1" : "0") +
", popupHovered=" +
(popupHovered ? "1" : "0"));
ImGui::CloseCurrentPopup();
}
EndPopup();
@@ -190,6 +201,50 @@ inline bool ToolbarSearchField(
return changed;
}
inline InlineRenameFieldResult DrawInlineRenameField(
const char* id,
char* buffer,
size_t bufferSize,
float width = -1.0f,
bool requestFocus = false,
ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll) {
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, InlineRenameFieldFramePadding());
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, InlineRenameFieldRounding());
ImGui::SetNextItemWidth(width);
if (requestFocus) {
ImGui::SetKeyboardFocusHere();
}
const bool submitted = ImGui::InputText(id, buffer, bufferSize, flags);
const bool active = ImGui::IsItemActive();
const bool deactivated = ImGui::IsItemDeactivated();
const bool cancelRequested = active && ImGui::IsKeyPressed(ImGuiKey_Escape);
ImGui::PopStyleVar(2);
return InlineRenameFieldResult{ submitted, cancelRequested, deactivated, active };
}
inline InlineRenameFieldResult DrawInlineRenameFieldAt(
const char* id,
const ImVec2& screenPos,
char* buffer,
size_t bufferSize,
float width,
bool requestFocus = false,
ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll) {
const ImVec2 restoreCursor = ImGui::GetCursorPos();
ImGui::SetCursorScreenPos(screenPos);
const InlineRenameFieldResult result = DrawInlineRenameField(
id,
buffer,
bufferSize,
width,
requestFocus,
flags);
ImGui::SetCursorPos(restoreCursor);
return result;
}
inline void DrawToolbarLabel(const char* text) {
ImGui::AlignTextToFramePadding();
ImGui::TextColored(HintTextColor(), "%s", text);
@@ -271,16 +326,22 @@ inline bool DrawBreadcrumbSegment(const char* label, bool clickable, bool curren
const ImVec2 padding = BreadcrumbSegmentPadding();
const ImVec2 textSize = ImGui::CalcTextSize(label);
const ImVec2 size(textSize.x + padding.x * 2.0f, BreadcrumbItemHeight());
bool pressed = false;
bool hovered = false;
DrawBreadcrumbTextItem(label, size, BreadcrumbSegmentTextColor(current, hovered), clickable, &pressed, &hovered);
ImGui::InvisibleButton("##BreadcrumbItem", size);
const bool hovered = clickable && ImGui::IsItemHovered();
const bool pressed = clickable && ImGui::IsItemClicked(ImGuiMouseButton_Left);
const ImVec2 itemMin = ImGui::GetItemRectMin();
const ImVec2 itemMax = ImGui::GetItemRectMax();
const float textX = itemMin.x + (size.x - textSize.x) * 0.5f;
const float textY = itemMin.y + (itemMax.y - itemMin.y - textSize.y) * 0.5f;
ImGui::GetWindowDrawList()->AddText(
ImVec2(textX, textY),
ImGui::GetColorU32(BreadcrumbSegmentTextColor(current, hovered)),
label);
if (hovered) {
const ImVec2 itemMin = ImGui::GetItemRectMin();
const ImVec2 itemMax = ImGui::GetItemRectMax();
const ImVec2 textOnlySize = ImGui::CalcTextSize(label);
const float textX = itemMin.x + (size.x - textOnlySize.x) * 0.5f;
const float textY = itemMin.y + (itemMax.y - itemMin.y - textOnlySize.y) * 0.5f;
ImGui::GetWindowDrawList()->AddText(
ImVec2(textX, textY),
ImGui::GetColorU32(BreadcrumbSegmentTextColor(current, true)),
@@ -301,7 +362,10 @@ inline void DrawToolbarBreadcrumbs(
size_t segmentCount,
GetNameFn&& getName,
NavigateFn&& navigateToSegment) {
const float lineY = ImGui::GetCursorPosY();
if (segmentCount == 0) {
ImGui::SetCursorPosY(lineY);
DrawBreadcrumbSegment(rootLabel, false, true);
return;
}
@@ -309,8 +373,10 @@ inline void DrawToolbarBreadcrumbs(
for (size_t i = 0; i < segmentCount; ++i) {
if (i > 0) {
ImGui::SameLine(0.0f, BreadcrumbSegmentSpacing());
ImGui::SetCursorPosY(lineY);
DrawBreadcrumbSeparator();
ImGui::SameLine(0.0f, BreadcrumbSegmentSpacing());
ImGui::SetCursorPosY(lineY);
}
const std::string label = (i == 0 && rootLabel && rootLabel[0] != '\0')
@@ -318,6 +384,7 @@ inline void DrawToolbarBreadcrumbs(
: getName(i);
const bool current = (i + 1 == segmentCount);
ImGui::SetCursorPosY(lineY);
ImGui::PushID(static_cast<int>(i));
if (DrawBreadcrumbSegment(label.c_str(), !current, current)) {
navigateToSegment(i);
@@ -346,7 +413,6 @@ inline AssetTileResult DrawAssetTile(
ImGui::InvisibleButton("##AssetBtn", tileSize);
const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
const bool contextRequested = ImGui::IsItemClicked(ImGuiMouseButton_Right);
const bool openRequested = ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0);
const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
@@ -388,7 +454,7 @@ inline AssetTileResult DrawAssetTile(
ImGui::PopClipRect();
}
return AssetTileResult{ clicked, contextRequested, openRequested, hovered, min, max, labelMin, labelMax };
return AssetTileResult{ clicked, openRequested, hovered, min, max, labelMin, labelMax };
}
template <typename DrawMenuFn>
@@ -397,6 +463,7 @@ inline ComponentSectionResult BeginComponentSection(
const char* label,
DrawMenuFn&& drawMenu,
bool defaultOpen = true) {
(void)drawMenu;
const ImGuiStyle& style = ImGui::GetStyle();
const ImVec2 framePadding = InspectorSectionFramePadding();
const float availableWidth = ImMax(ImGui::GetContentRegionAvail().x, 1.0f);
@@ -453,11 +520,6 @@ inline ComponentSectionResult BeginComponentSection(
drawList->PopClipRect();
}
if (BeginPopupContextItem("##ComponentSettings")) {
drawMenu();
EndPopup();
}
ImGui::PopID();
return ComponentSectionResult{ open, InspectorSectionContentIndent() };
}