diff --git a/loader/include/Geode/cocos/base_nodes/Layout.hpp b/loader/include/Geode/cocos/base_nodes/Layout.hpp index a24aebec..db1a3376 100644 --- a/loader/include/Geode/cocos/base_nodes/Layout.hpp +++ b/loader/include/Geode/cocos/base_nodes/Layout.hpp @@ -56,29 +56,28 @@ enum class Alignment { End, }; -/** - * Simple layout for arranging nodes in a row (horizontal line) - */ -class GEODE_DLL RowLayout : public Layout { +class GEODE_DLL AxisLayout : public Layout { +public: + enum Axis : bool { + Row, + Column, + }; + protected: - std::optional m_vAlignment = Alignment::Center; - float m_gap; - std::optional m_maxAutoScale = std::nullopt; + Axis m_axis; + Alignment m_axisAlignment = Alignment::Center; + Alignment m_crossAlignment = Alignment::Center; + float m_gap = 5.f; + bool m_autoScale = true; + bool m_shrinkCrossAxis = true; bool m_reverse = false; bool m_fitInside = false; + AxisLayout(Axis); + public: void apply(CCNode* on) override; - /** - * Create a new RowLayout. Note that this class is not automatically - * managed by default, so you must assign it to a CCNode or manually - * manage the memory yourself. See the chainable setters on RowLayout for - * what options you can customize for the layout - * @returns Created RowLayout - */ - static RowLayout* create(); - /** * Sets where to align nodes on the Y-axis. If nullopt, the * nodes' Y-position will not be affected, and the height of the node this @@ -89,16 +88,17 @@ public: * @param align Value * @returns The same RowLayout this was applied on */ - RowLayout* setVAlignment(std::optional align); + AxisLayout* setCrossAxisAlignment(Alignment align); + AxisLayout* setAxisAlignment(Alignment align); /** * The spacing between the children of the node this layout applies to. * Measured as the space between their edges, not centres */ - RowLayout* setGap(float gap); + AxisLayout* setGap(float gap); /** * Whether to reverse the direction of the children in this layout or not */ - RowLayout* setReverse(bool reverse); + AxisLayout* setReverse(bool reverse); /** * If a value is provided, then the node this layout applies to may be * automatically rescaled to fit its contents better. By default the value @@ -109,30 +109,43 @@ public: * inside of, and scales to fit that space. If the value is nullopt, the * unscaled content size is used instead */ - RowLayout* setMaxAutoScale(std::optional scale); + AxisLayout* setAutoScale(bool enable); /** * If true, the children of the node this layout is applied to will be * contained entirely within the bounds of the node's content size. If * false, the children's positions will be within the bounds, but they may * visually overflow depending on their anchor point */ - RowLayout* setFitInside(bool fit); + AxisLayout* setFitInside(bool fit); + AxisLayout* setShrinkCrossAxis(bool shrink); +}; + +/** + * Simple layout for arranging nodes in a row (horizontal line) + */ +class GEODE_DLL RowLayout : public AxisLayout { +protected: + RowLayout(); + +public: + /** + * Create a new RowLayout. Note that this class is not automatically + * managed by default, so you must assign it to a CCNode or manually + * manage the memory yourself. See the chainable setters on RowLayout for + * what options you can customize for the layout + * @returns Created RowLayout + */ + static RowLayout* create(); }; /** * Simple layout for arranging nodes in a column (vertical line) */ -class GEODE_DLL ColumnLayout : public Layout { +class GEODE_DLL ColumnLayout : public AxisLayout { protected: - std::optional m_hAlignment = Alignment::Center; - float m_gap; - std::optional m_maxAutoScale = std::nullopt; - bool m_reverse = false; - bool m_fitInside = false; + ColumnLayout(); public: - void apply(CCNode* on) override; - /** * Create a new ColumnLayout. Note that this class is not automatically * managed by default, so you must assign it to a CCNode or manually @@ -141,42 +154,6 @@ public: * @returns Created ColumnLayout */ static ColumnLayout* create(); - - /** - * Sets where to align nodes on the X-axis. If nullopt, the - * nodes' X-position will not be affected, and the width of the node this - * layout applies to isn't altered. If an alignment is given, the width - * of the node this layout applies to is shrunk to fit the width of the - * nodes and no more. Any nodes that don't fit inside this space are - * aligned based on the value - * @param align Value - * @returns The same RowLayout this was applied on - */ - ColumnLayout* setHAlignment(std::optional align); - /** - * The spacing between the children of the node this layout applies to. - * Measured as the space between their edges, not centres - */ - ColumnLayout* setGap(float gap); - /** - * Whether to reverse the direction of the children in this layout or not - */ - ColumnLayout* setReverse(bool reverse); - /** - * If a value is provided, then the node this layout applies to may be - * automatically rescaled to fit its contents better. By default the value - * is nullopt, which means that the layout doesn't affect the node's scale - * in any way, and any nodes that might overflow will be squished using - * other methods - */ - ColumnLayout* setMaxAutoScale(std::optional scale); - /** - * If true, the children of the node this layout is applied to will be - * contained entirely within the bounds of the node's content size. If - * false, the children's positions will be within the bounds, but they may - * visually overflow depending on their anchor point - */ - ColumnLayout* setFitInside(bool fit); }; /** diff --git a/loader/src/cocos2d-ext/Layout.cpp b/loader/src/cocos2d-ext/Layout.cpp index fd2d3367..f338797b 100644 --- a/loader/src/cocos2d-ext/Layout.cpp +++ b/loader/src/cocos2d-ext/Layout.cpp @@ -2,6 +2,7 @@ #include #include #include +#include USE_GEODE_NAMESPACE(); @@ -21,182 +22,230 @@ CCArray* Layout::getNodesToPosition(CCNode* on) { return filtered; } -void RowLayout::apply(CCNode* on) { - +void AxisLayout::apply(CCNode* on) { auto nodes = getNodesToPosition(on); if (m_reverse) { nodes->reverseObjects(); } - auto availableWidth = m_maxAutoScale.has_value() ? - on->getScaledContentSize().width : - on->getContentSize().width; + float availableAxisLength; + float originalCrossHeight; + + if (m_axis == Axis::Row) { + availableAxisLength = on->getContentSize().width; + originalCrossHeight = on->getContentSize().height; + } + else { + availableAxisLength = on->getContentSize().height; + originalCrossHeight = on->getContentSize().width; + } size_t ix = 0; - float totalWidth = .0f; + float totalAxisLength = .0f; + float maxCrossLength = 0.f; for (auto& node : CCArrayExt(nodes)) { + float axisLength; + float axisAnchor; + float crossLength; + if (m_axis == Axis::Row) { + axisLength = node->getScaledContentSize().width; + axisAnchor = node->getAnchorPoint().x; + crossLength = node->getScaledContentSize().height; + } + else { + axisLength = node->getScaledContentSize().height; + axisAnchor = node->getAnchorPoint().y; + crossLength = node->getScaledContentSize().width; + } + // if no need to fit fully inside and only one item exists, the total + // width taken up is 0 + if (nodes->count() == 1 && !m_fitInside) { + totalAxisLength = 0; + } // if no need to fit fully inside, figure out what part may overflow // for first item - if (ix == 0 && !m_fitInside) { - totalWidth += node->getScaledContentSize().width - * (1.f - node->getAnchorPoint().x); + else if (ix == 0 && !m_fitInside) { + totalAxisLength += axisLength * (1.f - axisAnchor); } // if no need to fit fully inside, figure out what part may overflow // for last item else if (ix == nodes->count() - 1 && !m_fitInside) { - totalWidth += node->getScaledContentSize().width - * node->getAnchorPoint().x; + totalAxisLength += axisLength * axisAnchor; } // otherwise either we need to fit fully inside or this node is not // at the start or end else { - totalWidth += node->getScaledContentSize().width; + totalAxisLength += axisLength; } if (ix) { - totalWidth += m_gap; + totalAxisLength += m_gap; + } + if (crossLength > maxCrossLength) { + maxCrossLength = crossLength; } ix++; } - auto squeeze = availableSize.width / totalWidth; - if (squeeze > 1.f) { - squeeze = 1.f; + const auto minScale = .65f; + + // assume intended scale is 1x + auto setScale = 1.f; + auto squeeze = 1.f; + + // check for overflow + // first try to make the node smaller + if (totalAxisLength > availableAxisLength && m_autoScale) { + setScale = availableAxisLength / totalAxisLength; + if (setScale < minScale) { + setScale = minScale; + } + totalAxisLength *= setScale; + } + + // if we're still overflowing, squeeze nodes closer together + if (totalAxisLength > availableAxisLength) { + squeeze = availableAxisLength / totalAxisLength; + totalAxisLength = availableAxisLength; + } + + // resize target to match settings + if (m_shrinkCrossAxis) { + if (m_axis == Axis::Row) { + on->setContentSize({ + availableAxisLength, + maxCrossLength, + }); + } + else { + on->setContentSize({ + maxCrossLength, + availableAxisLength, + }); + } } float pos; - switch (m_alignment) { - default: - case Alignment::Center: pos = -totalWidth / 2; break; - case Alignment::Begin: pos = -totalWidth; break; - case Alignment::End: pos = 0.f; break; + switch (m_axisAlignment) { + case Alignment::Begin: { + pos = 0.f; + } break; + + case Alignment::Center: { + pos = availableAxisLength / 2 - totalAxisLength / 2; + } break; + + case Alignment::End: { + pos = availableAxisLength - totalAxisLength; + } break; } + ix = 0; for (auto& node : CCArrayExt(nodes)) { - auto sw = node->getScaledContentSize().width; - float disp; - switch (m_alignment) { - default: - case Alignment::Center: disp = sw * node->getAnchorPoint().x; break; - case Alignment::Begin: disp = sw; break; - case Alignment::End: disp = 0.f; break; + // rescale node if overflowing + if (m_autoScale) { + // CCMenuItemSpriteExtra is quirky af + if (auto btn = typeinfo_cast(node)) { + btn->m_baseScale = setScale; + } + node->setScale(setScale); } - node->setPositionX(pos + disp); - if (m_alignVertically) { - node->setPositionY(m_alignVertically.value()); + float axisLength; + float axisAnchor; + float crossLength; + float crossAnchor; + if (m_axis == Axis::Row) { + axisLength = node->getScaledContentSize().width; + axisAnchor = node->getAnchorPoint().x; + crossLength = node->getScaledContentSize().height; + crossAnchor = node->getAnchorPoint().y; } - pos += (sw + m_gap) * squeeze; + else { + axisLength = node->getScaledContentSize().height; + axisAnchor = node->getAnchorPoint().y; + crossLength = node->getScaledContentSize().width; + crossAnchor = node->getAnchorPoint().x; + } + float axisPos; + if (ix == 0 && !m_fitInside) { + axisPos = pos; + pos += (axisLength * (1.f - axisAnchor) + m_gap * setScale) * squeeze; + } + else { + axisPos = pos + axisLength * axisAnchor * squeeze; + pos += (axisLength + m_gap * setScale) * squeeze; + } + float crossPos; + switch (m_crossAlignment) { + case Alignment::Begin: { + crossPos = crossLength * crossAnchor; + } break; + + case Alignment::Center: { + crossPos = maxCrossLength / 2 - crossLength * (.5f - crossAnchor); + } break; + + case Alignment::End: { + crossPos = maxCrossLength - crossLength * (1.f - crossAnchor); + } break; + } + if (m_axis == Axis::Row) { + node->setPosition(axisPos, crossPos); + } + else { + node->setPosition(crossPos, axisPos); + } + ix++; } } +AxisLayout::AxisLayout(Axis axis) : m_axis(axis) {} + +AxisLayout* AxisLayout::setCrossAxisAlignment(Alignment align) { + m_crossAlignment = align; + return this; +} + +AxisLayout* AxisLayout::setAxisAlignment(Alignment align) { + m_axisAlignment = align; + return this; +} + +AxisLayout* AxisLayout::setGap(float gap) { + m_gap = gap; + return this; +} + +AxisLayout* AxisLayout::setReverse(bool reverse) { + m_reverse = reverse; + return this; +} + +AxisLayout* AxisLayout::setAutoScale(bool scale) { + m_autoScale = scale; + return this; +} + +AxisLayout* AxisLayout::setFitInside(bool fit) { + m_fitInside = fit; + return this; +} + +AxisLayout* AxisLayout::setShrinkCrossAxis(bool shrink) { + m_shrinkCrossAxis = shrink; + return this; +} + +RowLayout::RowLayout() : AxisLayout(AxisLayout::Row) {} + RowLayout* RowLayout::create() { return new RowLayout(); } -RowLayout* RowLayout::setVAlignment(std::optional align) { - m_vAlignment = align; - return this; -} - -RowLayout* RowLayout::setGap(float gap) { - m_gap = gap; - return this; -} - -RowLayout* RowLayout::setReverse(bool reverse) { - m_reverse = reverse; - return this; -} - -RowLayout* RowLayout::setMaxAutoScale(std::optional scale) { - m_maxAutoScale = scale; - return this; -} - -RowLayout* RowLayout::setFitInside(bool fit) { - m_fitInside = fit; - return this; -} - -void ColumnLayout::apply(CCNode* on) { - float totalHeight = .0f; - size_t ix = 0; - auto nodes = getNodesToPosition(on); - auto availableSize = on->getScaledContentSize(); - for (auto& node : CCArrayExt(nodes)) { - totalHeight += node->getScaledContentSize().height; - if (ix) { - totalHeight += m_gap; - } - } - - auto squeeze = availableSize.height / totalHeight; - if (squeeze > 1.f) { - squeeze = 1.f; - } - if (totalHeight > availableSize.height) { - totalHeight = availableSize.height; - } - - float pos; - switch (m_alignment) { - default: - case Alignment::Center: pos = -totalHeight / 2; break; - case Alignment::Begin: pos = -totalHeight; break; - case Alignment::End: pos = 0.f; break; - } - if (m_reverse) { - nodes->reverseObjects(); - } - log::debug("start pos: {}", pos); - log::debug("squeeze: {}", squeeze); - for (auto& node : CCArrayExt(nodes)) { - auto sh = node->getScaledContentSize().height; - float disp; - switch (m_alignment) { - default: - case Alignment::Center: disp = sh * node->getAnchorPoint().y; break; - case Alignment::Begin: disp = (m_reverse ? 0.f : sh); break; - case Alignment::End: disp = (m_reverse ? sh : 0.f); break; - } - log::debug("positioning at: {}", pos + disp); - node->setPositionY(pos + disp); - if (m_alignHorizontally) { - node->setPositionX(m_alignHorizontally.value()); - } - auto opos = pos; - pos += (sh + m_gap) * squeeze; - log::debug("pos: {} -> {}", opos, pos); - } -} +ColumnLayout::ColumnLayout() : AxisLayout(AxisLayout::Column) {} ColumnLayout* ColumnLayout::create() { return new ColumnLayout(); } -ColumnLayout* ColumnLayout::setHAlignment(std::optional align) { - m_hAlignment = align; - return this; -} - -ColumnLayout* ColumnLayout::setGap(float gap) { - m_gap = gap; - return this; -} - -ColumnLayout* ColumnLayout::setReverse(bool reverse) { - m_reverse = reverse; - return this; -} - -ColumnLayout* ColumnLayout::setMaxAutoScale(std::optional scale) { - m_maxAutoScale = scale; - return this; -} - -ColumnLayout* ColumnLayout::setFitInside(bool fit) { - m_fitInside = fit; - return this; -} - void GridLayout::apply(CCNode* on) { // todo } diff --git a/loader/src/ids/AddIDs.hpp b/loader/src/ids/AddIDs.hpp index 7f00b54b..899bddbc 100644 --- a/loader/src/ids/AddIDs.hpp +++ b/loader/src/ids/AddIDs.hpp @@ -64,12 +64,13 @@ static CCMenu* detachAndCreateMenu(CCNode* parent, const char* menuID, Layout* l newMenu->setPosition(parent->convertToNodeSpace(oldMenu->convertToWorldSpace(first->getPosition()))); newMenu->setID(menuID); newMenu->setZOrder(oldMenu->getZOrder()); - newMenu->setLayout(layout); parent->addChild(newMenu); first->setPosition(0, 0); newMenu->addChild(first); first->release(); + + newMenu->setLayout(layout); (switchToMenu(args, newMenu), ...); diff --git a/loader/src/ids/CreatorLayer.cpp b/loader/src/ids/CreatorLayer.cpp index 4b2e2bee..4938e1d4 100644 --- a/loader/src/ids/CreatorLayer.cpp +++ b/loader/src/ids/CreatorLayer.cpp @@ -29,9 +29,11 @@ $register_ids(CreatorLayer) { detachAndCreateMenu( this, "top-right-menu", - ColumnLayout::create(), + ColumnLayout::create() + ->setFitInside(false) + ->setAxisAlignment(Alignment::Begin), lockBtn - )->setAnchorPoint({ 0.f, 0.f }); + )->setAnchorPoint({ .5f, 0.f }); } // move treasure room button to its own menu @@ -39,9 +41,11 @@ $register_ids(CreatorLayer) { detachAndCreateMenu( this, "bottom-right-menu", - ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::End), + ColumnLayout::create() + ->setFitInside(false) + ->setAxisAlignment(Alignment::End), roomBtn - ); + )->setAnchorPoint({ .5f, 1.f }); } } diff --git a/loader/src/ids/EditLevelLayer.cpp b/loader/src/ids/EditLevelLayer.cpp index d8b9a010..b8dabc44 100644 --- a/loader/src/ids/EditLevelLayer.cpp +++ b/loader/src/ids/EditLevelLayer.cpp @@ -57,10 +57,11 @@ $register_ids(EditLevelLayer) { menu->setLayout( ColumnLayout::create() ->setGap(7.f) + ->setFitInside(false) + ->setAxisAlignment(Alignment::Begin) ->setReverse(true) - ->setAlignment(Alignment::Begin) - ->setAlignHorizontally(0.f) ); + menu->setAnchorPoint({ .5f, 0.f }); menu->setZOrder(1); for (int i = 0; i < rand() % 4; i++) { diff --git a/loader/src/ids/EditorUI.cpp b/loader/src/ids/EditorUI.cpp index 9ae84896..d283ed5b 100644 --- a/loader/src/ids/EditorUI.cpp +++ b/loader/src/ids/EditorUI.cpp @@ -177,10 +177,12 @@ $register_ids(EditorUI) { detachAndCreateMenu( this, "top-right-menu", - RowLayout::create()->setAlignment(Alignment::End), + RowLayout::create() + ->setFitInside(false) + ->setAxisAlignment(Alignment::End), menu->getChildByID("pause-button"), menu->getChildByID("settings-button") - ); + )->setAnchorPoint({ 1.f, .5f }); detachAndCreateMenu( this, diff --git a/loader/src/ids/LevelBrowserLayer.cpp b/loader/src/ids/LevelBrowserLayer.cpp index c1c74149..a88a1fc2 100644 --- a/loader/src/ids/LevelBrowserLayer.cpp +++ b/loader/src/ids/LevelBrowserLayer.cpp @@ -21,12 +21,19 @@ $register_ids(LevelBrowserLayer) { detachAndCreateMenu( this, "my-levels-menu", - ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::End), + ColumnLayout::create() + ->setFitInside(false) + ->setAxisAlignment(Alignment::End), myLevelsBtn - ); + )->setAnchorPoint({ .5f, 1.f }); } - menu->setLayout(ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::End)); + menu->setLayout( + ColumnLayout::create() + ->setFitInside(false) + ->setAxisAlignment(Alignment::End) + ); + menu->setAnchorPoint({ .5f, 1.f }); } } } diff --git a/loader/src/ids/LevelInfoLayer.cpp b/loader/src/ids/LevelInfoLayer.cpp index 4a082700..a424a733 100644 --- a/loader/src/ids/LevelInfoLayer.cpp +++ b/loader/src/ids/LevelInfoLayer.cpp @@ -44,8 +44,13 @@ $register_ids(LevelInfoLayer) { if (auto name = setIDSafe(menu, 0, "creator-name")) { detachAndCreateMenu( - this, "creator-info-menu", ColumnLayout::create()->setAlignment(Alignment::Begin), name - ); + this, + "creator-info-menu", + ColumnLayout::create() + ->setFitInside(false) + ->setAxisAlignment(Alignment::Begin), + name + )->setAnchorPoint({ .5f, 0.f }); } auto leftSideMenu = CCMenu::create(); diff --git a/loader/src/ids/LevelSettingsLayer.cpp b/loader/src/ids/LevelSettingsLayer.cpp index ce857a83..0a52fac9 100644 --- a/loader/src/ids/LevelSettingsLayer.cpp +++ b/loader/src/ids/LevelSettingsLayer.cpp @@ -165,9 +165,11 @@ $register_ids(LevelSettingsLayer) { detachAndCreateMenu( this, "font-button-menu", - RowLayout::create()->setAlignment(Alignment::End), + RowLayout::create() + ->setFitInside(false) + ->setAxisAlignment(Alignment::End), menu->getChildByID("font-button") - ); + )->setAnchorPoint({ .5f, 1.f }); } } diff --git a/loader/src/ids/MenuLayer.cpp b/loader/src/ids/MenuLayer.cpp index 177ec766..8d913ecd 100644 --- a/loader/src/ids/MenuLayer.cpp +++ b/loader/src/ids/MenuLayer.cpp @@ -41,9 +41,17 @@ $register_ids(MenuLayer) { setIDSafe(menu, 2, "editor-button"); if (auto pfp = setIDSafe(menu, 3, "profile-button")) { - pfp->setPositionHint(PositionHint::Absolute); + detachAndCreateMenu( + this, "profile-menu", + ColumnLayout::create() + ->setFitInside(false) + ->setAxisAlignment(Alignment::Begin) + ->setReverse(true), + pfp + )->setAnchorPoint({ .5f, .0f }); } + menu->setContentSize({ 400.f, 65.f }); menu->setLayout(RowLayout::create()->setGap(18.f)); } // bottom menu @@ -57,10 +65,18 @@ $register_ids(MenuLayer) { // move daily chest to its own menu if (auto dailyChest = setIDSafe(menu, -1, "daily-chest-button")) { - detachAndCreateMenu(this, "right-side-menu", ColumnLayout::create(), dailyChest); + auto menu = detachAndCreateMenu( + this, + "right-side-menu", + ColumnLayout::create()->setFitInside(true), + dailyChest + ); + menu->setContentSize({ 65.f, 180.f }); + menu->updateLayout(); } - menu->setLayout(RowLayout::create()); + menu->setContentSize({ 360.f, 65.f }); + menu->setLayout(RowLayout::create()->setFitInside(true)); } // social media menu if (auto menu = getChildOfType(this, 2)) { @@ -79,7 +95,12 @@ $register_ids(MenuLayer) { if (auto closeBtn = setIDSafe(menu, 1, "close-button")) { detachAndCreateMenu( - this, "close-menu", RowLayout::create(), closeBtn + this, + "close-menu", + RowLayout::create() + ->setFitInside(false) + ->setAxisAlignment(Alignment::Begin), + closeBtn )->setAnchorPoint({ 0.f, .5f }); } }