#pragma once #include "Core.h" #include "MenuCommand.h" #include "StyleTokens.h" #include #include #include #include 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; }; struct AssetTileResult { bool clicked = false; bool openRequested = false; bool hovered = false; ImVec2 min = ImVec2(0.0f, 0.0f); ImVec2 max = ImVec2(0.0f, 0.0f); ImVec2 labelMin = ImVec2(0.0f, 0.0f); ImVec2 labelMax = ImVec2(0.0f, 0.0f); }; struct AssetTileOptions { ImVec2 size = AssetTileSize(); ImVec2 iconOffset = AssetTileIconOffset(); ImVec2 iconSize = AssetTileIconSize(); bool drawIdleFrame = true; bool drawSelectionBorder = true; bool drawLabel = true; }; inline ImVec2 ComputeAssetTileSize( const char* label, const AssetTileOptions& options = AssetTileOptions()) { const ImVec2 textSize = ImGui::CalcTextSize(label ? label : ""); ImVec2 tileSize = options.size; tileSize.x = std::max(tileSize.x, options.iconSize.x + AssetTileTextPadding().x * 2.0f); tileSize.y = std::max( tileSize.y, options.iconOffset.y + options.iconSize.y + (options.drawLabel ? AssetTileIconTextGap() + textSize.y + AssetTileTextPadding().y : 0.0f)); return tileSize; } enum class DialogActionResult { None, Primary, Secondary }; struct InlineRenameFieldResult { bool submitted = false; bool cancelRequested = false; bool deactivated = false; bool active = false; }; template inline bool DrawMenuScope(const char* label, DrawContentFn&& drawContent) { PushPopupWindowChrome(); if (!ImGui::BeginMenu(label)) { PopPopupWindowChrome(); return false; } PopPopupWindowChrome(); PushPopupContentChrome(); drawContent(); PopPopupContentChrome(); ImGui::EndMenu(); return true; } template inline bool DrawPopupSubmenuScope(const char* label, DrawContentFn&& drawContent) { if (!label || label[0] == '\0') { return false; } ImGui::PushID(label); const char* popupId = "##PopupSubmenu"; const ImVec2 labelSize = ImGui::CalcTextSize(label); const ImVec2 rowPos = ImGui::GetCursorScreenPos(); const float rowHeight = labelSize.y; const float rowWidth = ImMax(ImGui::GetContentRegionAvail().x, 1.0f); const bool popupOpen = ImGui::IsPopupOpen(popupId); if (ImGui::Selectable( "##PopupSubmenuRow", 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); } const ImVec2 itemMin = ImGui::GetItemRectMin(); const ImVec2 itemMax = ImGui::GetItemRectMax(); const ImVec2 parentWindowPos = ImGui::GetWindowPos(); const ImVec2 parentWindowSize = ImGui::GetWindowSize(); const float parentWindowRight = parentWindowPos.x + parentWindowSize.x; const float itemHeight = itemMax.y - itemMin.y; ImDrawList* drawList = ImGui::GetWindowDrawList(); drawList->AddText( ImVec2(itemMin.x + ImGui::GetStyle().FramePadding.x, itemMin.y + (itemHeight - labelSize.y) * 0.5f), ImGui::GetColorU32(ImGuiCol_Text), label); const float arrowExtent = PopupSubmenuArrowExtent(); const float arrowCenterX = itemMax.x - PopupSubmenuArrowTrailingInset() - arrowExtent * 0.5f; const float arrowCenterY = (itemMin.y + itemMax.y) * 0.5f; drawList->AddTriangleFilled( ImVec2(arrowCenterX - arrowExtent * 0.30f, arrowCenterY - arrowExtent * 0.50f), ImVec2(arrowCenterX - arrowExtent * 0.30f, arrowCenterY + arrowExtent * 0.50f), ImVec2(arrowCenterX + arrowExtent * 0.50f, arrowCenterY), ImGui::GetColorU32(ImGuiCol_Text)); ImGui::SetNextWindowPos( ImVec2(parentWindowRight + PopupSubmenuOpenOffsetX(), rowPos.y - PopupWindowPadding().y), ImGuiCond_Always); const bool open = BeginPopup( popupId, 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; } 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(); ImGui::PopID(); return true; } template inline bool DrawMenuCommand(const MenuCommand& command, ExecuteFn&& execute) { if (command.kind == MenuCommandKind::Separator) { ImGui::Separator(); return false; } if (!ImGui::MenuItem(command.label, command.shortcut, command.selected, command.enabled)) { return false; } execute(); return true; } template inline void DrawMenuCommands(const MenuCommand (&commands)[N], ExecuteFn&& execute) { for (size_t i = 0; i < N; ++i) { DrawMenuCommand(commands[i], [&]() { execute(i); }); } } inline bool ToolbarSearchField( const char* id, const char* hint, char* buffer, size_t bufferSize, float trailingWidth = 0.0f) { const float originalCursorY = ImGui::GetCursorPosY(); const float verticalOffset = SearchFieldVerticalOffset(); if (verticalOffset != 0.0f) { const float nextCursorY = originalCursorY + verticalOffset; ImGui::SetCursorPosY(nextCursorY > 0.0f ? nextCursorY : 0.0f); } ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, SearchFieldFramePadding()); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, SearchFieldFrameRounding()); ImGui::PushStyleColor(ImGuiCol_FrameBg, ToolbarButtonColor(false)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ToolbarButtonHoveredColor(false)); ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ToolbarButtonActiveColor()); const float width = ImGui::GetContentRegionAvail().x - trailingWidth; ImGui::SetNextItemWidth(width > 0.0f ? width : 0.0f); const bool changed = ImGui::InputTextWithHint(id, hint, buffer, bufferSize); const ImVec2 min = ImGui::GetItemRectMin(); const ImVec2 max = ImGui::GetItemRectMax(); const ImVec2 center(min.x + SearchFieldFramePadding().x * 0.5f, (min.y + max.y) * 0.5f); ImDrawList* drawList = ImGui::GetWindowDrawList(); const ImU32 glyphColor = ImGui::GetColorU32(ConsoleSecondaryTextColor()); drawList->AddCircle(center, 4.0f, glyphColor, 16, 1.5f); drawList->AddLine( ImVec2(center.x + 3.0f, center.y + 3.0f), ImVec2(center.x + 7.0f, center.y + 7.0f), glyphColor, 1.5f); ImGui::PopStyleColor(3); ImGui::PopStyleVar(2); 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::PushStyleColor(ImGuiCol_FrameBg, InlineRenameFieldBackgroundColor()); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, InlineRenameFieldHoveredColor()); ImGui::PushStyleColor(ImGuiCol_FrameBgActive, InlineRenameFieldActiveColor()); 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::PopStyleColor(3); 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); } inline bool ToolbarToggleButton(const char* label, bool& active, ImVec2 size = ImVec2(0.0f, 0.0f)) { if (!ToolbarButton(label, active, size)) { return false; } active = !active; return true; } inline void DrawToolbarRowGap() { ImGui::Dummy(ImVec2(0.0f, ToolbarRowGap())); } inline void DrawHintText(const char* text) { ImGui::TextColored(HintTextColor(), "%s", text); } inline void DrawEmptyState( const char* title, const char* subtitle = nullptr, ImVec2 start = ImVec2(10.0f, 10.0f)) { ImGui::SetCursorPos(start); ImGui::TextUnformatted(title); if (subtitle && subtitle[0] != '\0') { ImGui::SetCursorPos(ImVec2(start.x, start.y + EmptyStateLineOffset())); ImGui::TextColored(EmptyStateSubtitleColor(), "%s", subtitle); } } inline float BreadcrumbItemHeight() { return ImGui::GetTextLineHeight() + BreadcrumbSegmentPadding().y * 2.0f; } inline void DrawBreadcrumbTextItem( const char* label, const ImVec2& size, const ImVec4& color, bool clickable, bool* pressed = nullptr, bool* hovered = nullptr) { bool localPressed = false; if (clickable) { localPressed = ImGui::InvisibleButton("##BreadcrumbItem", size); } else { ImGui::Dummy(size); } const bool localHovered = clickable && ImGui::IsItemHovered(); const ImVec2 itemMin = ImGui::GetItemRectMin(); const ImVec2 itemMax = ImGui::GetItemRectMax(); const ImVec2 textSize = ImGui::CalcTextSize(label); 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(color), label); if (pressed) { *pressed = localPressed; } if (hovered) { *hovered = localHovered; } } inline bool DrawBreadcrumbSegment(const char* label, bool clickable, bool current = false) { if (!label || label[0] == '\0') { return false; } const ImVec2 padding = BreadcrumbSegmentPadding(); const ImVec2 textSize = ImGui::CalcTextSize(label); const ImVec2 size(textSize.x + padding.x * 2.0f, BreadcrumbItemHeight()); 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) { ImGui::GetWindowDrawList()->AddText( ImVec2(textX, textY), ImGui::GetColorU32(BreadcrumbSegmentTextColor(current, true)), label); } return pressed; } inline void DrawBreadcrumbSeparator(const char* label = ">") { const ImVec2 textSize = ImGui::CalcTextSize(label); DrawBreadcrumbTextItem(label, ImVec2(textSize.x, BreadcrumbItemHeight()), BreadcrumbSeparatorColor(), false); } template inline void DrawToolbarBreadcrumbs( const char* rootLabel, size_t segmentCount, GetNameFn&& getName, NavigateFn&& navigateToSegment) { const float lineY = ImGui::GetCursorPosY(); if (segmentCount == 0) { ImGui::SetCursorPosY(lineY); DrawBreadcrumbSegment(rootLabel, false, true); return; } 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') ? std::string(rootLabel) : getName(i); const bool current = (i + 1 == segmentCount); ImGui::SetCursorPosY(lineY); ImGui::PushID(static_cast(i)); if (DrawBreadcrumbSegment(label.c_str(), !current, current)) { navigateToSegment(i); } ImGui::PopID(); } } template inline AssetTileResult DrawAssetTile( const char* label, bool selected, bool dimmed, DrawIconFn&& drawIcon, const AssetTileOptions& options = AssetTileOptions()) { const ImVec2 textSize = ImGui::CalcTextSize(label ? label : ""); const ImVec2 tileSize = ComputeAssetTileSize(label, options); ImGui::InvisibleButton("##AssetBtn", tileSize); const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); const bool openRequested = ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0); const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); const ImVec2 min = ImGui::GetItemRectMin(); const ImVec2 max = ImVec2(min.x + tileSize.x, min.y + tileSize.y); ImDrawList* drawList = ImGui::GetWindowDrawList(); if (options.drawIdleFrame) { drawList->AddRectFilled(min, max, ImGui::GetColorU32(AssetTileIdleFillColor()), AssetTileRounding()); drawList->AddRect(min, max, ImGui::GetColorU32(AssetTileIdleBorderColor()), AssetTileRounding()); } if (selected) { drawList->AddRectFilled(min, max, ImGui::GetColorU32(AssetTileSelectedFillColor()), AssetTileRounding()); } if (selected && options.drawSelectionBorder) { drawList->AddRect(min, max, ImGui::GetColorU32(AssetTileSelectedBorderColor()), AssetTileRounding()); } if (dimmed) { drawList->AddRectFilled(min, max, ImGui::GetColorU32(AssetTileDraggedOverlayColor()), 0.0f); } const ImVec2 iconMin( min.x + (tileSize.x - options.iconSize.x) * 0.5f, min.y + options.iconOffset.y); const ImVec2 iconMax(iconMin.x + options.iconSize.x, iconMin.y + options.iconSize.y); drawIcon(drawList, iconMin, iconMax); const ImVec2 textPadding = AssetTileTextPadding(); const float labelHeight = (std::max)(ImGui::GetFrameHeight(), textSize.y); const ImVec2 labelMin(min.x + textPadding.x, max.y - labelHeight - textPadding.y * 0.5f); const ImVec2 labelMax(max.x - textPadding.x, labelMin.y + labelHeight); if (options.drawLabel) { const float textAreaWidth = labelMax.x - labelMin.x; const float centeredTextX = labelMin.x + std::max(0.0f, (textAreaWidth - textSize.x) * 0.5f); const float textY = labelMin.y + (std::max)(0.0f, (labelHeight - textSize.y) * 0.5f); ImGui::PushClipRect(labelMin, labelMax, true); drawList->AddText(ImVec2(centeredTextX, textY), ImGui::GetColorU32(AssetTileTextColor(selected)), label); ImGui::PopClipRect(); } return AssetTileResult{ clicked, openRequested, hovered, min, max, labelMin, labelMax }; } template inline ComponentSectionResult BeginComponentSection( const void* id, 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); const float arrowSlotWidth = ImGui::GetTreeNodeToLabelSpacing(); const ImVec2 labelSize = ImGui::CalcTextSize(label ? label : "", nullptr, false); const float rowHeight = ImMax(labelSize.y, ImGui::GetFontSize()) + framePadding.y * 2.0f; ImGui::PushID(id); const ImGuiID openStateId = ImGui::GetID("##ComponentSectionOpen"); ImGuiStorage* storage = ImGui::GetStateStorage(); bool open = storage->GetBool(openStateId, defaultOpen); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x, InspectorSectionItemSpacing().y)); ImGui::InvisibleButton("##ComponentSectionHeader", ImVec2(availableWidth, rowHeight)); ImGui::PopStyleVar(); const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); const bool held = ImGui::IsItemActive(); if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { open = !open; storage->SetBool(openStateId, open); } const ImVec2 itemMin = ImGui::GetItemRectMin(); const ImVec2 itemMax = ImGui::GetItemRectMax(); const ImRect frameRect(itemMin, itemMax); const ImRect arrowRect( ImVec2(itemMin.x + framePadding.x, itemMin.y), ImVec2(itemMin.x + arrowSlotWidth, itemMax.y)); ImDrawList* drawList = ImGui::GetWindowDrawList(); const ImU32 bgColor = ImGui::GetColorU32( (held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); drawList->AddRectFilled(frameRect.Min, frameRect.Max, bgColor, style.FrameRounding); if (style.FrameBorderSize > 0.0f) { drawList->AddRect( frameRect.Min, frameRect.Max, ImGui::GetColorU32(ImGuiCol_Border), style.FrameRounding, 0, style.FrameBorderSize); } DrawDisclosureArrow(drawList, arrowRect.Min, arrowRect.Max, open, ImGui::GetColorU32(DisclosureArrowColor())); if (label && label[0] != '\0') { const float textX = itemMin.x + arrowSlotWidth; const float textY = itemMin.y + ((itemMax.y - itemMin.y) - labelSize.y) * 0.5f; drawList->PushClipRect(ImVec2(textX, itemMin.y), itemMax, true); drawList->AddText(ImVec2(textX, textY), ImGui::GetColorU32(ImGuiCol_Text), label); drawList->PopClipRect(); } ImGui::PopID(); return ComponentSectionResult{ open, InspectorSectionContentIndent() }; } inline ComponentSectionResult BeginComponentSection( const void* id, const char* label, bool defaultOpen = true) { return BeginComponentSection(id, label, []() {}, defaultOpen); } inline void EndComponentSection(const ComponentSectionResult& section) { if (section.open && section.contentIndent > 0.0f) { ImGui::Unindent(section.contentIndent); } } inline bool InspectorActionButton(const char* label, ImVec2 size = ImVec2(-1.0f, 0.0f)) { ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, InspectorActionButtonPadding()); const bool pressed = ImGui::Button(label, size); ImGui::PopStyleVar(); return pressed; } inline bool BeginTitledPopup(const char* id, const char* title) { const bool open = BeginPopup(id); if (!open) { return false; } if (title && title[0] != '\0') { ImGui::TextUnformatted(title); ImGui::Separator(); } return true; } inline void EndTitledPopup() { EndPopup(); } inline DialogActionResult DrawDialogActionRow( const char* primaryLabel, const char* secondaryLabel, bool primaryEnabled = true, bool secondaryEnabled = true) { DialogActionResult result = DialogActionResult::None; ImGui::BeginDisabled(!primaryEnabled); if (ImGui::Button(primaryLabel, DialogActionButtonSize())) { result = DialogActionResult::Primary; } ImGui::EndDisabled(); ImGui::SameLine(); ImGui::BeginDisabled(!secondaryEnabled); if (ImGui::Button(secondaryLabel, DialogActionButtonSize())) { result = DialogActionResult::Secondary; } ImGui::EndDisabled(); return result; } inline void DrawRightAlignedText(const char* text, const ImVec4& color, float rightPadding = MenuBarStatusRightPadding()) { const ImVec2 textSize = ImGui::CalcTextSize(text); const float targetX = ImGui::GetWindowWidth() - textSize.x - rightPadding; if (targetX > ImGui::GetCursorPosX()) { ImGui::SetCursorPosX(targetX); } ImGui::TextColored(color, "%s", text); } inline void BeginTitledTooltip(const char* title) { ImGui::BeginTooltip(); if (title && title[0] != '\0') { ImGui::TextUnformatted(title); ImGui::Separator(); } } inline void EndTitledTooltip() { ImGui::EndTooltip(); } inline bool DrawConsoleLogRow(const char* text) { ImGui::TextUnformatted(text); if (ImGui::IsItemHovered()) { ImDrawList* drawList = ImGui::GetWindowDrawList(); drawList->AddRectFilled(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), ImGui::GetColorU32(ConsoleRowHoverFillColor())); } return ImGui::IsItemClicked(); } } } }