diff --git a/loader/include/Geode/cocos/base_nodes/CCNode.h b/loader/include/Geode/cocos/base_nodes/CCNode.h index 32e0d3c4..ce22271e 100644 --- a/loader/include/Geode/cocos/base_nodes/CCNode.h +++ b/loader/include/Geode/cocos/base_nodes/CCNode.h @@ -994,6 +994,18 @@ public: * @note Geode addition */ GEODE_DLL LayoutOptions* getLayoutOptions(); + /** + * Adds a child at an anchored position with an offset. The node is placed + * in its parent where the anchor specifies, and then the offset is used to + * relatively adjust the node's position + * @param child The child to add + * @param anchor Where the place the child relative to this node + * @param offset Where to place the child relative to the anchor + * @param useAnchorLayout If true, sets this node's layout to `AnchorLayout` + * if no other layout is already specified + * @note Geode addition + */ + GEODE_DLL void addChildAtPosition(CCNode* child, Anchor anchor, CCPoint const& offset = CCPointZero, bool useAnchorLayout = true); /** * Swap two children diff --git a/loader/include/Geode/cocos/base_nodes/Layout.hpp b/loader/include/Geode/cocos/base_nodes/Layout.hpp index faabe76a..44b5f8e4 100644 --- a/loader/include/Geode/cocos/base_nodes/Layout.hpp +++ b/loader/include/Geode/cocos/base_nodes/Layout.hpp @@ -371,6 +371,90 @@ public: static ColumnLayout* create(); }; +/** + * The relative position of a node to its parent in an AnchorLayout + */ +enum class Anchor { + Center, + TopLeft, + Top, + TopRight, + Right, + BottomRight, + Bottom, + BottomLeft, + Left, +}; + +/** + * Options for customizing a node's position in an AnchorLayout + */ +class GEODE_DLL AnchorLayoutOptions : public LayoutOptions { +protected: + Anchor m_anchor = Anchor::Center; + CCPoint m_offset = CCPointZero; + +public: + static AnchorLayoutOptions* create(); + + Anchor getAnchor() const; + CCPoint getOffset() const; + + AnchorLayoutOptions* setAnchor(Anchor anchor); + AnchorLayoutOptions* setOffset(CCPoint const& offset); +}; + +/** + * A layout for positioning nodes at specific positions relative to their + * parent's content size. See `Anchor` for available anchoring options. Useful + * for example for popups, where a popup using `AnchorLayout` can be + * automatically resized without needing to manually shuffle nodes around + */ +class GEODE_DLL AnchorLayout : public Layout { +public: + static AnchorLayout* create(); + + void apply(CCNode* on) override; + CCSize getSizeHint(CCNode* on) const override; + + /** + * Get a position according to anchoring rules, with the same algorithm as + * `AnchorLayout` uses to position its nodes + * @param in The node whose content size to use as a reference + * @param anchor The anchor position + * @param offset Offset from the anchor + * @returns A position in `in` for the anchored and offsetted location + */ + static CCPoint getAnchoredPosition(CCNode* in, Anchor anchor, CCPoint const& offset); +}; + +/** + * A layout for automatically copying the content size of a node to other nodes. + * Basically main use case is for FLAlertLayers (setting the size of the + * background and `m_buttonMenu` based on `m_mainLayer`) + */ +class CopySizeLayout : public cocos2d::AnchorLayout { +protected: + cocos2d::CCArray* m_targets; + +public: + static CopySizeLayout* create(); + virtual ~CopySizeLayout(); + + /** + * Add a target to be automatically resized. Any targets' layouts will + * also be updated when this layout is updated + */ + CopySizeLayout* add(cocos2d::CCNode* target); + /** + * Remove a target from being automatically resized + */ + CopySizeLayout* remove(cocos2d::CCNode* target); + + void apply(cocos2d::CCNode* in) override; + cocos2d::CCSize getSizeHint(cocos2d::CCNode* in) const override; +}; + #pragma warning(pop) NS_CC_END diff --git a/loader/include/Geode/ui/Popup.hpp b/loader/include/Geode/ui/Popup.hpp index 5dc513dc..bc0c0504 100644 --- a/loader/include/Geode/ui/Popup.hpp +++ b/loader/include/Geode/ui/Popup.hpp @@ -13,6 +13,7 @@ namespace geode { cocos2d::extension::CCScale9Sprite* m_bgSprite; cocos2d::CCLabelBMFont* m_title = nullptr; CCMenuItemSpriteExtra* m_closeBtn; + bool m_dynamic; ~Popup() override { cocos2d::CCTouchDispatcher::get()->unregisterForcePrio(this); @@ -22,10 +23,13 @@ namespace geode { cocos2d::CCTouchDispatcher::get()->addTargetedDelegate(this, -500, true); } - bool init( - float width, float height, InitArgs... args, char const* bg = "GJ_square01.png", - cocos2d::CCRect bgRect = { 0, 0, 80, 80 } + private: + bool initBase( + float width, float height, InitArgs... args, char const* bg, + cocos2d::CCRect bgRect, bool dynamic ) { + m_dynamic = dynamic; + auto winSize = cocos2d::CCDirector::get()->getWinSize(); m_size = cocos2d::CCSize { width, height }; @@ -44,6 +48,17 @@ namespace geode { m_buttonMenu->setZOrder(100); m_mainLayer->addChild(m_buttonMenu); + if (dynamic) { + m_mainLayer->ignoreAnchorPointForPosition(false); + m_mainLayer->setPosition(winSize / 2); + m_mainLayer->setContentSize(m_size); + m_mainLayer->setLayout( + cocos2d::CopySizeLayout::create() + ->add(m_buttonMenu) + ->add(m_bgSprite) + ); + } + this->setTouchEnabled(true); auto closeSpr = cocos2d::CCSprite::createWithSpriteFrameName("GJ_closeBtn_001.png"); @@ -52,8 +67,13 @@ namespace geode { m_closeBtn = CCMenuItemSpriteExtra::create( closeSpr, this, (cocos2d::SEL_MenuHandler)(&Popup::onClose) ); - m_closeBtn->setPosition(-m_size.width / 2 + 3.f, m_size.height / 2 - 3.f); - m_buttonMenu->addChild(m_closeBtn); + if (dynamic) { + m_buttonMenu->addChildAtPosition(m_closeBtn, cocos2d::Anchor::TopLeft, { 3.f, -3.f }); + } + else { + m_closeBtn->setPosition(-m_size.width / 2 + 3.f, m_size.height / 2 - 3.f); + m_buttonMenu->addChild(m_closeBtn); + } if (!setup(std::forward(args)...)) { return false; @@ -65,6 +85,27 @@ namespace geode { return true; } + protected: + [[deprecated("Use Popup::initAnchored instead, as it has more reasonable menu and layer content sizes")]] + bool init( + float width, float height, InitArgs... args, char const* bg = "GJ_square01.png", + cocos2d::CCRect bgRect = { 0, 0, 80, 80 } + ) { + return this->initBase(width, height, std::forward(args)..., bg, bgRect, false); + } + + /** + * Init with AnchorLayout and the content size of `m_buttonMenu` and + * `m_bgSprite` being tied to the size of `m_mainLayer` (rather than + * being the size of the window) + */ + bool initAnchored( + float width, float height, InitArgs... args, char const* bg = "GJ_square01.png", + cocos2d::CCRect bgRect = { 0, 0, 80, 80 } + ) { + return this->initBase(width, height, std::forward(args)..., bg, bgRect, true); + } + virtual bool setup(InitArgs... args) = 0; void keyDown(cocos2d::enumKeyCodes key) { @@ -86,13 +127,17 @@ namespace geode { ) { if (m_title) { m_title->setString(title.c_str()); - } else { - auto winSize = cocos2d::CCDirector::sharedDirector()->getWinSize(); + } + else { m_title = cocos2d::CCLabelBMFont::create(title.c_str(), font); - m_title->setPosition( - winSize.width / 2, winSize.height / 2 + m_size.height / 2 - offset - ); - m_mainLayer->addChild(m_title, 2); + m_title->setZOrder(2); + if (m_dynamic) { + m_mainLayer->addChildAtPosition(m_title, cocos2d::Anchor::Top, ccp(0, -offset)); + } + else { + auto winSize = cocos2d::CCDirector::get()->getWinSize(); + m_title->setPosition(winSize / 2 + ccp(0, m_size.height / 2 - offset)); + } } m_title->limitLabelWidth(m_size.width - 20.f, scale, .1f); } diff --git a/loader/src/cocos2d-ext/AnchorLayout.cpp b/loader/src/cocos2d-ext/AnchorLayout.cpp new file mode 100644 index 00000000..ddb953e7 --- /dev/null +++ b/loader/src/cocos2d-ext/AnchorLayout.cpp @@ -0,0 +1,82 @@ +#include +#include +#include +#include +#include +#include + +using namespace geode::prelude; + +AnchorLayoutOptions* AnchorLayoutOptions::create() { + return new AnchorLayoutOptions(); +} + +Anchor AnchorLayoutOptions::getAnchor() const { + return m_anchor; +} + +CCPoint AnchorLayoutOptions::getOffset() const { + return m_offset; +} + +AnchorLayoutOptions* AnchorLayoutOptions::setAnchor(Anchor anchor) { + m_anchor = anchor; + return this; +} + +AnchorLayoutOptions* AnchorLayoutOptions::setOffset(CCPoint const& offset) { + m_offset = offset; + return this; +} + +AnchorLayout* AnchorLayout::create() { + auto ret = new AnchorLayout(); + ret->autorelease(); + return ret; +} + +void AnchorLayout::apply(CCNode* on) { + on->ignoreAnchorPointForPosition(false); + for (auto node : CCArrayExt(this->getNodesToPosition(on))) { + if (auto opts = typeinfo_cast(node->getLayoutOptions())) { + auto pos = opts->getOffset(); + auto size = on->getContentSize(); + switch (opts->getAnchor()) { + default: + case Anchor::Center: pos += size / 2; break; + case Anchor::TopLeft: pos += ccp(0, size.height); break; + case Anchor::Top: pos += ccp(size.width / 2, size.height); break; + case Anchor::TopRight: pos += ccp(size.width, size.height); break; + case Anchor::Right: pos += ccp(size.width, size.height / 2); break; + case Anchor::BottomRight: pos += ccp(size.width, 0); break; + case Anchor::Bottom: pos += ccp(size.width / 2, 0); break; + case Anchor::BottomLeft: pos += ccp(0, 0); break; + case Anchor::Left: pos += ccp(0, size.height / 2); break; + } + node->ignoreAnchorPointForPosition(false); + node->setPosition(pos); + } + } +} + +CCSize AnchorLayout::getSizeHint(CCNode* on) const { + return on->getContentSize(); +} + +CCPoint AnchorLayout::getAnchoredPosition(CCNode* in, Anchor anchor, CCPoint const& offset) { + auto pos = offset; + auto size = in->getContentSize(); + switch (anchor) { + default: + case Anchor::Center: pos += size / 2; break; + case Anchor::TopLeft: pos += ccp(0, size.height); break; + case Anchor::Top: pos += ccp(size.width / 2, size.height); break; + case Anchor::TopRight: pos += ccp(size.width, size.height); break; + case Anchor::Right: pos += ccp(size.width, size.height / 2); break; + case Anchor::BottomRight: pos += ccp(size.width, 0); break; + case Anchor::Bottom: pos += ccp(size.width / 2, 0); break; + case Anchor::BottomLeft: pos += ccp(0, 0); break; + case Anchor::Left: pos += ccp(0, size.height / 2); break; + } + return pos; +} diff --git a/loader/src/cocos2d-ext/AxisLayout.cpp b/loader/src/cocos2d-ext/AxisLayout.cpp new file mode 100644 index 00000000..6c0cb22c --- /dev/null +++ b/loader/src/cocos2d-ext/AxisLayout.cpp @@ -0,0 +1,1002 @@ +#include +#include +#include +#include +#include +#include + +using namespace geode::prelude; + +// if 5k iterations isn't enough to fit the layout, then something is wrong +static size_t RECURSION_DEPTH_LIMIT = 5000; + +static AxisLayoutOptions const* axisOpts(CCNode* node) { + if (!node) return nullptr; + return typeinfo_cast(node->getLayoutOptions()); +} + +static bool isOptsBreakLine(AxisLayoutOptions const* opts) { + if (opts) { + return opts->getBreakLine(); + } + return false; +} + +static bool isOptsSameLine(AxisLayoutOptions const* opts) { + if (opts) { + return opts->getSameLine(); + } + return false; +} + +static int optsScalePrio(AxisLayoutOptions const* opts) { + if (opts) { + return opts->getScalePriority(); + } + return AXISLAYOUT_DEFAULT_PRIORITY; +} + +static float optsMinScale(AxisLayoutOptions const* opts) { + if (opts) { + return opts->getMinScale(); + } + return AXISLAYOUT_DEFAULT_MIN_SCALE; +} + +static float optsMaxScale(AxisLayoutOptions const* opts) { + if (opts) { + return opts->getMaxScale(); + } + return 1.f; +} + +static float optsRelScale(AxisLayoutOptions const* opts) { + if (opts) { + return opts->getRelativeScale(); + } + return 1.f; +} + +static float scaleByOpts(AxisLayoutOptions const* opts, float scale, int prio, bool squishMode) { + if (prio > optsScalePrio(opts)) { + return optsMaxScale(opts) * optsRelScale(opts); + } + // otherwise if it matches scale it down by the factor + else if (!squishMode && prio == optsScalePrio(opts)) { + auto trueScale = scale; + auto min = optsMinScale(opts); + auto max = optsMaxScale(opts); + if (trueScale < min) { + trueScale = min; + } + if (trueScale > max) { + trueScale = max; + } + return trueScale * optsRelScale(opts); + } + // otherwise it's been scaled down to minimum + else { + return optsMinScale(opts) * optsRelScale(opts); + } +} + +struct AxisLayout::Row : public CCObject { + float nextOverflowScaleDownFactor; + float nextOverflowSquishFactor; + float axisLength; + float crossLength; + float axisEndsLength; + + // all layout calculations happen within a single frame so no Ref needed + CCArray* nodes; + + // calculated values for scale, squish and prio to fit the nodes in this + // row when positioning + float scale; + float squish; + float prio; + + Row( + float scaleFactor, + float squishFactor, + float axisLength, + float crossLength, + float axisEndsLength, + CCArray* nodes, + float scale, + float squish, + float prio + ) : nextOverflowScaleDownFactor(scaleFactor), + nextOverflowSquishFactor(squishFactor), + axisLength(axisLength), + crossLength(crossLength), + axisEndsLength(axisEndsLength), + nodes(nodes), + scale(scale), + squish(squish), + prio(prio) + { + this->autorelease(); + } + + void accountSpacers(Axis axis, float availableLength, float crossLength) { + std::vector spacers; + for (auto& node : CCArrayExt(nodes)) { + if (auto spacer = typeinfo_cast(node)) { + spacers.push_back(spacer); + } + } + if (spacers.size()) { + auto unusedSpace = availableLength - this->axisLength; + size_t sum = 0; + for (auto& spacer : spacers) { + sum += spacer->getGrow(); + } + for (auto& spacer : spacers) { + auto size = unusedSpace * spacer->getGrow() / static_cast(sum); + if (axis == Axis::Row) { + spacer->setContentSize({ size, crossLength }); + } + else { + spacer->setContentSize({ crossLength, size }); + } + } + this->axisLength = availableLength; + } + } +}; + +struct AxisPosition { + float axisLength; + float axisAnchor; + float crossLength; + float crossAnchor; +}; + +static AxisPosition nodeAxis(CCNode* node, Axis axis, float scale) { + auto scaledSize = node->getScaledContentSize() * scale; + std::optional axisLength = std::nullopt; + if (auto opts = axisOpts(node)) { + axisLength = opts->getLength(); + } + // CCMenuItemToggler is a common quirky class + if (auto toggle = typeinfo_cast(node)) { + scaledSize = toggle->m_offButton->getScaledContentSize(); + } + auto anchor = node->getAnchorPoint(); + if (axis == Axis::Row) { + return AxisPosition { + .axisLength = axisLength.value_or(scaledSize.width), + .axisAnchor = anchor.x, + .crossLength = scaledSize.height, + .crossAnchor = anchor.y, + }; + } + else { + return AxisPosition { + .axisLength = axisLength.value_or(scaledSize.height), + .axisAnchor = anchor.y, + .crossLength = scaledSize.width, + .crossAnchor = anchor.x, + }; + } +} + +float AxisLayout::nextGap(AxisLayoutOptions const* now, AxisLayoutOptions const* next) const { + std::optional gap; + if (now) { + gap = now->getNextGap(); + } + if (next && (!gap || gap.value() < next->getPrevGap())) { + gap = next->getPrevGap(); + } + return gap.value_or(m_gap); +} + +bool AxisLayout::shouldAutoScale(AxisLayoutOptions const* opts) const { + if (opts) { + return opts->getAutoScale().value_or(m_autoScale); + } + else { + return m_autoScale; + } +} + +float AxisLayout::minScaleForPrio(CCArray* nodes, int prio) const { + float min = AXISLAYOUT_DEFAULT_MIN_SCALE; + bool first = true; + for (auto node : CCArrayExt(nodes)) { + auto scale = optsMinScale(axisOpts(node)); + if (first) { + min = scale; + first = false; + } + else if (scale < min) { + min = scale; + } + } + return min; +} + +float AxisLayout::maxScaleForPrio(CCArray* nodes, int prio) const { + float max = 1.f; + bool first = true; + for (auto node : CCArrayExt(nodes)) { + auto scale = optsMaxScale(axisOpts(node)); + if (first) { + max = scale; + first = false; + } + else if (scale > max) { + max = scale; + } + } + return max; +} + +AxisLayout::Row* AxisLayout::fitInRow( + CCNode* on, CCArray* nodes, + std::pair const& minMaxPrios, + bool doAutoScale, + float scale, float squish, int prio +) const { + float nextAxisScalableLength; + float nextAxisUnscalableLength; + float axisUnsquishedLength; + float axisLength; + float crossLength; + auto res = CCArray::create(); + + auto available = nodeAxis(on, m_axis, 1.f / on->getScale()); + + auto fit = [&](CCArray* nodes) { + nextAxisScalableLength = 0.f; + nextAxisUnscalableLength = 0.f; + axisUnsquishedLength = 0.f; + axisLength = 0.f; + crossLength = 0.f; + AxisLayoutOptions const* prev = nullptr; + size_t ix = 0; + for (auto& node : CCArrayExt(nodes)) { + auto opts = axisOpts(node); + if (this->shouldAutoScale(opts)) { + node->setScale(1.f); + } + auto nodeScale = scaleByOpts(opts, scale, prio, false); + auto pos = nodeAxis(node, m_axis, nodeScale * squish); + auto squishPos = nodeAxis(node, m_axis, scaleByOpts(opts, scale, prio, true)); + if (prio == optsScalePrio(opts)) { + nextAxisScalableLength += pos.axisLength; + } + else { + nextAxisUnscalableLength += pos.axisLength; + } + // if multiple rows are allowed and this row is full, time for the + // next row + // also force at least one object to be added to this row, because if + // it's too large for this row it's gonna be too large for all rows + if ( + m_growCrossAxis && ( + (nextAxisScalableLength + nextAxisUnscalableLength > available.axisLength) && + ix != 0 && !isOptsSameLine(opts) + ) + ) { + break; + } + if (nodes != res) { + res->addObject(node); + } + if (ix) { + auto gap = nextGap(prev, opts); + // if we've exhausted all priority scale options, scale gap too + if (prio == minMaxPrios.first) { + nextAxisScalableLength += gap * scale * squish; + axisLength += gap * scale * squish; + axisUnsquishedLength += gap * scale; + } + else { + nextAxisUnscalableLength += gap * squish; + axisLength += gap * squish; + axisUnsquishedLength += gap; + } + } + axisLength += pos.axisLength; + axisUnsquishedLength += squishPos.axisLength; + // squishing doesn't affect cross length, that's done separately + if (pos.crossLength / squish > crossLength) { + crossLength = pos.crossLength / squish; + } + prev = opts; + if (m_growCrossAxis && isOptsBreakLine(opts)) { + break; + } + ix++; + } + }; + + fit(nodes); + + // whoops! removing objects from a CCArray while iterating is totes potes UB + for (int i = 0; i < res->count(); i++) { + nodes->removeFirstObject(); + } + + // todo: make this calculation more smart to avoid so much unnecessary recursion + auto scaleDownFactor = scale - .002f; + auto squishFactor = available.axisLength / (axisUnsquishedLength + .01f) * squish; + + // calculate row scale, squish, and prio + int tries = 1000; + while (axisLength > available.axisLength) { + if (this->canTryScalingDown(res, prio, scale, scale - .002f, minMaxPrios)) { + scale -= .002f; + } + else { + squish = available.axisLength / axisUnsquishedLength; + } + fit(res); + // Avoid infinite loops + if (tries-- <= 0) { + break; + } + } + + // reverse row if needed + if (m_axisReverse) { + res->reverseObjects(); + } + + float axisEndsLength = 0.f; + if (res->count()) { + auto first = static_cast(res->firstObject()); + auto last = static_cast(res->lastObject()); + axisEndsLength = ( + first->getScaledContentSize().width * + scaleByOpts(axisOpts(first), scale, prio, false) / 2 + + last->getScaledContentSize().width * + scaleByOpts(axisOpts(last), scale, prio, false) / 2 + ); + } + + return new Row( + // how much should the nodes be scaled down to fit the next row + // the .01f is because floating point arithmetic is imprecise and you + // end up in a situation where it confidently tells you that + // 241 > 241 == true + scaleDownFactor, + // how much should the nodes be squished to fit the next item in this + // row + squishFactor, + axisLength, crossLength, axisEndsLength, + res, + scale, squish, prio + ); +} + +bool AxisLayout::canTryScalingDown( + CCArray* nodes, + int& prio, float& scale, + float crossScaleDownFactor, + std::pair const& minMaxPrios +) const { + bool attemptRescale = false; + auto minScaleForPrio = this->minScaleForPrio(nodes, prio); + if ( + // if the scale is less than the lowest min scale allowed, then + // trying to scale will have no effect and not help anywmore + crossScaleDownFactor < minScaleForPrio || + // if the scale down factor is really close to the same as before, + // then we've entered an infinite loop (float == float is unreliable) + (fabsf(crossScaleDownFactor - scale) < .001f) + ) { + // is there still some lower priority nodes we could try scaling? + if (prio > minMaxPrios.first) { + while (true) { + prio -= 1; + auto mscale = this->maxScaleForPrio(nodes, prio); + if (!mscale) { + continue; + } + scale = mscale; + break; + } + attemptRescale = true; + } + // otherwise set scale to min and squish + else { + scale = minScaleForPrio; + } + } + // otherwise scale as usual + else { + attemptRescale = true; + scale = crossScaleDownFactor; + } + return attemptRescale; +} + +void AxisLayout::tryFitLayout( + CCNode* on, CCArray* nodes, + std::pair const& minMaxPrios, + bool doAutoScale, + float scale, float squish, int prio, + size_t depth +) const { + // where do all of these magical calculations come from? + // idk i got tired of doing the math but they work so ¯\_(ツ)_/¯ + // like i genuinely have no clue fr why some of these work tho, + // i just threw in random equations and numbers until it worked + + auto rows = CCArray::create(); + float maxRowAxisLength = 0.f; + float totalRowCrossLength = 0.f; + float crossScaleDownFactor = 0.f; + float crossSquishFactor = 0.f; + + // make spacers have zero size so they don't affect spacing calculations + for (auto& node : CCArrayExt(nodes)) { + if (auto spacer = typeinfo_cast(node)) { + spacer->setContentSize(CCSizeZero); + } + } + + // fit everything into rows while possible + size_t ix = 0; + auto newNodes = nodes->shallowCopy(); + while (newNodes->count()) { + auto row = this->fitInRow( + on, newNodes, + minMaxPrios, doAutoScale, + scale, squish, prio + ); + rows->addObject(row); + if ( + row->nextOverflowScaleDownFactor > crossScaleDownFactor && + row->nextOverflowScaleDownFactor < scale + ) { + crossScaleDownFactor = row->nextOverflowScaleDownFactor; + } + if ( + row->nextOverflowSquishFactor > crossSquishFactor && + row->nextOverflowSquishFactor < squish + ) { + crossSquishFactor = row->nextOverflowSquishFactor; + } + totalRowCrossLength += row->crossLength; + if (ix) { + totalRowCrossLength += m_gap; + } + if (row->axisLength > maxRowAxisLength) { + maxRowAxisLength = row->axisLength; + } + ix++; + } + newNodes->release(); + + if (!rows->count()) { + return; + } + + auto available = nodeAxis(on, m_axis, 1.f / on->getScale()); + if (available.axisLength <= 0.f) { + return; + } + + // if cross axis overflow not allowed and it's overflowing, try to scale + // down layout if there are any nodes with auto-scale enabled (or + // auto-scale is enabled by default) + if ( + !m_allowCrossAxisOverflow && + doAutoScale && + totalRowCrossLength > available.crossLength && + depth < RECURSION_DEPTH_LIMIT + ) { + if (this->canTryScalingDown(nodes, prio, scale, crossScaleDownFactor, minMaxPrios)) { + rows->release(); + return this->tryFitLayout( + on, nodes, + minMaxPrios, doAutoScale, + scale, squish, prio, + depth + 1 + ); + } + } + + // if we're still overflowing, squeeze nodes closer together + if ( + !m_allowCrossAxisOverflow && + totalRowCrossLength > available.crossLength && + depth < RECURSION_DEPTH_LIMIT + ) { + // if squishing rows would take less squishing that squishing columns, + // then squish rows + if ( + !m_growCrossAxis || + totalRowCrossLength / available.crossLength < crossSquishFactor + ) { + rows->release(); + return this->tryFitLayout( + on, nodes, + minMaxPrios, doAutoScale, + scale, crossSquishFactor, prio, + depth + 1 + ); + } + } + + // if we're here, the nodes are ready to be positioned + + if (m_crossReverse) { + rows->reverseObjects(); + } + + // resize cross axis if needed + if (m_allowCrossAxisOverflow) { + available.crossLength = totalRowCrossLength; + if (m_axis == Axis::Row) { + on->setContentSize({ + available.axisLength, + totalRowCrossLength, + }); + } + else { + on->setContentSize({ + totalRowCrossLength, + available.axisLength, + }); + } + } + + float columnSquish = 1.f; + if (!m_allowCrossAxisOverflow && totalRowCrossLength > available.crossLength) { + columnSquish = available.crossLength / totalRowCrossLength; + totalRowCrossLength *= columnSquish; + } + + float rowsEndsLength = 0.f; + if (rows->count()) { + auto first = static_cast(rows->firstObject()); + auto last = static_cast(rows->lastObject()); + rowsEndsLength = first->crossLength / 2 + last->crossLength / 2; + } + + float rowCrossPos; + switch (m_crossAlignment) { + case AxisAlignment::Start: { + rowCrossPos = totalRowCrossLength - rowsEndsLength * 1.5f * scale * (1.f - columnSquish); + } break; + + case AxisAlignment::Even: { + totalRowCrossLength = available.crossLength; + rowCrossPos = totalRowCrossLength - rowsEndsLength * 1.5f * scale * (1.f - columnSquish); + } break; + + case AxisAlignment::Center: { + rowCrossPos = available.crossLength / 2 + totalRowCrossLength / 2 - + rowsEndsLength * 1.5f * scale * (1.f - columnSquish); + } break; + + case AxisAlignment::End: { + rowCrossPos = available.crossLength - + rowsEndsLength * 1.5f * scale * (1.f - columnSquish); + } break; + } + + float rowEvenSpace = available.crossLength / rows->count(); + + for (auto row : CCArrayExt(rows)) { + row->accountSpacers(m_axis, available.axisLength, available.crossLength); + + if (m_crossAlignment == AxisAlignment::Even) { + rowCrossPos -= rowEvenSpace / 2 + row->crossLength / 2; + } + else { + rowCrossPos -= row->crossLength * columnSquish; + } + + float rowAxisPos; + switch (m_axisAlignment) { + case AxisAlignment::Start: { + rowAxisPos = 0.f; + } break; + + case AxisAlignment::Even: { + rowAxisPos = 0.f; + } break; + + case AxisAlignment::Center: { + rowAxisPos = available.axisLength / 2 - row->axisLength / 2; + } break; + + case AxisAlignment::End: { + rowAxisPos = available.axisLength - row->axisLength; + } break; + } + + float evenSpace = available.axisLength / row->nodes->count(); + + size_t ix = 0; + AxisLayoutOptions const* prev = nullptr; + for (auto& node : CCArrayExt(row->nodes)) { + auto opts = axisOpts(node); + // rescale node if overflowing + if (this->shouldAutoScale(opts)) { + auto nodeScale = scaleByOpts(opts, row->scale, row->prio, false); + // CCMenuItemSpriteExtra is quirky af + if (auto btn = typeinfo_cast(node)) { + btn->m_baseScale = nodeScale; + } + node->setScale(nodeScale); + } + if (!ix) { + rowAxisPos += row->axisEndsLength * row->scale / 2 * (1.f - row->squish); + } + auto pos = nodeAxis(node, m_axis, row->squish); + float axisPos; + if (m_axisAlignment == AxisAlignment::Even) { + axisPos = rowAxisPos + evenSpace / 2 - pos.axisLength * (.5f - pos.axisAnchor); + rowAxisPos += evenSpace - + row->axisEndsLength * row->scale * (1.f - row->squish) * 1.f / nodes->count(); + } + else { + if (ix) { + if (row->prio == minMaxPrios.first) { + rowAxisPos += this->nextGap(prev, opts) * row->scale * row->squish; + } + else { + rowAxisPos += this->nextGap(prev, opts) * row->squish; + } + } + axisPos = rowAxisPos + pos.axisLength * pos.axisAnchor; + rowAxisPos += pos.axisLength - + row->axisEndsLength * row->scale * (1.f - row->squish) * 1.f / nodes->count(); + } + float crossOffset; + switch (m_crossLineAlignment) { + case AxisAlignment::Start: { + crossOffset = pos.crossLength * pos.crossAnchor; + } break; + + case AxisAlignment::Center: case AxisAlignment::Even: { + crossOffset = row->crossLength / 2 - pos.crossLength * (.5f - pos.crossAnchor); + } break; + + case AxisAlignment::End: { + crossOffset = row->crossLength - pos.crossLength * (1.f - pos.crossAnchor); + } break; + } + if (m_axis == Axis::Row) { + node->setPosition(axisPos, rowCrossPos + crossOffset); + } + else { + node->setPosition(rowCrossPos + crossOffset, axisPos); + } + prev = opts; + ix++; + } + + if (m_crossAlignment == AxisAlignment::Even) { + rowCrossPos -= rowEvenSpace / 2 - row->crossLength / 2 - + rowsEndsLength * 1.5f * row->scale * (1.f - columnSquish) * 1.f / rows->count(); + } + else { + rowCrossPos -= m_gap * columnSquish - + rowsEndsLength * 1.5f * row->scale * (1.f - columnSquish) * 1.f / rows->count(); + } + } +} + +void AxisLayout::apply(CCNode* on) { + auto nodes = getNodesToPosition(on); + + std::pair minMaxPrio; + bool doAutoScale = false; + + float totalLength = 0; + AxisLayoutOptions const* prev = nullptr; + + bool first = true; + for (auto node : CCArrayExt(nodes)) { + // Require all nodes not to have this stupid option enabled because it + // screws up all position calculations + node->ignoreAnchorPointForPosition(false); + int prio = 0; + auto opts = axisOpts(node); + if (opts) { + prio = opts->getScalePriority(); + // this does cause a recheck of m_autoScale every iteration but it + // should be pretty fast and this correctly handles the situation + // where auto-scale is enabled on the layout but explicitly + // disabled on all its children + if (opts->getAutoScale().value_or(m_autoScale)) { + doAutoScale = true; + } + } + else { + if (m_autoScale) { + doAutoScale = true; + } + } + if (first) { + minMaxPrio = { prio, prio }; + first = false; + } + else { + if (prio < minMaxPrio.first) { + minMaxPrio.first = prio; + } + if (prio > minMaxPrio.second) { + minMaxPrio.second = prio; + } + } + if (m_autoGrowAxisMinLength.has_value()) { + totalLength += nodeAxis(node, m_axis, 1.f).axisLength + this->nextGap(prev, opts); + prev = opts; + } + } + + if (m_autoGrowAxisMinLength.has_value()) { + if (totalLength < m_autoGrowAxisMinLength.value()) { + totalLength = m_autoGrowAxisMinLength.value(); + } + if (m_axis == Axis::Row) { + on->setContentSize({ totalLength, on->getContentSize().height }); + } + else { + on->setContentSize({ on->getContentSize().width, totalLength }); + } + } + + this->tryFitLayout( + on, nodes, + minMaxPrio, doAutoScale, + this->maxScaleForPrio(nodes, minMaxPrio.second), 1.f, minMaxPrio.second, + 0 + ); +} + +CCSize AxisLayout::getSizeHint(CCNode* on) const { + // Ideal is single row / column with no scaling + auto nodes = getNodesToPosition(on); + float length = 0.f; + float cross = 0.f; + for (auto& node : CCArrayExt(nodes)) { + auto axis = nodeAxis(node, m_axis, 1.f); + length += axis.axisLength; + if (axis.crossLength > cross) { + axis.crossLength = cross; + } + } + if (!m_allowCrossAxisOverflow) { + cross = nodeAxis(on, m_axis, 1.f).crossLength; + } + if (m_axis == Axis::Row) { + return { length, cross }; + } + else { + return { cross, length }; + } +} + +AxisLayout::AxisLayout(Axis axis) : m_axis(axis) {} + +Axis AxisLayout::getAxis() const { + return m_axis; +} + +AxisAlignment AxisLayout::getCrossAxisAlignment() const { + return m_crossAlignment; +} + +AxisAlignment AxisLayout::getCrossAxisLineAlignment() const { + return m_crossLineAlignment; +} + +AxisAlignment AxisLayout::getAxisAlignment() const { + return m_axisAlignment; +} + +float AxisLayout::getGap() const { + return m_gap; +} + +bool AxisLayout::getAxisReverse() const { + return m_axisReverse; +} + +bool AxisLayout::getCrossAxisReverse() const { + return m_crossReverse; +} + +bool AxisLayout::getAutoScale() const { + return m_autoScale; +} + +bool AxisLayout::getGrowCrossAxis() const { + return m_growCrossAxis; +} + +bool AxisLayout::getCrossAxisOverflow() const { + return m_allowCrossAxisOverflow; +} + +std::optional AxisLayout::getAutoGrowAxis() const { + return m_autoGrowAxisMinLength; +} + +AxisLayout* AxisLayout::setAxis(Axis axis) { + m_axis = axis; + return this; +} + +AxisLayout* AxisLayout::setCrossAxisAlignment(AxisAlignment align) { + m_crossAlignment = align; + return this; +} + +AxisLayout* AxisLayout::setCrossAxisLineAlignment(AxisAlignment align) { + m_crossLineAlignment = align; + return this; +} + +AxisLayout* AxisLayout::setAxisAlignment(AxisAlignment align) { + m_axisAlignment = align; + return this; +} + +AxisLayout* AxisLayout::setGap(float gap) { + m_gap = gap; + return this; +} + +AxisLayout* AxisLayout::setAxisReverse(bool reverse) { + m_axisReverse = reverse; + return this; +} + +AxisLayout* AxisLayout::setCrossAxisReverse(bool reverse) { + m_crossReverse = reverse; + return this; +} + +AxisLayout* AxisLayout::setCrossAxisOverflow(bool fit) { + m_allowCrossAxisOverflow = fit; + return this; +} + +AxisLayout* AxisLayout::setAutoScale(bool scale) { + m_autoScale = scale; + return this; +} + +AxisLayout* AxisLayout::setGrowCrossAxis(bool shrink) { + m_growCrossAxis = shrink; + return this; +} + +AxisLayout* AxisLayout::setAutoGrowAxis(std::optional allowAndMinLength) { + m_autoGrowAxisMinLength = allowAndMinLength; + return this; +} + +AxisLayout* AxisLayout::create(Axis axis) { + auto ret = new AxisLayout(axis); + ret->autorelease(); + return ret; +} + +// RowLayout + +RowLayout::RowLayout() : AxisLayout(Axis::Row) {} + +RowLayout* RowLayout::create() { + auto ret = new RowLayout(); + ret->autorelease(); + return ret; +} + +// ColumnLayout + +ColumnLayout::ColumnLayout() : AxisLayout(Axis::Column) {} + +ColumnLayout* ColumnLayout::create() { + auto ret = new ColumnLayout(); + ret->autorelease(); + return ret; +} + +// AxisLayoutOptions + +AxisLayoutOptions* AxisLayoutOptions::create() { + return new AxisLayoutOptions(); +} + +std::optional AxisLayoutOptions::getAutoScale() const { + return m_autoScale; +} + +float AxisLayoutOptions::getMaxScale() const { + return m_maxScale; +} + +float AxisLayoutOptions::getMinScale() const { + return m_minScale; +} + +float AxisLayoutOptions::getRelativeScale() const { + return m_relativeScale; +} + +std::optional AxisLayoutOptions::getLength() const { + return m_length; +} + +std::optional AxisLayoutOptions::getPrevGap() const { + return m_prevGap; +} + +std::optional AxisLayoutOptions::getNextGap() const { + return m_nextGap; +} + +bool AxisLayoutOptions::getBreakLine() const { + return m_breakLine; +} + +bool AxisLayoutOptions::getSameLine() const { + return m_sameLine; +} + +int AxisLayoutOptions::getScalePriority() const { + return m_scalePriority; +} + +AxisLayoutOptions* AxisLayoutOptions::setMaxScale(float scale) { + m_maxScale = scale; + return this; +} + +AxisLayoutOptions* AxisLayoutOptions::setMinScale(float scale) { + m_minScale = scale; + return this; +} + +AxisLayoutOptions* AxisLayoutOptions::setRelativeScale(float scale) { + m_relativeScale = scale; + return this; +} + +AxisLayoutOptions* AxisLayoutOptions::setAutoScale(std::optional enabled) { + m_autoScale = enabled; + return this; +} + +AxisLayoutOptions* AxisLayoutOptions::setLength(std::optional length) { + m_length = length; + return this; +} + +AxisLayoutOptions* AxisLayoutOptions::setPrevGap(std::optional gap) { + m_prevGap = gap; + return this; +} + +AxisLayoutOptions* AxisLayoutOptions::setNextGap(std::optional gap) { + m_nextGap = gap; + return this; +} + +AxisLayoutOptions* AxisLayoutOptions::setBreakLine(bool enable) { + m_breakLine = enable; + return this; +} + +AxisLayoutOptions* AxisLayoutOptions::setSameLine(bool enable) { + m_sameLine = enable; + return this; +} + +AxisLayoutOptions* AxisLayoutOptions::setScalePriority(int priority) { + m_scalePriority = priority; + return this; +} diff --git a/loader/src/cocos2d-ext/CopySizeLayout.cpp b/loader/src/cocos2d-ext/CopySizeLayout.cpp new file mode 100644 index 00000000..a8ed26b1 --- /dev/null +++ b/loader/src/cocos2d-ext/CopySizeLayout.cpp @@ -0,0 +1,46 @@ +#include +#include +#include +#include +#include +#include + +using namespace geode::prelude; + +CopySizeLayout* CopySizeLayout::create() { + auto ret = new CopySizeLayout(); + ret->m_targets = CCArray::create(); + ret->m_targets->retain(); + ret->autorelease(); + return ret; +} + +CopySizeLayout::~CopySizeLayout() { + m_targets->release(); +} + +CopySizeLayout* CopySizeLayout::add(CCNode* target) { + m_targets->addObject(target); + return this; +} + +CopySizeLayout* CopySizeLayout::remove(CCNode* target) { + m_targets->removeObject(target); + return this; +} + +void CopySizeLayout::apply(CCNode* in) { + AnchorLayout::apply(in); + for (auto& node : CCArrayExt(m_targets)) { + // Prevent accidental infinite loop + if (node == in) continue; + node->ignoreAnchorPointForPosition(false); + node->setContentSize(in->getContentSize()); + node->setPosition(in->getContentSize() / 2); + node->updateLayout(); + } +} + +CCSize CopySizeLayout::getSizeHint(CCNode* in) const { + return in->getContentSize(); +} diff --git a/loader/src/cocos2d-ext/Layout.cpp b/loader/src/cocos2d-ext/Layout.cpp index 4c3a60fd..b1a0a037 100644 --- a/loader/src/cocos2d-ext/Layout.cpp +++ b/loader/src/cocos2d-ext/Layout.cpp @@ -9,9 +9,6 @@ using namespace geode::prelude; #pragma warning(disable: 4273) -// if 5k iterations isn't enough to fit the layout, then something is wrong -static size_t RECURSION_DEPTH_LIMIT = 5000; - void CCNode::swapChildIndices(CCNode* first, CCNode* second) { m_pChildren->exchangeObject(first, second); std::swap(first->m_nZOrder, second->m_nZOrder); @@ -67,1052 +64,3 @@ void Layout::ignoreInvisibleChildren(bool ignore) { bool Layout::isIgnoreInvisibleChildren() const { return m_ignoreInvisibleChildren; } - -static AxisLayoutOptions const* axisOpts(CCNode* node) { - if (!node) return nullptr; - return typeinfo_cast(node->getLayoutOptions()); -} - -static bool isOptsBreakLine(AxisLayoutOptions const* opts) { - if (opts) { - return opts->getBreakLine(); - } - return false; -} - -static bool isOptsSameLine(AxisLayoutOptions const* opts) { - if (opts) { - return opts->getSameLine(); - } - return false; -} - -static int optsScalePrio(AxisLayoutOptions const* opts) { - if (opts) { - return opts->getScalePriority(); - } - return AXISLAYOUT_DEFAULT_PRIORITY; -} - -static float optsMinScale(AxisLayoutOptions const* opts) { - if (opts) { - return opts->getMinScale(); - } - return AXISLAYOUT_DEFAULT_MIN_SCALE; -} - -static float optsMaxScale(AxisLayoutOptions const* opts) { - if (opts) { - return opts->getMaxScale(); - } - return 1.f; -} - -static float optsRelScale(AxisLayoutOptions const* opts) { - if (opts) { - return opts->getRelativeScale(); - } - return 1.f; -} - -static float scaleByOpts(AxisLayoutOptions const* opts, float scale, int prio, bool squishMode) { - if (prio > optsScalePrio(opts)) { - return optsMaxScale(opts) * optsRelScale(opts); - } - // otherwise if it matches scale it down by the factor - else if (!squishMode && prio == optsScalePrio(opts)) { - auto trueScale = scale; - auto min = optsMinScale(opts); - auto max = optsMaxScale(opts); - if (trueScale < min) { - trueScale = min; - } - if (trueScale > max) { - trueScale = max; - } - return trueScale * optsRelScale(opts); - } - // otherwise it's been scaled down to minimum - else { - return optsMinScale(opts) * optsRelScale(opts); - } -} - -struct AxisLayout::Row : public CCObject { - float nextOverflowScaleDownFactor; - float nextOverflowSquishFactor; - float axisLength; - float crossLength; - float axisEndsLength; - - // all layout calculations happen within a single frame so no Ref needed - CCArray* nodes; - - // calculated values for scale, squish and prio to fit the nodes in this - // row when positioning - float scale; - float squish; - float prio; - - Row( - float scaleFactor, - float squishFactor, - float axisLength, - float crossLength, - float axisEndsLength, - CCArray* nodes, - float scale, - float squish, - float prio - ) : nextOverflowScaleDownFactor(scaleFactor), - nextOverflowSquishFactor(squishFactor), - axisLength(axisLength), - crossLength(crossLength), - axisEndsLength(axisEndsLength), - nodes(nodes), - scale(scale), - squish(squish), - prio(prio) - { - this->autorelease(); - } - - void accountSpacers(Axis axis, float availableLength, float crossLength) { - std::vector spacers; - for (auto& node : CCArrayExt(nodes)) { - if (auto spacer = typeinfo_cast(node)) { - spacers.push_back(spacer); - } - } - if (spacers.size()) { - auto unusedSpace = availableLength - this->axisLength; - size_t sum = 0; - for (auto& spacer : spacers) { - sum += spacer->getGrow(); - } - for (auto& spacer : spacers) { - auto size = unusedSpace * spacer->getGrow() / static_cast(sum); - if (axis == Axis::Row) { - spacer->setContentSize({ size, crossLength }); - } - else { - spacer->setContentSize({ crossLength, size }); - } - } - this->axisLength = availableLength; - } - } -}; - -struct AxisPosition { - float axisLength; - float axisAnchor; - float crossLength; - float crossAnchor; -}; - -static AxisPosition nodeAxis(CCNode* node, Axis axis, float scale) { - auto scaledSize = node->getScaledContentSize() * scale; - std::optional axisLength = std::nullopt; - if (auto opts = axisOpts(node)) { - axisLength = opts->getLength(); - } - // CCMenuItemToggler is a common quirky class - if (auto toggle = typeinfo_cast(node)) { - scaledSize = toggle->m_offButton->getScaledContentSize(); - } - auto anchor = node->getAnchorPoint(); - if (axis == Axis::Row) { - return AxisPosition { - .axisLength = axisLength.value_or(scaledSize.width), - .axisAnchor = anchor.x, - .crossLength = scaledSize.height, - .crossAnchor = anchor.y, - }; - } - else { - return AxisPosition { - .axisLength = axisLength.value_or(scaledSize.height), - .axisAnchor = anchor.y, - .crossLength = scaledSize.width, - .crossAnchor = anchor.x, - }; - } -} - -float AxisLayout::nextGap(AxisLayoutOptions const* now, AxisLayoutOptions const* next) const { - std::optional gap; - if (now) { - gap = now->getNextGap(); - } - if (next && (!gap || gap.value() < next->getPrevGap())) { - gap = next->getPrevGap(); - } - return gap.value_or(m_gap); -} - -bool AxisLayout::shouldAutoScale(AxisLayoutOptions const* opts) const { - if (opts) { - return opts->getAutoScale().value_or(m_autoScale); - } - else { - return m_autoScale; - } -} - -float AxisLayout::minScaleForPrio(CCArray* nodes, int prio) const { - float min = AXISLAYOUT_DEFAULT_MIN_SCALE; - bool first = true; - for (auto node : CCArrayExt(nodes)) { - auto scale = optsMinScale(axisOpts(node)); - if (first) { - min = scale; - first = false; - } - else if (scale < min) { - min = scale; - } - } - return min; -} - -float AxisLayout::maxScaleForPrio(CCArray* nodes, int prio) const { - float max = 1.f; - bool first = true; - for (auto node : CCArrayExt(nodes)) { - auto scale = optsMaxScale(axisOpts(node)); - if (first) { - max = scale; - first = false; - } - else if (scale > max) { - max = scale; - } - } - return max; -} - -AxisLayout::Row* AxisLayout::fitInRow( - CCNode* on, CCArray* nodes, - std::pair const& minMaxPrios, - bool doAutoScale, - float scale, float squish, int prio -) const { - float nextAxisScalableLength; - float nextAxisUnscalableLength; - float axisUnsquishedLength; - float axisLength; - float crossLength; - auto res = CCArray::create(); - - auto available = nodeAxis(on, m_axis, 1.f / on->getScale()); - - auto fit = [&](CCArray* nodes) { - nextAxisScalableLength = 0.f; - nextAxisUnscalableLength = 0.f; - axisUnsquishedLength = 0.f; - axisLength = 0.f; - crossLength = 0.f; - AxisLayoutOptions const* prev = nullptr; - size_t ix = 0; - for (auto& node : CCArrayExt(nodes)) { - auto opts = axisOpts(node); - if (this->shouldAutoScale(opts)) { - node->setScale(1.f); - } - auto nodeScale = scaleByOpts(opts, scale, prio, false); - auto pos = nodeAxis(node, m_axis, nodeScale * squish); - auto squishPos = nodeAxis(node, m_axis, scaleByOpts(opts, scale, prio, true)); - if (prio == optsScalePrio(opts)) { - nextAxisScalableLength += pos.axisLength; - } - else { - nextAxisUnscalableLength += pos.axisLength; - } - // if multiple rows are allowed and this row is full, time for the - // next row - // also force at least one object to be added to this row, because if - // it's too large for this row it's gonna be too large for all rows - if ( - m_growCrossAxis && ( - (nextAxisScalableLength + nextAxisUnscalableLength > available.axisLength) && - ix != 0 && !isOptsSameLine(opts) - ) - ) { - break; - } - if (nodes != res) { - res->addObject(node); - } - if (ix) { - auto gap = nextGap(prev, opts); - // if we've exhausted all priority scale options, scale gap too - if (prio == minMaxPrios.first) { - nextAxisScalableLength += gap * scale * squish; - axisLength += gap * scale * squish; - axisUnsquishedLength += gap * scale; - } - else { - nextAxisUnscalableLength += gap * squish; - axisLength += gap * squish; - axisUnsquishedLength += gap; - } - } - axisLength += pos.axisLength; - axisUnsquishedLength += squishPos.axisLength; - // squishing doesn't affect cross length, that's done separately - if (pos.crossLength / squish > crossLength) { - crossLength = pos.crossLength / squish; - } - prev = opts; - if (m_growCrossAxis && isOptsBreakLine(opts)) { - break; - } - ix++; - } - }; - - fit(nodes); - - // whoops! removing objects from a CCArray while iterating is totes potes UB - for (int i = 0; i < res->count(); i++) { - nodes->removeFirstObject(); - } - - // todo: make this calculation more smart to avoid so much unnecessary recursion - auto scaleDownFactor = scale - .002f; - auto squishFactor = available.axisLength / (axisUnsquishedLength + .01f) * squish; - - // calculate row scale, squish, and prio - int tries = 1000; - while (axisLength > available.axisLength) { - if (this->canTryScalingDown(res, prio, scale, scale - .002f, minMaxPrios)) { - scale -= .002f; - } - else { - squish = available.axisLength / axisUnsquishedLength; - } - fit(res); - // Avoid infinite loops - if (tries-- <= 0) { - break; - } - } - - // reverse row if needed - if (m_axisReverse) { - res->reverseObjects(); - } - - float axisEndsLength = 0.f; - if (res->count()) { - auto first = static_cast(res->firstObject()); - auto last = static_cast(res->lastObject()); - axisEndsLength = ( - first->getScaledContentSize().width * - scaleByOpts(axisOpts(first), scale, prio, false) / 2 + - last->getScaledContentSize().width * - scaleByOpts(axisOpts(last), scale, prio, false) / 2 - ); - } - - return new Row( - // how much should the nodes be scaled down to fit the next row - // the .01f is because floating point arithmetic is imprecise and you - // end up in a situation where it confidently tells you that - // 241 > 241 == true - scaleDownFactor, - // how much should the nodes be squished to fit the next item in this - // row - squishFactor, - axisLength, crossLength, axisEndsLength, - res, - scale, squish, prio - ); -} - -bool AxisLayout::canTryScalingDown( - CCArray* nodes, - int& prio, float& scale, - float crossScaleDownFactor, - std::pair const& minMaxPrios -) const { - bool attemptRescale = false; - auto minScaleForPrio = this->minScaleForPrio(nodes, prio); - if ( - // if the scale is less than the lowest min scale allowed, then - // trying to scale will have no effect and not help anywmore - crossScaleDownFactor < minScaleForPrio || - // if the scale down factor is really close to the same as before, - // then we've entered an infinite loop (float == float is unreliable) - (fabsf(crossScaleDownFactor - scale) < .001f) - ) { - // is there still some lower priority nodes we could try scaling? - if (prio > minMaxPrios.first) { - while (true) { - prio -= 1; - auto mscale = this->maxScaleForPrio(nodes, prio); - if (!mscale) { - continue; - } - scale = mscale; - break; - } - attemptRescale = true; - } - // otherwise set scale to min and squish - else { - scale = minScaleForPrio; - } - } - // otherwise scale as usual - else { - attemptRescale = true; - scale = crossScaleDownFactor; - } - return attemptRescale; -} - -void AxisLayout::tryFitLayout( - CCNode* on, CCArray* nodes, - std::pair const& minMaxPrios, - bool doAutoScale, - float scale, float squish, int prio, - size_t depth -) const { - // where do all of these magical calculations come from? - // idk i got tired of doing the math but they work so ¯\_(ツ)_/¯ - // like i genuinely have no clue fr why some of these work tho, - // i just threw in random equations and numbers until it worked - - auto rows = CCArray::create(); - float maxRowAxisLength = 0.f; - float totalRowCrossLength = 0.f; - float crossScaleDownFactor = 0.f; - float crossSquishFactor = 0.f; - - // make spacers have zero size so they don't affect spacing calculations - for (auto& node : CCArrayExt(nodes)) { - if (auto spacer = typeinfo_cast(node)) { - spacer->setContentSize(CCSizeZero); - } - } - - // fit everything into rows while possible - size_t ix = 0; - auto newNodes = nodes->shallowCopy(); - while (newNodes->count()) { - auto row = this->fitInRow( - on, newNodes, - minMaxPrios, doAutoScale, - scale, squish, prio - ); - rows->addObject(row); - if ( - row->nextOverflowScaleDownFactor > crossScaleDownFactor && - row->nextOverflowScaleDownFactor < scale - ) { - crossScaleDownFactor = row->nextOverflowScaleDownFactor; - } - if ( - row->nextOverflowSquishFactor > crossSquishFactor && - row->nextOverflowSquishFactor < squish - ) { - crossSquishFactor = row->nextOverflowSquishFactor; - } - totalRowCrossLength += row->crossLength; - if (ix) { - totalRowCrossLength += m_gap; - } - if (row->axisLength > maxRowAxisLength) { - maxRowAxisLength = row->axisLength; - } - ix++; - } - newNodes->release(); - - if (!rows->count()) { - return; - } - - auto available = nodeAxis(on, m_axis, 1.f / on->getScale()); - if (available.axisLength <= 0.f) { - return; - } - - // if cross axis overflow not allowed and it's overflowing, try to scale - // down layout if there are any nodes with auto-scale enabled (or - // auto-scale is enabled by default) - if ( - !m_allowCrossAxisOverflow && - doAutoScale && - totalRowCrossLength > available.crossLength && - depth < RECURSION_DEPTH_LIMIT - ) { - if (this->canTryScalingDown(nodes, prio, scale, crossScaleDownFactor, minMaxPrios)) { - rows->release(); - return this->tryFitLayout( - on, nodes, - minMaxPrios, doAutoScale, - scale, squish, prio, - depth + 1 - ); - } - } - - // if we're still overflowing, squeeze nodes closer together - if ( - !m_allowCrossAxisOverflow && - totalRowCrossLength > available.crossLength && - depth < RECURSION_DEPTH_LIMIT - ) { - // if squishing rows would take less squishing that squishing columns, - // then squish rows - if ( - !m_growCrossAxis || - totalRowCrossLength / available.crossLength < crossSquishFactor - ) { - rows->release(); - return this->tryFitLayout( - on, nodes, - minMaxPrios, doAutoScale, - scale, crossSquishFactor, prio, - depth + 1 - ); - } - } - - // if we're here, the nodes are ready to be positioned - - if (m_crossReverse) { - rows->reverseObjects(); - } - - // resize cross axis if needed - if (m_allowCrossAxisOverflow) { - available.crossLength = totalRowCrossLength; - if (m_axis == Axis::Row) { - on->setContentSize({ - available.axisLength, - totalRowCrossLength, - }); - } - else { - on->setContentSize({ - totalRowCrossLength, - available.axisLength, - }); - } - } - - float columnSquish = 1.f; - if (!m_allowCrossAxisOverflow && totalRowCrossLength > available.crossLength) { - columnSquish = available.crossLength / totalRowCrossLength; - totalRowCrossLength *= columnSquish; - } - - float rowsEndsLength = 0.f; - if (rows->count()) { - auto first = static_cast(rows->firstObject()); - auto last = static_cast(rows->lastObject()); - rowsEndsLength = first->crossLength / 2 + last->crossLength / 2; - } - - float rowCrossPos; - switch (m_crossAlignment) { - case AxisAlignment::Start: { - rowCrossPos = totalRowCrossLength - rowsEndsLength * 1.5f * scale * (1.f - columnSquish); - } break; - - case AxisAlignment::Even: { - totalRowCrossLength = available.crossLength; - rowCrossPos = totalRowCrossLength - rowsEndsLength * 1.5f * scale * (1.f - columnSquish); - } break; - - case AxisAlignment::Center: { - rowCrossPos = available.crossLength / 2 + totalRowCrossLength / 2 - - rowsEndsLength * 1.5f * scale * (1.f - columnSquish); - } break; - - case AxisAlignment::End: { - rowCrossPos = available.crossLength - - rowsEndsLength * 1.5f * scale * (1.f - columnSquish); - } break; - } - - float rowEvenSpace = available.crossLength / rows->count(); - - for (auto row : CCArrayExt(rows)) { - row->accountSpacers(m_axis, available.axisLength, available.crossLength); - - if (m_crossAlignment == AxisAlignment::Even) { - rowCrossPos -= rowEvenSpace / 2 + row->crossLength / 2; - } - else { - rowCrossPos -= row->crossLength * columnSquish; - } - - float rowAxisPos; - switch (m_axisAlignment) { - case AxisAlignment::Start: { - rowAxisPos = 0.f; - } break; - - case AxisAlignment::Even: { - rowAxisPos = 0.f; - } break; - - case AxisAlignment::Center: { - rowAxisPos = available.axisLength / 2 - row->axisLength / 2; - } break; - - case AxisAlignment::End: { - rowAxisPos = available.axisLength - row->axisLength; - } break; - } - - float evenSpace = available.axisLength / row->nodes->count(); - - size_t ix = 0; - AxisLayoutOptions const* prev = nullptr; - for (auto& node : CCArrayExt(row->nodes)) { - auto opts = axisOpts(node); - // rescale node if overflowing - if (this->shouldAutoScale(opts)) { - auto nodeScale = scaleByOpts(opts, row->scale, row->prio, false); - // CCMenuItemSpriteExtra is quirky af - if (auto btn = typeinfo_cast(node)) { - btn->m_baseScale = nodeScale; - } - node->setScale(nodeScale); - } - if (!ix) { - rowAxisPos += row->axisEndsLength * row->scale / 2 * (1.f - row->squish); - } - auto pos = nodeAxis(node, m_axis, row->squish); - float axisPos; - if (m_axisAlignment == AxisAlignment::Even) { - axisPos = rowAxisPos + evenSpace / 2 - pos.axisLength * (.5f - pos.axisAnchor); - rowAxisPos += evenSpace - - row->axisEndsLength * row->scale * (1.f - row->squish) * 1.f / nodes->count(); - } - else { - if (ix) { - if (row->prio == minMaxPrios.first) { - rowAxisPos += this->nextGap(prev, opts) * row->scale * row->squish; - } - else { - rowAxisPos += this->nextGap(prev, opts) * row->squish; - } - } - axisPos = rowAxisPos + pos.axisLength * pos.axisAnchor; - rowAxisPos += pos.axisLength - - row->axisEndsLength * row->scale * (1.f - row->squish) * 1.f / nodes->count(); - } - float crossOffset; - switch (m_crossLineAlignment) { - case AxisAlignment::Start: { - crossOffset = pos.crossLength * pos.crossAnchor; - } break; - - case AxisAlignment::Center: case AxisAlignment::Even: { - crossOffset = row->crossLength / 2 - pos.crossLength * (.5f - pos.crossAnchor); - } break; - - case AxisAlignment::End: { - crossOffset = row->crossLength - pos.crossLength * (1.f - pos.crossAnchor); - } break; - } - if (m_axis == Axis::Row) { - node->setPosition(axisPos, rowCrossPos + crossOffset); - } - else { - node->setPosition(rowCrossPos + crossOffset, axisPos); - } - prev = opts; - ix++; - } - - if (m_crossAlignment == AxisAlignment::Even) { - rowCrossPos -= rowEvenSpace / 2 - row->crossLength / 2 - - rowsEndsLength * 1.5f * row->scale * (1.f - columnSquish) * 1.f / rows->count(); - } - else { - rowCrossPos -= m_gap * columnSquish - - rowsEndsLength * 1.5f * row->scale * (1.f - columnSquish) * 1.f / rows->count(); - } - } -} - -void AxisLayout::apply(CCNode* on) { - auto nodes = getNodesToPosition(on); - - std::pair minMaxPrio; - bool doAutoScale = false; - - float totalLength = 0; - AxisLayoutOptions const* prev = nullptr; - - bool first = true; - for (auto node : CCArrayExt(nodes)) { - // Require all nodes not to have this stupid option enabled because it - // screws up all position calculations - node->ignoreAnchorPointForPosition(false); - int prio = 0; - auto opts = axisOpts(node); - if (opts) { - prio = opts->getScalePriority(); - // this does cause a recheck of m_autoScale every iteration but it - // should be pretty fast and this correctly handles the situation - // where auto-scale is enabled on the layout but explicitly - // disabled on all its children - if (opts->getAutoScale().value_or(m_autoScale)) { - doAutoScale = true; - } - } - else { - if (m_autoScale) { - doAutoScale = true; - } - } - if (first) { - minMaxPrio = { prio, prio }; - first = false; - } - else { - if (prio < minMaxPrio.first) { - minMaxPrio.first = prio; - } - if (prio > minMaxPrio.second) { - minMaxPrio.second = prio; - } - } - if (m_autoGrowAxisMinLength.has_value()) { - totalLength += nodeAxis(node, m_axis, 1.f).axisLength + this->nextGap(prev, opts); - prev = opts; - } - } - - if (m_autoGrowAxisMinLength.has_value()) { - if (totalLength < m_autoGrowAxisMinLength.value()) { - totalLength = m_autoGrowAxisMinLength.value(); - } - if (m_axis == Axis::Row) { - on->setContentSize({ totalLength, on->getContentSize().height }); - } - else { - on->setContentSize({ on->getContentSize().width, totalLength }); - } - } - - this->tryFitLayout( - on, nodes, - minMaxPrio, doAutoScale, - this->maxScaleForPrio(nodes, minMaxPrio.second), 1.f, minMaxPrio.second, - 0 - ); -} - -CCSize AxisLayout::getSizeHint(CCNode* on) const { - // Ideal is single row / column with no scaling - auto nodes = getNodesToPosition(on); - float length = 0.f; - float cross = 0.f; - for (auto& node : CCArrayExt(nodes)) { - auto axis = nodeAxis(node, m_axis, 1.f); - length += axis.axisLength; - if (axis.crossLength > cross) { - axis.crossLength = cross; - } - } - if (!m_allowCrossAxisOverflow) { - cross = nodeAxis(on, m_axis, 1.f).crossLength; - } - if (m_axis == Axis::Row) { - return { length, cross }; - } - else { - return { cross, length }; - } -} - -AxisLayout::AxisLayout(Axis axis) : m_axis(axis) {} - -Axis AxisLayout::getAxis() const { - return m_axis; -} - -AxisAlignment AxisLayout::getCrossAxisAlignment() const { - return m_crossAlignment; -} - -AxisAlignment AxisLayout::getCrossAxisLineAlignment() const { - return m_crossLineAlignment; -} - -AxisAlignment AxisLayout::getAxisAlignment() const { - return m_axisAlignment; -} - -float AxisLayout::getGap() const { - return m_gap; -} - -bool AxisLayout::getAxisReverse() const { - return m_axisReverse; -} - -bool AxisLayout::getCrossAxisReverse() const { - return m_crossReverse; -} - -bool AxisLayout::getAutoScale() const { - return m_autoScale; -} - -bool AxisLayout::getGrowCrossAxis() const { - return m_growCrossAxis; -} - -bool AxisLayout::getCrossAxisOverflow() const { - return m_allowCrossAxisOverflow; -} - -std::optional AxisLayout::getAutoGrowAxis() const { - return m_autoGrowAxisMinLength; -} - -AxisLayout* AxisLayout::setAxis(Axis axis) { - m_axis = axis; - return this; -} - -AxisLayout* AxisLayout::setCrossAxisAlignment(AxisAlignment align) { - m_crossAlignment = align; - return this; -} - -AxisLayout* AxisLayout::setCrossAxisLineAlignment(AxisAlignment align) { - m_crossLineAlignment = align; - return this; -} - -AxisLayout* AxisLayout::setAxisAlignment(AxisAlignment align) { - m_axisAlignment = align; - return this; -} - -AxisLayout* AxisLayout::setGap(float gap) { - m_gap = gap; - return this; -} - -AxisLayout* AxisLayout::setAxisReverse(bool reverse) { - m_axisReverse = reverse; - return this; -} - -AxisLayout* AxisLayout::setCrossAxisReverse(bool reverse) { - m_crossReverse = reverse; - return this; -} - -AxisLayout* AxisLayout::setCrossAxisOverflow(bool fit) { - m_allowCrossAxisOverflow = fit; - return this; -} - -AxisLayout* AxisLayout::setAutoScale(bool scale) { - m_autoScale = scale; - return this; -} - -AxisLayout* AxisLayout::setGrowCrossAxis(bool shrink) { - m_growCrossAxis = shrink; - return this; -} - -AxisLayout* AxisLayout::setAutoGrowAxis(std::optional allowAndMinLength) { - m_autoGrowAxisMinLength = allowAndMinLength; - return this; -} - -AxisLayout* AxisLayout::create(Axis axis) { - auto ret = new AxisLayout(axis); - ret->autorelease(); - return ret; -} - -// RowLayout - -RowLayout::RowLayout() : AxisLayout(Axis::Row) {} - -RowLayout* RowLayout::create() { - auto ret = new RowLayout(); - ret->autorelease(); - return ret; -} - -// ColumnLayout - -ColumnLayout::ColumnLayout() : AxisLayout(Axis::Column) {} - -ColumnLayout* ColumnLayout::create() { - auto ret = new ColumnLayout(); - ret->autorelease(); - return ret; -} - -// AxisLayoutOptions - -AxisLayoutOptions* AxisLayoutOptions::create() { - return new AxisLayoutOptions(); -} - -std::optional AxisLayoutOptions::getAutoScale() const { - return m_autoScale; -} - -float AxisLayoutOptions::getMaxScale() const { - return m_maxScale; -} - -float AxisLayoutOptions::getMinScale() const { - return m_minScale; -} - -float AxisLayoutOptions::getRelativeScale() const { - return m_relativeScale; -} - -std::optional AxisLayoutOptions::getLength() const { - return m_length; -} - -std::optional AxisLayoutOptions::getPrevGap() const { - return m_prevGap; -} - -std::optional AxisLayoutOptions::getNextGap() const { - return m_nextGap; -} - -bool AxisLayoutOptions::getBreakLine() const { - return m_breakLine; -} - -bool AxisLayoutOptions::getSameLine() const { - return m_sameLine; -} - -int AxisLayoutOptions::getScalePriority() const { - return m_scalePriority; -} - -AxisLayoutOptions* AxisLayoutOptions::setMaxScale(float scale) { - m_maxScale = scale; - return this; -} - -AxisLayoutOptions* AxisLayoutOptions::setMinScale(float scale) { - m_minScale = scale; - return this; -} - -AxisLayoutOptions* AxisLayoutOptions::setRelativeScale(float scale) { - m_relativeScale = scale; - return this; -} - -AxisLayoutOptions* AxisLayoutOptions::setAutoScale(std::optional enabled) { - m_autoScale = enabled; - return this; -} - -AxisLayoutOptions* AxisLayoutOptions::setLength(std::optional length) { - m_length = length; - return this; -} - -AxisLayoutOptions* AxisLayoutOptions::setPrevGap(std::optional gap) { - m_prevGap = gap; - return this; -} - -AxisLayoutOptions* AxisLayoutOptions::setNextGap(std::optional gap) { - m_nextGap = gap; - return this; -} - -AxisLayoutOptions* AxisLayoutOptions::setBreakLine(bool enable) { - m_breakLine = enable; - return this; -} - -AxisLayoutOptions* AxisLayoutOptions::setSameLine(bool enable) { - m_sameLine = enable; - return this; -} - -AxisLayoutOptions* AxisLayoutOptions::setScalePriority(int priority) { - m_scalePriority = priority; - return this; -} - -bool SpacerNode::init(size_t grow) { - if (!CCNode::init()) - return false; - - m_grow = grow; - - return true; -} - -SpacerNode* SpacerNode::create(size_t grow) { - auto ret = new SpacerNode; - if (ret && ret->init(grow)) { - ret->autorelease(); - return ret; - } - CC_SAFE_DELETE(ret); - return nullptr; -} - -void SpacerNode::setGrow(size_t grow) { - m_grow = grow; -} - -size_t SpacerNode::getGrow() const { - return m_grow; -} - -bool SpacerNodeChild::init(CCNode* child, size_t grow) { - if (!SpacerNode::init(grow)) - return false; - - if (child) { - this->addChild(child); - m_child = child; - } - - return true; -} - -SpacerNodeChild* SpacerNodeChild::create(CCNode* child, size_t grow) { - auto ret = new SpacerNodeChild; - if (ret && ret->init(child, grow)) { - ret->autorelease(); - return ret; - } - CC_SAFE_DELETE(ret); - return nullptr; -} - -void SpacerNodeChild::setContentSize(CCSize const& size) { - CCNode::setContentSize(size); - if (m_child) { - m_child->setPosition(CCPointZero); - m_child->setContentSize(size); - m_child->setAnchorPoint(CCPointZero); - } -} \ No newline at end of file diff --git a/loader/src/cocos2d-ext/SpacerNode.cpp b/loader/src/cocos2d-ext/SpacerNode.cpp new file mode 100644 index 00000000..2039d740 --- /dev/null +++ b/loader/src/cocos2d-ext/SpacerNode.cpp @@ -0,0 +1,66 @@ +#include +#include +#include +#include +#include +#include + +using namespace geode::prelude; + +bool SpacerNode::init(size_t grow) { + if (!CCNode::init()) + return false; + + m_grow = grow; + + return true; +} + +SpacerNode* SpacerNode::create(size_t grow) { + auto ret = new SpacerNode; + if (ret && ret->init(grow)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +void SpacerNode::setGrow(size_t grow) { + m_grow = grow; +} + +size_t SpacerNode::getGrow() const { + return m_grow; +} + +bool SpacerNodeChild::init(CCNode* child, size_t grow) { + if (!SpacerNode::init(grow)) + return false; + + if (child) { + this->addChild(child); + m_child = child; + } + + return true; +} + +SpacerNodeChild* SpacerNodeChild::create(CCNode* child, size_t grow) { + auto ret = new SpacerNodeChild; + if (ret && ret->init(child, grow)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +void SpacerNodeChild::setContentSize(CCSize const& size) { + CCNode::setContentSize(size); + if (m_child) { + m_child->setPosition(CCPointZero); + m_child->setContentSize(size); + m_child->setAnchorPoint(CCPointZero); + } +} diff --git a/loader/src/hooks/GeodeNodeMetadata.cpp b/loader/src/hooks/GeodeNodeMetadata.cpp index 5b27626a..9ff19e5b 100644 --- a/loader/src/hooks/GeodeNodeMetadata.cpp +++ b/loader/src/hooks/GeodeNodeMetadata.cpp @@ -237,4 +237,16 @@ size_t CCNode::getEventListenerCount() { GeodeNodeMetadata::set(this)->m_eventListeners.size(); } +void CCNode::addChildAtPosition(CCNode* child, Anchor anchor, CCPoint const& offset, bool useAnchorLayout) { + auto layout = this->getLayout(); + if (!layout && useAnchorLayout) { + this->setLayout(AnchorLayout::create()); + } + child->setPosition(AnchorLayout::getAnchoredPosition(this, anchor, offset)); + if (useAnchorLayout) { + child->setLayoutOptions(AnchorLayoutOptions::create()->setAnchor(anchor)->setOffset(offset)); + } + this->addChild(child); +} + #pragma warning(pop) diff --git a/loader/src/ui/internal/info/ModInfoPopup.cpp b/loader/src/ui/internal/info/ModInfoPopup.cpp index 7b1da9bf..9b41a92f 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.cpp +++ b/loader/src/ui/internal/info/ModInfoPopup.cpp @@ -37,16 +37,15 @@ bool ModInfoPopup::setup(ModMetadata const& metadata, ModListLayer* list) { constexpr float logoOffset = 10.f; auto topNode = CCNode::create(); - topNode->setContentSize({350.f, 80.f}); + topNode->setAnchorPoint({ .5f, .5f }); + topNode->setContentSize({ 350.f, 80.f }); topNode->setLayout( RowLayout::create() ->setAxisAlignment(AxisAlignment::Center) ->setAutoScale(false) ->setCrossAxisOverflow(true) ); - m_mainLayer->addChild(topNode); - topNode->setAnchorPoint({.5f, .5f}); - topNode->setPosition(winSize.width / 2, winSize.height / 2 + 115.f); + m_mainLayer->addChildAtPosition(topNode, Anchor::Top, ccp(0, -30)); auto logoSpr = this->createLogo({logoSize, logoSize}); topNode->addChild(logoSpr); @@ -88,18 +87,13 @@ bool ModInfoPopup::setup(ModMetadata const& metadata, ModListLayer* list) { (metadata.getDetails() ? metadata.getDetails().value() : "### No description provided."), { 350.f, 137.5f } ); - m_detailsArea->setPosition( - winSize.width / 2 - m_detailsArea->getScaledContentSize().width / 2, - winSize.height / 2 - m_detailsArea->getScaledContentSize().height / 2 - 20.f - ); - m_mainLayer->addChild(m_detailsArea); + m_mainLayer->addChildAtPosition(m_detailsArea, Anchor::Center, ccp(0, -20)); m_scrollbar = Scrollbar::create(m_detailsArea->getScrollLayer()); - m_scrollbar->setPosition( - winSize.width / 2 + m_detailsArea->getScaledContentSize().width / 2 + 20.f, - winSize.height / 2 - 20.f + m_mainLayer->addChildAtPosition( + m_scrollbar, Anchor::Center, + ccp(m_detailsArea->getScaledContentSize().width / 2 + 20.f, -20) ); - m_mainLayer->addChild(m_scrollbar); // changelog if (metadata.getChangelog()) { @@ -127,10 +121,10 @@ bool ModInfoPopup::setup(ModMetadata const& metadata, ModListLayer* list) { changelogBtnOnSpr->setScale(.65f); auto changelogBtn = CCMenuItemToggler::create( - changelogBtnOffSpr, changelogBtnOnSpr, this, menu_selector(ModInfoPopup::onChangelog) + changelogBtnOffSpr, changelogBtnOnSpr, + this, menu_selector(ModInfoPopup::onChangelog) ); - changelogBtn->setPosition(-LAYER_SIZE.width / 2 + 21.5f, .0f); - m_buttonMenu->addChild(changelogBtn); + m_buttonMenu->addChildAtPosition(changelogBtn, Anchor::Left, ccp(21.5f, 0)); } // mod metadata @@ -138,8 +132,7 @@ bool ModInfoPopup::setup(ModMetadata const& metadata, ModListLayer* list) { infoSpr->setScale(.85f); m_infoBtn = CCMenuItemSpriteExtra::create(infoSpr, this, menu_selector(ModInfoPopup::onInfo)); - m_infoBtn->setPosition(LAYER_SIZE.width / 2 - 25.f, LAYER_SIZE.height / 2 - 25.f); - m_buttonMenu->addChild(m_infoBtn); + m_buttonMenu->addChildAtPosition(m_infoBtn, Anchor::TopRight, ccp(-25, -25)); // repo button if (metadata.getRepository()) { @@ -148,8 +141,7 @@ bool ModInfoPopup::setup(ModMetadata const& metadata, ModListLayer* list) { this, menu_selector(ModInfoPopup::onRepository) ); - repoBtn->setPosition(LAYER_SIZE.width / 2 - 25.f, -LAYER_SIZE.height / 2 + 25.f); - m_buttonMenu->addChild(repoBtn); + m_buttonMenu->addChildAtPosition(repoBtn, Anchor::BottomRight, ccp(-25, 25)); } // support button @@ -159,8 +151,7 @@ bool ModInfoPopup::setup(ModMetadata const& metadata, ModListLayer* list) { this, menu_selector(ModInfoPopup::onSupport) ); - supportBtn->setPosition(LAYER_SIZE.width / 2 - 60.f, -LAYER_SIZE.height / 2 + 25.f); - m_buttonMenu->addChild(supportBtn); + m_buttonMenu->addChildAtPosition(supportBtn, Anchor::BottomRight, ccp(-60, 25)); } return true; @@ -204,40 +195,12 @@ void ModInfoPopup::onInfo(CCObject*) { } void ModInfoPopup::onChangelog(CCObject* sender) { - auto winSize = CCDirector::get()->getWinSize(); - - if (!m_changelogArea) { - m_changelogArea = MDTextArea::create(this->getMetadata().getChangelog().value(), { 350.f, 137.5f }); - m_changelogArea->setPosition( - -5000.f, winSize.height / 2 - m_changelogArea->getScaledContentSize().height / 2 - 20.f - ); - m_changelogArea->setVisible(false); - m_mainLayer->addChild(m_changelogArea); - } - auto toggle = static_cast(sender); - - m_detailsArea->setVisible(toggle->isToggled()); - // as it turns out, cocos2d is stupid and still passes touch - // events to invisible nodes - m_detailsArea->setPositionX( - toggle->isToggled() ? winSize.width / 2 - m_detailsArea->getScaledContentSize().width / 2 : - -5000.f - ); - - m_changelogArea->setVisible(!toggle->isToggled()); - // as it turns out, cocos2d is stupid and still passes touch - // events to invisible nodes - m_changelogArea->setPositionX( - !toggle->isToggled() ? winSize.width / 2 - m_changelogArea->getScaledContentSize().width / 2 : - -5000.f - ); - - m_scrollbar->setTarget( + m_detailsArea->setString(( toggle->isToggled() ? - m_detailsArea->getScrollLayer() : - m_changelogArea->getScrollLayer() - ); + this->getMetadata().getDetails().value() : + this->getMetadata().getChangelog().value() + ).c_str()); } void ModInfoPopup::setInstallStatus(std::optional const& progress) { @@ -349,14 +312,15 @@ LocalModInfoPopup::LocalModInfoPopup() ModInstallFilter("") ) {} - bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) { m_item = Index::get()->getMajorItem(mod->getMetadata().getID()); - if (m_item) + if (m_item) { m_installListener.setFilter(m_item->getMetadata().getID()); + } + m_mod = mod; - if (!ModInfoPopup::init(LAYER_SIZE.width, LAYER_SIZE.height, mod->getMetadata(), list)) return false; + if (!ModInfoPopup::initAnchored(LAYER_SIZE.width, LAYER_SIZE.height, mod->getMetadata(), list)) return false; auto winSize = CCDirector::sharedDirector()->getWinSize(); @@ -364,10 +328,10 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) { auto settingsSpr = CCSprite::createWithSpriteFrameName("GJ_optionsBtn_001.png"); settingsSpr->setScale(.65f); - auto settingsBtn = - CCMenuItemSpriteExtra::create(settingsSpr, this, menu_selector(LocalModInfoPopup::onSettings)); - settingsBtn->setPosition(-LAYER_SIZE.width / 2 + 25.f, -LAYER_SIZE.height / 2 + 25.f); - m_buttonMenu->addChild(settingsBtn); + auto settingsBtn = CCMenuItemSpriteExtra::create( + settingsSpr, this, menu_selector(LocalModInfoPopup::onSettings) + ); + m_buttonMenu->addChildAtPosition(settingsBtn, Anchor::BottomLeft, ccp(25, 25)); // Check if a config directory for the mod exists if (ghc::filesystem::exists(mod->getConfigDir(false))) { @@ -379,8 +343,7 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) { auto configBtn = CCMenuItemSpriteExtra::create( configSpr, this, menu_selector(LocalModInfoPopup::onOpenConfigDir) ); - configBtn->setPosition(-LAYER_SIZE.width / 2 + 65.f, -LAYER_SIZE.height / 2 + 25.f); - m_buttonMenu->addChild(configBtn); + m_buttonMenu->addChildAtPosition(configBtn, Anchor::BottomLeft, ccp(65, 25)); } if (!mod->hasSettings()) { @@ -397,9 +360,8 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) { auto enableBtn = CCMenuItemToggler::create( disableBtnSpr, enableBtnSpr, this, menu_selector(LocalModInfoPopup::onEnableMod) ); - enableBtn->setPosition(-155.f, 75.f); enableBtn->toggle(!mod->shouldLoad()); - m_buttonMenu->addChild(enableBtn); + m_buttonMenu->addChildAtPosition(enableBtn, Anchor::Center, ccp(-155, 75)); if (mod->isInternal()) { enableBtn->setTarget(this, menu_selector(LocalModInfoPopup::onDisablingNotSupported)); @@ -420,8 +382,7 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) { auto uninstallBtn = CCMenuItemSpriteExtra::create( uninstallBtnSpr, this, menu_selector(LocalModInfoPopup::onUninstall) ); - uninstallBtn->setPosition(-85.f, 75.f); - m_buttonMenu->addChild(uninstallBtn); + m_buttonMenu->addChildAtPosition(uninstallBtn, Anchor::Center, ccp(-85, 75)); // todo: show update button on loader that invokes the installer if (m_item && Index::get()->isUpdateAvailable(m_item)) { @@ -434,20 +395,21 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) { m_installBtnSpr->setScale(.6f); m_installBtn = CCMenuItemSpriteExtra::create(m_installBtnSpr, this, menu_selector(LocalModInfoPopup::onUpdate)); - m_installBtn->setPosition(-8.0f, 75.f); - m_buttonMenu->addChild(m_installBtn); + m_buttonMenu->addChildAtPosition(m_installBtn, Anchor::Center, ccp(-8, 75)); m_installStatus = DownloadStatusNode::create(); - m_installStatus->setPosition(winSize.width / 2 + 105.f, winSize.height / 2 + 75.f); m_installStatus->setVisible(false); - m_mainLayer->addChild(m_installStatus); + m_mainLayer->addChildAtPosition(m_installStatus, Anchor::Center, ccp(105, 75)); auto minorIndexItem = Index::get()->getItem( mod->getMetadata().getID(), ComparableVersionInfo(mod->getMetadata().getVersion(), VersionCompare::MoreEq) ); - // TODO: use column layout here? + auto availableContainer = CCNode::create(); + availableContainer->setLayout(ColumnLayout::create()->setGap(2)); + availableContainer->setAnchorPoint({ .0f, .5f }); + availableContainer->setContentSize({ 200.f, 45.f }); if (m_item->getMetadata().getVersion().getMajor() > minorIndexItem->getMetadata().getVersion().getMajor()) { // has major update @@ -458,8 +420,7 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) { m_latestVersionLabel->setScale(.35f); m_latestVersionLabel->setAnchorPoint({.0f, .5f}); m_latestVersionLabel->setColor({94, 219, 255}); - m_latestVersionLabel->setPosition(winSize.width / 2 + 35.f, winSize.height / 2 + 75.f); - m_mainLayer->addChild(m_latestVersionLabel); + availableContainer->addChild(m_latestVersionLabel); } if (minorIndexItem->getMetadata().getVersion() > mod->getMetadata().getVersion()) { @@ -471,20 +432,11 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) { m_minorVersionLabel->setScale(.35f); m_minorVersionLabel->setAnchorPoint({.0f, .5f}); m_minorVersionLabel->setColor({94, 219, 255}); - if (m_latestVersionLabel) { - m_latestVersionLabel->setPosition( - winSize.width / 2 + 35.f, winSize.height / 2 + 81.f - ); - m_minorVersionLabel->setPosition( - winSize.width / 2 + 35.f, winSize.height / 2 + 69.f - ); - } else { - m_minorVersionLabel->setPosition( - winSize.width / 2 + 35.f, winSize.height / 2 + 75.f - ); - } - m_mainLayer->addChild(m_minorVersionLabel); + availableContainer->addChild(m_minorVersionLabel); } + + availableContainer->updateLayout(); + m_mainLayer->addChildAtPosition(availableContainer, Anchor::Center, ccp(35, 75)); } } if (mod == Mod::get()) { @@ -494,11 +446,10 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) { "chatFont.fnt" ); label->setAlignment(kCCTextAlignmentRight); - label->setAnchorPoint(ccp(1, 0)); - label->setScale(0.6f); - label->setPosition(winSize.width - 3.f, 3.f); + label->setAnchorPoint({ .0f, .5f }); + label->setScale(.5f); label->setOpacity(89); - m_mainLayer->addChild(label); + m_mainLayer->addChildAtPosition(label, Anchor::BottomRight, ccp(5, 0)); } // issue report button @@ -511,8 +462,7 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) { auto issuesBtn = CCMenuItemSpriteExtra::create( issuesBtnSpr, this, menu_selector(LocalModInfoPopup::onIssues) ); - issuesBtn->setPosition(0.f, -LAYER_SIZE.height / 2 + 25.f); - m_buttonMenu->addChild(issuesBtn); + m_buttonMenu->addChildAtPosition(issuesBtn, Anchor::Bottom, ccp(0, 25)); } return true; @@ -685,7 +635,7 @@ bool IndexItemInfoPopup::init(IndexItemHandle item, ModListLayer* list) { auto winSize = CCDirector::sharedDirector()->getWinSize(); - if (!ModInfoPopup::init(LAYER_SIZE.width, LAYER_SIZE.height, item->getMetadata(), list)) return false; + if (!ModInfoPopup::initAnchored(LAYER_SIZE.width, LAYER_SIZE.height, item->getMetadata(), list)) return false; // bruh why is this here if we are allowing for browsing already installed mods // if (item->isInstalled()) return true; @@ -701,13 +651,11 @@ bool IndexItemInfoPopup::init(IndexItemHandle item, ModListLayer* list) { m_installBtn = CCMenuItemSpriteExtra::create( m_installBtnSpr, this, menu_selector(IndexItemInfoPopup::onInstall) ); - m_installBtn->setPosition(-143.0f, 75.f); - m_buttonMenu->addChild(m_installBtn); + m_buttonMenu->addChildAtPosition(m_installBtn, Anchor::Center, ccp(-143, 75)); m_installStatus = DownloadStatusNode::create(); - m_installStatus->setPosition(winSize.width / 2 - 25.f, winSize.height / 2 + 75.f); m_installStatus->setVisible(false); - m_mainLayer->addChild(m_installStatus); + m_mainLayer->addChildAtPosition(m_installStatus, Anchor::Center, ccp(-25, 75)); return true; } diff --git a/loader/src/ui/internal/info/ModInfoPopup.hpp b/loader/src/ui/internal/info/ModInfoPopup.hpp index 52c01a6e..029ceef6 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.hpp +++ b/loader/src/ui/internal/info/ModInfoPopup.hpp @@ -36,7 +36,6 @@ protected: CCLabelBMFont* m_latestVersionLabel = nullptr; CCLabelBMFont* m_minorVersionLabel = nullptr; MDTextArea* m_detailsArea; - MDTextArea* m_changelogArea = nullptr; Scrollbar* m_scrollbar; IndexItemHandle m_item; diff --git a/loader/src/ui/nodes/MDPopup.cpp b/loader/src/ui/nodes/MDPopup.cpp index 2f0929da..83f4e5b5 100644 --- a/loader/src/ui/nodes/MDPopup.cpp +++ b/loader/src/ui/nodes/MDPopup.cpp @@ -19,7 +19,7 @@ bool MDPopup::setup( m_size.height - 120.f, }; auto content = MDTextArea::create(info, contentSize); - content->setPosition(winSize / 2 - contentSize / 2); + content->setPosition(winSize / 2); m_mainLayer->addChild(content); auto btnSpr = ButtonSprite::create(btn1Text); diff --git a/loader/src/ui/nodes/MDTextArea.cpp b/loader/src/ui/nodes/MDTextArea.cpp index b650f7e3..f0dbd9c9 100644 --- a/loader/src/ui/nodes/MDTextArea.cpp +++ b/loader/src/ui/nodes/MDTextArea.cpp @@ -108,6 +108,9 @@ Result colorForIdentifier(std::string const& tag) { bool MDTextArea::init(std::string const& str, CCSize const& size) { if (!CCLayer::init()) return false; + this->ignoreAnchorPointForPosition(false); + this->setAnchorPoint({ .5f, .5f }); + m_text = str; m_size = size - CCSize { 15.f, 0.f }; this->setContentSize(m_size); @@ -726,7 +729,6 @@ void MDTextArea::updateLabel() { m_content->setPositionY(-2.5f); } - m_scrollLayer->moveToTop(); } diff --git a/loader/src/ui/nodes/Popup.cpp b/loader/src/ui/nodes/Popup.cpp index f244933e..c2c074bb 100644 --- a/loader/src/ui/nodes/Popup.cpp +++ b/loader/src/ui/nodes/Popup.cpp @@ -2,6 +2,71 @@ using namespace geode::prelude; +// static void fixChildPositions(CCNode* in, CCSize const& size) { +// auto winSize = CCDirector::get()->getWinSize(); +// auto offset = size / 2 - in->getContentSize() / 2; + +// for (auto node : CCArrayExt(in->getChildren())) { +// node->setPosition(node->getPosition() + offset); + +// if (node->isIgnoreAnchorPointForPosition()) { +// node->setPosition(node->getPosition() + node->getScaledContentSize() * node->getAnchorPoint()); +// node->ignoreAnchorPointForPosition(false); +// } + +// constexpr int LEFT = 0b0001; +// constexpr int RIGHT = 0b0010; +// constexpr int BOTTOM = 0b0100; +// constexpr int TOP = 0b1000; + +// int p = 0b0000; +// if (node->getPositionX() <= winSize.width / 2 - size.width * 0.25) { +// p |= LEFT; +// } +// else if (node->getPositionX() >= winSize.width / 2 + size.width * 0.25) { +// p |= RIGHT; +// } +// if (node->getPositionY() <= winSize.height / 2 - size.height * 0.25) { +// p |= BOTTOM; +// } +// else if (node->getPositionY() >= winSize.height / 2 + size.height * 0.25) { +// p |= TOP; +// } + +// Anchor anchor = Anchor::Center; +// switch (p) { +// case LEFT | BOTTOM: anchor = Anchor::BottomLeft; break; +// case LEFT | TOP: anchor = Anchor::TopLeft; break; +// case LEFT: anchor = Anchor::Left; break; +// case RIGHT | BOTTOM: anchor = Anchor::BottomRight; break; +// case RIGHT | TOP: anchor = Anchor::TopRight; break; +// case RIGHT: anchor = Anchor::Right; break; +// case TOP: anchor = Anchor::Top; break; +// case BOTTOM: anchor = Anchor::Bottom; break; +// } + +// auto anchorPos = AnchorLayout::getAnchoredPosition(in, anchor, ccp(0, 0)); +// node->setLayoutOptions( +// AnchorLayoutOptions::create() +// ->setAnchor(anchor) +// ->setOffset(node->getPosition() - anchorPos) +// ); +// } + +// in->ignoreAnchorPointForPosition(false); +// } + +// void geode::enableDynamicLayoutForPopup(FLAlertLayer* alert, CCNode* bg) { +// auto winSize = CCDirector::get()->getWinSize(); + +// auto size = bg->getContentSize(); + +// alert->m_mainLayer->ignoreAnchorPointForPosition(false); +// alert->m_mainLayer->setContentSize(size); +// alert->m_mainLayer->setPosition(winSize / 2); +// alert->m_mainLayer->setLayout(AutoPopupLayout::create(alert->m_buttonMenu, bg)); +// } + class QuickPopup : public FLAlertLayer, public FLAlertLayerProtocol { protected: MiniFunction m_selected;