Refactor editor UI architecture
This commit is contained in:
403
editor/src/UI/Widgets.h
Normal file
403
editor/src/UI/Widgets.h
Normal file
@@ -0,0 +1,403 @@
|
||||
#pragma once
|
||||
|
||||
#include "StyleTokens.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace UI {
|
||||
|
||||
struct ComponentSectionResult {
|
||||
bool open = false;
|
||||
bool removeRequested = false;
|
||||
};
|
||||
|
||||
struct HierarchyNodeResult {
|
||||
bool open = false;
|
||||
bool clicked = false;
|
||||
bool doubleClicked = false;
|
||||
};
|
||||
|
||||
struct AssetTileResult {
|
||||
bool clicked = false;
|
||||
bool contextRequested = false;
|
||||
bool openRequested = false;
|
||||
bool hovered = false;
|
||||
ImVec2 min = ImVec2(0.0f, 0.0f);
|
||||
ImVec2 max = ImVec2(0.0f, 0.0f);
|
||||
};
|
||||
|
||||
enum class AssetIconKind {
|
||||
Folder,
|
||||
File
|
||||
};
|
||||
|
||||
enum class DialogActionResult {
|
||||
None,
|
||||
Primary,
|
||||
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 };
|
||||
}
|
||||
};
|
||||
|
||||
template <typename DrawContentFn>
|
||||
inline bool DrawMenuScope(const char* label, DrawContentFn&& drawContent) {
|
||||
if (!ImGui::BeginMenu(label)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
drawContent();
|
||||
ImGui::EndMenu();
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename ExecuteFn>
|
||||
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 <size_t N, typename ExecuteFn>
|
||||
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) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, SearchFieldFramePadding());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, SearchFieldFrameRounding());
|
||||
const float width = ImGui::GetContentRegionAvail().x - trailingWidth;
|
||||
ImGui::SetNextItemWidth(width > 0.0f ? width : 0.0f);
|
||||
const bool changed = ImGui::InputTextWithHint(id, hint, buffer, bufferSize);
|
||||
ImGui::PopStyleVar(2);
|
||||
return changed;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename GetNameFn, typename NavigateFn>
|
||||
inline void DrawToolbarBreadcrumbs(
|
||||
const char* rootLabel,
|
||||
size_t segmentCount,
|
||||
GetNameFn&& getName,
|
||||
NavigateFn&& navigateToSegment) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
||||
|
||||
if (segmentCount == 0) {
|
||||
ImGui::TextUnformatted(rootLabel);
|
||||
ImGui::PopStyleColor(2);
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < segmentCount; ++i) {
|
||||
if (i > 0) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("/");
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
const std::string label = getName(i);
|
||||
if (i + 1 < segmentCount) {
|
||||
if (ImGui::Button(label.c_str())) {
|
||||
navigateToSegment(i);
|
||||
}
|
||||
} else {
|
||||
ImGui::Text("%s", label.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PopStyleColor(2);
|
||||
}
|
||||
|
||||
inline HierarchyNodeResult DrawHierarchyNode(
|
||||
const void* id,
|
||||
const char* label,
|
||||
bool selected,
|
||||
bool leaf) {
|
||||
ImGuiTreeNodeFlags flags =
|
||||
ImGuiTreeNodeFlags_OpenOnArrow |
|
||||
ImGuiTreeNodeFlags_SpanAvailWidth |
|
||||
ImGuiTreeNodeFlags_FramePadding;
|
||||
if (leaf) {
|
||||
flags |= ImGuiTreeNodeFlags_Leaf;
|
||||
}
|
||||
if (selected) {
|
||||
flags |= ImGuiTreeNodeFlags_Selected;
|
||||
}
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, HierarchyNodeFramePadding());
|
||||
const bool open = ImGui::TreeNodeEx(id, flags, "%s", label);
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
return HierarchyNodeResult{
|
||||
open,
|
||||
ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen(),
|
||||
ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)
|
||||
};
|
||||
}
|
||||
|
||||
inline void EndHierarchyNode() {
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
template <typename DrawIconFn>
|
||||
inline AssetTileResult DrawAssetTile(
|
||||
const char* label,
|
||||
bool selected,
|
||||
bool dimmed,
|
||||
DrawIconFn&& drawIcon) {
|
||||
const ImVec2 tileSize = AssetTileSize();
|
||||
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);
|
||||
|
||||
const ImVec2 min = ImGui::GetItemRectMin();
|
||||
const ImVec2 max = ImVec2(min.x + tileSize.x, min.y + tileSize.y);
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
|
||||
if (hovered || selected) {
|
||||
drawList->AddRectFilled(min, max, ImGui::GetColorU32(selected ? AssetTileSelectedFillColor() : AssetTileHoverFillColor()), AssetTileRounding());
|
||||
}
|
||||
if (selected) {
|
||||
drawList->AddRect(min, max, ImGui::GetColorU32(AssetTileSelectedBorderColor()), AssetTileRounding());
|
||||
}
|
||||
if (dimmed) {
|
||||
drawList->AddRectFilled(min, max, ImGui::GetColorU32(AssetTileDraggedOverlayColor()), 0.0f);
|
||||
}
|
||||
|
||||
const ImVec2 iconOffset = AssetTileIconOffset();
|
||||
const ImVec2 iconSize = AssetTileIconSize();
|
||||
const ImVec2 iconMin(min.x + iconOffset.x, min.y + iconOffset.y);
|
||||
const ImVec2 iconMax(iconMin.x + iconSize.x, iconMin.y + iconSize.y);
|
||||
drawIcon(drawList, iconMin, iconMax);
|
||||
|
||||
const ImVec2 textSize = ImGui::CalcTextSize(label);
|
||||
const float textY = max.y - textSize.y - AssetTileTextPadding().y;
|
||||
ImGui::PushClipRect(ImVec2(min.x + AssetTileTextPadding().x, min.y), ImVec2(max.x - AssetTileTextPadding().x, max.y), true);
|
||||
drawList->AddText(ImVec2(min.x + AssetTileTextPadding().x, textY), ImGui::GetColorU32(AssetTileTextColor(selected)), label);
|
||||
ImGui::PopClipRect();
|
||||
|
||||
return AssetTileResult{ clicked, contextRequested, openRequested, hovered, min, max };
|
||||
}
|
||||
|
||||
inline void DrawAssetIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, AssetIconKind kind) {
|
||||
if (kind == AssetIconKind::Folder) {
|
||||
const ImU32 fillColor = ImGui::GetColorU32(AssetFolderIconFillColor());
|
||||
const ImU32 lineColor = ImGui::GetColorU32(AssetFolderIconLineColor());
|
||||
const float width = max.x - min.x;
|
||||
const float height = max.y - min.y;
|
||||
const ImVec2 tabMax(min.x + width * 0.45f, min.y + height * 0.35f);
|
||||
drawList->AddRectFilled(ImVec2(min.x, min.y + height * 0.14f), tabMax, fillColor, 2.0f);
|
||||
drawList->AddRectFilled(ImVec2(min.x, min.y + height * 0.28f), max, fillColor, 2.0f);
|
||||
drawList->AddRect(ImVec2(min.x, min.y + height * 0.14f), tabMax, lineColor, 2.0f);
|
||||
drawList->AddRect(ImVec2(min.x, min.y + height * 0.28f), max, lineColor, 2.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
const ImU32 fillColor = ImGui::GetColorU32(AssetFileIconFillColor());
|
||||
const ImU32 lineColor = ImGui::GetColorU32(AssetFileIconLineColor());
|
||||
const ImVec2 foldA(max.x - 8.0f, min.y);
|
||||
const ImVec2 foldB(max.x, min.y + 8.0f);
|
||||
drawList->AddRectFilled(min, max, fillColor, 2.0f);
|
||||
drawList->AddRect(min, max, lineColor, 2.0f);
|
||||
drawList->AddTriangleFilled(foldA, ImVec2(max.x, min.y), foldB, ImGui::GetColorU32(AssetFileFoldColor()));
|
||||
drawList->AddLine(foldA, foldB, lineColor);
|
||||
}
|
||||
|
||||
inline ComponentSectionResult BeginComponentSection(
|
||||
const void* id,
|
||||
const char* label,
|
||||
bool canRemove,
|
||||
bool defaultOpen = true) {
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, InspectorSectionFramePadding());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x, InspectorSectionItemSpacing().y));
|
||||
|
||||
ImGuiTreeNodeFlags flags =
|
||||
ImGuiTreeNodeFlags_Framed |
|
||||
ImGuiTreeNodeFlags_SpanAvailWidth |
|
||||
ImGuiTreeNodeFlags_FramePadding |
|
||||
ImGuiTreeNodeFlags_AllowOverlap;
|
||||
if (defaultOpen) {
|
||||
flags |= ImGuiTreeNodeFlags_DefaultOpen;
|
||||
}
|
||||
|
||||
const bool open = ImGui::TreeNodeEx(id, flags, "%s", label);
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
bool removeRequested = false;
|
||||
if (BeginPopupContextItem("##ComponentSettings")) {
|
||||
DrawMenuCommand(MenuCommand::Action("Remove Component", nullptr, false, canRemove), [&]() {
|
||||
removeRequested = true;
|
||||
});
|
||||
EndPopup();
|
||||
}
|
||||
|
||||
return ComponentSectionResult{ open, removeRequested };
|
||||
}
|
||||
|
||||
inline void EndComponentSection() {
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user