diff --git a/loader/include/Geode/ui/Button.hpp b/loader/include/Geode/ui/Button.hpp index b2eebcfa7..92d378339 100644 --- a/loader/include/Geode/ui/Button.hpp +++ b/loader/include/Geode/ui/Button.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include namespace geode { @@ -108,7 +109,7 @@ namespace geode { int getTouchPriority(); /** - * Set the touch multiplier, which increases the distance from the + * Set the touch multiplier, which increases the distance from the * center that the button can be pressed (default is 1) */ void setTouchMultiplier(float multipler); @@ -145,7 +146,7 @@ namespace geode { */ virtual void setEnabled(bool enabled); virtual bool isEnabled(); - + /** * Get the selected state of the button */ @@ -208,7 +209,7 @@ namespace geode { * Unregister a button (must be called in onExit) */ void unregisterButton(Button* button); - + /** * Pass moves from ccTouchMoved to all buttons sharing a parent */ diff --git a/loader/include/Geode/ui/Toggler.hpp b/loader/include/Geode/ui/Toggler.hpp new file mode 100644 index 000000000..0df068d12 --- /dev/null +++ b/loader/include/Geode/ui/Toggler.hpp @@ -0,0 +1,185 @@ +#pragma once + +#include +#include +#include +#include + +namespace geode { + /// A customizable toggle button for interactive on/off controls. + /// + /// Supports custom visuals, state management, and callbacks. + class GEODE_DLL Toggler : public Button { + public: + using TogglerCallback = geode::Function; + + /// Make a toggle with custom on/off nodes. + /// + /// @param offNode Node shown when off. + /// @param onNode Node shown when on. + /// @param toggleCallback Called when the toggle changes. + /// + /// @example + /// // These are just example sprites, use `createWithStandardSprites` if you + /// // need a GD-style checkbox. + /// auto offNode = CCSprite::createWithSpriteFrameName("GJ_checkOff_001.png"); + /// auto onNode = CCSprite::createWithSpriteFrameName("GJ_checkOn_001.png"); + /// + /// auto toggler = Toggler::create( + /// offNode, + /// onNode, + /// [](Toggler* sender, bool isToggled) { + /// log::info("Toggled: {}", isToggled); + /// } + /// ); + static Toggler* create( + cocos2d::CCNode* offNode, + cocos2d::CCNode* onNode, + TogglerCallback toggleCallback = nullptr + ); + + /// Make a toggle with GD's standard checkbox sprites. + /// + /// @param toggleCallback Called when the toggle changes. + /// + /// @example + /// auto toggler = Toggler::createWithStandardSprites( + /// [](Toggler* sender, bool isToggled) { + /// log::info("Checkbox: {}", isToggled); + /// } + /// ); + static Toggler* createWithStandardSprites( + TogglerCallback toggleCallback = nullptr + ); + + /// Make a toggle with GD's standard checkbox sprites. + /// + /// @param scale Checkbox scale. + /// @param toggleCallback Called when the toggle changes. + static Toggler* createWithStandardSprites( + float scale, + TogglerCallback toggleCallback = nullptr + ); + + /// Make a toggle using sprites from files. + /// + /// @param offFileName Sprite shown when off. + /// @param onFileName Sprite shown when on. + /// @param toggleCallback Called when the toggle changes. + /// + /// @example + /// auto toggler = Toggler::createWithSprites( + /// "off.png"_spr, + /// "on.png"_spr, + /// [](Toggler* sender, bool isToggled) { + /// log::info("Custom toggle: {}", isToggled); + /// } + /// ); + static Toggler* createWithSprites( + geode::ZStringView offFileName, + geode::ZStringView onFileName, + TogglerCallback toggleCallback = nullptr + ); + + /// Make a toggle using sprites from files. + /// + /// @param offFileName Sprite shown when off. + /// @param onFileName Sprite shown when on. + /// @param scale Sprite scale. + /// @param toggleCallback Called when the toggle changes. + static Toggler* createWithSprites( + geode::ZStringView offFileName, + geode::ZStringView onFileName, + float scale, + TogglerCallback toggleCallback = nullptr + ); + + /// Make a toggle using sprites from a sprite sheet. + /// + /// @param offFrameName Frame shown when off. + /// @param onFrameName Frame shown when on. + /// @param toggleCallback Called when the toggle changes. + /// + /// @example + /// // These are just example sprites, use `createWithStandardSprites` if you + /// // need a GD-style checkbox. + /// auto toggler = Toggler::createWithSpriteFrameNames( + /// "GJ_checkOff_001.png", + /// "GJ_checkOn_001.png", + /// [](Toggler* sender, bool isToggled) { + /// log::info("Checkbox state changed: {}", isToggled); + /// } + /// ); + static Toggler* createWithSpriteFrameNames( + geode::ZStringView offFrameName, + geode::ZStringView onFrameName, + TogglerCallback toggleCallback = nullptr + ); + + /// Make a toggle using sprites from a sprite sheet. + /// + /// @param offFrameName Frame shown when off. + /// @param onFrameName Frame shown when on. + /// @param scale Sprite scale. + /// @param toggleCallback Called when the toggle changes. + static Toggler* createWithSpriteFrameNames( + geode::ZStringView offFrameName, + geode::ZStringView onFrameName, + float scale, + TogglerCallback toggleCallback = nullptr + ); + + /// Check if the toggle is on. + bool isToggled() const; + + /// Set whether the toggle is on. + /// + /// @param toggled Whether the toggle should be on. + /// @param triggerCallback Whether to trigger the callback if the state changes. + void setToggled(bool toggled, bool triggerCallback = false); + + /// Toggle between the on and off states. + /// + /// @param triggerCallback Whether to trigger the callback. + void toggle(bool triggerCallback = true); + + /// Get the node shown when the toggle is off. + cocos2d::CCNode* getOffNode() const; + + /// Change the node shown when the toggle is off. + /// + /// @param node Node to show when off. + void setOffNode(cocos2d::CCNode* node); + + /// Get the node shown when the toggle is on. + cocos2d::CCNode* getOnNode() const; + + /// Change the node shown when the toggle is on. + /// + /// @param node Node to show when on. + void setOnNode(cocos2d::CCNode* node); + + /// Change the callback. + /// + /// @param callback Callback to trigger when the toggle changes. + void setToggleCallback(TogglerCallback callback); + + void activate() override; + + protected: + Toggler(); + ~Toggler(); + + bool init( + cocos2d::CCNode* offNode, + cocos2d::CCNode* onNode, + TogglerCallback toggleCallback + ); + + void updateDisplay(); + + private: + class Impl; + std::unique_ptr m_impl; + }; +} diff --git a/loader/src/ui/nodes/Button.cpp b/loader/src/ui/nodes/Button.cpp index eedc6ba9e..ac6819952 100644 --- a/loader/src/ui/nodes/Button.cpp +++ b/loader/src/ui/nodes/Button.cpp @@ -19,7 +19,7 @@ class Button::Impl final { float m_touchMultiplier = 1.f; CCPoint m_offset = {0, -15}; - + float m_selectedDuration = 0.3f; float m_unselectedDuration = 0.4f; @@ -158,7 +158,9 @@ CCNode* Button::getDisplayNode() { } void Button::setDisplayNode(CCNode* node) { - m_impl->m_displayNode->removeFromParent(); + if (m_impl->m_displayNode) { + m_impl->m_displayNode->removeFromParent(); + } m_impl->m_displayNode = node; @@ -187,7 +189,7 @@ CCActionInterval* Button::clickActionForType() { } return nullptr; -} +} CCActionInterval* Button::releaseActionForType() { switch (m_impl->m_animationType) { @@ -206,7 +208,7 @@ CCActionInterval* Button::releaseActionForType() { return CCEaseInOut::create(moveTo, 2.f); } } - + return nullptr; } @@ -297,7 +299,7 @@ void Button::selected() { void Button::unselected() { if (!m_impl->m_selected) return; - + stopAction(m_impl->m_activeClickAction); stopAction(m_impl->m_activeReleaseAction); diff --git a/loader/src/ui/nodes/Toggler.cpp b/loader/src/ui/nodes/Toggler.cpp new file mode 100644 index 000000000..49fc6fe69 --- /dev/null +++ b/loader/src/ui/nodes/Toggler.cpp @@ -0,0 +1,210 @@ +#include + +using namespace geode::prelude; + +class Toggler::Impl final { +public: + TogglerCallback m_toggleCallback = nullptr; + bool m_isToggled = false; + + CCNode* m_offNode = nullptr; + CCNode* m_onNode = nullptr; +}; + +Toggler::Toggler() : m_impl(std::make_unique()) {} + +Toggler::~Toggler() = default; + +Toggler* Toggler::create( + CCNode* offNode, + CCNode* onNode, + TogglerCallback toggleCallback +) { + auto ret = new Toggler(); + if (ret->init(offNode, onNode, std::move(toggleCallback))) { + ret->autorelease(); + return ret; + } + delete ret; + return nullptr; +} + +static Toggler* createScaled( + CCNode* offNode, + CCNode* onNode, + float scale, + Toggler::TogglerCallback toggleCallback +) { + if (!offNode || !onNode) return nullptr; + + offNode->setScale(scale); + onNode->setScale(scale); + + return Toggler::create(offNode, onNode, std::move(toggleCallback)); +} + +Toggler* Toggler::createWithStandardSprites( + TogglerCallback toggleCallback +) { + return createWithStandardSprites(1.0f, std::move(toggleCallback)); +} + +Toggler* Toggler::createWithStandardSprites( + float scale, + TogglerCallback toggleCallback +) { + return createScaled( + CCSprite::createWithSpriteFrameName("GJ_checkOff_001.png"), + CCSprite::createWithSpriteFrameName("GJ_checkOn_001.png"), + scale, + std::move(toggleCallback) + ); +} + +Toggler* Toggler::createWithSprites( + ZStringView offFileName, + ZStringView onFileName, + TogglerCallback toggleCallback +) { + return createWithSprites(offFileName, onFileName, 1.0f, std::move(toggleCallback)); +} + +Toggler* Toggler::createWithSprites( + ZStringView offFileName, + ZStringView onFileName, + float scale, + TogglerCallback toggleCallback +) { + return createScaled( + CCSprite::create(offFileName.c_str()), + CCSprite::create(onFileName.c_str()), + scale, + std::move(toggleCallback) + ); +} + +Toggler* Toggler::createWithSpriteFrameNames( + ZStringView offFrameName, + ZStringView onFrameName, + TogglerCallback toggleCallback +) { + return createWithSpriteFrameNames( + offFrameName, onFrameName, 1.0f, std::move(toggleCallback) + ); +} + +Toggler* Toggler::createWithSpriteFrameNames( + ZStringView offFrameName, + ZStringView onFrameName, + float scale, + TogglerCallback toggleCallback +) { + return createScaled( + CCSprite::createWithSpriteFrameName(offFrameName.c_str()), + CCSprite::createWithSpriteFrameName(onFrameName.c_str()), + scale, + std::move(toggleCallback) + ); +} + +bool Toggler::init( + CCNode* offNode, + CCNode* onNode, + TogglerCallback toggleCallback +) { + if (!offNode || !onNode) return false; + + if (!Button::init(nullptr)) return false; + + m_impl->m_offNode = offNode; + m_impl->m_onNode = onNode; + m_impl->m_toggleCallback = std::move(toggleCallback); + + addChild(offNode); + addChild(onNode); + + updateDisplay(); + + return true; +} + +bool Toggler::isToggled() const { + return m_impl->m_isToggled; +} + +void Toggler::setToggled(bool toggled, bool triggerCallback) { + if (m_impl->m_isToggled == toggled) return; + + m_impl->m_isToggled = toggled; + updateDisplay(); + + if (triggerCallback && m_impl->m_toggleCallback) { + m_impl->m_toggleCallback(this, toggled); + } +} + +void Toggler::toggle(bool triggerCallback) { + setToggled(!m_impl->m_isToggled, triggerCallback); +} + +CCNode* Toggler::getOffNode() const { + return m_impl->m_offNode; +} + +void Toggler::setOffNode(CCNode* node) { + if (!node || node == m_impl->m_offNode) return; + + node->setScale(m_impl->m_offNode->getScale()); + + m_impl->m_offNode->removeFromParentAndCleanup(true); + + m_impl->m_offNode = node; + addChild(node); + + updateDisplay(); +} + +CCNode* Toggler::getOnNode() const { + return m_impl->m_onNode; +} + +void Toggler::setOnNode(CCNode* node) { + if (!node || node == m_impl->m_onNode) return; + + node->setScale(m_impl->m_onNode->getScale()); + + m_impl->m_onNode->removeFromParentAndCleanup(true); + + m_impl->m_onNode = node; + addChild(node); + + updateDisplay(); +} + +void Toggler::setToggleCallback(TogglerCallback callback) { + m_impl->m_toggleCallback = std::move(callback); +} + +void Toggler::activate() { + if (!isEnabled()) return; + + Button::activate(); + toggle(true); +} + +void Toggler::updateDisplay() { + auto activeNode = m_impl->m_isToggled + ? m_impl->m_onNode + : m_impl->m_offNode; + + auto size = activeNode->getScaledContentSize(); + auto center = size * 0.5f; + + m_impl->m_offNode->setVisible(!m_impl->m_isToggled); + m_impl->m_onNode->setVisible(m_impl->m_isToggled); + + m_impl->m_offNode->setPosition(center); + m_impl->m_onNode->setPosition(center); + + setContentSize(size); +}