diff --git a/loader/include/Geode/cocos/base_nodes/Layout.hpp b/loader/include/Geode/cocos/base_nodes/Layout.hpp index fbc5272b..8a31c25e 100644 --- a/loader/include/Geode/cocos/base_nodes/Layout.hpp +++ b/loader/include/Geode/cocos/base_nodes/Layout.hpp @@ -95,23 +95,24 @@ constexpr int AXISLAYOUT_DEFAULT_PRIORITY = 0; */ class GEODE_DLL AxisLayoutOptions : public LayoutOptions { protected: - std::optional<bool> m_autoScale = std::nullopt; - float m_maxScale = 1.f; - float m_minScale = AXISLAYOUT_DEFAULT_MIN_SCALE; - float m_relativeScale = 1.f; - std::optional<float> m_length = std::nullopt; - std::optional<float> m_nextGap = std::nullopt; - std::optional<float> m_prevGap = std::nullopt; - bool m_breakLine = false; - bool m_sameLine = false; - int m_scalePriority = AXISLAYOUT_DEFAULT_PRIORITY; + class Impl; + + std::unique_ptr<Impl> m_impl; + + AxisLayoutOptions(); public: static AxisLayoutOptions* create(); + virtual ~AxisLayoutOptions(); + std::optional<bool> getAutoScale() const; + // @note Use hasExplicitMaxScale to know if the default scale has been overwritten float getMaxScale() const; + // @note Use hasExplicitMinScale to know if the default scale has been overwritten float getMinScale() const; + bool hasExplicitMaxScale() const; + bool hasExplicitMinScale() const; float getRelativeScale() const; std::optional<float> getLength() const; std::optional<float> getPrevGap() const; @@ -119,19 +120,28 @@ public: bool getBreakLine() const; bool getSameLine() const; int getScalePriority() const; + std::optional<AxisAlignment> getCrossAxisAlignment() const; /** * Set the maximum scale this node can be if it's contained in an * auto-scaled layout. Default is 1 */ + [[deprecated("Use AxisLayoutOptions::setScaleLimits")]] AxisLayoutOptions* setMaxScale(float scale); /** * Set the minimum scale this node can be if it's contained in an * auto-scaled layout. Default is AXISLAYOUT_DEFAULT_MIN_SCALE */ + [[deprecated("Use AxisLayoutOptions::setScaleLimits")]] AxisLayoutOptions* setMinScale(float scale); + /** + * Set the limits to what the node can be scaled to. Passing `std::nullopt` + * uses the parent layout's default min / max scales + */ + AxisLayoutOptions* setScaleLimits(std::optional<float> min, std::optional<float> max); + /** * Set the relative scale of this node compared to other nodes if it's * contained in an auto-scaled layout. Default is 1 @@ -183,6 +193,11 @@ public: * each other with no gaps */ AxisLayoutOptions* setScalePriority(int priority); + + /** + * Override the cross axis alignment for this node in the layout + */ + AxisLayoutOptions* setCrossAxisAlignment(std::optional<AxisAlignment> alignment); }; /** @@ -214,43 +229,9 @@ public: */ class GEODE_DLL AxisLayout : public Layout { protected: - Axis m_axis; - AxisAlignment m_axisAlignment = AxisAlignment::Center; - AxisAlignment m_crossAlignment = AxisAlignment::Center; - AxisAlignment m_crossLineAlignment = AxisAlignment::Center; - float m_gap = 5.f; - bool m_autoScale = true; - bool m_axisReverse = false; - bool m_crossReverse = false; - bool m_allowCrossAxisOverflow = true; - bool m_growCrossAxis = false; - std::optional<float> m_autoGrowAxisMinLength; + class Impl; - struct Row; - - float minScaleForPrio(CCArray* nodes, int prio) const; - float maxScaleForPrio(CCArray* nodes, int prio) const; - bool shouldAutoScale(AxisLayoutOptions const* opts) const; - bool canTryScalingDown( - CCArray* nodes, - int& prio, float& scale, - float crossScaleDownFactor, - std::pair<int, int> const& minMaxPrios - ) const; - float nextGap(AxisLayoutOptions const* now, AxisLayoutOptions const* next) const; - Row* fitInRow( - CCNode* on, CCArray* nodes, - std::pair<int, int> const& minMaxPrios, - bool doAutoScale, - float scale, float squish, int prio - ) const; - void tryFitLayout( - CCNode* on, CCArray* nodes, - std::pair<int, int> const& minMaxPrios, - bool doAutoScale, - float scale, float squish, int prio, - size_t depth - ) const; + std::unique_ptr<Impl> m_impl; AxisLayout(Axis); @@ -267,6 +248,8 @@ public: */ static AxisLayout* create(Axis axis = Axis::Row); + virtual ~AxisLayout(); + void apply(CCNode* on) override; CCSize getSizeHint(CCNode* on) const override; @@ -281,6 +264,8 @@ public: bool getGrowCrossAxis() const; bool getCrossAxisOverflow() const; std::optional<float> getAutoGrowAxis() const; + float getDefaultMinScale() const; + float getDefaultMaxScale() const; AxisLayout* setAxis(Axis axis); /** @@ -333,6 +318,10 @@ public: * Useful for scrollable list layer contents */ AxisLayout* setAutoGrowAxis(std::optional<float> allowAndMinLength); + /** + * Set the default minimum/maximum scales for nodes in the layout + */ + AxisLayout* setDefaultScaleLimits(float min, float max); }; /** diff --git a/loader/src/cocos2d-ext/AxisLayout.cpp b/loader/src/cocos2d-ext/AxisLayout.cpp index 36ff6821..262df0f9 100644 --- a/loader/src/cocos2d-ext/AxisLayout.cpp +++ b/loader/src/cocos2d-ext/AxisLayout.cpp @@ -36,18 +36,18 @@ static int optsScalePrio(AxisLayoutOptions const* opts) { return AXISLAYOUT_DEFAULT_PRIORITY; } -static float optsMinScale(AxisLayoutOptions const* opts) { - if (opts) { +static float optsMinScale(AxisLayoutOptions const* opts, float defaultMinScale) { + if (opts && opts->hasExplicitMinScale()) { return opts->getMinScale(); } - return AXISLAYOUT_DEFAULT_MIN_SCALE; + return defaultMinScale; } -static float optsMaxScale(AxisLayoutOptions const* opts) { - if (opts) { +static float optsMaxScale(AxisLayoutOptions const* opts, float defaultMaxScale) { + if (opts && opts->hasExplicitMaxScale()) { return opts->getMaxScale(); } - return 1.f; + return defaultMaxScale; } static float optsRelScale(AxisLayoutOptions const* opts) { @@ -57,15 +57,19 @@ static float optsRelScale(AxisLayoutOptions const* opts) { return 1.f; } -static float scaleByOpts(AxisLayoutOptions const* opts, float scale, int prio, bool squishMode) { +static float scaleByOpts( + AxisLayoutOptions const* opts, + float scale, int prio, bool squishMode, + float defaultMinScale, float defaultMaxScale +) { if (prio > optsScalePrio(opts)) { - return optsMaxScale(opts) * optsRelScale(opts); + return optsMaxScale(opts, defaultMaxScale) * 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); + auto min = optsMinScale(opts, defaultMinScale); + auto max = optsMaxScale(opts, defaultMaxScale); if (trueScale < min) { trueScale = min; } @@ -76,75 +80,16 @@ static float scaleByOpts(AxisLayoutOptions const* opts, float scale, int prio, b } // otherwise it's been scaled down to minimum else { - return optsMinScale(opts) * optsRelScale(opts); + return optsMinScale(opts, defaultMinScale) * 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(); +static AxisAlignment optsCrossAxisAlign(AxisLayoutOptions const* opts, AxisAlignment def) { + if (opts && opts->getCrossAxisAlignment()) { + return *opts->getCrossAxisAlignment(); } - - void accountSpacers(Axis axis, float availableLength, float crossLength) { - std::vector<SpacerNode*> spacers; - for (auto& node : CCArrayExt<CCNode*>(nodes)) { - if (auto spacer = typeinfo_cast<SpacerNode*>(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<float>(sum); - if (axis == Axis::Row) { - spacer->setContentSize({ size, crossLength }); - } - else { - spacer->setContentSize({ crossLength, size }); - } - } - this->axisLength = availableLength; - } - } -}; + return def; +} struct AxisPosition { float axisLength; @@ -160,9 +105,9 @@ static AxisPosition nodeAxis(CCNode* node, Axis axis, float scale) { axisLength = opts->getLength(); } // CCMenuItemToggler is a common quirky class - if (auto toggle = typeinfo_cast<CCMenuItemToggler*>(node)) { - scaledSize = toggle->m_offButton->getScaledContentSize(); - } + // if (auto toggle = typeinfo_cast<CCMenuItemToggler*>(node)) { + // scaledSize = toggle->m_offButton->getScaledContentSize(); + // } auto anchor = node->getAnchorPoint(); if (axis == Axis::Row) { return AxisPosition { @@ -182,509 +127,592 @@ static AxisPosition nodeAxis(CCNode* node, Axis axis, float scale) { } } -float AxisLayout::nextGap(AxisLayoutOptions const* now, AxisLayoutOptions const* next) const { - std::optional<float> gap; - if (now) { - gap = now->getNextGap(); - } - if (next && (!gap || gap.value() < next->getPrevGap())) { - gap = next->getPrevGap(); - } - return gap.value_or(m_gap); -} +class AxisLayout::Impl { +public: + Axis m_axis; + AxisAlignment m_axisAlignment = AxisAlignment::Center; + AxisAlignment m_crossAlignment = AxisAlignment::Center; + AxisAlignment m_crossLineAlignment = AxisAlignment::Center; + float m_gap = 5.f; + bool m_autoScale = true; + bool m_axisReverse = false; + bool m_crossReverse = false; + bool m_allowCrossAxisOverflow = true; + bool m_growCrossAxis = false; + std::optional<float> m_autoGrowAxisMinLength; + std::pair<float, float> m_defaultScaleLimits = { AXISLAYOUT_DEFAULT_MIN_SCALE, 1 }; -bool AxisLayout::shouldAutoScale(AxisLayoutOptions const* opts) const { - if (opts) { - return opts->getAutoScale().value_or(m_autoScale); - } - else { - return m_autoScale; - } -} + struct Row : public CCObject { + float nextOverflowScaleDownFactor; + float nextOverflowSquishFactor; + float axisLength; + float crossLength; + float axisEndsLength; -float AxisLayout::minScaleForPrio(CCArray* nodes, int prio) const { - float min = AXISLAYOUT_DEFAULT_MIN_SCALE; - bool first = true; - for (auto node : CCArrayExt<CCNode*>(nodes)) { - auto scale = optsMinScale(axisOpts(node)); - if (first) { - min = scale; - first = false; + // 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(); } - 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<CCNode*>(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<int, int> 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<CCNode*>(nodes)) { - auto opts = axisOpts(node); - if (this->shouldAutoScale(opts)) { - node->setScale(1.f); + void accountSpacers(Axis axis, float availableLength, float crossLength) { + std::vector<SpacerNode*> spacers; + for (auto& node : CCArrayExt<CCNode*>(nodes)) { + if (auto spacer = typeinfo_cast<SpacerNode*>(node)) { + spacers.push_back(spacer); + } } - 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; + 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<float>(sum); + if (axis == Axis::Row) { + spacer->setContentSize({ size, crossLength }); + } + else { + spacer->setContentSize({ crossLength, size }); + } + } + this->axisLength = availableLength; } + } + }; + + float minScaleForPrio(CCArray* nodes, int prio) const { + float min = m_defaultScaleLimits.first; + bool first = true; + for (auto node : CCArrayExt<CCNode*>(nodes)) { + auto scale = optsMinScale(axisOpts(node), m_defaultScaleLimits.first); + if (first) { + min = scale; + first = false; + } + else if (scale < min) { + min = scale; + } + } + return min; + } + + float maxScaleForPrio(CCArray* nodes, int prio) const { + float max = m_defaultScaleLimits.second; + bool first = true; + for (auto node : CCArrayExt<CCNode*>(nodes)) { + auto scale = optsMaxScale(axisOpts(node), m_defaultScaleLimits.second); + if (first) { + max = scale; + first = false; + } + else if (scale > max) { + max = scale; + } + } + return max; + } + + bool shouldAutoScale(AxisLayoutOptions const* opts) const { + if (opts) { + return opts->getAutoScale().value_or(m_autoScale); + } + else { + return m_autoScale; + } + } + + bool canTryScalingDown( + CCArray* nodes, + int& prio, float& scale, + float crossScaleDownFactor, + std::pair<int, int> 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 { - nextAxisUnscalableLength += pos.axisLength; + scale = minScaleForPrio; } - // 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; + } + // otherwise scale as usual + else { + attemptRescale = true; + scale = crossScaleDownFactor; + } + return attemptRescale; + } + + float nextGap(AxisLayoutOptions const* now, AxisLayoutOptions const* next) const { + std::optional<float> gap; + if (now) { + gap = now->getNextGap(); + } + if (next && (!gap || gap.value() < next->getPrevGap())) { + gap = next->getPrevGap(); + } + return gap.value_or(m_gap); + } + + Row* fitInRow( + CCNode* on, CCArray* nodes, + std::pair<int, int> 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<CCNode*>(nodes)) { + auto opts = axisOpts(node); + if (this->shouldAutoScale(opts)) { + node->setScale(1.f); + } + auto nodeScale = scaleByOpts(opts, scale, prio, false, m_defaultScaleLimits.first, m_defaultScaleLimits.second); + auto pos = nodeAxis(node, m_axis, nodeScale * squish); + auto squishPos = nodeAxis(node, m_axis, scaleByOpts(opts, scale, prio, true, m_defaultScaleLimits.first, m_defaultScaleLimits.second)); + if (prio == optsScalePrio(opts)) { + nextAxisScalableLength += pos.axisLength; } else { - nextAxisUnscalableLength += gap * squish; - axisLength += gap * squish; - axisUnsquishedLength += gap; + 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++; } - 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; + }; + + 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; } - prev = opts; - if (m_growCrossAxis && isOptsBreakLine(opts)) { + 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<CCNode*>(res->firstObject()); + auto last = static_cast<CCNode*>(res->lastObject()); + axisEndsLength = ( + first->getScaledContentSize().width * + scaleByOpts(axisOpts(first), scale, prio, false, m_defaultScaleLimits.first, m_defaultScaleLimits.second) / 2 + + last->getScaledContentSize().width * + scaleByOpts(axisOpts(last), scale, prio, false, m_defaultScaleLimits.first, m_defaultScaleLimits.second) / 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 + ); + } + + void tryFitLayout( + CCNode* on, CCArray* nodes, + std::pair<int, int> 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<CCNode*>(nodes)) { + if (auto spacer = typeinfo_cast<SpacerNode*>(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(); - 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; + if (!rows->count()) { + return; } - else { - squish = available.axisLength / axisUnsquishedLength; + + auto available = nodeAxis(on, m_axis, 1.f / on->getScale()); + if (available.axisLength <= 0.f) { + return; } - 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<CCNode*>(res->firstObject()); - auto last = static_cast<CCNode*>(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<int, int> 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; + // 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 + ); } - 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<int, int> 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<CCNode*>(nodes)) { - if (auto spacer = typeinfo_cast<SpacerNode*>(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 we're still overflowing, squeeze nodes closer together if ( - row->nextOverflowScaleDownFactor > crossScaleDownFactor && - row->nextOverflowScaleDownFactor < scale + !m_allowCrossAxisOverflow && + totalRowCrossLength > available.crossLength && + depth < RECURSION_DEPTH_LIMIT ) { - 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<Row*>(rows->firstObject()); - auto last = static_cast<Row*>(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<Row*>(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; + // 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 + ); + } } - float rowAxisPos; - switch (m_axisAlignment) { - case AxisAlignment::Start: { - rowAxisPos = 0.f; + // 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<Row*>(rows->firstObject()); + auto last = static_cast<Row*>(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: { - rowAxisPos = 0.f; + case AxisAlignment::Even: { + totalRowCrossLength = available.crossLength; + rowCrossPos = totalRowCrossLength - rowsEndsLength * 1.5f * scale * (1.f - columnSquish); } break; case AxisAlignment::Center: { - rowAxisPos = available.axisLength / 2 - row->axisLength / 2; + rowCrossPos = available.crossLength / 2 + totalRowCrossLength / 2 - + rowsEndsLength * 1.5f * scale * (1.f - columnSquish); } break; case AxisAlignment::End: { - rowAxisPos = available.axisLength - row->axisLength; + rowCrossPos = available.crossLength - + rowsEndsLength * 1.5f * scale * (1.f - columnSquish); } break; } - float evenSpace = available.axisLength / row->nodes->count(); + float rowEvenSpace = available.crossLength / rows->count(); - size_t ix = 0; - AxisLayoutOptions const* prev = nullptr; - for (auto& node : CCArrayExt<CCNode*>(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<CCMenuItemSpriteExtra*>(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(); + for (auto row : CCArrayExt<Row*>(rows)) { + row->accountSpacers(m_axis, available.axisLength, available.crossLength); + + if (m_crossAlignment == AxisAlignment::Even) { + rowCrossPos -= rowEvenSpace / 2 + row->crossLength / 2; } 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(); + rowCrossPos -= row->crossLength * columnSquish; } - float crossOffset; - switch (m_crossLineAlignment) { - case AxisAlignment::Start: { - crossOffset = pos.crossLength * pos.crossAnchor; + + float rowAxisPos; + switch (m_axisAlignment) { + case AxisAlignment::Start: { + rowAxisPos = 0.f; } break; - case AxisAlignment::Center: case AxisAlignment::Even: { - crossOffset = row->crossLength / 2 - pos.crossLength * (.5f - pos.crossAnchor); + case AxisAlignment::Even: { + rowAxisPos = 0.f; + } break; + + case AxisAlignment::Center: { + rowAxisPos = available.axisLength / 2 - row->axisLength / 2; } break; case AxisAlignment::End: { - crossOffset = row->crossLength - pos.crossLength * (1.f - pos.crossAnchor); + rowAxisPos = available.axisLength - row->axisLength; } break; } - if (m_axis == Axis::Row) { - node->setPosition(axisPos, rowCrossPos + crossOffset); + + float evenSpace = available.axisLength / row->nodes->count(); + + size_t ix = 0; + AxisLayoutOptions const* prev = nullptr; + for (auto& node : CCArrayExt<CCNode*>(row->nodes)) { + auto opts = axisOpts(node); + // rescale node if overflowing + // do not scale spacers since that screws up their content size + if (this->shouldAutoScale(opts) && !typeinfo_cast<SpacerNode*>(node)) { + auto nodeScale = scaleByOpts(opts, row->scale, row->prio, false, m_defaultScaleLimits.first, m_defaultScaleLimits.second); + // CCMenuItemSpriteExtra is quirky af + if (auto btn = typeinfo_cast<CCMenuItemSpriteExtra*>(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 (optsCrossAxisAlign(opts, 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 { - node->setPosition(rowCrossPos + crossOffset, axisPos); + rowCrossPos -= m_gap * columnSquish - + rowsEndsLength * 1.5f * row->scale * (1.f - columnSquish) * 1.f / rows->count(); } - 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); @@ -708,12 +736,12 @@ void AxisLayout::apply(CCNode* on) { // 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)) { + if (opts->getAutoScale().value_or(m_impl->m_autoScale)) { doAutoScale = true; } } else { - if (m_autoScale) { + if (m_impl->m_autoScale) { doAutoScale = true; } } @@ -729,17 +757,17 @@ void AxisLayout::apply(CCNode* on) { minMaxPrio.second = prio; } } - if (m_autoGrowAxisMinLength.has_value()) { - totalLength += nodeAxis(node, m_axis, 1.f).axisLength + this->nextGap(prev, opts); + if (m_impl->m_autoGrowAxisMinLength.has_value()) { + totalLength += nodeAxis(node, m_impl->m_axis, 1.f).axisLength + m_impl->nextGap(prev, opts); prev = opts; } } - if (m_autoGrowAxisMinLength.has_value()) { - if (totalLength < m_autoGrowAxisMinLength.value()) { - totalLength = m_autoGrowAxisMinLength.value(); + if (m_impl->m_autoGrowAxisMinLength.has_value()) { + if (totalLength < m_impl->m_autoGrowAxisMinLength.value()) { + totalLength = m_impl->m_autoGrowAxisMinLength.value(); } - if (m_axis == Axis::Row) { + if (m_impl->m_axis == Axis::Row) { on->setContentSize({ totalLength, on->getContentSize().height }); } else { @@ -747,10 +775,10 @@ void AxisLayout::apply(CCNode* on) { } } - this->tryFitLayout( + m_impl->tryFitLayout( on, nodes, minMaxPrio, doAutoScale, - this->maxScaleForPrio(nodes, minMaxPrio.second), 1.f, minMaxPrio.second, + m_impl->maxScaleForPrio(nodes, minMaxPrio.second), 1.f, minMaxPrio.second, 0 ); } @@ -761,16 +789,16 @@ CCSize AxisLayout::getSizeHint(CCNode* on) const { float length = 0.f; float cross = 0.f; for (auto& node : CCArrayExt<CCNode*>(nodes)) { - auto axis = nodeAxis(node, m_axis, 1.f); + auto axis = nodeAxis(node, m_impl->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_impl->m_allowCrossAxisOverflow) { + cross = nodeAxis(on, m_impl->m_axis, 1.f).crossLength; } - if (m_axis == Axis::Row) { + if (m_impl->m_axis == Axis::Row) { return { length, cross }; } else { @@ -778,106 +806,99 @@ CCSize AxisLayout::getSizeHint(CCNode* on) const { } } -AxisLayout::AxisLayout(Axis axis) : m_axis(axis) {} - Axis AxisLayout::getAxis() const { - return m_axis; + return m_impl->m_axis; } - AxisAlignment AxisLayout::getCrossAxisAlignment() const { - return m_crossAlignment; + return m_impl->m_crossAlignment; } - AxisAlignment AxisLayout::getCrossAxisLineAlignment() const { - return m_crossLineAlignment; + return m_impl->m_crossLineAlignment; } - AxisAlignment AxisLayout::getAxisAlignment() const { - return m_axisAlignment; + return m_impl->m_axisAlignment; } - float AxisLayout::getGap() const { - return m_gap; + return m_impl->m_gap; } - bool AxisLayout::getAxisReverse() const { - return m_axisReverse; + return m_impl->m_axisReverse; } - bool AxisLayout::getCrossAxisReverse() const { - return m_crossReverse; + return m_impl->m_crossReverse; } - bool AxisLayout::getAutoScale() const { - return m_autoScale; + return m_impl->m_autoScale; } - bool AxisLayout::getGrowCrossAxis() const { - return m_growCrossAxis; + return m_impl->m_growCrossAxis; } - bool AxisLayout::getCrossAxisOverflow() const { - return m_allowCrossAxisOverflow; + return m_impl->m_allowCrossAxisOverflow; } - std::optional<float> AxisLayout::getAutoGrowAxis() const { - return m_autoGrowAxisMinLength; + return m_impl->m_autoGrowAxisMinLength; +} +float AxisLayout::getDefaultMinScale() const { + return m_impl->m_defaultScaleLimits.first; +} +float AxisLayout::getDefaultMaxScale() const { + return m_impl->m_defaultScaleLimits.second; } AxisLayout* AxisLayout::setAxis(Axis axis) { - m_axis = axis; + m_impl->m_axis = axis; return this; } - AxisLayout* AxisLayout::setCrossAxisAlignment(AxisAlignment align) { - m_crossAlignment = align; + m_impl->m_crossAlignment = align; return this; } - AxisLayout* AxisLayout::setCrossAxisLineAlignment(AxisAlignment align) { - m_crossLineAlignment = align; + m_impl->m_crossLineAlignment = align; return this; } - AxisLayout* AxisLayout::setAxisAlignment(AxisAlignment align) { - m_axisAlignment = align; + m_impl->m_axisAlignment = align; return this; } - AxisLayout* AxisLayout::setGap(float gap) { - m_gap = gap; + m_impl->m_gap = gap; return this; } - AxisLayout* AxisLayout::setAxisReverse(bool reverse) { - m_axisReverse = reverse; + m_impl->m_axisReverse = reverse; return this; } - AxisLayout* AxisLayout::setCrossAxisReverse(bool reverse) { - m_crossReverse = reverse; + m_impl->m_crossReverse = reverse; return this; } - AxisLayout* AxisLayout::setCrossAxisOverflow(bool fit) { - m_allowCrossAxisOverflow = fit; + m_impl->m_allowCrossAxisOverflow = fit; return this; } - AxisLayout* AxisLayout::setAutoScale(bool scale) { - m_autoScale = scale; + m_impl->m_autoScale = scale; return this; } - AxisLayout* AxisLayout::setGrowCrossAxis(bool shrink) { - m_growCrossAxis = shrink; + m_impl->m_growCrossAxis = shrink; + return this; +} +AxisLayout* AxisLayout::setAutoGrowAxis(std::optional<float> allowAndMinLength) { + m_impl->m_autoGrowAxisMinLength = allowAndMinLength; + return this; +} +AxisLayout* AxisLayout::setDefaultScaleLimits(float min, float max) { + m_impl->m_defaultScaleLimits = { min, max }; return this; } -AxisLayout* AxisLayout::setAutoGrowAxis(std::optional<float> allowAndMinLength) { - m_autoGrowAxisMinLength = allowAndMinLength; - return this; +AxisLayout::AxisLayout(Axis axis) : m_impl(std::make_unique<Impl>()) { + m_impl->m_axis = axis; } +AxisLayout::~AxisLayout() {} AxisLayout* AxisLayout::create(Axis axis) { auto ret = new AxisLayout(axis); @@ -907,98 +928,114 @@ ColumnLayout* ColumnLayout::create() { // AxisLayoutOptions +class AxisLayoutOptions::Impl { +public: + std::optional<bool> m_autoScale = std::nullopt; + std::pair<std::optional<float>, std::optional<float>> m_scaleLimits; + float m_relativeScale = 1.f; + std::optional<float> m_length = std::nullopt; + std::optional<float> m_nextGap = std::nullopt; + std::optional<float> m_prevGap = std::nullopt; + bool m_breakLine = false; + bool m_sameLine = false; + int m_scalePriority = AXISLAYOUT_DEFAULT_PRIORITY; + std::optional<AxisAlignment> m_crossAxisAlignment; +}; + AxisLayoutOptions* AxisLayoutOptions::create() { auto ret = new AxisLayoutOptions(); ret->autorelease(); return ret; } +AxisLayoutOptions::AxisLayoutOptions() : m_impl(std::make_unique<Impl>()) {} +AxisLayoutOptions::~AxisLayoutOptions() = default; + std::optional<bool> AxisLayoutOptions::getAutoScale() const { - return m_autoScale; + return m_impl->m_autoScale; } - float AxisLayoutOptions::getMaxScale() const { - return m_maxScale; + return m_impl->m_scaleLimits.second.value_or(1.f); } - float AxisLayoutOptions::getMinScale() const { - return m_minScale; + return m_impl->m_scaleLimits.first.value_or(AXISLAYOUT_DEFAULT_MIN_SCALE); +} +bool AxisLayoutOptions::hasExplicitMaxScale() const { + return m_impl->m_scaleLimits.second.has_value(); +} +bool AxisLayoutOptions::hasExplicitMinScale() const { + return m_impl->m_scaleLimits.first.has_value(); } - float AxisLayoutOptions::getRelativeScale() const { - return m_relativeScale; + return m_impl->m_relativeScale; } - std::optional<float> AxisLayoutOptions::getLength() const { - return m_length; + return m_impl->m_length; } - std::optional<float> AxisLayoutOptions::getPrevGap() const { - return m_prevGap; + return m_impl->m_prevGap; } - std::optional<float> AxisLayoutOptions::getNextGap() const { - return m_nextGap; + return m_impl->m_nextGap; } - bool AxisLayoutOptions::getBreakLine() const { - return m_breakLine; + return m_impl->m_breakLine; } - bool AxisLayoutOptions::getSameLine() const { - return m_sameLine; + return m_impl->m_sameLine; } - int AxisLayoutOptions::getScalePriority() const { - return m_scalePriority; + return m_impl->m_scalePriority; +} +std::optional<AxisAlignment> AxisLayoutOptions::getCrossAxisAlignment() const { + return m_impl->m_crossAxisAlignment; } AxisLayoutOptions* AxisLayoutOptions::setMaxScale(float scale) { - m_maxScale = scale; + m_impl->m_scaleLimits.second = scale; return this; } - AxisLayoutOptions* AxisLayoutOptions::setMinScale(float scale) { - m_minScale = scale; + m_impl->m_scaleLimits.first = scale; + return this; +} +AxisLayoutOptions* AxisLayoutOptions::setScaleLimits(std::optional<float> min, std::optional<float> max) { + m_impl->m_scaleLimits = { min, max }; return this; } - AxisLayoutOptions* AxisLayoutOptions::setRelativeScale(float scale) { - m_relativeScale = scale; + m_impl->m_relativeScale = scale; return this; } - AxisLayoutOptions* AxisLayoutOptions::setAutoScale(std::optional<bool> enabled) { - m_autoScale = enabled; + m_impl->m_autoScale = enabled; return this; } - AxisLayoutOptions* AxisLayoutOptions::setLength(std::optional<float> length) { - m_length = length; + m_impl->m_length = length; return this; } - AxisLayoutOptions* AxisLayoutOptions::setPrevGap(std::optional<float> gap) { - m_prevGap = gap; + m_impl->m_prevGap = gap; return this; } - AxisLayoutOptions* AxisLayoutOptions::setNextGap(std::optional<float> gap) { - m_nextGap = gap; + m_impl->m_nextGap = gap; return this; } - AxisLayoutOptions* AxisLayoutOptions::setBreakLine(bool enable) { - m_breakLine = enable; + m_impl->m_breakLine = enable; return this; } - AxisLayoutOptions* AxisLayoutOptions::setSameLine(bool enable) { - m_sameLine = enable; + m_impl->m_sameLine = enable; return this; } - AxisLayoutOptions* AxisLayoutOptions::setScalePriority(int priority) { - m_scalePriority = priority; + m_impl->m_scalePriority = priority; + return this; +} +AxisLayoutOptions* AxisLayoutOptions::setCrossAxisAlignment(std::optional<AxisAlignment> alignment) { + m_impl->m_crossAxisAlignment = alignment; return this; }