#include "Editor/EditorGUI.h" void EditorGUI::Draw(World* world) { ImGui::ShowTestWindow(); drawMenu(); drawEntities(world); drawComponents(m_CurrentSelection); } void EditorGUI::SelectEntity(EntityWrapper entity) { m_CurrentSelection = entity; if (m_OnEntitySelected != nullptr) { m_OnEntitySelected(entity); } } void EditorGUI::drawMenu() { } void EditorGUI::drawEntities(World* world) { if (!ImGui::Begin("Entities")) { return; } float buttonWidth = (ImGui::GetContentRegionAvailWidth() - 10.f) / 3.f ; if (ImGui::Button("Create", ImVec2(buttonWidth, 0))) { if (m_OnEntityCreate != nullptr) { EntityWrapper newEntity = m_OnEntityCreate(EntityWrapper(world, EntityID_Invalid)); SelectEntity(newEntity); } } ImGui::SameLine(0.f, 5.f); if (ImGui::Button("Import", ImVec2(buttonWidth, 0))) { entityImport(world); } ImGui::SameLine(0.f, 5.f); ImGui::Button("Reference", ImVec2(buttonWidth, 0)); ImGui::ItemSize(ImVec2(0, 3)); drawEntitiesRecursive(world, EntityID_Invalid); // Draw any potential modals before ending this scope drawModals(); ImGui::End(); } void EditorGUI::drawEntitiesRecursive(World* world, EntityID parent) { auto entityChildren = world->GetEntityChildren(); auto range = entityChildren.equal_range(parent); for (auto it = range.first; it != range.second; it++) { if (EditorGUI::drawEntityNode(EntityWrapper(world, it->second))) { drawEntitiesRecursive(world, it->second); ImGui::TreePop(); } } } bool EditorGUI::drawEntityNode(EntityWrapper entity) { ImVec2 pos = ImGui::GetCursorScreenPos(); float width = ImGui::GetContentRegionAvailWidth(); ImRect bb(pos + ImVec2(20, 0), pos + ImVec2(width, 14)); auto window = ImGui::GetCurrentWindow(); if (m_CurrentSelection == entity) { const ImU32 col = window->Color(ImGuiCol_HeaderActive); window->DrawList->AddRectFilled(bb.Min, bb.Max, col); } ImGuiID id = window->GetID((std::string("#SelectButton") + std::to_string(entity.ID)).c_str()); bool hovered = false; bool held = false; if (ImGui::ButtonBehavior(bb, id, &hovered, &held)) { SelectEntity(entity); } //if (held) { // ImVec2 entityDragDelta = ImGui::GetMouseDragDelta(0); // if (std::abs(entityDragDelta.x) > 0 && std::abs(entityDragDelta.y) > 0) { // if (m_UIDraggingEntity == EntityID_Invalid) { // m_UIDraggingEntity = entity; // LOG_DEBUG("Started drag of entity %i", m_UIDraggingEntity); // } // ImGui::SetNextWindowPos(ImGui::GetIO().MousePos + ImVec2(20, 0)); // ImGui::Begin("Change parent", nullptr, ImVec2(0, 0), 0.3f, ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoSavedSettings); // ImGui::Text("#%i", m_UIDraggingEntity); // ImGui::End(); // } //} // Compose title std::stringstream nodeTitle; const std::string& entityName = entity.World->GetName(entity); if (!entityName.empty()) { nodeTitle << entityName; } else { nodeTitle << "#" << entity.ID; } if (m_EntityFiles.count(entity) == 1) { nodeTitle << " (" << m_EntityFiles.at(entity).filename().string() << ")"; } ImGui::SetNextTreeNodeOpened(true, ImGuiSetCond_Once); if (ImGui::TreeNode(nodeTitle.str().c_str())) { //if (m_UIDraggingEntity != EntityID_Invalid && ImGui::IsItemHoveredRect() && ImGui::IsMouseReleased(0)) { // LOG_DEBUG("Changed parent of %i to %i", m_UIDraggingEntity, entity); // changeParent(m_UIDraggingEntity, entity); // m_UIDraggingEntity = EntityID_Invalid; //} if (ImGui::BeginPopupContextItem("entity context menu")) { if (ImGui::Button("Save")) { entitySave(entity); } ImGui::SameLine(); if (ImGui::Button("Delete")) { if (m_OnEntityDelete != nullptr) { m_OnEntityDelete(entity); ImGui::CloseCurrentPopup(); } if (!m_CurrentSelection.Valid()) { SelectEntity(EntityWrapper::Invalid); } } drawModals(); ImGui::EndPopup(); } return true; } else { return false; } } void EditorGUI::drawComponents(EntityWrapper entity) { std::stringstream title; title << "Components"; if (entity.Valid()) { title << " #" << entity.ID << "###Components"; } if (!ImGui::Begin(title.str().c_str())) { ImGui::End(); return; } if (!entity.Valid()) { ImGui::End(); return; } auto& pools = entity.World->GetComponentPools(); // Create list of component types available to be added std::vector componentTypes; for (auto& pair : pools) { // Don't list components the entity already has attached if (!entity.HasComponent(pair.first)) { componentTypes.push_back(pair.first.c_str()); } } // Draw combo box ImGui::PushItemWidth(ImGui::GetContentRegionAvailWidth() - 10.f); int selectedItem = -1; if (ImGui::Combo("", &selectedItem, componentTypes.data(), componentTypes.size())) { if (selectedItem != -1) { if (m_OnComponentAttach != nullptr) { std::string chosenComponentType(componentTypes.at(selectedItem)); m_OnComponentAttach(entity, chosenComponentType); } } } ImGui::PopItemWidth(); for (auto& pair : pools) { const std::string& componentType = pair.first; auto pool = pair.second; // Don't show components the entity doesn't have attached if (!entity.HasComponent(componentType)) { continue; } // Handle deletion with early out if (createDeleteButton(componentType)) { if (m_OnComponentDelete != nullptr) { m_OnComponentDelete(entity, componentType); continue; } } // Draw the actual component node drawComponentNode(entity, pool->ComponentInfo()); } ImGui::End(); } bool EditorGUI::drawComponentNode(EntityWrapper entity, const ComponentInfo& ci) { if (!ImGui::CollapsingHeader(ci.Name.c_str(), nullptr, true, true)) { return false; } // Show component annotation const std::string annotation = ci.Meta->Annotation; if (!annotation.empty()) { ImGui::TextWrapped(annotation.c_str()); } // Draw component fields ComponentWrapper& component = entity.World->GetComponent(entity.ID, ci.Name); for (auto& kv : ci.Fields) { const std::string& fieldName = kv.first; const ComponentInfo::Field_t& field = kv.second; // Draw the field widget based on its type drawComponentField(component, field); ImGui::SameLine(); // Draw field name ImGui::Text(fieldName.c_str()); // Draw potential field annotation auto fieldAnnotationIt = ci.Meta->FieldAnnotations.find(fieldName); if (fieldAnnotationIt != ci.Meta->FieldAnnotations.end()) { ImGui::SameLine(); ImGui::TextDisabled("(?)"); if (ImGui::IsItemHovered()) { ImGui::SetTooltip(fieldAnnotationIt->second.c_str()); } } } return true; } void EditorGUI::drawComponentField(ComponentWrapper& c, const ComponentInfo::Field_t& field) { // Push an unique widget id so different components with fields with equal names are still counted as different ImGui::PushID((c.Info.Name + field.Name).c_str()); if (field.Type == "Vector") { drawComponentField_Vector(c, field); } else if (field.Type == "Color") { drawComponentField_Color(c, field); //} else if (field.Type == "Quaternion") { } else if (field.Type == "int") { drawComponentField_int(c, field); } else if (field.Type == "enum") { drawComponentField_enum(c, field); } else if (field.Type == "float") { drawComponentField_float(c, field); } else if (field.Type == "double") { drawComponentField_double(c, field); } else if (field.Type == "bool") { drawComponentField_bool(c, field); } else if (field.Type == "string") { drawComponentField_string(c, field); } else { ImGui::TextDisabled(field.Type.c_str()); } ImGui::PopID(); } void EditorGUI::drawComponentField_Vector(ComponentWrapper &c, const ComponentInfo::Field_t &field) { auto& val = c.Field(field.Name); if (field.Name == "Scale") { // Limit scale values to a minimum of 0 ImGui::DragFloat3("", glm::value_ptr(val), 0.1f, 0.f, std::numeric_limits::max()); } else if (field.Name == "Orientation") { // Make orentations have a period of 2*Pi glm::vec3 tempVal = glm::fmod(val, glm::vec3(glm::two_pi())); if (ImGui::SliderFloat3("", glm::value_ptr(tempVal), 0.f, glm::two_pi())) { val = tempVal; } } else { ImGui::DragFloat3("", glm::value_ptr(val), 0.1f, std::numeric_limits::lowest(), std::numeric_limits::max()); } } void EditorGUI::drawComponentField_Color(ComponentWrapper &c, const ComponentInfo::Field_t &field) { auto& val = c.Field(field.Name); ImGui::ColorEdit4("", glm::value_ptr(val), true); } void EditorGUI::drawComponentField_int(ComponentWrapper &c, const ComponentInfo::Field_t &field) { auto& val = c.Field(field.Name); ImGui::InputInt("", &val); } void EditorGUI::drawComponentField_enum(ComponentWrapper &c, const ComponentInfo::Field_t &field) { auto fieldEnumDefIt = c.Info.Meta->FieldEnumDefinitions.find(field.Name); if (fieldEnumDefIt == c.Info.Meta->FieldEnumDefinitions.end()) { drawComponentField_int(c, field); return; } auto& val = c.Field(field.Name); int selectedItem = -1; std::stringstream enumKeys; std::vector enumValues; int i = 0; for (auto& kv : fieldEnumDefIt->second) { enumKeys << kv.first << " (" << kv.second << ")" << '\0'; enumValues.push_back(kv.second); if (val == kv.second) { selectedItem = i; } } if (ImGui::Combo("", &selectedItem, enumKeys.str().c_str())) { val = enumValues.at(selectedItem); } } void EditorGUI::drawComponentField_float(ComponentWrapper &c, const ComponentInfo::Field_t &field) { auto& val = c.Field(field.Name); ImGui::InputFloat("", &val, 0.01f, 1.f); } void EditorGUI::drawComponentField_double(ComponentWrapper &c, const ComponentInfo::Field_t &field) { float tempVal = static_cast(c.Field(field.Name)); if (ImGui::InputFloat("", &tempVal, 0.01f, 1.f)) { c.SetField(field.Name, static_cast(tempVal)); } } void EditorGUI::drawComponentField_bool(ComponentWrapper &c, const ComponentInfo::Field_t &field) { auto& val = c.Field(field.Name); ImGui::Checkbox("", &val); } void EditorGUI::drawComponentField_string(ComponentWrapper &c, const ComponentInfo::Field_t &field) { auto& val = c.Field(field.Name); char tempString[1024]; // Let's just hope this is an sufficiently large buffer for strings :) tempString[1023] = '\0'; // Null terminator just in case the string is larger than the buffer // Copy the string into the buffer, taking the null terminator into account memcpy(tempString, val.c_str(), std::min(val.length() + 1, sizeof(tempString) - 1)); if (ImGui::InputText("", tempString, sizeof(tempString))) { val = std::string(tempString); } // TODO: Handle drag and drop of files } void EditorGUI::drawModals() { if (ImGui::BeginPopupModal("Import failed", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("Entity import failed. Check console for more information.\n\n"); ImGui::SetCursorPosX(ImGui::GetContentRegionAvailWidth() - 120); if (ImGui::Button("OK", ImVec2(120, 0))) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } if (ImGui::BeginPopupModal("Save failed", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("Entity save failed on an exception.\nMessage: %s\n\n", m_LastErrorMessage.c_str()); ImGui::SetCursorPosX(ImGui::GetContentRegionAvailWidth() - 120); if (ImGui::Button("OK", ImVec2(120, 0))) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } } bool EditorGUI::createDeleteButton(const std::string& componentType) { float width = ImGui::GetContentRegionAvailWidth(); ImGuiWindow* window = ImGui::GetCurrentWindow(); auto pos = ImGui::GetCursorScreenPos() + ImVec2(width - 14.f, 1); ImRect bb = ImRect(pos, pos + ImVec2(14.f, 14.f)); std::string idString = "#DELETE"; idString += componentType; ImGuiID id = window->GetID(idString.c_str()); bool hovered; bool held; bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held); //ImU32 col = window->Color((held && hovered) ? ImGuiCol_CloseButtonActive : hovered ? ImGuiCol_CloseButtonHovered : ImGuiCol_CloseButton); ImU32 col = window->Color((held && hovered) ? ImGuiCol_CloseButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); window->DrawList->AddCircleFilled(bb.GetCenter(), 7.f, col, 16); return pressed; } boost::filesystem::path EditorGUI::fileOpenDialog() { namespace bfs = boost::filesystem; nfdchar_t* outPath = nullptr; nfdresult_t result = NFD_OpenDialog("xml", bfs::absolute(m_DefaultEntityPath).string().c_str(), &outPath); if (result == NFD_ERROR) { LOG_ERROR("NFD Error: %s", NFD_GetError()); return bfs::path(); } else if (result == NFD_CANCEL) { return bfs::path(); } else { return bfs::absolute(outPath); } } boost::filesystem::path EditorGUI::fileSaveDialog() { namespace bfs = boost::filesystem; nfdchar_t* outPath = nullptr; nfdresult_t result = NFD_SaveDialog("xml", bfs::absolute(m_DefaultEntityPath).string().c_str(), &outPath); if (result == NFD_ERROR) { LOG_ERROR("NFD Error: %s", NFD_GetError()); return bfs::path(); } else if (result == NFD_CANCEL) { return bfs::path(); } else { return bfs::absolute(outPath); } } void EditorGUI::entityImport(World* world) { boost::filesystem::path filePath = fileOpenDialog(); if (filePath.empty()) { return; } EntityWrapper entity = m_OnEntityImport(EntityWrapper(world, EntityID_Invalid), filePath); if (entity.Valid()) { m_EntityFiles[entity] = filePath; SelectEntity(entity); } else { ImGui::OpenPopup("Import failed"); } } void EditorGUI::entitySave(EntityWrapper entity) { boost::filesystem::path filePath; if (m_EntityFiles.count(entity) == 1) { filePath = m_EntityFiles.at(entity); } else { filePath = fileSaveDialog(); } if (filePath.empty()) { return; } try { m_OnEntitySave(entity, filePath); m_EntityFiles[entity] = filePath; } catch (const std::exception& e) { m_LastErrorMessage = e.what(); ImGui::OpenPopup("Save failed"); } }