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;
 }