diff --git a/CMakeLists.txt b/CMakeLists.txt index 2852111..b9023ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,7 @@ add_subdirectory($ENV{GEODE_SDK} ${CMAKE_CURRENT_BINARY_DIR}/geode) CPMAddPackage("gh:ocornut/imgui@1.91.0-docking") -target_include_directories(${PROJECT_NAME} PRIVATE ${imgui_SOURCE_DIR}) +target_include_directories(${PROJECT_NAME} PRIVATE ${imgui_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/include) target_sources(${PROJECT_NAME} PRIVATE ${imgui_SOURCE_DIR}/imgui.cpp diff --git a/include/API.hpp b/include/API.hpp new file mode 100644 index 0000000..f9eb35b --- /dev/null +++ b/include/API.hpp @@ -0,0 +1,153 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +namespace devtools { + template + concept IsCCNode = std::is_base_of_v>; + + template + concept SupportedProperty = std::is_arithmetic_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v; + + struct RegisterNodeEvent final : geode::Event { + RegisterNodeEvent(std::function&& callback) + : callback(std::move(callback)) {} + std::function callback; + }; + + template + struct HandlePropertyEvent final : geode::Event { + HandlePropertyEvent(const char* n, T* p) : prop(p), name(n) {} + T* prop; + const char* name = nullptr; + bool changed = false; + }; + + struct DrawLabelEvent final : geode::Event { + DrawLabelEvent(const char* t) : text(t) {} + const char* text = nullptr; + }; + + template + struct EnumerableEvent final : geode::Event { + EnumerableEvent(const char* l, T* v, std::initializer_list> i) + : label(l), value(v), items(i) {} + const char* label = nullptr; + T* value; + std::initializer_list> items; + bool changed = false; + }; + + struct ButtonEvent final : geode::Event { + ButtonEvent(const char* l) : label(l) {} + const char* label = nullptr; + bool clicked = false; + }; + + /// @brief Checks if DevTools is currently loaded. + /// @return True if DevTools is loaded, false otherwise. + inline bool isLoaded() { + return geode::Loader::get()->getLoadedMod("geode.devtools") != nullptr; + } + + /// @brief Waits for DevTools to be loaded and then calls the provided callback. + /// @param callback The function to call once DevTools is loaded. + template + void waitForDevTools(F&& callback) { + if (isLoaded()) { + callback(); + } else { + auto devtools = geode::Loader::get()->getInstalledMod("geode.devtools"); + if (!devtools || !devtools->isEnabled()) return; + + new geode::EventListener( + [callback = std::forward(callback)](geode::ModStateEvent*) { + callback(); + }, + geode::ModStateFilter(devtools, geode::ModEventType::Loaded) + ); + } + } + + /// @brief Registers a callback that will be called whenever a node of type T is opened in Attributes tab. + /// @param callback The function to call with the node when it is opened. + /// @see `devtools::property`, `devtools::label`, `devtools::enumerable`, `devtools::button` + template *> F> requires IsCCNode + void registerNode(F&& callback) { + RegisterNodeEvent([callback = std::forward(callback)](cocos2d::CCNode* node) { + if (auto casted = geode::cast::typeinfo_cast*>(node)) { + callback(casted); + } + }).post(); + } + + /// @brief Renders a property editor for the given value in the DevTools UI. + /// @param name The name of the property to display. + /// @param prop The property value to edit. + /// @return True if the property was changed, false otherwise. + /// @warning This function should only ever be called from within a registered node callback. + template requires SupportedProperty + bool property(const char* name, T& prop) { + HandlePropertyEvent event(name, &prop); + event.post(); + return event.changed; + } + + /// @brief Renders a label in the DevTools UI. + /// @param text The text to display in the label. + /// @warning This function should only ever be called from within a registered node callback. + inline void label(const char* text) { + DrawLabelEvent(text).post(); + } + + /// @brief Renders an enumerable property editor using radio buttons for the given value in the DevTools UI. + /// @param label The label for the enumerable property. + /// @param value The value to edit, which should be an enum or integral type. + /// @param items A list of pairs where each pair contains a value and its corresponding label. + /// @return True if the value was changed, false otherwise. + /// @warning This function should only ever be called from within a registered node callback. + template requires std::is_integral_v> + bool enumerable(const char* label, T& value, std::initializer_list> items) { + using ValueType = std::underlying_type_t; + EnumerableEvent event( + label, reinterpret_cast(&value), + *reinterpret_cast>*>(&items) + ); + event.post(); + return event.changed; + } + + /// @brief Renders a button in the DevTools UI. + /// @param label The label for the button. + /// @return True if the button was clicked, false otherwise. + /// @warning This function should only ever be called from within a registered node callback. + inline bool button(const char* label) { + ButtonEvent event(label); + event.post(); + return event.clicked; + } + + /// @brief Renders a button in the DevTools UI and calls the provided callback if the button is clicked. + /// @param label The label for the button. + /// @param callback The function to call when the button is clicked. + /// @warning This function should only ever be called from within a registered node callback. + template + void button(const char* label, F&& callback) { + if (button(label)) { + callback(); + } + } +} \ No newline at end of file diff --git a/mod.json b/mod.json index dbab282..c5ae72a 100644 --- a/mod.json +++ b/mod.json @@ -11,6 +11,7 @@ "name": "DevTools", "developer": "Geode Team", "description": "Developer tools for Geode", + "api": { "include": ["include/*.hpp"] }, "links": { "source": "https://github.com/geode-sdk/DevTools" }, diff --git a/src/API.cpp b/src/API.cpp new file mode 100644 index 0000000..6e33800 --- /dev/null +++ b/src/API.cpp @@ -0,0 +1,139 @@ +#include +#include "DevTools.hpp" +#include "ImGui.hpp" +#include + +using namespace geode::prelude; + +template +static void handleType() { + new EventListener>>(+[](devtools::HandlePropertyEvent* event) { + constexpr bool isSigned = std::is_signed_v; + constexpr ImGuiDataType dataType = sizeof(T) == 1 ? (isSigned ? ImGuiDataType_S8 : ImGuiDataType_U8) : + sizeof(T) == 2 ? (isSigned ? ImGuiDataType_S16 : ImGuiDataType_U16) : + sizeof(T) == 4 ? (isSigned ? ImGuiDataType_S32 : ImGuiDataType_U32) : + isSigned ? ImGuiDataType_S64 : ImGuiDataType_U64; + event->changed = ImGui::InputScalar(event->name, dataType, event->prop); + return ListenerResult::Stop; + }); + + new EventListener>>(+[](devtools::EnumerableEvent* event) { + ImGui::Text("%s:", event->label); + size_t i = 0; + for (auto& [value, label] : event->items) { + if (ImGui::RadioButton(label, *event->value == value)) { + *event->value = value; + event->changed = true; + } + if (i < event->items.size() - 1) { + ImGui::SameLine(); + } + i++; + } + return ListenerResult::Stop; + }); +} + +$execute { + new EventListener>(+[](devtools::RegisterNodeEvent* event) { + DevTools::get()->addCustomCallback(std::move(event->callback)); + return ListenerResult::Stop; + }); + + // Scalars & Enums + handleType(); + handleType(); + handleType(); + handleType(); + handleType(); + handleType(); + handleType(); + handleType(); + handleType(); + handleType(); + + // checkbox + new EventListener>>(+[](devtools::HandlePropertyEvent* event) { + event->changed = ImGui::Checkbox(event->name, event->prop); + return ListenerResult::Stop; + }); + + // float and double + new EventListener>>(+[](devtools::HandlePropertyEvent* event) { + event->changed = ImGui::InputFloat(event->name, event->prop); + return ListenerResult::Stop; + }); + new EventListener>>(+[](devtools::HandlePropertyEvent* event) { + event->changed = ImGui::InputDouble(event->name, event->prop); + return ListenerResult::Stop; + }); + + // string + new EventListener>>(+[](devtools::HandlePropertyEvent* event) { + event->changed = ImGui::InputText(event->name, event->prop); + return ListenerResult::Stop; + }); + + // colors + new EventListener>>(+[](devtools::HandlePropertyEvent* event) { + auto color = ImVec4( + event->prop->r / 255.f, + event->prop->g / 255.f, + event->prop->b / 255.f, + 1.0f + ); + if (ImGui::ColorEdit3(event->name, &color.x)) { + event->changed = true; + event->prop->r = static_cast(color.x * 255); + event->prop->g = static_cast(color.y * 255); + event->prop->b = static_cast(color.z * 255); + } + return ListenerResult::Stop; + }); + new EventListener>>(+[](devtools::HandlePropertyEvent* event) { + auto color = ImVec4( + event->prop->r / 255.f, + event->prop->g / 255.f, + event->prop->b / 255.f, + event->prop->a / 255.f + ); + if (ImGui::ColorEdit4(event->name, &color.x)) { + event->changed = true; + event->prop->r = static_cast(color.x * 255); + event->prop->g = static_cast(color.y * 255); + event->prop->b = static_cast(color.z * 255); + event->prop->a = static_cast(color.w * 255); + } + return ListenerResult::Stop; + }); + new EventListener>>(+[](devtools::HandlePropertyEvent* event) { + event->changed = ImGui::ColorEdit4(event->name, reinterpret_cast(event->prop)); + return ListenerResult::Stop; + }); + + // points/sizes + new EventListener>>(+[](devtools::HandlePropertyEvent* event) { + event->changed = ImGui::InputFloat2(event->name, reinterpret_cast(event->prop)); + return ListenerResult::Stop; + }); + new EventListener>>(+[](devtools::HandlePropertyEvent* event) { + event->changed = ImGui::InputFloat2(event->name, reinterpret_cast(event->prop)); + return ListenerResult::Stop; + }); + new EventListener>>(+[](devtools::HandlePropertyEvent* event) { + event->changed = ImGui::InputFloat4(event->name, reinterpret_cast(event->prop)); + return ListenerResult::Stop; + }); + + // label + new EventListener>(+[](devtools::DrawLabelEvent* event) { + ImGui::Text("%s", event->text); + return ListenerResult::Stop; + }); + + // button + new EventListener>(+[](devtools::ButtonEvent* event) { + event->clicked = ImGui::Button(event->label); + return ListenerResult::Stop; + }); +} \ No newline at end of file diff --git a/src/DevTools.cpp b/src/DevTools.cpp index 1434ca5..294855f 100644 --- a/src/DevTools.cpp +++ b/src/DevTools.cpp @@ -86,6 +86,10 @@ void DevTools::highlightNode(CCNode* node, HighlightMode mode) { m_toHighlight.push_back({ node, mode }); } +void DevTools::addCustomCallback(std::function callback) { + m_customCallbacks.push_back(std::move(callback)); +} + void DevTools::drawPage(const char* name, void(DevTools::*pageFun)()) { if (ImGui::Begin(name, nullptr, ImGuiWindowFlags_HorizontalScrollbar)) { (this->*pageFun)(); diff --git a/src/DevTools.hpp b/src/DevTools.hpp index 4859192..a27dfa5 100644 --- a/src/DevTools.hpp +++ b/src/DevTools.hpp @@ -48,6 +48,7 @@ class DevTools { CCTexture2D* m_fontTexture = nullptr; Ref m_selectedNode; std::vector> m_toHighlight; + std::vector> m_customCallbacks; void setupFonts(); void setupPlatform(); @@ -103,6 +104,8 @@ class DevTools { void selectNode(CCNode* node); void highlightNode(CCNode* node, HighlightMode mode); + void addCustomCallback(std::function callback); + void sceneChanged(); void render(GLRenderCtx* ctx); diff --git a/src/pages/Attributes.cpp b/src/pages/Attributes.cpp index 5e983d4..f308f63 100644 --- a/src/pages/Attributes.cpp +++ b/src/pages/Attributes.cpp @@ -39,7 +39,7 @@ void DevTools::drawNodeAttributes(CCNode* node) { drawColorAttributes(node); drawLabelAttributes(node); drawAxisGapAttribute(node); - + ImGui::NewLine(); ImGui::Separator(); ImGui::NewLine(); @@ -47,8 +47,18 @@ void DevTools::drawNodeAttributes(CCNode* node) { drawTextureAttributes(node); drawMenuItemAttributes(node); + for (auto& callback : m_customCallbacks) { + ImGui::PushID(&callback); + callback(node); + ImGui::PopID(); + } + + ImGui::NewLine(); + ImGui::Separator(); + ImGui::NewLine(); + drawLayoutOptionsAttributes(node); - + ImGui::NewLine(); ImGui::Separator(); ImGui::NewLine();