From 3f7a9ed8d3eff63e51794b11c730ecc911e92ee6 Mon Sep 17 00:00:00 2001
From: HJfod <60038575+HJfod@users.noreply.github.com>
Date: Mon, 10 Oct 2022 20:58:47 +0300
Subject: [PATCH] initial layout stuff

---
 loader/CMakeLists.txt                         |   1 +
 loader/include/Geode/UI.hpp                   |   4 +
 .../Geode/cocos/cocos2dx/base_nodes/CCNode.h  |   8 +
 .../cocos/cocos2dx/base_nodes/Layout.hpp      | 153 ++++++++++++++++++
 .../Geode/cocos/cocos2dx/cocoa/CCObject.h     |  16 +-
 loader/include/Geode/modify/Modify.hpp        |   1 -
 loader/include/Geode/ui/ColorPickPopup.hpp    |   2 +
 loader/include/Geode/ui/EnterLayerEvent.hpp   |  61 +++++++
 loader/src/cocos2d-ext/Layout.cpp             | 138 ++++++++++++++++
 loader/src/hooks/GeodeNodeMetadata.cpp        |  35 ++++
 loader/src/hooks/MenuLayer.cpp                |  84 +---------
 loader/src/ids/MenuLayer.cpp                  |  81 ++++++++++
 12 files changed, 498 insertions(+), 86 deletions(-)
 create mode 100644 loader/include/Geode/cocos/cocos2dx/base_nodes/Layout.hpp
 create mode 100644 loader/include/Geode/ui/EnterLayerEvent.hpp
 create mode 100644 loader/src/cocos2d-ext/Layout.cpp
 create mode 100644 loader/src/ids/MenuLayer.cpp

diff --git a/loader/CMakeLists.txt b/loader/CMakeLists.txt
index 013cecfc..f25f18f4 100644
--- a/loader/CMakeLists.txt
+++ b/loader/CMakeLists.txt
@@ -14,6 +14,7 @@ file(GLOB CORE_SOURCES
 	src/cocos2d-ext/*.cpp
 	src/core/*.cpp
 	src/hooks/*.cpp
+	src/ids/*.cpp
 	src/internal/*.cpp
 	src/internal/windows/*.cpp
 	src/internal/mac/*.cpp
diff --git a/loader/include/Geode/UI.hpp b/loader/include/Geode/UI.hpp
index 8f8bb18d..8b6e4b60 100644
--- a/loader/include/Geode/UI.hpp
+++ b/loader/include/Geode/UI.hpp
@@ -2,13 +2,17 @@
 
 #include "ui/BasedButtonSprite.hpp"
 #include "ui/BasedButton.hpp"
+#include "ui/ColorPickPopup.hpp"
+#include "ui/EnterLayerEvent.hpp"
 #include "ui/IconButtonSprite.hpp"
 #include "ui/InputNode.hpp"
 #include "ui/ListView.hpp"
+#include "ui/MDPopup.hpp"
 #include "ui/MDTextArea.hpp"
 #include "ui/Notification.hpp"
 #include "ui/Popup.hpp"
 #include "ui/SceneManager.hpp"
 #include "ui/Scrollbar.hpp"
 #include "ui/ScrollLayer.hpp"
+#include "ui/SelectList.hpp"
 #include "ui/TextRenderer.hpp"
diff --git a/loader/include/Geode/cocos/cocos2dx/base_nodes/CCNode.h b/loader/include/Geode/cocos/cocos2dx/base_nodes/CCNode.h
index 10055ebf..80679282 100644
--- a/loader/include/Geode/cocos/cocos2dx/base_nodes/CCNode.h
+++ b/loader/include/Geode/cocos/cocos2dx/base_nodes/CCNode.h
@@ -37,6 +37,7 @@
 #include "kazmath/kazmath.h"
 #include "script_support/CCScriptSupport.h"
 #include "CCProtocols.h"
+#include "Layout.hpp"
 
 NS_CC_BEGIN
 
@@ -875,6 +876,13 @@ public:
          * @returns The child, or nullptr if none was found
          */
         CCNode* getChildByIDRecursive(std::string const& id);
+
+        void setLayout(Layout* layout, bool apply = true);
+        Layout* getLayout();
+        void updateLayout();
+
+        void setPositionHint(PositionHint hint);
+        PositionHint getPositionHint();
     );
     
     /// @{
diff --git a/loader/include/Geode/cocos/cocos2dx/base_nodes/Layout.hpp b/loader/include/Geode/cocos/cocos2dx/base_nodes/Layout.hpp
new file mode 100644
index 00000000..5697cbb0
--- /dev/null
+++ b/loader/include/Geode/cocos/cocos2dx/base_nodes/Layout.hpp
@@ -0,0 +1,153 @@
+#pragma once
+
+#include "ccMacros.h"
+#include "cocoa/CCAffineTransform.h"
+#include "cocoa/CCArray.h"
+#include <Geode/platform/platform.hpp>
+#include <optional>
+#include <unordered_map>
+
+NS_CC_BEGIN
+
+class CCNode;
+
+/**
+ * Layouts automatically handle the positioning of nodes. Use CCNode::setLayout 
+ * to apply a layout to a node, and then use CCNode::updateLayout to apply 
+ * the layout's positioning. Geode comes with a few default layouts like 
+ * RowLayout, ColumnLayout, and GridLayout, but if you need a different kind 
+ * of layout you can inherit from the Layout class.
+ */
+class Layout {
+public:
+    /**
+     * Automatically apply the layout's positioning on a set of nodes
+     * @param nodes Nodes to position 
+     * @param availableSize Give hints to the layout about how much space is 
+     * available. Note that the layout may still overflow
+     */
+    virtual void apply(CCArray* nodes, CCSize const& availableSize) = 0;
+};
+
+/**
+ * Determines how a node should be positioned within its parent, if that 
+ * parent has an automatically positioning layout
+ */
+enum class PositionHint {
+    // The container can determine the best position 
+    // for this node
+    Default,
+    // The container's layout should not affect the 
+    // position of this node
+    Absolute,
+};
+
+/**
+ * Specifies the alignment of something
+ */
+enum class Alignment {
+    Begin,
+    Center,
+    End,
+};
+
+/**
+ * Simple layout for arranging nodes in a row (horizontal line)
+ */
+class GEODE_DLL RowLayout : public Layout {
+protected:
+    Alignment m_alignment = Alignment::Center;
+    std::optional<float> m_alignVertically;
+    float m_gap;
+
+public:
+    void apply(CCArray* nodes, CCSize const& availableSize) 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.
+     * @param gap Space between nodes
+     * @param alignVertically Whether to align the nodes vertically, and if so, 
+     * what Y position to align them at
+     * @returns Created RowLayout
+     */
+    static RowLayout* create(
+        float gap = 5.f,
+        std::optional<float> alignVertically = std::nullopt
+    );
+
+    RowLayout* setAlignment(Alignment align);
+    RowLayout* setGap(float gap);
+    RowLayout* setAlignVertically(std::optional<float> align);
+};
+
+/**
+ * Simple layout for arranging nodes in a column (vertical line)
+ */
+class GEODE_DLL ColumnLayout : public Layout {
+protected:
+    Alignment m_alignment = Alignment::Center;
+    std::optional<float> m_alignHorizontally;
+    float m_gap;
+
+public:
+    void apply(CCArray* nodes, CCSize const& availableSize) override;
+
+    static ColumnLayout* create(
+        float gap = 5.f,
+        std::optional<float> alignHorizontally = std::nullopt
+    );
+
+    ColumnLayout* setAlignment(Alignment align);
+    ColumnLayout* setGap(float gap);
+    ColumnLayout* setAlignHorizontally(std::optional<float> align);
+};
+
+/**
+ * Grid direction; which direction the grid should add its next row to if the 
+ * current row is full
+ */
+enum class GridDirection {
+    // Downward
+    Column,
+    // Upward
+    ReverseColumn,
+    // Right
+    Row,
+    // Left
+    ReverseRow,
+};
+
+/**
+ * Grid alignment; same as normal Alignment but also features the "Stretch" 
+ * option which will stretch the row out to be the same size as the others
+*/
+enum class GridAlignment {
+    Begin,
+    Center,
+    Stretch,
+    End,
+};
+
+class GEODE_DLL GridLayout : public Layout {
+protected:
+    GridDirection m_direction = GridDirection::Column;
+    GridAlignment m_alignment = GridAlignment::Center;
+    std::optional<size_t> m_rowSize;
+
+public:
+    void apply(CCArray* nodes, CCSize const& availableSize) override;
+
+    static GridLayout* create(
+        std::optional<size_t> rowSize,
+        GridAlignment alignment = GridAlignment::Center,
+        GridDirection direction = GridDirection::Column
+    );
+
+    GridLayout* setDirection(GridDirection direction);
+    GridLayout* setAlignment(GridAlignment alignment);
+    GridLayout* setRowSize(std::optional<size_t> rowSize);
+};
+
+NS_CC_END
diff --git a/loader/include/Geode/cocos/cocos2dx/cocoa/CCObject.h b/loader/include/Geode/cocos/cocos2dx/cocoa/CCObject.h
index a73dfa2d..f7408b3a 100644
--- a/loader/include/Geode/cocos/cocos2dx/cocoa/CCObject.h
+++ b/loader/include/Geode/cocos/cocos2dx/cocoa/CCObject.h
@@ -173,14 +173,14 @@ typedef void (CCObject::*SEL_MenuHandler)(CCObject*);
 typedef void (CCObject::*SEL_EventHandler)(CCEvent*);
 typedef int (CCObject::*SEL_Compare)(CCObject*);
 
-#define schedule_selector(_SELECTOR) (SEL_SCHEDULE)(&_SELECTOR)
-#define callfunc_selector(_SELECTOR) (SEL_CallFunc)(&_SELECTOR)
-#define callfuncN_selector(_SELECTOR) (SEL_CallFuncN)(&_SELECTOR)
-#define callfuncND_selector(_SELECTOR) (SEL_CallFuncND)(&_SELECTOR)
-#define callfuncO_selector(_SELECTOR) (SEL_CallFuncO)(&_SELECTOR)
-#define menu_selector(_SELECTOR) (SEL_MenuHandler)(&_SELECTOR)
-#define event_selector(_SELECTOR) (SEL_EventHandler)(&_SELECTOR)
-#define compare_selector(_SELECTOR) (SEL_Compare)(&_SELECTOR)
+#define schedule_selector(_SELECTOR) (cocos2d::SEL_SCHEDULE)(&_SELECTOR)
+#define callfunc_selector(_SELECTOR) (cocos2d::SEL_CallFunc)(&_SELECTOR)
+#define callfuncN_selector(_SELECTOR) (cocos2d::SEL_CallFuncN)(&_SELECTOR)
+#define callfuncND_selector(_SELECTOR) (cocos2d::SEL_CallFuncND)(&_SELECTOR)
+#define callfuncO_selector(_SELECTOR) (cocos2d::SEL_CallFuncO)(&_SELECTOR)
+#define menu_selector(_SELECTOR) (cocos2d::SEL_MenuHandler)(&_SELECTOR)
+#define event_selector(_SELECTOR) (cocos2d::SEL_EventHandler)(&_SELECTOR)
+#define compare_selector(_SELECTOR) (cocos2d::SEL_Compare)(&_SELECTOR)
 
 // end of base_nodes group
 /// @}
diff --git a/loader/include/Geode/modify/Modify.hpp b/loader/include/Geode/modify/Modify.hpp
index 9347de7e..de22a8ac 100644
--- a/loader/include/Geode/modify/Modify.hpp
+++ b/loader/include/Geode/modify/Modify.hpp
@@ -19,7 +19,6 @@ if constexpr (derived##index::uuid != nullptr && (void*)base##index::uuid != (vo
 
 
 namespace geode::modifier {
-
 	template <class Derived, class Base>
 	class Modify;
 
diff --git a/loader/include/Geode/ui/ColorPickPopup.hpp b/loader/include/Geode/ui/ColorPickPopup.hpp
index 49d3f17d..6e71f0de 100644
--- a/loader/include/Geode/ui/ColorPickPopup.hpp
+++ b/loader/include/Geode/ui/ColorPickPopup.hpp
@@ -1,3 +1,5 @@
+#pragma once
+
 #include "Popup.hpp"
 #include "InputNode.hpp"
 
diff --git a/loader/include/Geode/ui/EnterLayerEvent.hpp b/loader/include/Geode/ui/EnterLayerEvent.hpp
new file mode 100644
index 00000000..c3186226
--- /dev/null
+++ b/loader/include/Geode/ui/EnterLayerEvent.hpp
@@ -0,0 +1,61 @@
+#pragma once
+
+#include "../loader/Event.hpp"
+
+namespace cocos2d {
+    class CCNode;
+}
+
+namespace geode {
+    template<class T>
+    concept InheritsCCNode = std::is_base_of_v<cocos2d::CCNode, T>;
+
+    template<InheritsCCNode T>
+    class EnterLayerEvent : public Event {
+    protected:
+        std::string m_layerID;
+        T* m_layer;
+    
+    public:
+        EnterLayerEvent(
+            std::string const& layerID,
+            T* layer
+        ) : m_layerID(layerID),
+            m_layer(layer) {}
+
+        std::string getID() const {
+            return m_layerID;
+        }
+
+        T* getLayer() const {
+            return m_layer;
+        }
+    };
+
+    template<class T, class N>
+    concept InheritsEnterLayer = std::is_base_of_v<EnterLayerEvent<N>, T>;
+
+    template<class N, InheritsEnterLayer<N> T>
+	class EnterLayerEventHandler : public EventHandler<EnterLayerEvent<N>> {
+	public:
+		using Consumer = void(*)(T*);
+        
+	protected:
+		Consumer m_consumer;
+		std::optional<std::string> m_targetID;
+	
+	public:
+        PassThrough handle(EnterLayerEvent<N>* event) override {
+            if (m_targetID == event->getID()) {
+                m_consumer(static_cast<T*>(event));
+            }
+			return PassThrough::Propagate;
+		}
+
+		EnterLayerEventHandler(
+			std::optional<std::string> const& id,
+			Consumer handler
+		) : m_targetID(id),
+            m_consumer(handler) {}
+	};
+}
diff --git a/loader/src/cocos2d-ext/Layout.cpp b/loader/src/cocos2d-ext/Layout.cpp
new file mode 100644
index 00000000..ddf9473a
--- /dev/null
+++ b/loader/src/cocos2d-ext/Layout.cpp
@@ -0,0 +1,138 @@
+#include <cocos2d.h>
+#include <Geode/utils/WackyGeodeMacros.hpp>
+
+using namespace cocos2d;
+
+void RowLayout::apply(CCArray* nodes, CCSize const& availableSize) {
+    float totalWidth = .0f;
+    CCARRAY_FOREACH_B_BASE(nodes, node, CCNode*, ix) {
+        totalWidth += node->getScaledContentSize().width;
+        if (ix) {
+            totalWidth += m_gap;
+        }
+    }
+
+    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;
+    }
+    CCARRAY_FOREACH_B_TYPE(nodes, node, CCNode) {
+        auto sw = node->getScaledContentSize().width;
+        node->setPositionX(pos + sw / 2);
+        if (m_alignVertically) {
+            node->setPositionY(m_alignVertically.value());
+        }
+        pos += sw + m_gap;
+    }
+}
+
+RowLayout* RowLayout::create(
+    float gap,
+    std::optional<float> alignVertically
+) {
+    auto ret = new RowLayout;
+    ret->m_gap = gap;
+    ret->m_alignVertically = alignVertically;
+    return ret;
+}
+
+RowLayout* RowLayout::setAlignment(Alignment align) {
+    m_alignment = align;
+    return this;
+}
+
+RowLayout* RowLayout::setGap(float gap) {
+    m_gap = gap;
+    return this;
+}
+
+RowLayout* RowLayout::setAlignVertically(std::optional<float> align) {
+    m_alignVertically = align;
+    return this;
+}
+
+void ColumnLayout::apply(CCArray* nodes, CCSize const& availableSize) {
+    float totalHeight = .0f;
+    CCARRAY_FOREACH_B_BASE(nodes, node, CCNode*, ix) {
+        totalHeight += node->getScaledContentSize().height;
+        if (ix) {
+            totalHeight += m_gap;
+        }
+    }
+
+    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;
+    }
+    CCARRAY_FOREACH_B_TYPE(nodes, node, CCNode) {
+        auto sw = node->getScaledContentSize().height;
+        node->setPositionY(pos + sw / 2);
+        if (m_alignHorizontally) {
+            node->setPositionX(m_alignHorizontally.value());
+        }
+        pos += sw + m_gap;
+    }
+}
+
+ColumnLayout* ColumnLayout::create(
+    float gap,
+    std::optional<float> alignHorizontally
+) {
+    auto ret = new ColumnLayout;
+    ret->m_gap = gap;
+    ret->m_alignHorizontally = alignHorizontally;
+    return ret;
+}
+
+ColumnLayout* ColumnLayout::setAlignment(Alignment align) {
+    m_alignment = align;
+    return this;
+}
+
+ColumnLayout* ColumnLayout::setGap(float gap) {
+    m_gap = gap;
+    return this;
+}
+
+ColumnLayout* ColumnLayout::setAlignHorizontally(std::optional<float> align) {
+    m_alignHorizontally = align;
+    return this;
+}
+
+void GridLayout::apply(CCArray* nodes, CCSize const& availableSize) {
+    // todo
+}
+
+GridLayout* GridLayout::create(
+    std::optional<size_t> rowSize,
+    GridAlignment alignment,
+    GridDirection direction
+) {
+    auto ret = new GridLayout;
+    ret->m_rowSize = rowSize;
+    ret->m_alignment = alignment;
+    ret->m_direction = direction;
+    return ret;
+}
+
+GridLayout* GridLayout::setDirection(GridDirection direction) {
+    m_direction = direction;
+    return this;
+}
+
+GridLayout* GridLayout::setAlignment(GridAlignment alignment) {
+    m_alignment = alignment;
+    return this;
+}
+
+GridLayout* GridLayout::setRowSize(std::optional<size_t> rowSize) {
+    m_rowSize = rowSize;
+    return this;
+}
+
diff --git a/loader/src/hooks/GeodeNodeMetadata.cpp b/loader/src/hooks/GeodeNodeMetadata.cpp
index 8bb95f69..b3bcf056 100644
--- a/loader/src/hooks/GeodeNodeMetadata.cpp
+++ b/loader/src/hooks/GeodeNodeMetadata.cpp
@@ -18,6 +18,8 @@ private:
     FieldContainer* m_fieldContainer;
     Ref<cocos2d::CCObject> m_userObject;
     std::string m_id = "";
+    std::unique_ptr<Layout> m_layout = nullptr;
+    PositionHint m_positionHint = PositionHint::Default;
 
     friend class ProxyCCNode;
     friend class cocos2d::CCNode;
@@ -102,4 +104,37 @@ CCNode* CCNode::getChildByIDRecursive(std::string const& id) {
     return nullptr;
 }
 
+void CCNode::setLayout(Layout* layout, bool apply) {
+    GeodeNodeMetadata::set(this)->m_layout.reset(layout);
+    if (apply) {
+        this->updateLayout();
+    }
+}
+
+Layout* CCNode::getLayout() {
+    return GeodeNodeMetadata::set(this)->m_layout.get();
+}
+
+void CCNode::updateLayout() {
+    if (auto layout = GeodeNodeMetadata::set(this)->m_layout.get()) {
+        // nodes with absolute position should never be rearranged
+        auto filtered = CCArray::createWithCapacity(m_pChildren->capacity());
+        CCARRAY_FOREACH_B_TYPE(m_pChildren, child, CCNode) {
+            if (child->getPositionHint() != PositionHint::Absolute) {
+                filtered->addObject(child);
+            }
+        }
+        layout->apply(filtered, m_obContentSize);
+        filtered->release();
+    }
+}
+
+void CCNode::setPositionHint(PositionHint hint) {
+    GeodeNodeMetadata::set(this)->m_positionHint = hint;
+}
+
+PositionHint CCNode::getPositionHint() {
+    return GeodeNodeMetadata::set(this)->m_positionHint;
+}
+
 #pragma warning(pop)
diff --git a/loader/src/hooks/MenuLayer.cpp b/loader/src/hooks/MenuLayer.cpp
index a6c741ff..a5bec5bf 100644
--- a/loader/src/hooks/MenuLayer.cpp
+++ b/loader/src/hooks/MenuLayer.cpp
@@ -1,4 +1,4 @@
-#include <Geode/Geode.hpp>
+#include <Geode/Bindings.hpp>
 #include <Geode/utils/WackyGeodeMacros.hpp>
 #include <Geode/ui/BasedButtonSprite.hpp>
 #include <Geode/ui/Notification.hpp>
@@ -139,77 +139,12 @@ class $modify(CustomMenuLayer, MenuLayer) {
 	bool init() {
 		if (!MenuLayer::init())
 			return false;
-		
+
 		Loader::get()->updateResourcePaths();
 
-		auto setIDSafe = +[](CCNode* node, int index, const char* id) {
-			if (auto child = getChild(node, index)) {
-				child->setID(id);
-			}
-		};
-
-		// set IDs to everything
-		this->setID("main-menu-layer");
-		setIDSafe(this, 0, "main-menu-bg");
-		getChildOfType<CCSprite>(this, 0)->setID("main-title");
-
-		if (PlatformToolbox::isControllerConnected()) {
-			getChildOfType<CCSprite>(this, 1)->setID("play-gamepad-icon");
-			getChildOfType<CCSprite>(this, 2)->setID("editor-gamepad-icon");
-			getChildOfType<CCSprite>(this, 3)->setID("icon-kit-gamepad-icon");
-
-			getChildOfType<CCSprite>(this, 4)->setID("settings-gamepad-icon");
-			getChildOfType<CCSprite>(this, 5)->setID("mouse-gamepad-icon");
-			getChildOfType<CCSprite>(this, 6)->setID("click-gamepad-icon");
-
-			getChildOfType<CCLabelBMFont>(this, 0)->setID("mouse-gamepad-label");
-			getChildOfType<CCLabelBMFont>(this, 1)->setID("click-gamepad-label");
-
-			getChildOfType<CCLabelBMFont>(this, 2)->setID("player-username");
-		} else {
-			getChildOfType<CCLabelBMFont>(this, 0)->setID("player-username");
-		}
-		if (auto menu = getChildOfType<CCMenu>(this, 0)) {
-			menu->setID("main-menu");
-			setIDSafe(menu, 0, "play-button");
-			setIDSafe(menu, 1, "icon-kit-button");
-			setIDSafe(menu, 2, "editor-button");
-			setIDSafe(menu, 3, "profile-button");
-		}
-		if (auto menu = getChildOfType<CCMenu>(this, 1)) {
-			menu->setID("bottom-menu");
-			setIDSafe(menu, 0, "achievements-button");
-			setIDSafe(menu, 1, "settings-button");
-			setIDSafe(menu, 2, "stats-button");
-			setIDSafe(menu, 3, "newgrounds-button");
-			setIDSafe(menu, -1,"daily-chest-button");
-		}
-		if (auto menu = getChildOfType<CCMenu>(this, 2)) {
-			menu->setID("social-media-menu");
-			setIDSafe(menu, 0, "robtop-logo-button");
-			setIDSafe(menu, 1, "facebook-button");
-			setIDSafe(menu, 2, "twitter-button");
-			setIDSafe(menu, 3, "youtube-button");
-		}
-		if (auto menu = getChildOfType<CCMenu>(this, 3)) {
-			menu->setID("more-games-menu");
-			setIDSafe(menu, 0, "more-games-button");
-			setIDSafe(menu, 1, "close-button");
-		}
-
 		auto winSize = CCDirector::sharedDirector()->getWinSize();
 
 		// add geode button
-		auto bottomMenu = static_cast<CCMenu*>(this->getChildByID("bottom-menu"));
-
-		// keep chest in the same position
-		auto chest = bottomMenu->getChildByID("daily-chest-button");
-		if (chest) {
-			chest->retain();
-			chest->removeFromParent();
-		}
-		
-		auto y = getChild(bottomMenu, 0)->getPositionY();
 		
 		g_geodeButton = SafeCreate<CCSprite>()
 			.with(CircleButtonSprite::createWithSpriteFrameName(
@@ -221,21 +156,16 @@ class $modify(CustomMenuLayer, MenuLayer) {
 			.orMake<ButtonSprite>("!!");
 
 		addUpdateIcon();
+
+		auto bottomMenu = static_cast<CCMenu*>(this->getChildByID("bottom-menu"));
+
 		auto btn = CCMenuItemSpriteExtra::create(
 			g_geodeButton.data(), this, menu_selector(CustomMenuLayer::onGeode)
 		);
 		btn->setID("geode-button");
 		bottomMenu->addChild(btn);
 
-		bottomMenu->alignItemsHorizontallyWithPadding(3.f);
-
-		CCARRAY_FOREACH_B_TYPE(bottomMenu->getChildren(), node, CCNode) {
-			node->setPositionY(y);
-		}
-		if (chest) {
-			bottomMenu->addChild(chest);
-			chest->release();
-		}
+		bottomMenu->updateLayout();
 
 		if (auto node = this->getChildByID("settings-gamepad-icon")) {
 			node->setPositionX(bottomMenu->getChildByID(
@@ -286,7 +216,7 @@ class $modify(CustomMenuLayer, MenuLayer) {
 
 			Index::get()->updateIndex(updateIndexProgress);
 		}
-
+	
 		return true;
 	}
 
diff --git a/loader/src/ids/MenuLayer.cpp b/loader/src/ids/MenuLayer.cpp
new file mode 100644
index 00000000..3ee8f760
--- /dev/null
+++ b/loader/src/ids/MenuLayer.cpp
@@ -0,0 +1,81 @@
+#include <Geode/Modify.hpp>
+#include <Geode/Bindings.hpp>
+#include <Geode/utils/cocos.hpp>
+#include <Geode/ui/EnterLayerEvent.hpp>
+
+USE_GEODE_NAMESPACE();
+
+class $modify(MenuLayer) {
+    bool init() {
+		auto setIDSafe = +[](CCNode* node, int index, const char* id) -> CCNode* {
+			if (auto child = getChild(node, index)) {
+				child->setID(id);
+				return child;
+			}
+			return nullptr;
+		};
+
+		// set IDs to everything
+		this->setID("main-menu-layer");
+		setIDSafe(this, 0, "main-menu-bg");
+		getChildOfType<CCSprite>(this, 0)->setID("main-title");
+
+		if (PlatformToolbox::isControllerConnected()) {
+			getChildOfType<CCSprite>(this, 1)->setID("play-gamepad-icon");
+			getChildOfType<CCSprite>(this, 2)->setID("editor-gamepad-icon");
+			getChildOfType<CCSprite>(this, 3)->setID("icon-kit-gamepad-icon");
+
+			getChildOfType<CCSprite>(this, 4)->setID("settings-gamepad-icon");
+			getChildOfType<CCSprite>(this, 5)->setID("mouse-gamepad-icon");
+			getChildOfType<CCSprite>(this, 6)->setID("click-gamepad-icon");
+
+			getChildOfType<CCLabelBMFont>(this, 0)->setID("mouse-gamepad-label");
+			getChildOfType<CCLabelBMFont>(this, 1)->setID("click-gamepad-label");
+
+			getChildOfType<CCLabelBMFont>(this, 2)->setID("player-username");
+		} else {
+			getChildOfType<CCLabelBMFont>(this, 0)->setID("player-username");
+		}
+		if (auto menu = getChildOfType<CCMenu>(this, 0)) {
+			menu->setID("main-menu");
+			setIDSafe(menu, 0, "play-button");
+			auto iconKit = setIDSafe(menu, 1, "icon-kit-button");
+			setIDSafe(menu, 2, "editor-button");
+			
+			if (auto pfp = setIDSafe(menu, 3, "profile-button")) {
+				pfp->setPositionHint(PositionHint::Absolute);
+			}
+
+			menu->setLayout(RowLayout::create(5.f, 0.f));
+		}
+		if (auto menu = getChildOfType<CCMenu>(this, 1)) {
+			menu->setID("bottom-menu");
+			auto ach = setIDSafe(menu, 0, "achievements-button");
+			setIDSafe(menu, 1, "settings-button");
+			setIDSafe(menu, 2, "stats-button");
+			setIDSafe(menu, 3, "newgrounds-button");
+
+			if (auto dailyChest = setIDSafe(menu, -1, "daily-chest-button")) {
+				dailyChest->setPositionHint(PositionHint::Absolute);
+			}
+
+			menu->setLayout(RowLayout::create(5.f, ach->getPositionY()));
+		}
+		if (auto menu = getChildOfType<CCMenu>(this, 2)) {
+			menu->setID("social-media-menu");
+			setIDSafe(menu, 0, "robtop-logo-button");
+			setIDSafe(menu, 1, "facebook-button");
+			setIDSafe(menu, 2, "twitter-button");
+			setIDSafe(menu, 3, "youtube-button");
+		}
+		if (auto menu = getChildOfType<CCMenu>(this, 3)) {
+			menu->setID("more-games-menu");
+			setIDSafe(menu, 0, "more-games-button");
+			setIDSafe(menu, 1, "close-button");
+		}
+
+        EnterLayerEvent("MenuLayer", this).post();
+
+        return true;
+    }
+};