diff --git a/CMakeLists.txt b/CMakeLists.txt index a5798580..218f26d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,14 +15,32 @@ string(STRIP "${GEODE_VERSION}" GEODE_VERSION) # Check if version has a tag like v1.0.0-alpha string(FIND ${GEODE_VERSION} "-" GEODE_VERSION_HAS_TAG) -if (GEODE_VERSION_HAS_TAG) - string(REGEX MATCH "[a-z]+[0-9]?$" GEODE_VERSION_TAG ${GEODE_VERSION}) +if (NOT ${GEODE_VERSION_HAS_TAG} EQUAL "-1") + string(REGEX MATCH "[a-z]+(\.[0-9]+)?$" GEODE_VERSION_TAG ${GEODE_VERSION}) string(SUBSTRING "${GEODE_VERSION}" 0 ${GEODE_VERSION_HAS_TAG} GEODE_VERSION) + string(FIND ${GEODE_VERSION_TAG} "." GEODE_VERSION_TAG_HAS_NUMBER) + + # Extract tag type and number from tag + if (NOT ${GEODE_VERSION_TAG_HAS_NUMBER} EQUAL "-1") + string(SUBSTRING "${GEODE_VERSION_TAG}" 0 ${GEODE_VERSION_TAG_HAS_NUMBER} GEODE_VERSION_TAG_TYPE) + math(EXPR GEODE_VERSION_TAG_HAS_NUMBER "${GEODE_VERSION_TAG_HAS_NUMBER} + 1") + string(SUBSTRING "${GEODE_VERSION_TAG}" ${GEODE_VERSION_TAG_HAS_NUMBER} -1 GEODE_VERSION_TAG_NUMBER) + else() + set(GEODE_VERSION_TAG_TYPE "${GEODE_VERSION_TAG}") + set(GEODE_VERSION_TAG_NUMBER "") + endif() + + # Capitalize first letter of tag type + string(SUBSTRING ${GEODE_VERSION_TAG_TYPE} 0 1 FIRST_LETTER) + string(TOUPPER ${FIRST_LETTER} FIRST_LETTER) + string(REGEX REPLACE "^.(.*)" "${FIRST_LETTER}\\1" GEODE_VERSION_TAG_TYPE "${GEODE_VERSION_TAG_TYPE}") else() set(GEODE_VERSION_TAG "") + set(GEODE_VERSION_TAG_TYPE "") + set(GEODE_VERSION_TAG_NUMBER "") endif() -message(STATUS "Version: ${GEODE_VERSION}, tag: ${GEODE_VERSION_TAG}") +message(STATUS "Version: ${GEODE_VERSION}, tag: ${GEODE_VERSION_TAG} (type: ${GEODE_VERSION_TAG_TYPE}, number: ${GEODE_VERSION_TAG_NUMBER})") project(geode-sdk VERSION ${GEODE_VERSION} LANGUAGES CXX C) @@ -60,7 +78,7 @@ if (GEODE_DISABLE_FMT_CONSTEVAL) target_compile_definitions(${PROJECT_NAME} INTERFACE -DFMT_CONSTEVAL=) endif() -CPMAddPackage("gh:geode-sdk/json#2b76460") +CPMAddPackage("gh:geode-sdk/json#cef9c64") CPMAddPackage("gh:fmtlib/fmt#9.1.0") CPMAddPackage("gh:gulrak/filesystem#3e5b930") diff --git a/VERSION b/VERSION index 537aabf7..844dc4b5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0-beta \ No newline at end of file +1.0.0-beta.6 \ No newline at end of file diff --git a/bindings/GeometryDash.bro b/bindings/GeometryDash.bro index 91741b45..349db2b5 100644 --- a/bindings/GeometryDash.bro +++ b/bindings/GeometryDash.bro @@ -296,6 +296,7 @@ class CCCircleWave : cocos2d::CCNode { PAD = win 0x4; float m_currentRadius; float m_currentOpacity; + cocos2d::ccColor3B m_color; cocos2d::CCPoint m_circleCenter; int m_filled; int m_lineWidth; @@ -1120,7 +1121,7 @@ class EditorPauseLayer : CCBlockLayer, FLAlertLayerProtocol { if (!EditorUI::get()) return nullptr; auto editor = LevelEditorLayer::get(); - for (auto i = 0; i < editor->getChildrenCount(); ++i) { + for (auto i = 0u; i < editor->getChildrenCount(); ++i) { if (auto layer = cast::safe_cast<EditorPauseLayer*>(editor->getChildren()->objectAtIndex(i))) { return layer; } @@ -1154,11 +1155,13 @@ class EditorPauseLayer : CCBlockLayer, FLAlertLayerProtocol { void uncheckAllPortals(cocos2d::CCObject* sender) = win 0x74760; void onResetUnusedColors(cocos2d::CCObject* sender) = win 0x74810; void doResetUnused() = win 0x165070; + void updateSongButton() = win 0x74f10, mac 0x13e530; + void onSong(cocos2d::CCObject*) = win 0x74e70, mac 0x13e470; bool m_saved; PAD = mac 0x8, win 0x4; - CCMenuItemSpriteExtra* m_button0; - CCMenuItemSpriteExtra* m_button1; + CCMenuItemSpriteExtra* m_guidelinesOffButton; + CCMenuItemSpriteExtra* m_guidelinesOnButton; LevelEditorLayer* m_editorLayer; } @@ -2615,6 +2618,9 @@ class GameLevelManager : cocos2d::CCNode { void storeUserNames(gd::string) = win 0xa1840; gd::string userNameForUserID(int id) = win 0xa1c20; void updateUserScore() = win 0xada60; + void downloadLevel(int id, bool downloadData) = win 0xaa730; + bool hasDownloadedLevel(int id) = win 0xab830; + GJGameLevel* getSavedLevel(int id) = win 0xa2ee0; inline static GameLevelManager* get() { return GameLevelManager::sharedState(); @@ -2820,7 +2826,7 @@ class GameManager : GManager { void getGTexture(int) = mac 0x1cca40, win 0xc9a50; virtual bool init() = mac 0x1c2ec0, win 0xc4ad0; void reportAchievementWithID(char const*, int, bool) = mac 0x1c6460, win 0xc64c0; - cocos2d::CCSize* resolutionForKey(cocos2d::CCSize*, int) = mac 0x1d0b40, win 0xceca0; + cocos2d::CCSize resolutionForKey(int) = mac 0x1d0b40, win 0xceca0; virtual void update(float) = mac 0x1d0270, win 0xce440; bool isColorUnlocked(int _id, bool _type) = mac 0x1c3b90, win 0xc53f0; bool isIconUnlocked(int _id, IconType _type) = mac 0x1c35b0, win 0xc4fc0; @@ -3271,6 +3277,9 @@ class GameSoundManager : cocos2d::CCNode { void asynchronousSetup() = win 0x25520; ~GameSoundManager() = mac 0x362c00, win 0x25640; static GameSoundManager* sharedManager() = mac 0x3610f0, win 0x24800; + inline static GameSoundManager* get() { + return GameSoundManager::sharedManager(); + } cocos2d::CCDictionary* m_dictionary1; cocos2d::CCDictionary* m_dictionary2; @@ -3962,6 +3971,9 @@ class MenuLayer : cocos2d::CCLayer, FLAlertLayerProtocol, GooglePlayDelegate { void onYouTube(cocos2d::CCObject*) = win 0x1919A0; static cocos2d::CCScene* scene(bool) = mac 0x1d12d0, win 0x190720, ios 0x19e57c; static MenuLayer* node() = win 0x190550; + inline static MenuLayer* create() { + return MenuLayer::node(); + } cocos2d::CCSprite* m_googlePlaySprite; cocos2d::CCSprite* m_viewProfileInfoText; @@ -4562,7 +4574,7 @@ class PlayerObject : GameObject, AnimatedSpriteDelegate { void modeDidChange() = mac 0x22bfd0; void placeStreakPoint() = mac 0x21af90, win 0x1f95e0; void playBurstEffect() = mac 0x21c780, win 0x1f6790; - void playDeathEffect() = mac 0x225930, win 0x2efbe0; + void playDeathEffect() = mac 0x225930, win 0x1efbe0; void playDynamicSpiderRun() = mac 0x222ec0, win 0x1f9d80; void playerDestroyed(bool) = mac 0x2256d0, win 0x1efaa0; bool playerIsFalling() = mac 0x21c730, win 0x1f5d60; diff --git a/loader/CMakeLists.txt b/loader/CMakeLists.txt index cced6f1a..8470206f 100644 --- a/loader/CMakeLists.txt +++ b/loader/CMakeLists.txt @@ -1,8 +1,21 @@ cmake_minimum_required(VERSION 3.21 FATAL_ERROR) project(geode-loader VERSION ${GEODE_VERSION} LANGUAGES C CXX) -set(PROJECT_VERSION_TYPE geode::VersionTag::Beta) -set(PROJECT_VERSION_SUFFIX -beta) +if (GEODE_VERSION_TAG_TYPE) + if (GEODE_VERSION_TAG_NUMBER) + set(PROJECT_VERSION_TAG_CONSTR "geode::VersionTag(geode::VersionTag::${GEODE_VERSION_TAG_TYPE}, ${GEODE_VERSION_TAG_NUMBER})") + else() + set(PROJECT_VERSION_TAG_CONSTR "geode::VersionTag::${GEODE_VERSION_TAG_TYPE}") + endif() +else() + set(PROJECT_VERSION_TAG_CONSTR "std::nullopt") +endif() + +if (GEODE_VERSION_TAG) + set(PROJECT_VERSION_SUFFIX "-${GEODE_VERSION_TAG}") +else() + set(PROJECT_VERSION_SUFFIX "") +endif() # Package info file for internal representation configure_file(resources/mod.json.in ${CMAKE_CURRENT_SOURCE_DIR}/resources/mod.json) diff --git a/loader/include/Geode/cocos/base_nodes/CCNode.h b/loader/include/Geode/cocos/base_nodes/CCNode.h index 62916885..809f4e97 100644 --- a/loader/include/Geode/cocos/base_nodes/CCNode.h +++ b/loader/include/Geode/cocos/base_nodes/CCNode.h @@ -616,17 +616,15 @@ public: * Return an array of children * * Composing a "tree" structure is a very important feature of CCNode - * Here's a sample code of traversing children array: - * @code + * @example + * // Here's a sample code of traversing children array: * CCNode* node = NULL; * CCARRAY_FOREACH(parent->getChildren(), node) * { * node->setPosition(0,0); * } - * @endcode - * This sample code traverses all children nodes, and set theie position to (0,0) - * - * @return An array of children + * // This sample code traverses all children nodes, and set theie position to (0,0) + * @returns An array of children */ virtual CCArray* getChildren(); @@ -761,7 +759,7 @@ public: * Returns a tag that is used to identify the node easily. * * You can set tags to node then identify them easily. - * @code + * @example * #define TAG_PLAYER 1 * #define TAG_MONSTER 2 * #define TAG_BOSS 3 @@ -786,9 +784,7 @@ public: * break; * } * } - * @endcode - * - * @return A interger that identifies the node. + * @returns A interger that identifies the node. */ RT_REMOVE( virtual int getTag() const; ) /** @@ -883,6 +879,26 @@ public: */ GEODE_DLL CCNode* getChildByIDRecursive(std::string const& id); + /** + * Add a child before a specified existing child + * @param child The node to add. The node may not be a child of another + * node already + * @param before The child the node is added before of. If this is null or + * not a child of this node, the new child will be placed at the start of the + * child list + */ + GEODE_DLL void insertBefore(CCNode* child, CCNode* before); + + /** + * Add a child after an specified existing child + * @param child The node to add. The node may not be a child of another + * node already + * @param after The child the node is added after of. If this is null or + * not a child of this node, the new child will be placed at the end of the + * child list + */ + GEODE_DLL void insertAfter(CCNode* child, CCNode* after); + /** * Set an attribute on a node. Attributes are a system added by Geode, * where a node may have any sort of extra data associated with it. Used @@ -921,9 +937,14 @@ public: * has been added, call updateLayout * @param layout Layout to set to this node * @param apply Whether to call updateLayout now or not + * @param respectAnchor If true, if the target node is + * isIgnoreAnchorPointForPosition, then it is set to false and the children + * are automatically moved to match where they should be positioned. + * Visually, this should result in no difference; however, when dealing with + * CCLayers / CCMenus, this will change where the children are located * @note Geode addition */ - GEODE_DLL void setLayout(Layout* layout, bool apply = true); + GEODE_DLL void setLayout(Layout* layout, bool apply = true, bool respectAnchor = true); /** * Get the Layout for this node * @returns The current layout, or nullptr if no layout is set @@ -935,23 +956,22 @@ public: * set, nothing happens * @note Geode addition */ - GEODE_DLL void updateLayout(); - + GEODE_DLL void updateLayout(bool updateChildOrder = true); /** - * Give a hint to the current Layout about where this node should be - * positioned in it. Allows detaching the node from the current - * layout by setting position to absolute - * @param hint The hint to set - * @note The layout definitely should, but might not respect the hint - * given + * Set the layout options for this node. Layout options can be used to + * control how this node is positioned in its parent's Layout, for example + * setting the grow size for a flex layout + * @param options The layout options + * @param apply Whether to update the layout of the parent node * @note Geode addition */ - GEODE_DLL void setPositionHint(PositionHint hint); + GEODE_DLL void setLayoutOptions(LayoutOptions* options, bool apply = true); /** - * Get the current position hint for this node + * Get the layout options for this node + * @returns The current layout options, or nullptr if no options are set * @note Geode addition */ - GEODE_DLL PositionHint getPositionHint(); + GEODE_DLL LayoutOptions* getLayoutOptions(); /** * Swap two children diff --git a/loader/include/Geode/cocos/base_nodes/Layout.hpp b/loader/include/Geode/cocos/base_nodes/Layout.hpp index 1367986c..afc1d19b 100644 --- a/loader/include/Geode/cocos/base_nodes/Layout.hpp +++ b/loader/include/Geode/cocos/base_nodes/Layout.hpp @@ -18,136 +18,334 @@ class CCNode; * RowLayout, ColumnLayout, and GridLayout, but if you need a different kind * of layout you can inherit from the Layout class. */ -class Layout { +class GEODE_DLL Layout { +protected: + static CCArray* getNodesToPosition(CCNode* forNode); + 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 + * @param on Node to apply the layout on. Position's the node's children + * according to the layout. The content size of the node should be + * respected as a boundary the layout shouldn't overflow. The node may be + * rescaled to better fit its contents */ - virtual void apply(CCArray* nodes, CCSize const& availableSize) = 0; + virtual void apply(CCNode* on) = 0; + + virtual ~Layout() = default; +}; + +class GEODE_DLL LayoutOptions { +public: + virtual ~LayoutOptions() = default; }; /** - * Determines how a node should be positioned within its parent, if that - * parent has an automatically positioning layout + * The direction of an AxisLayout */ -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, +enum class Axis { + Row, + Column, }; /** - * Specifies the alignment of something + * Specifies the alignment of something in an AxisLayout */ -enum class Alignment { - Begin, +enum class AxisAlignment { + // Align items to the start + // |ooo......| + Start, + // All items are centered + // |...ooo...| Center, + // Align items to the end + // |......ooo| End, + // Each item gets the same portion from the layout (disregards gap) + // |.o..o..o.| + Even, +}; + +constexpr float AXISLAYOUT_DEFAULT_MIN_SCALE = 0.65f; +constexpr int AXISLAYOUT_DEFAULT_PRIORITY = 0; + +/** + * Options for controlling the behaviour of individual nodes in an AxisLayout + * @example + * auto node = CCNode::create(); + * // this node will have 10 units of spacing between it and the next one + * node->setLayoutOptions( + * AxisLayoutOptions::create() + * ->setNextGap(10.f) + * ); + * someNodeWithALayout->addChild(node); + */ +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; + +public: + static AxisLayoutOptions* create(); + + std::optional<bool> getAutoScale() const; + float getMaxScale() const; + float getMinScale() const; + float getRelativeScale() const; + std::optional<float> getLength() const; + std::optional<float> getPrevGap() const; + std::optional<float> getNextGap() const; + bool getBreakLine() const; + bool getSameLine() const; + int getScalePriority() const; + + /** + * Set the maximum scale this node can be if it's contained in an + * auto-scaled layout. Default is 1 + */ + 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 + */ + AxisLayoutOptions* setMinScale(float scale); + + /** + * Set the relative scale of this node compared to other nodes if it's + * contained in an auto-scaled layout. Default is 1 + */ + AxisLayoutOptions* setRelativeScale(float scale); + + /** + * Set auto-scaling for this node, overriding the layout's auto-scale + * setting. If nullopt, the layout's auto-scale options will be used + */ + AxisLayoutOptions* setAutoScale(std::optional<bool> enabled); + + /** + * Set an absolute length for this node. If nullopt, the length will be + * dynamically calculated based on content size + */ + AxisLayoutOptions* setLength(std::optional<float> length); + + /** + * Override the default gap in the layout between this node and the + * previous one. If nullopt, the default gap of the layout will be used + */ + AxisLayoutOptions* setPrevGap(std::optional<float> gap); + + /** + * Override the default gap in the layout between this node and the next + * one. If nullopt, the default gap of the layout will be used + */ + AxisLayoutOptions* setNextGap(std::optional<float> gap); + + /** + * If enabled, the node will always cause a growable axis layout to break + * into a new line even if the current line could've fit the next node + */ + AxisLayoutOptions* setBreakLine(bool enable); + + /** + * If enabled, the node will be forced to be on the same line as the + * previous node even if doing this would overflow + */ + AxisLayoutOptions* setSameLine(bool enable); + + /** + * Set the scale priority of this node. Nodes with higher priority will be + * scaled down first before nodes with lower priority when an auto-scaled + * layout attempts to fit its contents. Default is + * AXISLAYOUT_DEFAULT_PRIORITY + * @note For optimal performance, the priorities should all be close to + * each other with no gaps + */ + AxisLayoutOptions* setScalePriority(int priority); +}; + +/** + * A multi-purpose dynamic layout for arranging nodes along an axis. Can be + * used to arrange nodes in a single line, a grid, or a flex layout. The + * RowLayout and ColumnLayout classes function as simple thin wrappers over + * AxisLayout. The positioning of individual nodes in the layout can be + * further controlled using AxisLayoutOptions + * @warning Calculating layouts can get increasingly expensive for large + * amounts of child nodes being fit into a small space - while this should + * never prove a real performance concern as most layouts only have a few + * hundred children at the very most, be aware that you probably shouldn't + * call CCNode::updateLayout every frame for a menu with thousands of children + * @example + * auto menu = CCMenu::create(); + * // The menu's children will be arranged horizontally, unless they overflow + * // the content size width in which case a new line will be inserted and + * // aligned to the left. The menu automatically will automatically grow in + * // height to fit all the rows + * menu->setLayout( + * RowLayout::create() + * ->setGap(10.f) + * ->setGrowCrossAxis(true) + * ->setAxisAlignment(AxisAlignment::Start) + * ); + * menu->setContentSize({ 200.f, 0.f }); + * menu->addChild(...); + * menu->updateLayout(); + */ +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; + + 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 + ) const; + + AxisLayout(Axis); + +public: + /** + * Create a new AxisLayout. 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 AxisLayout for + * what options you can customize for the layout + * @param axis The direction of the layout + * @note For convenience, you can use the RowLayout and ColumnLayout + * classes, which are just thin wrappers over AxisLayout + * @returns Created AxisLayout + */ + static AxisLayout* create(Axis axis = Axis::Row); + + void apply(CCNode* on) override; + + Axis getAxis() const; + AxisAlignment getAxisAlignment() const; + AxisAlignment getCrossAxisAlignment() const; + AxisAlignment getCrossAxisLineAlignment() const; + float getGap() const; + bool getAxisReverse() const; + bool getCrossAxisReverse() const; + bool getAutoScale() const; + bool getGrowCrossAxis() const; + bool getCrossAxisOverflow() const; + + AxisLayout* setAxis(Axis axis); + /** + * Sets where to align the target node's children on the main axis (X-axis + * for Row, Y-axis for Column) + */ + AxisLayout* setAxisAlignment(AxisAlignment align); + /** + * Sets where to align the target node's children on the cross-axis (Y-axis + * for Row, X-axis for Column) + */ + AxisLayout* setCrossAxisAlignment(AxisAlignment align); + /** + * Sets where to align the target node's children on the cross-axis for + * each row (Y-axis for Row, X-axis for Column) + */ + AxisLayout* setCrossAxisLineAlignment(AxisAlignment align); + /** + * The spacing between the children of the node this layout applies to. + * Measured as the space between their edges, not centres. Does not apply + * on the main / cross axis if their alignment is AxisAlignment::Even + */ + AxisLayout* setGap(float gap); + /** + * Whether to reverse the direction of the children in this layout or not + */ + AxisLayout* setAxisReverse(bool reverse); + /** + * Whether to reverse the direction of the rows on the cross-axis or not + */ + AxisLayout* setCrossAxisReverse(bool reverse); + /** + * If enabled, then the layout may scale the target's children if they are + * about to overflow. Assumes that all the childrens' intended scale is 1 + */ + AxisLayout* setAutoScale(bool enable); + /** + * If true, if the main axis overflows extra nodes will be placed on new + * rows/columns on the cross-axis + */ + AxisLayout* setGrowCrossAxis(bool expand); + /** + * If true, the cross-axis content size of the target node will be + * automatically adjusted to fit the children + */ + AxisLayout* setCrossAxisOverflow(bool allow); }; /** * Simple layout for arranging nodes in a row (horizontal line) */ -class GEODE_DLL RowLayout : public Layout { +class GEODE_DLL RowLayout : public AxisLayout { protected: - Alignment m_alignment = Alignment::Center; - std::optional<float> m_alignVertically; - float m_gap; + RowLayout(); 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 + * 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( - float gap = 5.f, - std::optional<float> alignVertically = std::nullopt - ); - - RowLayout* setAlignment(Alignment align); - RowLayout* setGap(float gap); - RowLayout* setAlignVertically(std::optional<float> align); + 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: - Alignment m_alignment = Alignment::Center; - std::optional<float> m_alignHorizontally; - float m_gap; + ColumnLayout(); 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); + /** + * Create a new ColumnLayout. 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 ColumnLayout + */ + static ColumnLayout* create(); }; NS_CC_END diff --git a/loader/include/Geode/cocos/cocoa/CCArray.h b/loader/include/Geode/cocos/cocoa/CCArray.h index 8ce4f710..f6d53a8b 100644 --- a/loader/include/Geode/cocos/cocoa/CCArray.h +++ b/loader/include/Geode/cocos/cocoa/CCArray.h @@ -234,13 +234,11 @@ public: void fastRemoveObject(CCObject* object); /** Fast way to remove an element with a certain index */ void fastRemoveObjectAtIndex(unsigned int index); - - RT_ADD( - /** - * Rob modification - * Fast way to remove an element with a certain index */ - void fastRemoveObjectAtIndexNew(unsigned int index); - ); + /** + * Fast way to remove an element with a certain index + * @note RobTop addition + */ + void fastRemoveObjectAtIndexNew(unsigned int index); // Rearranging Content @@ -263,6 +261,13 @@ public: */ virtual CCObject* copyWithZone(CCZone* pZone); + /** + * Creates a shallow copy of this array, aka only clones the pointers to + * the array members and not the members themselves + * @returns New array with same members + */ + GEODE_DLL CCArray* shallowCopy(); + /* override functions */ virtual void acceptVisitor(CCDataVisitor &visitor); diff --git a/loader/include/Geode/cocos/cocoa/CCGeometry.h b/loader/include/Geode/cocos/cocoa/CCGeometry.h index 2e31ccf3..3783b140 100644 --- a/loader/include/Geode/cocos/cocoa/CCGeometry.h +++ b/loader/include/Geode/cocos/cocoa/CCGeometry.h @@ -360,6 +360,13 @@ public: inline bool equals(const CCSize& target) const { return (fabs(this->width - target.width) < FLT_EPSILON) && (fabs(this->height - target.height) < FLT_EPSILON); } + /** + * Get the aspect ratio of this CCSize + * @note Geode addition + */ + inline float aspect() const { + return this->width / this->height; + } }; // alk cont diff --git a/loader/include/Geode/cocos/platform/win32/CCEGLView.h b/loader/include/Geode/cocos/platform/win32/CCEGLView.h index 59c12c73..a8a7b838 100644 --- a/loader/include/Geode/cocos/platform/win32/CCEGLView.h +++ b/loader/include/Geode/cocos/platform/win32/CCEGLView.h @@ -96,18 +96,35 @@ public: */ static CCEGLView* sharedOpenGLView(); + /** + * @note Geode addition + */ static GEODE_DLL CCEGLView* get(); - RT_ADD( static CCEGLView* create(const gd::string&); ) + /** + * @note RobTop addition + */ + static CCEGLView* create(const gd::string&); - RT_ADD( - //actually this is my function but i dont wanna make a new macro for it - inline CCPoint getMousePosition() { return { m_fMouseX, m_fMouseY }; } + /** + * @note Geode addition + */ + inline CCPoint getMousePosition() { return { m_fMouseX, m_fMouseY }; } - void toggleFullScreen(bool fullscreen); + /** + * @note RobTop addition + */ + void toggleFullScreen(bool fullscreen); - GLFWwindow* getWindow(void) const; - ) + /** + * @note RobTop addition + */ + GLFWwindow* getWindow(void) const; + + /** + * @note RobTop addition + */ + CCSize getDisplaySize(); protected: static CCEGLView* s_pEglView; diff --git a/loader/include/Geode/loader/Log.hpp b/loader/include/Geode/loader/Log.hpp index 8681efaf..38c88627 100644 --- a/loader/include/Geode/loader/Log.hpp +++ b/loader/include/Geode/loader/Log.hpp @@ -58,6 +58,44 @@ namespace geode { return buf.str(); } + // todo: maybe add a debugParse function for these? + + template <class T> + requires requires(T t) { + parse(t); + } + std::string parse(std::optional<T> const& thing) { + if (thing.has_value()) { + return "opt(" + parse(thing.value()) + ")"; + } + return "nullopt"; + } + + template <class A, class B> + requires requires(A a, B b) { + parse(a); + parse(b); + } + std::string parse(std::pair<A, B> const& thing) { + return "(" + parse(thing.first) + ", " + parse(thing.second) + ")"; + } + + template <class... T, std::size_t... Is> + std::string parseTupleImpl(std::tuple<T...> const& tuple, std::index_sequence<Is...>) { + std::string ret = "("; + ((ret += (Is == 0 ? "" : ", ") + parse(std::get<Is>(tuple))), ...); + ret += ")"; + return ret; + } + + template <class... T> + requires requires(T... t) { + (parse(t), ...); + } + std::string parse(std::tuple<T...> const& tuple) { + return parseTupleImpl(tuple, std::index_sequence_for<T...> {}); + } + // Log component system struct GEODE_DLL ComponentTrait { diff --git a/loader/include/Geode/platform/windows.hpp b/loader/include/Geode/platform/windows.hpp index 5af71b81..33dbe263 100644 --- a/loader/include/Geode/platform/windows.hpp +++ b/loader/include/Geode/platform/windows.hpp @@ -75,6 +75,10 @@ namespace geode::cast { std::is_polymorphic_v<std::remove_pointer_t<Before>>, "Input is not a polymorphic type" ); + if (!ptr) { + return After(); + } + auto basePtr = dynamic_cast<void*>(ptr); auto vftable = *reinterpret_cast<VftableType**>(basePtr); diff --git a/loader/include/Geode/ui/BasedButtonSprite.hpp b/loader/include/Geode/ui/BasedButtonSprite.hpp index 4aba6218..3704b600 100644 --- a/loader/include/Geode/ui/BasedButtonSprite.hpp +++ b/loader/include/Geode/ui/BasedButtonSprite.hpp @@ -2,6 +2,8 @@ #include <cocos2d.h> +#pragma warning(disable: 4275) + namespace geode { enum class CircleBaseSize { Tiny = 0, // Equivalent to the tiny delete button diff --git a/loader/include/Geode/utils/JsonValidation.hpp b/loader/include/Geode/utils/JsonValidation.hpp index eafc44df..188631b9 100644 --- a/loader/include/Geode/utils/JsonValidation.hpp +++ b/loader/include/Geode/utils/JsonValidation.hpp @@ -77,7 +77,7 @@ namespace geode { struct JsonMaybeObject; struct JsonMaybeValue; - struct JsonMaybeSomething { + struct GEODE_DLL JsonMaybeSomething { protected: JsonChecker& m_checker; json::Value& m_json; @@ -87,29 +87,29 @@ namespace geode { friend struct JsonMaybeObject; friend struct JsonMaybeValue; - GEODE_DLL void setError(std::string const& error); + void setError(std::string const& error); public: - GEODE_DLL json::Value& json(); + json::Value& json(); - GEODE_DLL JsonMaybeSomething( + JsonMaybeSomething( JsonChecker& checker, json::Value& json, std::string const& hierarchy, bool hasValue ); - GEODE_DLL bool isError() const; - GEODE_DLL std::string getError() const; + bool isError() const; + std::string getError() const; - GEODE_DLL operator bool() const; + operator bool() const; }; - struct JsonMaybeValue : public JsonMaybeSomething { + struct GEODE_DLL JsonMaybeValue : public JsonMaybeSomething { bool m_inferType = true; - GEODE_DLL JsonMaybeValue( + JsonMaybeValue( JsonChecker& checker, json::Value& json, std::string const& hierarchy, bool hasValue ); - GEODE_DLL JsonMaybeSomething& self(); + JsonMaybeSomething& self(); template <json::Type T> JsonMaybeValue& as() { @@ -124,7 +124,7 @@ namespace geode { return *this; } - GEODE_DLL JsonMaybeValue& array(); + JsonMaybeValue& array(); template <json::Type... T> JsonMaybeValue& asOneOf() { @@ -231,7 +231,7 @@ namespace geode { return T(); } - GEODE_DLL JsonMaybeObject obj(); + JsonMaybeObject obj(); template <class T> struct Iterator { @@ -257,46 +257,46 @@ namespace geode { } }; - GEODE_DLL JsonMaybeValue at(size_t i); + JsonMaybeValue at(size_t i); - GEODE_DLL Iterator<JsonMaybeValue> iterate(); + Iterator<JsonMaybeValue> iterate(); - GEODE_DLL Iterator<std::pair<std::string, JsonMaybeValue>> items(); + Iterator<std::pair<std::string, JsonMaybeValue>> items(); }; - struct JsonMaybeObject : JsonMaybeSomething { + struct GEODE_DLL JsonMaybeObject : JsonMaybeSomething { std::set<std::string> m_knownKeys; - GEODE_DLL JsonMaybeObject( + JsonMaybeObject( JsonChecker& checker, json::Value& json, std::string const& hierarchy, bool hasValue ); - GEODE_DLL JsonMaybeSomething& self(); + JsonMaybeSomething& self(); - GEODE_DLL void addKnownKey(std::string const& key); + void addKnownKey(std::string const& key); - GEODE_DLL json::Value& json(); + json::Value& json(); - GEODE_DLL JsonMaybeValue emptyValue(); + JsonMaybeValue emptyValue(); - GEODE_DLL JsonMaybeValue has(std::string const& key); + JsonMaybeValue has(std::string const& key); - GEODE_DLL JsonMaybeValue needs(std::string const& key); + JsonMaybeValue needs(std::string const& key); - GEODE_DLL void checkUnknownKeys(); + void checkUnknownKeys(); }; - struct JsonChecker { + struct GEODE_DLL JsonChecker { std::variant<std::monostate, std::string> m_result; json::Value& m_json; - GEODE_DLL JsonChecker(json::Value& json); + JsonChecker(json::Value& json); - GEODE_DLL bool isError() const; + bool isError() const; - GEODE_DLL std::string getError() const; + std::string getError() const; - GEODE_DLL JsonMaybeValue root(std::string const& hierarchy); + JsonMaybeValue root(std::string const& hierarchy); }; } diff --git a/loader/include/Geode/utils/VersionInfo.hpp b/loader/include/Geode/utils/VersionInfo.hpp index f94472ca..d9df8c88 100644 --- a/loader/include/Geode/utils/VersionInfo.hpp +++ b/loader/include/Geode/utils/VersionInfo.hpp @@ -14,24 +14,73 @@ namespace geode { }; /** - * A version label, like v1.0.0-alpha or v2.3.4-prerelease. Purely semantic, - * and not used in comparisons; so for example v1.0.0-alpha == v1.0.0. + * A version label, like v1.0.0-alpha or v2.3.4-prerelease. Limited to these + * options; arbitary identifiers are not supported. Additional numbering + * may be added after the identifier, such as v1.0.0-beta.1 */ - enum class VersionTag { - Alpha, - Beta, - Prerelease, + struct VersionTag { + enum { + Alpha, + Beta, + Prerelease, + } value; + std::optional<size_t> number; + + using Type = decltype(value); + + constexpr VersionTag(Type const& value) : value(value) {} + constexpr VersionTag(Type const& value, std::optional<size_t> number) + : value(value), number(number) {} + + constexpr bool operator==(VersionTag const& other) const { + return value == other.value && number == other.number; + } + constexpr bool operator<(VersionTag const& other) const { + if (value == other.value) { + if (number && other.number) return number < other.number; + if (number) return true; + if (other.number) return false; + return false; + } + return value < other.value; + } + constexpr bool operator<=(VersionTag const& other) const { + if (value == other.value) { + if (number && other.number) return number <= other.number; + if (number) return true; + if (other.number) return false; + return true; + } + return value <= other.value; + } + constexpr bool operator>(VersionTag const& other) const { + if (value == other.value) { + if (number && other.number) return number > other.number; + if (number) return true; + if (other.number) return false; + return false; + } + return value > other.value; + } + constexpr bool operator>=(VersionTag const& other) const { + if (value == other.value) { + if (number && other.number) return number >= other.number; + if (number) return true; + if (other.number) return false; + return true; + } + return value >= other.value; + } + + static Result<VersionTag> parse(std::stringstream& str); + std::string toSuffixString() const; + std::string toString() const; }; - GEODE_DLL std::optional<VersionTag> versionTagFromString(std::string const& str); - GEODE_DLL std::string versionTagToSuffixString(VersionTag tag); - GEODE_DLL std::string versionTagToString(VersionTag tag); /** - * Class representing version information. Not strictly semver, notably in - * regard to identifiers; identifiers are restricted to a few common ones, - * and are purely semantic, i.e. not used in comparisons. See VersionTag - * for details - * @class VersionInfo + * Class representing version information. Uses a limited subset of SemVer; + * identifiers are restricted to a few predefined ones, and only one + * identifier is allowed. See VersionTag for details */ class GEODE_DLL VersionInfo final { protected: @@ -78,24 +127,24 @@ namespace geode { // Apple clang does not support operator<=>! Yippee! constexpr bool operator==(VersionInfo const& other) const { - return std::tie(m_major, m_minor, m_patch) == - std::tie(other.m_major, other.m_minor, other.m_patch); + return std::tie(m_major, m_minor, m_patch, m_tag) == + std::tie(other.m_major, other.m_minor, other.m_patch, other.m_tag); } constexpr bool operator<(VersionInfo const& other) const { - return std::tie(m_major, m_minor, m_patch) < - std::tie(other.m_major, other.m_minor, other.m_patch); + return std::tie(m_major, m_minor, m_patch, m_tag) < + std::tie(other.m_major, other.m_minor, other.m_patch, other.m_tag); } constexpr bool operator<=(VersionInfo const& other) const { - return std::tie(m_major, m_minor, m_patch) <= - std::tie(other.m_major, other.m_minor, other.m_patch); + return std::tie(m_major, m_minor, m_patch, m_tag) <= + std::tie(other.m_major, other.m_minor, other.m_patch, other.m_tag); } constexpr bool operator>(VersionInfo const& other) const { - return std::tie(m_major, m_minor, m_patch) > - std::tie(other.m_major, other.m_minor, other.m_patch); + return std::tie(m_major, m_minor, m_patch, m_tag) > + std::tie(other.m_major, other.m_minor, other.m_patch, other.m_tag); } constexpr bool operator>=(VersionInfo const& other) const { - return std::tie(m_major, m_minor, m_patch) >= - std::tie(other.m_major, other.m_minor, other.m_patch); + return std::tie(m_major, m_minor, m_patch, m_tag) >= + std::tie(other.m_major, other.m_minor, other.m_patch, other.m_tag); } std::string toString(bool includeTag = true) const; diff --git a/loader/include/Geode/utils/cocos.hpp b/loader/include/Geode/utils/cocos.hpp index 401400fb..c16280e7 100644 --- a/loader/include/Geode/utils/cocos.hpp +++ b/loader/include/Geode/utils/cocos.hpp @@ -410,16 +410,31 @@ namespace geode::cocos { * or nullptr if index exceeds bounds */ template <class Type = cocos2d::CCNode> - static Type* getChildOfType(cocos2d::CCNode* node, size_t index) { + static Type* getChildOfType(cocos2d::CCNode* node, int index) { size_t indexCounter = 0; - for (size_t i = 0; i < node->getChildrenCount(); ++i) { - auto obj = cast::typeinfo_cast<Type*>(node->getChildren()->objectAtIndex(i)); - if (obj != nullptr) { - if (indexCounter == index) { - return obj; + // start from end for negative index + if (index < 0) { + index = -index - 1; + for (size_t i = node->getChildrenCount() - 1; i >= 0; i--) { + auto obj = cast::typeinfo_cast<Type*>(node->getChildren()->objectAtIndex(i)); + if (obj != nullptr) { + if (indexCounter == index) { + return obj; + } + ++indexCounter; + } + } + } + else { + for (size_t i = 0; i < node->getChildrenCount(); i++) { + auto obj = cast::typeinfo_cast<Type*>(node->getChildren()->objectAtIndex(i)); + if (obj != nullptr) { + if (indexCounter == index) { + return obj; + } + ++indexCounter; } - ++indexCounter; } } @@ -548,86 +563,6 @@ namespace geode::cocos { */ GEODE_DLL bool fileExistsInSearchPaths(char const* filename); - template <typename T> - struct CCArrayIterator { - public: - CCArrayIterator(T* p) : m_ptr(p) {} - - T* m_ptr; - - auto& operator*() { - return *m_ptr; - } - - auto& operator*() const { - return *m_ptr; - } - - auto operator->() { - return m_ptr; - } - - auto operator->() const { - return m_ptr; - } - - auto& operator++() { - ++m_ptr; - return *this; - } - - auto& operator--() { - --m_ptr; - return *this; - } - - auto& operator+=(size_t val) { - m_ptr += val; - return *this; - } - - auto& operator-=(size_t val) { - m_ptr -= val; - return *this; - } - - auto operator+(size_t val) const { - return CCArrayIterator<T>(m_ptr + val); - } - - auto operator-(size_t val) const { - return CCArrayIterator<T>(m_ptr - val); - } - - auto operator-(CCArrayIterator<T> const& other) const { - return m_ptr - other.m_ptr; - } - - bool operator<(CCArrayIterator<T> const& other) const { - return m_ptr < other.m_ptr; - } - - bool operator>(CCArrayIterator<T> const& other) const { - return m_ptr > other.m_ptr; - } - - bool operator<=(CCArrayIterator<T> const& other) const { - return m_ptr <= other.m_ptr; - } - - bool operator>=(CCArrayIterator<T> const& other) const { - return m_ptr >= other.m_ptr; - } - - bool operator==(CCArrayIterator<T> const& other) const { - return m_ptr == other.m_ptr; - } - - bool operator!=(CCArrayIterator<T> const& other) const { - return m_ptr != other.m_ptr; - } - }; - inline void ccDrawColor4B(cocos2d::ccColor4B const& color) { cocos2d::ccDrawColor4B(color.r, color.g, color.b, color.a); } @@ -766,16 +701,6 @@ namespace std { return std::hash<T*>()(ref.data()); } }; - - template <typename T> - struct iterator_traits<geode::cocos::CCArrayIterator<T>> { - using difference_type = ptrdiff_t; - using value_type = T; - using pointer = T*; - using reference = T&; - using iterator_category = - std::random_access_iterator_tag; // its random access but im too lazy to implement it - }; } // more utils @@ -807,9 +732,14 @@ namespace geode::cocos { using T = std::remove_pointer_t<_Type>; public: + using value_type = T; + using iterator = T**; + using const_iterator = const T**; + CCArrayExt() : m_arr(cocos2d::CCArray::create()) {} - CCArrayExt(cocos2d::CCArray* arr) : m_arr(arr) {} + CCArrayExt(cocos2d::CCArray* arr) + : m_arr(arr) {} CCArrayExt(CCArrayExt const& a) : m_arr(a.m_arr) {} @@ -819,18 +749,26 @@ namespace geode::cocos { ~CCArrayExt() {} - auto begin() { + T** begin() const { if (!m_arr) { - return CCArrayIterator<T*>(nullptr); + return nullptr; } - return CCArrayIterator<T*>(reinterpret_cast<T**>(m_arr->data->arr)); + return reinterpret_cast<T**>(m_arr->data->arr); } - auto end() { + T** end() const { if (!m_arr) { - return CCArrayIterator<T*>(nullptr); + return nullptr; } - return CCArrayIterator<T*>(reinterpret_cast<T**>(m_arr->data->arr) + m_arr->count()); + return reinterpret_cast<T**>(m_arr->data->arr) + m_arr->count(); + } + + auto rbegin() const { + return std::reverse_iterator(this->end()); + } + + auto rend() const { + return std::reverse_iterator(this->begin()); } size_t size() const { diff --git a/loader/include/Geode/utils/ranges.hpp b/loader/include/Geode/utils/ranges.hpp index 0481285b..324abad4 100644 --- a/loader/include/Geode/utils/ranges.hpp +++ b/loader/include/Geode/utils/ranges.hpp @@ -289,23 +289,21 @@ namespace geode::utils::ranges { return member(*it); } - template <ValidConstContainer C> - struct ConstReverseWrapper { - C const& iter; + template <class C> + struct ReverseWrapper { + C iter; + + decltype(auto) begin() { + return std::rbegin(iter); + } + + decltype(auto) end() { + return std::rend(iter); + } }; - template <ValidConstContainer C> - auto begin(ConstReverseWrapper<C> const& c) { - return std::rbegin(c.iter); - } - - template <ValidConstContainer C> - auto end(ConstReverseWrapper<C> const& c) { - return std::rend(c.iter); - } - - template <ValidConstContainer C> - ConstReverseWrapper<C> reverse(C const& iter) { - return { iter }; + template <class C> + auto reverse(C&& iter) { + return ReverseWrapper<C>{std::forward<C>(iter)}; } } diff --git a/loader/resources/mod.json.in b/loader/resources/mod.json.in index b4baa56d..5953f0c6 100644 --- a/loader/resources/mod.json.in +++ b/loader/resources/mod.json.in @@ -1,5 +1,5 @@ { - "geode": "@PROJECT_VERSION@", + "geode": "@PROJECT_VERSION@@PROJECT_VERSION_SUFFIX@", "id": "geode.loader", "version": "@PROJECT_VERSION@@PROJECT_VERSION_SUFFIX@", "name": "Geode", diff --git a/loader/src/cocos2d-ext/CCArray.cpp b/loader/src/cocos2d-ext/CCArray.cpp index b863faf3..f288dfb4 100644 --- a/loader/src/cocos2d-ext/CCArray.cpp +++ b/loader/src/cocos2d-ext/CCArray.cpp @@ -16,4 +16,10 @@ void CCArray::removeFirstObject(bool bReleaseObj) { this->removeObjectAtIndex(0, bReleaseObj); } +CCArray* CCArray::shallowCopy() { + auto r = CCArray::createWithCapacity(this->capacity()); + r->addObjectsFromArray(this); + return r; +} + #pragma warning(pop) diff --git a/loader/src/cocos2d-ext/Layout.cpp b/loader/src/cocos2d-ext/Layout.cpp index c02b4cd3..e829754e 100644 --- a/loader/src/cocos2d-ext/Layout.cpp +++ b/loader/src/cocos2d-ext/Layout.cpp @@ -1,5 +1,9 @@ #include <cocos2d.h> #include <Geode/utils/cocos.hpp> +#include <Geode/utils/ranges.hpp> +#include <Geode/loader/Log.hpp> +#include <Geode/binding/CCMenuItemSpriteExtra.hpp> +#include <Geode/binding/CCMenuItemToggler.hpp> USE_GEODE_NAMESPACE(); @@ -7,155 +11,922 @@ USE_GEODE_NAMESPACE(); void CCNode::swapChildIndices(CCNode* first, CCNode* second) { m_pChildren->exchangeObject(first, second); + std::swap(first->m_nZOrder, second->m_nZOrder); + std::swap(first->m_uOrderOfArrival, second->m_uOrderOfArrival); } -void RowLayout::apply(CCArray* nodes, CCSize const& availableSize) { - float totalWidth = .0f; +void CCNode::insertBefore(CCNode* child, CCNode* before) { + this->addChild(child); + if ( + (before && m_pChildren->containsObject(before)) || + (before = static_cast<CCNode*>(m_pChildren->firstObject())) + ) { + child->setZOrder(before->getZOrder()); + child->setOrderOfArrival(before->getOrderOfArrival() - 1); + } +} + +void CCNode::insertAfter(CCNode* child, CCNode* after) { + this->addChild(child); + if (m_pChildren->containsObject(after)) { + child->setZOrder(after->getZOrder()); + child->setOrderOfArrival(after->getOrderOfArrival() + 1); + } +} + +CCArray* Layout::getNodesToPosition(CCNode* on) { + if (!on->getChildren()) { + return CCArray::create(); + } + return on->getChildren()->shallowCopy(); +} + +static AxisLayoutOptions const* axisOpts(CCNode* node) { + if (!node) return nullptr; + return typeinfo_cast<AxisLayoutOptions*>(node->getLayoutOptions()); +} + +static bool isOptsBreakLine(AxisLayoutOptions const* opts) { + if (opts) { + return opts->getBreakLine(); + } + return false; +} + +static bool isOptsSameLine(AxisLayoutOptions const* opts) { + if (opts) { + return opts->getSameLine(); + } + return false; +} + +static int optsScalePrio(AxisLayoutOptions const* opts) { + if (opts) { + return opts->getScalePriority(); + } + return AXISLAYOUT_DEFAULT_PRIORITY; +} + +static float optsMinScale(AxisLayoutOptions const* opts) { + if (opts) { + return opts->getMinScale(); + } + return AXISLAYOUT_DEFAULT_MIN_SCALE; +} + +static float optsMaxScale(AxisLayoutOptions const* opts) { + if (opts) { + return opts->getMaxScale(); + } + return 1.f; +} + +static float optsRelScale(AxisLayoutOptions const* opts) { + if (opts) { + return opts->getRelativeScale(); + } + return 1.f; +} + +static float scaleByOpts(AxisLayoutOptions const* opts, float scale, int prio, bool squishMode) { + if (prio > optsScalePrio(opts)) { + return optsMaxScale(opts) * optsRelScale(opts); + } + // otherwise if it matches scale it down by the factor + else if (!squishMode && prio == optsScalePrio(opts)) { + auto trueScale = scale; + auto min = optsMinScale(opts); + auto max = optsMaxScale(opts); + if (trueScale < min) { + trueScale = min; + } + if (trueScale > max) { + trueScale = max; + } + return trueScale * optsRelScale(opts); + } + // otherwise it's been scaled down to minimum + else { + return optsMinScale(opts) * optsRelScale(opts); + } +} + +struct AxisLayout::Row : public CCObject { + float nextOverflowScaleDownFactor; + float nextOverflowSquishFactor; + float axisLength; + float crossLength; + float axisEndsLength; + + // all layout calculations happen within a single frame so no Ref needed + CCArray* nodes; + + // calculated values for scale, squish and prio to fit the nodes in this + // row when positioning + float scale; + float squish; + float prio; + + Row( + float scaleFactor, + float squishFactor, + float axisLength, + float crossLength, + float axisEndsLength, + CCArray* nodes, + float scale, + float squish, + float prio + ) : nextOverflowScaleDownFactor(scaleFactor), + nextOverflowSquishFactor(squishFactor), + axisLength(axisLength), + crossLength(crossLength), + axisEndsLength(axisEndsLength), + nodes(nodes), + scale(scale), + squish(squish), + prio(prio) + { + this->autorelease(); + } +}; + +struct AxisPosition { + float axisLength; + float axisAnchor; + float crossLength; + float crossAnchor; +}; + +static AxisPosition nodeAxis(CCNode* node, Axis axis, float scale) { + auto scaledSize = node->getScaledContentSize() * scale; + std::optional<float> axisLength = std::nullopt; + if (auto opts = axisOpts(node)) { + axisLength = opts->getLength(); + } + // CCMenuItemToggler is a common quirky class + if (auto toggle = typeinfo_cast<CCMenuItemToggler*>(node)) { + scaledSize = toggle->m_offButton->getScaledContentSize(); + } + auto anchor = node->getAnchorPoint(); + if (axis == Axis::Row) { + return AxisPosition { + .axisLength = axisLength.value_or(scaledSize.width), + .axisAnchor = anchor.x, + .crossLength = scaledSize.height, + .crossAnchor = anchor.y, + }; + } + else { + return AxisPosition { + .axisLength = axisLength.value_or(scaledSize.height), + .axisAnchor = anchor.y, + .crossLength = scaledSize.width, + .crossAnchor = anchor.x, + }; + } +} + +float AxisLayout::nextGap(AxisLayoutOptions const* now, AxisLayoutOptions const* next) const { + std::optional<float> gap; + if (now) { + gap = now->getNextGap(); + } + if (next && (!gap || gap.value() < next->getPrevGap())) { + gap = next->getPrevGap(); + } + return gap.value_or(m_gap); +} + +bool AxisLayout::shouldAutoScale(AxisLayoutOptions const* opts) const { + if (!opts) return m_autoScale; + return opts->getAutoScale().value_or(m_autoScale); +} + +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; + } + 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); + + 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); + auto pos = nodeAxis(node, m_axis, nodeScale * squish); + auto squishPos = nodeAxis(node, m_axis, scaleByOpts(opts, scale, prio, true)); + if (prio == optsScalePrio(opts)) { + nextAxisScalableLength += pos.axisLength; + } + else { + nextAxisUnscalableLength += pos.axisLength; + } + // if multiple rows are allowed and this row is full, time for the + // next row + // also force at least one object to be added to this row, because if + // it's too large for this row it's gonna be too large for all rows + if ( + m_growCrossAxis && (( + (nextAxisScalableLength + nextAxisUnscalableLength > available.axisLength) && + ix != 0 && + !isOptsSameLine(opts) + )) + ) { + break; + } + if (nodes != res) { + res->addObject(node); + } + if (ix) { + auto gap = nextGap(prev, opts); + // if we've exhausted all priority scale options, scale gap too + if (prio == minMaxPrios.first) { + nextAxisScalableLength += gap * scale * squish; + axisLength += gap * scale * squish; + axisUnsquishedLength += gap * scale; + } + else { + nextAxisUnscalableLength += gap * squish; + axisLength += gap * squish; + axisUnsquishedLength += gap; + } + } + axisLength += pos.axisLength; + axisUnsquishedLength += squishPos.axisLength; + // squishing doesn't affect cross length, that's done separately + if (pos.crossLength / squish > crossLength) { + crossLength = pos.crossLength / squish; + } + prev = opts; + if (m_growCrossAxis && isOptsBreakLine(opts)) { + break; + } + ix++; + } + }; + + fit(nodes); + + // whoops! removing objects from a CCArray while iterating is totes potes UB + for (int i = 0; i < res->count(); i++) { + nodes->removeFirstObject(); + } + + auto scaleDownFactor = scale - .025f; + 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 - .025f, minMaxPrios + )) { + scale -= .025f; + } + else { + squish = available.axisLength / (axisUnsquishedLength + .01f) * squish; + } + 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 + // todo: make this calculation more smart to avoid so much unnecessary recursion + 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 the same as before, then we've + // entered an infinite loop + crossScaleDownFactor == scale + ) { + // is there still some lower priority nodes we could try scaling? + if (prio > minMaxPrios.first) { + while (true) { + prio -= 1; + auto mscale = this->maxScaleForPrio(nodes, prio); + if (!mscale) { + continue; + } + scale = mscale; + break; + } + attemptRescale = true; + } + // otherwise set scale to min and squish + else { + scale = minScaleForPrio; + } + } + // otherwise scale as usual + else { + attemptRescale = true; + } + return attemptRescale; +} + +void AxisLayout::tryFitLayout( + CCNode* on, CCArray* nodes, + std::pair<int, int> const& minMaxPrios, + bool doAutoScale, + float scale, float squish, int prio +) 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; + + // fit everything into rows while possible size_t ix = 0; - for (auto& node : CCArrayExt<CCNode*>(nodes)) { - totalWidth += node->getScaledContentSize().width; + 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) { - totalWidth += m_gap; + totalRowCrossLength += m_gap; + } + if (row->axisLength > maxRowAxisLength) { + maxRowAxisLength = row->axisLength; } ix++; } + newNodes->release(); - 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; + if (!rows->count()) { + return; } - for (auto& node : CCArrayExt<CCNode*>(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; + + auto available = nodeAxis(on, m_axis, 1.f); + + 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 + ) { + if (this->canTryScalingDown( + nodes, prio, scale, crossScaleDownFactor, minMaxPrios + )) { + rows->release(); + return this->tryFitLayout( + on, nodes, + minMaxPrios, doAutoScale, + scale, squish, prio + ); } - node->setPositionX(pos + disp); - if (m_alignVertically) { - node->setPositionY(m_alignVertically.value()); + } + + // if we're still overflowing, squeeze nodes closer together + if ( + !m_allowCrossAxisOverflow && + totalRowCrossLength > available.crossLength + ) { + // 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 + ); + } + } + + // 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)) { + if (m_crossAlignment == AxisAlignment::Even) { + rowCrossPos -= rowEvenSpace / 2 + row->crossLength / 2; + } + else { + rowCrossPos -= row->crossLength * columnSquish; + } + + float rowAxisPos; + switch (m_axisAlignment) { + case AxisAlignment::Start: { + rowAxisPos = 0.f; + } break; + + case AxisAlignment::Even: { + rowAxisPos = 0.f; + } break; + + case AxisAlignment::Center: { + rowAxisPos = available.axisLength / 2 - row->axisLength / 2; + } break; + + case AxisAlignment::End: { + rowAxisPos = available.axisLength - row->axisLength; + } break; + } + + float evenSpace = available.axisLength / row->nodes->count(); + + size_t ix = 0; + AxisLayoutOptions const* prev = nullptr; + for (auto& node : CCArrayExt<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(); + } + else { + if (ix) { + if (row->prio == minMaxPrios.first) { + rowAxisPos += this->nextGap(prev, opts) * row->scale * row->squish; + } + else { + rowAxisPos += this->nextGap(prev, opts) * row->squish; + } + } + axisPos = rowAxisPos + pos.axisLength * pos.axisAnchor; + rowAxisPos += pos.axisLength - + row->axisEndsLength * row->scale * (1.f - row->squish) * 1.f / nodes->count(); + } + float crossOffset; + switch (m_crossLineAlignment) { + case AxisAlignment::Start: { + crossOffset = pos.crossLength * pos.crossAnchor; + } break; + + case AxisAlignment::Center: case AxisAlignment::Even: { + crossOffset = row->crossLength / 2 - pos.crossLength * (.5f - pos.crossAnchor); + } break; + + case AxisAlignment::End: { + crossOffset = row->crossLength - pos.crossLength * (1.f - pos.crossAnchor); + } break; + } + if (m_axis == Axis::Row) { + node->setPosition(axisPos, rowCrossPos + crossOffset); + } + else { + node->setPosition(rowCrossPos + crossOffset, axisPos); + } + prev = opts; + ix++; + } + + if (m_crossAlignment == AxisAlignment::Even) { + rowCrossPos -= rowEvenSpace / 2 - row->crossLength / 2 - + rowsEndsLength * 1.5f * row->scale * (1.f - columnSquish) * 1.f / rows->count(); + } + else { + rowCrossPos -= m_gap * columnSquish - + rowsEndsLength * 1.5f * row->scale * (1.f - columnSquish) * 1.f / rows->count(); } - 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; +void AxisLayout::apply(CCNode* on) { + auto nodes = getNodesToPosition(on); + + std::pair<int, int> minMaxPrio; + bool doAutoScale = false; + + bool first = true; + for (auto node : CCArrayExt<CCNode>(nodes)) { + int prio = 0; + if (auto opts = axisOpts(node)) { + prio = opts->getScalePriority(); + // this does cause a recheck of m_autoScale every iteration but it + // should be pretty fast and this correctly handles the situation + // where auto-scale is enabled on the layout but explicitly + // disabled on all its children + if (opts->getAutoScale().value_or(m_autoScale)) { + doAutoScale = true; + } + } + else { + if (m_autoScale) { + doAutoScale = true; + } + } + if (first) { + minMaxPrio = { prio, prio }; + first = false; + } + else { + if (prio < minMaxPrio.first) { + minMaxPrio.first = prio; + } + if (prio > minMaxPrio.second) { + minMaxPrio.second = prio; + } + } + } + + this->tryFitLayout( + on, nodes, + minMaxPrio, doAutoScale, + this->maxScaleForPrio(nodes, minMaxPrio.second), 1.f, minMaxPrio.second + ); } -RowLayout* RowLayout::setAlignment(Alignment align) { - m_alignment = align; +AxisLayout::AxisLayout(Axis axis) : m_axis(axis) {} + +Axis AxisLayout::getAxis() const { + return m_axis; +} + +AxisAlignment AxisLayout::getCrossAxisAlignment() const { + return m_crossAlignment; +} + +AxisAlignment AxisLayout::getCrossAxisLineAlignment() const { + return m_crossLineAlignment; +} + +AxisAlignment AxisLayout::getAxisAlignment() const { + return m_axisAlignment; +} + +float AxisLayout::getGap() const { + return m_gap; +} + +bool AxisLayout::getAxisReverse() const { + return m_axisReverse; +} + +bool AxisLayout::getCrossAxisReverse() const { + return m_crossReverse; +} + +bool AxisLayout::getAutoScale() const { + return m_autoScale; +} + +bool AxisLayout::getGrowCrossAxis() const { + return m_growCrossAxis; +} + +bool AxisLayout::getCrossAxisOverflow() const { + return m_allowCrossAxisOverflow; +} + +AxisLayout* AxisLayout::setAxis(Axis axis) { + m_axis = axis; return this; } -RowLayout* RowLayout::setGap(float gap) { +AxisLayout* AxisLayout::setCrossAxisAlignment(AxisAlignment align) { + m_crossAlignment = align; + return this; +} + +AxisLayout* AxisLayout::setCrossAxisLineAlignment(AxisAlignment align) { + m_crossLineAlignment = align; + return this; +} + +AxisLayout* AxisLayout::setAxisAlignment(AxisAlignment align) { + m_axisAlignment = align; + return this; +} + +AxisLayout* AxisLayout::setGap(float gap) { m_gap = gap; return this; } -RowLayout* RowLayout::setAlignVertically(std::optional<float> align) { - m_alignVertically = align; +AxisLayout* AxisLayout::setAxisReverse(bool reverse) { + m_axisReverse = reverse; return this; } -void ColumnLayout::apply(CCArray* nodes, CCSize const& availableSize) { - float totalHeight = .0f; - size_t ix = 0; - for (auto& node : CCArrayExt<CCNode*>(nodes)) { - 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; - } - for (auto& node : CCArrayExt<CCNode*>(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 = sh; break; - case Alignment::End: disp = 0.f; break; - } - node->setPositionY(pos + disp); - if (m_alignHorizontally) { - node->setPositionX(m_alignHorizontally.value()); - } - pos += sh + 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; +AxisLayout* AxisLayout::setCrossAxisReverse(bool reverse) { + m_crossReverse = reverse; return this; } -ColumnLayout* ColumnLayout::setGap(float gap) { - m_gap = gap; +AxisLayout* AxisLayout::setCrossAxisOverflow(bool fit) { + m_allowCrossAxisOverflow = fit; return this; } -ColumnLayout* ColumnLayout::setAlignHorizontally(std::optional<float> align) { - m_alignHorizontally = align; +AxisLayout* AxisLayout::setAutoScale(bool scale) { + m_autoScale = scale; 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; +AxisLayout* AxisLayout::setGrowCrossAxis(bool shrink) { + m_growCrossAxis = shrink; return this; } -GridLayout* GridLayout::setAlignment(GridAlignment alignment) { - m_alignment = alignment; +AxisLayout* AxisLayout::create(Axis axis) { + return new AxisLayout(axis); +} + +// RowLayout + +RowLayout::RowLayout() : AxisLayout(Axis::Row) {} + +RowLayout* RowLayout::create() { + return new RowLayout(); +} + +// ColumnLayout + +ColumnLayout::ColumnLayout() : AxisLayout(Axis::Column) {} + +ColumnLayout* ColumnLayout::create() { + return new ColumnLayout(); +} + +// AxisLayoutOptions + +AxisLayoutOptions* AxisLayoutOptions::create() { + return new AxisLayoutOptions(); +} + +std::optional<bool> AxisLayoutOptions::getAutoScale() const { + return m_autoScale; +} + +float AxisLayoutOptions::getMaxScale() const { + return m_maxScale; +} + +float AxisLayoutOptions::getMinScale() const { + return m_minScale; +} + +float AxisLayoutOptions::getRelativeScale() const { + return m_relativeScale; +} + +std::optional<float> AxisLayoutOptions::getLength() const { + return m_length; +} + +std::optional<float> AxisLayoutOptions::getPrevGap() const { + return m_prevGap; +} + +std::optional<float> AxisLayoutOptions::getNextGap() const { + return m_nextGap; +} + +bool AxisLayoutOptions::getBreakLine() const { + return m_breakLine; +} + +bool AxisLayoutOptions::getSameLine() const { + return m_sameLine; +} + +int AxisLayoutOptions::getScalePriority() const { + return m_scalePriority; +} + +AxisLayoutOptions* AxisLayoutOptions::setMaxScale(float scale) { + m_maxScale = scale; return this; } -GridLayout* GridLayout::setRowSize(std::optional<size_t> rowSize) { - m_rowSize = rowSize; +AxisLayoutOptions* AxisLayoutOptions::setMinScale(float scale) { + m_minScale = scale; return this; } +AxisLayoutOptions* AxisLayoutOptions::setRelativeScale(float scale) { + m_relativeScale = scale; + return this; +} + +AxisLayoutOptions* AxisLayoutOptions::setAutoScale(std::optional<bool> enabled) { + m_autoScale = enabled; + return this; +} + +AxisLayoutOptions* AxisLayoutOptions::setLength(std::optional<float> length) { + m_length = length; + return this; +} + +AxisLayoutOptions* AxisLayoutOptions::setPrevGap(std::optional<float> gap) { + m_prevGap = gap; + return this; +} + +AxisLayoutOptions* AxisLayoutOptions::setNextGap(std::optional<float> gap) { + m_nextGap = gap; + return this; +} + +AxisLayoutOptions* AxisLayoutOptions::setBreakLine(bool enable) { + m_breakLine = enable; + return this; +} + +AxisLayoutOptions* AxisLayoutOptions::setSameLine(bool enable) { + m_sameLine = enable; + return this; +} + +AxisLayoutOptions* AxisLayoutOptions::setScalePriority(int priority) { + m_scalePriority = priority; + return this; +} diff --git a/loader/src/hooks/GeodeNodeMetadata.cpp b/loader/src/hooks/GeodeNodeMetadata.cpp index 777635ab..f520a98b 100644 --- a/loader/src/hooks/GeodeNodeMetadata.cpp +++ b/loader/src/hooks/GeodeNodeMetadata.cpp @@ -20,7 +20,7 @@ private: Ref<cocos2d::CCObject> m_userObject; std::string m_id = ""; std::unique_ptr<Layout> m_layout = nullptr; - PositionHint m_positionHint = PositionHint::Default; + std::unique_ptr<LayoutOptions> m_layoutOptions = nullptr; std::unordered_map<std::string, std::any> m_attributes; friend class ProxyCCNode; @@ -118,7 +118,13 @@ CCNode* CCNode::getChildByIDRecursive(std::string const& id) { return nullptr; } -void CCNode::setLayout(Layout* layout, bool apply) { +void CCNode::setLayout(Layout* layout, bool apply, bool respectAnchor) { + if (respectAnchor && this->isIgnoreAnchorPointForPosition()) { + for (auto child : CCArrayExt<CCNode>(m_pChildren)) { + child->setPosition(child->getPosition() + this->getScaledContentSize()); + } + this->ignoreAnchorPointForPosition(false); + } GeodeNodeMetadata::set(this)->m_layout.reset(layout); if (apply) { this->updateLayout(); @@ -129,26 +135,24 @@ 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::create(); - for (auto& child : CCArrayExt<CCNode>(m_pChildren)) { - if (child->getPositionHint() != PositionHint::Absolute) { - filtered->addObject(child); - } - } - layout->apply(filtered, m_obContentSize); - filtered->release(); +void CCNode::setLayoutOptions(LayoutOptions* options, bool apply) { + GeodeNodeMetadata::set(this)->m_layoutOptions.reset(options); + if (apply && m_pParent) { + m_pParent->updateLayout(); } } -void CCNode::setPositionHint(PositionHint hint) { - GeodeNodeMetadata::set(this)->m_positionHint = hint; +LayoutOptions* CCNode::getLayoutOptions() { + return GeodeNodeMetadata::set(this)->m_layoutOptions.get(); } -PositionHint CCNode::getPositionHint() { - return GeodeNodeMetadata::set(this)->m_positionHint; +void CCNode::updateLayout(bool updateChildOrder) { + if (updateChildOrder) { + this->sortAllChildren(); + } + if (auto layout = GeodeNodeMetadata::set(this)->m_layout.get()) { + layout->apply(this); + } } void CCNode::setAttribute(std::string const& attr, std::any value) { diff --git a/loader/src/ids/AddIDs.hpp b/loader/src/ids/AddIDs.hpp index 7f00b54b..77693e91 100644 --- a/loader/src/ids/AddIDs.hpp +++ b/loader/src/ids/AddIDs.hpp @@ -33,10 +33,13 @@ void setIDs(CCNode* node, int startIndex, Args... args) { } static void switchToMenu(CCNode* node, CCMenu* menu) { + if (!node || !menu) return; + auto worldPos = node->getParent()->convertToWorldSpace(node->getPosition()); node->retain(); node->removeFromParent(); + node->setZOrder(0); menu->addChild(node); node->setPosition(menu->convertToNodeSpace(worldPos)); @@ -55,6 +58,14 @@ static void switchChildrenToMenu(CCNode* parent, CCMenu* menu, Args... args) { template <typename T, typename ...Args> static CCMenu* detachAndCreateMenu(CCNode* parent, const char* menuID, Layout* layout, T first, Args... args) { + if (!first) { + auto menu = CCMenu::create(); + menu->setID(menuID); + menu->setLayout(layout); + parent->addChild(menu); + return menu; + } + auto oldMenu = first->getParent(); first->retain(); @@ -64,14 +75,25 @@ 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); + first->setZOrder(0); newMenu->addChild(first); first->release(); (switchToMenu(args, newMenu), ...); + + newMenu->setLayout(layout); return newMenu; } + +static CCSize getSizeSafe(CCNode* node) { + if (node) { + return node->getScaledContentSize(); + } + else { + return CCSizeZero; + } +} diff --git a/loader/src/ids/CreatorLayer.cpp b/loader/src/ids/CreatorLayer.cpp index d4745678..1b0b05a8 100644 --- a/loader/src/ids/CreatorLayer.cpp +++ b/loader/src/ids/CreatorLayer.cpp @@ -6,49 +6,118 @@ USE_GEODE_NAMESPACE(); +template<class... Args> +static void reorderButtons(Args... args) { + int ooa = 0; + for (auto& arg : { args... }) { + if (arg) { + arg->setOrderOfArrival(ooa); + ooa += 1; + } + } +} + $register_ids(CreatorLayer) { setIDSafe<CCSprite>(this, 0, "background"); + auto winSize = CCDirector::get()->getWinSize(); + if (auto menu = getChildOfType<CCMenu>(this, 0)) { menu->setID("creator-buttons-menu"); - setIDSafe(menu, 0, "create-button"); - setIDSafe(menu, 1, "saved-button"); - setIDSafe(menu, 2, "scores-button"); - setIDSafe(menu, 3, "quests-button"); - setIDSafe(menu, 4, "daily-button"); - setIDSafe(menu, 5, "weekly-button"); - setIDSafe(menu, 6, "featured-button"); - setIDSafe(menu, 7, "hall-of-fame-button"); - setIDSafe(menu, 8, "map-packs-button"); - setIDSafe(menu, 9, "search-button"); - setIDSafe(menu, 10, "gauntlets-button"); - // move vault button to its own menu if (auto lockBtn = setIDSafe(menu, -2, "vault-button")) { - detachAndCreateMenu( + auto menu = detachAndCreateMenu( this, "top-right-menu", - ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::Begin), + ColumnLayout::create() + ->setAxisReverse(true) + ->setAxisAlignment(AxisAlignment::End), lockBtn ); + menu->setPositionY( + menu->getPositionY() - 150.f / 2 + + lockBtn->getScaledContentSize().height / 2 + ); + menu->setContentSize({ 60.f, 150.f }); + menu->updateLayout(); } // move treasure room button to its own menu if (auto roomBtn = setIDSafe(menu, -1, "treasure-room-button")) { - detachAndCreateMenu( + auto menu = detachAndCreateMenu( this, "bottom-right-menu", - ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::End), + ColumnLayout::create() + ->setAxisAlignment(AxisAlignment::Start), roomBtn ); + menu->setPositionY( + menu->getPositionY() + 125.f / 2 - + roomBtn->getScaledContentSize().height / 2 + ); + menu->setContentSize({ 60.f, 125.f }); + menu->updateLayout(); } + + // row order is inverted because of layout + + reorderButtons( + setIDSafe(menu, 6, "featured-button"), + setIDSafe(menu, 7, "hall-of-fame-button"), + setIDSafe(menu, 8, "map-packs-button"), + + setIDSafe(menu, 9, "search-button"), + setIDSafe(menu, 3, "quests-button"), + setIDSafe(menu, 4, "daily-button"), + setIDSafe(menu, 5, "weekly-button"), + + setIDSafe(menu, 10, "gauntlets-button"), + setIDSafe(menu, 0, "create-button"), + setIDSafe(menu, 1, "saved-button"), + setIDSafe(menu, 2, "scores-button") + ); + + if (winSize.width / winSize.height <= 5.1f / 3.f) { + menu->setContentSize({ winSize.width - 80.f, 310.f }); + } + else { + menu->setContentSize({ winSize.width - 120.f, 310.f }); + } + menu->setLayout( + RowLayout::create() + ->setGap(12.f) + ->setCrossAxisReverse(true) + ->setGrowCrossAxis(true) + ->setCrossAxisOverflow(false) + ); } if (auto menu = getChildOfType<CCMenu>(this, 1)) { menu->setID("exit-menu"); - setIDSafe(menu, 0, "exit-button"); + auto exitBtn = setIDSafe(menu, 0, "exit-button"); + menu->setPositionY( + menu->getPositionY() - 125.f / 2 + + getSizeSafe(exitBtn).height / 2 + ); + menu->setContentSize({ 60.f, 125.f }); + menu->setLayout( + ColumnLayout::create() + ->setAxisAlignment(AxisAlignment::End) + ); } + + // add a menu to the bottom left corner that is empty but prolly a place mods + // want to add stuff to + auto menu = CCMenu::create(); + menu->setPosition(24.f, 0.f + 125.f / 2); + menu->setID("bottom-left-menu"); + menu->setContentSize({ 60.f, 125.f }); + menu->setLayout( + ColumnLayout::create() + ->setAxisAlignment(AxisAlignment::Start) + ); + this->addChild(menu); } struct CreatorLayerIDs : Modify<CreatorLayerIDs, CreatorLayer> { diff --git a/loader/src/ids/EditLevelLayer.cpp b/loader/src/ids/EditLevelLayer.cpp index 8901f4cb..5a0fc534 100644 --- a/loader/src/ids/EditLevelLayer.cpp +++ b/loader/src/ids/EditLevelLayer.cpp @@ -3,6 +3,7 @@ #include <Geode/Bindings.hpp> #include <Geode/modify/EditLevelLayer.hpp> #include <Geode/utils/cocos.hpp> +#include <Geode/ui/BasedButtonSprite.hpp> USE_GEODE_NAMESPACE(); @@ -18,22 +19,26 @@ $register_ids(EditLevelLayer) { "description-background", "description-input", "description-text-area", - "level-action-menu", + "level-edit-menu", "level-length", "level-song", "level-verified", "version-label", "level-id-label", - "right-side-menu", - "back-button-menu", + "level-actions-menu", + "back-menu", "info-button-menu" ); - if (auto menu = this->getChildByID("level-action-menu")) { + auto winSize = CCDirector::get()->getWinSize(); + + if (auto menu = this->getChildByID("level-edit-menu")) { setIDs(menu, 0, "edit-button", "play-button", "share-button"); + menu->setContentSize({ winSize.width - 160.f, 100.f }); + menu->setLayout(RowLayout::create()->setGap(25.f)); } - if (auto menu = this->getChildByID("right-side-menu")) { + if (auto menu = this->getChildByID("level-actions-menu")) { setIDs( menu, 0, @@ -44,12 +49,42 @@ $register_ids(EditLevelLayer) { "folder-button" ); - detachAndCreateMenu( - menu, "folder-menu", ColumnLayout::create(), menu->getChildByID("folder-button") + auto folderMenu = detachAndCreateMenu( + this, "folder-menu", + ColumnLayout::create(), + menu->getChildByID("folder-button") ); + folderMenu->setContentSize({ 50.f, 215.f }); + folderMenu->updateLayout(); + + menu->setPosition( + menu->getPositionX() + static_cast<CCNode*>( + menu->getChildren()->firstObject() + )->getPositionX(), + winSize.height / 2 + ); + menu->setContentSize({ 60.f, winSize.height - 15.f }); + menu->setLayout( + ColumnLayout::create() + ->setGap(7.f) + ->setAxisAlignment(AxisAlignment::End) + ->setAxisReverse(true) + ); + menu->setZOrder(1); } - if (auto menu = this->getChildByID("back-button-menu")) setIDSafe(menu, 0, "back-button"); + if (auto menu = this->getChildByID("back-menu")) { + auto backBtn = setIDSafe(menu, 0, "back-button"); + menu->setPositionX( + menu->getPositionX() + 100.f / 2 - + getSizeSafe(backBtn).width / 2 + ); + menu->setContentSize({ 100.f, 50.f }); + menu->setLayout( + RowLayout::create() + ->setAxisAlignment(AxisAlignment::Start) + ); + } if (auto menu = this->getChildByID("info-button-menu")) setIDSafe(menu, 0, "info-button"); } diff --git a/loader/src/ids/EditorPauseLayer.cpp b/loader/src/ids/EditorPauseLayer.cpp new file mode 100644 index 00000000..f1ae68fa --- /dev/null +++ b/loader/src/ids/EditorPauseLayer.cpp @@ -0,0 +1,273 @@ +#include "AddIDs.hpp" + +#include <Geode/modify/EditorPauseLayer.hpp> + +USE_GEODE_NAMESPACE(); + +// special class for this because making it a CCMenuItemToggler would be very UB +// (not gonna reinterpret_cast that into the members) +class GuidelinesButton : public CCMenuItemSpriteExtra { +protected: + bool init() { + if (!CCMenuItemSpriteExtra::init( + CCSprite::createWithSpriteFrameName("GJ_audioOffBtn_001.png"), + nullptr, + this, nullptr + )) return false; + + this->updateSprite(); + + return true; + } + + void updateSprite() { + this->setNormalImage(CCSprite::createWithSpriteFrameName( + GameManager::get()->m_showSongMarkers ? + "GJ_audioOnBtn_001.png" : + "GJ_audioOffBtn_001.png" + )); + } + + void activate() override { + CCMenuItemSpriteExtra::activate(); + GameManager::get()->m_showSongMarkers ^= 1; + this->updateSprite(); + } + +public: + static GuidelinesButton* create() { + auto ret = new GuidelinesButton(); + if (ret && ret->init()) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; + } +}; + +$register_ids(EditorPauseLayer) { + auto winSize = CCDirector::get()->getWinSize(); + + if (auto menu = getChildOfType<CCMenu>(this, 0)) { + menu->setID("resume-menu"); + + setIDs( + menu, 0, + "resume-button", + "save-and-play-button", + "save-and-exit-button", + "save-button", + "exit-button" + ); + + menu->setContentSize({ 100.f, 220.f }); + menu->setLayout( + ColumnLayout::create() + ->setGap(12.5f) + ->setAxisReverse(true) + ); + } + + setIDs( + this, 2, + "ignore-damage-label", + "follow-player-label", + "select-filter-label", + "show-grid-label", + "show-object-info-label", + "show-ground-label", + "preview-mode-label", + + "object-count-label", + "length-label", + "length-name-label" + ); + + if (auto menu = getChildOfType<CCMenu>(this, 1)) { + menu->setID("bottom-menu"); + + setIDs( + menu, 0, + "guidelines-enable-button", + "help-button", + "guidelines-disable-button", + + "uncheck-portals-button", + "reset-unused-button", + "create-edges-button", + "create-outlines-button", + "create-base-button", + "build-helper-button", + + "align-x-button", + "align-y-button", + "select-all-button", + "select-all-left-button", + "select-all-right-button", + + "ignore-damage-toggle", + "follow-player-toggle", + "select-filter-toggle", + "show-grid-toggle", + "show-object-info-toggle", + "show-ground-toggle", + "preview-mode-toggle", + + "keys-button", + "settings-button" + ); + + auto smallActionsMenu = detachAndCreateMenu( + this, + "small-actions-menu", + ColumnLayout::create() + ->setAxisAlignment(AxisAlignment::Start) + ->setAxisReverse(true), + menu->getChildByID("align-x-button"), + menu->getChildByID("align-y-button"), + menu->getChildByID("select-all-button"), + menu->getChildByID("select-all-left-button"), + menu->getChildByID("select-all-right-button") + ); + smallActionsMenu->setContentSize({ 100.f, 240.f }); + smallActionsMenu->setPositionY(130.f); + smallActionsMenu->updateLayout(); + + auto actionsMenu = detachAndCreateMenu( + this, + "actions-menu", + ColumnLayout::create() + ->setAxisAlignment(AxisAlignment::Start) + ->setAxisReverse(true), + menu->getChildByID("keys-button"), + menu->getChildByID("build-helper-button"), + menu->getChildByID("create-base-button"), + menu->getChildByID("create-outlines-button"), + menu->getChildByID("create-edges-button"), + menu->getChildByID("reset-unused-button"), + menu->getChildByID("uncheck-portals-button") + ); + if (auto keysBtn = actionsMenu->getChildByID("keys-button")) { + keysBtn->setLayoutOptions(AxisLayoutOptions::create()->setPrevGap(10.f)); + } + actionsMenu->setContentSize({ 100.f, 240.f }); + actionsMenu->setPositionY(130.f); + actionsMenu->updateLayout(); + + auto optionsMenu = detachAndCreateMenu( + this, + "options-menu", + RowLayout::create() + ->setGap(0.f) + ->setAxisAlignment(AxisAlignment::Start) + ->setGrowCrossAxis(true) + ->setCrossAxisAlignment(AxisAlignment::Start) + ->setCrossAxisOverflow(false), + menu->getChildByID("preview-mode-toggle"), + this->getChildByID("preview-mode-label"), + menu->getChildByID("show-ground-toggle"), + this->getChildByID("show-ground-label"), + menu->getChildByID("show-object-info-toggle"), + this->getChildByID("show-object-info-label"), + menu->getChildByID("show-grid-toggle"), + this->getChildByID("show-grid-label"), + menu->getChildByID("select-filter-toggle"), + this->getChildByID("select-filter-label"), + menu->getChildByID("follow-player-toggle"), + this->getChildByID("follow-player-label"), + menu->getChildByID("ignore-damage-toggle"), + this->getChildByID("ignore-damage-label") + ); + for (auto node : CCArrayExt<CCNode>(optionsMenu->getChildren())) { + if (auto label = typeinfo_cast<CCLabelBMFont*>(node)) { + label->setLayoutOptions( + AxisLayoutOptions::create() + ->setSameLine(true) + ->setBreakLine(true) + ->setPrevGap(5.f) + ->setMinScale(.1f) + ->setMaxScale(.5f) + ->setScalePriority(1) + ); + } + } + optionsMenu->setContentSize({ 120.f, winSize.height - 100.f }); + optionsMenu->setPosition(70.f, winSize.height / 2 - 50.f + 10.f); + optionsMenu->updateLayout(); + + auto settingsMenu = detachAndCreateMenu( + this, + "settings-menu", + RowLayout::create() + ->setAxisReverse(true), + menu->getChildByID("settings-button") + ); + settingsMenu->setContentSize({ 95.f, 50.f }); + settingsMenu->updateLayout(); + + auto guidelinesMenu = menu; + + // replace the two guidelines buttons with a single toggle + guidelinesMenu->getChildByID("guidelines-enable-button")->removeFromParent(); + guidelinesMenu->getChildByID("guidelines-disable-button")->removeFromParent(); + + auto glToggle = GuidelinesButton::create(); + glToggle->setID("guidelines-enable-toggle"); + guidelinesMenu->insertBefore(glToggle, nullptr); + m_guidelinesOffButton = m_guidelinesOnButton = glToggle; + this->updateSongButton(); + + guidelinesMenu->setID("guidelines-menu"); + guidelinesMenu->setContentSize({ winSize.width / 2, 50.f }); + guidelinesMenu->setLayout(RowLayout::create()); + + auto topMenu = CCMenu::create(); + topMenu->setContentSize({ winSize.width / 2, 50.f }); + topMenu->setPosition(winSize.width / 2, winSize.height - 30.f); + topMenu->setID("top-menu"); + topMenu->setLayout(RowLayout::create()); + this->addChild(topMenu); + } + + if (auto menu = detachAndCreateMenu( + this, "info-menu", + ColumnLayout::create() + ->setGap(10.f) + ->setAxisAlignment(AxisAlignment::End) + ->setAxisReverse(true) + ->setCrossAxisOverflow(false) + ->setCrossAxisLineAlignment(AxisAlignment::Start), + this->getChildByID("object-count-label"), + this->getChildByID("length-label"), + this->getChildByID("length-name-label") + )) { + for (auto child : CCArrayExt<CCNode>(menu->getChildren())) { + child->setLayoutOptions( + AxisLayoutOptions::create() + ->setMinScale(.1f) + ->setMaxScale(.6f) + ->setBreakLine(true) + ); + } + menu->setContentSize({ 165.f, 100.f }); + menu->setPosition(85.f, winSize.height - 55.f); + menu->updateLayout(); + } +} + +struct EditorPauseLayerIDs : Modify<EditorPauseLayerIDs, EditorPauseLayer> { + static void onModify(auto& self) { + if (!self.setHookPriority("EditorPauseLayer::init", GEODE_ID_PRIORITY)) { + log::warn("Failed to set EditorPauseLayer::init hook priority, node IDs may not work properly"); + } + } + + bool init(LevelEditorLayer* lel) { + if (!EditorPauseLayer::init(lel)) return false; + + NodeIDs::get()->provide(this); + + return true; + } +}; diff --git a/loader/src/ids/EditorUI.cpp b/loader/src/ids/EditorUI.cpp index 9ae84896..b6853f32 100644 --- a/loader/src/ids/EditorUI.cpp +++ b/loader/src/ids/EditorUI.cpp @@ -11,6 +11,8 @@ $register_ids(EditorUI) { setIDSafe(this, this->getChildrenCount() - 2, "layer-index-label"); setIDSafe(this, this->getChildrenCount() - 1, "object-info-label"); + auto winSize = CCDirector::get()->getWinSize(); + if (auto menu = getChildOfType<CCMenu>(this, 0)) { menu->setID("toolbar-categories-menu"); @@ -28,7 +30,7 @@ $register_ids(EditorUI) { "undo-button", "redo-button", - "delete-button", + "delete-trash-button", "music-playback-button", @@ -42,51 +44,105 @@ $register_ids(EditorUI) { "unlink-button" ); - detachAndCreateMenu( + auto toolbarTogglesMenu = detachAndCreateMenu( this, "toolbar-toggles-menu", - GridLayout::create(2, GridAlignment::Begin, GridDirection::Column), + RowLayout::create() + ->setCrossAxisOverflow(false) + ->setGrowCrossAxis(true) + ->setAxisAlignment(AxisAlignment::Center) + ->setCrossAxisAlignment(AxisAlignment::Center), menu->getChildByID("swipe-button"), + menu->getChildByID("rotate-button"), menu->getChildByID("free-move-button"), - menu->getChildByID("snap-button"), - menu->getChildByID("rotate-button") + menu->getChildByID("snap-button") ); + toolbarTogglesMenu->setPosition( + winSize.width - 47.f, + 45.f + ); + toolbarTogglesMenu->setContentSize({ 90.f, 90.f }); + toolbarTogglesMenu->updateLayout(); - detachAndCreateMenu( + auto undoMenuWidth = winSize.width / 2 - 90.f; + auto undoMenu = detachAndCreateMenu( this, - "top-left-menu", - RowLayout::create(), + "undo-menu", + RowLayout::create() + ->setAxisAlignment(AxisAlignment::Start) + ->setGap(10.f), menu->getChildByID("undo-button"), menu->getChildByID("redo-button"), - menu->getChildByID("delete-button") + menu->getChildByID("delete-trash-button") ); - - detachAndCreateMenu( - this, "playback-menu", RowLayout::create(), menu->getChildByID("music-playback-button") + undoMenu->setContentSize({ undoMenuWidth, 50.f }); + undoMenu->setPositionX( + undoMenu->getPositionX() + undoMenuWidth / 2 - + getSizeSafe(undoMenu->getChildByID("undo-button")).width / 2 ); + undoMenu->updateLayout(); - detachAndCreateMenu( + auto playBackMenu = detachAndCreateMenu( + this, + "playback-menu", + RowLayout::create() + ->setAxisAlignment(AxisAlignment::Start), + menu->getChildByID("music-playback-button") + ); + playBackMenu->setContentSize({ 100.f, 50.f }); + playBackMenu->setPositionX( + playBackMenu->getPositionX() + 100.f / 2 - + getSizeSafe(playBackMenu->getChildByID("music-playback-button")).width / 2 + ); + playBackMenu->updateLayout(); + + auto playTestMenu = detachAndCreateMenu( this, "playtest-menu", - RowLayout::create(), + RowLayout::create() + ->setAxisAlignment(AxisAlignment::Start), menu->getChildByID("playtest-button"), menu->getChildByID("stop-playtest-button") ); + playTestMenu->setContentSize({ 100.f, 50.f }); + playTestMenu->setPositionX( + playTestMenu->getPositionX() + 100.f / 2 - + getSizeSafe(playTestMenu->getChildByID("playtest-button")).width / 2 + ); + playTestMenu->updateLayout(); - detachAndCreateMenu( + auto zoomMenuHeight = winSize.height - 245.f; + auto zoomMenu = detachAndCreateMenu( this, "zoom-menu", - ColumnLayout::create(), - menu->getChildByID("zoom-in-button"), - menu->getChildByID("zoom-out-button") + ColumnLayout::create() + ->setAxisAlignment(AxisAlignment::Start), + menu->getChildByID("zoom-out-button"), + menu->getChildByID("zoom-in-button") ); + zoomMenu->setPositionY(150.f * winSize.height / 320); + zoomMenu->setContentSize({ 50.f, zoomMenuHeight }); + zoomMenu->updateLayout(); - detachAndCreateMenu( + auto linkMenu = detachAndCreateMenu( this, "link-menu", - ColumnLayout::create(), - menu->getChildByID("link-button"), - menu->getChildByID("unlink-button") + ColumnLayout::create() + ->setAxisAlignment(AxisAlignment::Start) + ->setGrowCrossAxis(true), + menu->getChildByID("unlink-button"), + menu->getChildByID("link-button") + ); + linkMenu->setPositionY(150.f * winSize.height / 320); + linkMenu->setContentSize({ 125.f, zoomMenuHeight }); + linkMenu->updateLayout(); + + menu->setPosition(42.f, 45.f); + menu->setContentSize({ 100.f, 90.f }); + menu->setLayout( + ColumnLayout::create() + ->setGap(4.f) + ->setAxisReverse(true) ); } @@ -108,24 +164,43 @@ $register_ids(EditorUI) { "delete-help-icon" ); - detachAndCreateMenu( + auto deleteButtonMenu = detachAndCreateMenu( menu, "delete-button-menu", - GridLayout::create(2, GridAlignment::Begin, GridDirection::Column), + ColumnLayout::create() + ->setCrossAxisOverflow(false) + ->setGrowCrossAxis(true) + ->setAxisReverse(true) + ->setCrossAxisReverse(true) + ->setAxisAlignment(AxisAlignment::End) + ->setCrossAxisAlignment(AxisAlignment::Center), menu->getChildByID("delete-button"), menu->getChildByID("delete-all-of-button"), menu->getChildByID("delete-startpos-button") ); - - detachAndCreateMenu( + deleteButtonMenu->setPosition(-88.5f, 0.f); + deleteButtonMenu->setContentSize({ winSize.width / 2 - 120.f, 80.f }); + deleteButtonMenu->updateLayout(); + + auto filterMenuWidth = winSize.width / 2 - 150.f; + auto deleteFilterMenu = detachAndCreateMenu( menu, "delete-filter-menu", - GridLayout::create(2, GridAlignment::Begin, GridDirection::Column), + ColumnLayout::create() + ->setCrossAxisOverflow(false) + ->setGrowCrossAxis(true) + ->setAxisReverse(true) + ->setCrossAxisReverse(true) + ->setAxisAlignment(AxisAlignment::End) + ->setCrossAxisAlignment(AxisAlignment::Start), menu->getChildByID("delete-filter-none"), menu->getChildByID("delete-filter-static"), menu->getChildByID("delete-filter-detail"), menu->getChildByID("delete-filter-custom") ); + deleteFilterMenu->setPosition(48.5f + filterMenuWidth / 2, 0.f); + deleteFilterMenu->setContentSize({ filterMenuWidth, 80.f }); + deleteFilterMenu->updateLayout(); } if (auto menu = getChildOfType<CCMenu>(this, 2)) { @@ -134,20 +209,27 @@ $register_ids(EditorUI) { setIDs( menu, 0, - "static-tab-1", - "static-tab-2", - "static-tab-3", + "block-tab", + "half-block-tab", + "outline-tab", "slope-tab", "hazard-tab", "3d-tab", "portal-tab", - "deco-tab-1", - "deco-tab-2", + "ground-deco-tab", + "air-deco-tab", "pulse-deco-tab", "sawblade-tab", "trigger-tab", "custom-tab" ); + + menu->setPosition(winSize.width / 2, 100.f); + menu->setContentSize({ winSize.width, 50.f }); + menu->setLayout( + RowLayout::create() + ->setGap(0.f) + ); } if (auto menu = getChildOfType<CCMenu>(this, 3)) { @@ -174,18 +256,33 @@ $register_ids(EditorUI) { "all-layers-button" ); - detachAndCreateMenu( + auto topRightMenuWidth = winSize.width / 2 - 140.f; + auto topRightMenu = detachAndCreateMenu( this, - "top-right-menu", - RowLayout::create()->setAlignment(Alignment::End), + "settings-menu", + RowLayout::create() + ->setAxisReverse(true) + ->setAxisAlignment(AxisAlignment::End), menu->getChildByID("pause-button"), menu->getChildByID("settings-button") ); + topRightMenu->setContentSize({ topRightMenuWidth, 60.f }); + topRightMenu->setPositionX( + topRightMenu->getPositionX() - topRightMenuWidth / 2 + + getSizeSafe(topRightMenu->getChildByID("pause-button")).width / 2 + ); + topRightMenu->updateLayout(); - detachAndCreateMenu( + auto rightMenu = detachAndCreateMenu( this, "editor-buttons-menu", - GridLayout::create(4, GridAlignment::End, GridDirection::Column), + ColumnLayout::create() + ->setAxisAlignment(AxisAlignment::End) + ->setCrossAxisAlignment(AxisAlignment::End) + ->setGap(-3.5f) + ->setGrowCrossAxis(true) + ->setCrossAxisOverflow(false) + ->setAxisReverse(true), menu->getChildByID("copy-paste-button"), menu->getChildByID("edit-object-button"), menu->getChildByID("paste-color-button"), @@ -199,16 +296,35 @@ $register_ids(EditorUI) { menu->getChildByID("copy-values-button"), menu->getChildByID("hsv-button") ); + for (auto btn : CCArrayExt<CCNode>(rightMenu->getChildren())) { + btn->setContentSize({ 40.f, 40.f }); + } + rightMenu->setContentSize({ 210.f, 160.f }); + rightMenu->setPosition( + winSize.width - 210.f / 2 - 5.f, + winSize.height / 2 + 42.5f + ); + rightMenu->updateLayout(); - detachAndCreateMenu( + this->getChildByID("layer-index-label")->setLayoutOptions( + AxisLayoutOptions::create() + ->setAutoScale(false) + ->setLength(25.f) + ); + + auto layerMenu = detachAndCreateMenu( this, "layer-menu", - RowLayout::create(), + RowLayout::create() + ->setAxisAlignment(AxisAlignment::Start), menu->getChildByID("all-layers-button"), menu->getChildByID("prev-layer-button"), this->getChildByID("layer-index-label"), menu->getChildByID("next-layer-button") ); + layerMenu->setPositionX(winSize.width - 110.f / 2); + layerMenu->setContentSize({ 110.f, 30.f }); + layerMenu->updateLayout(); } } diff --git a/loader/src/ids/GJGarageLayer.cpp b/loader/src/ids/GJGarageLayer.cpp index 3d973602..55d3d663 100644 --- a/loader/src/ids/GJGarageLayer.cpp +++ b/loader/src/ids/GJGarageLayer.cpp @@ -10,8 +10,10 @@ $register_ids(GJGarageLayer) { setIDSafe(this, 2, "username-label"); setIDSafe(this, 6, "player-icon"); + auto winSize = CCDirector::get()->getWinSize(); + if (auto menu = getChildOfType<CCMenu>(this, 0)) { - menu->setID("icon-select-menu"); + menu->setID("category-menu"); setIDs( menu, @@ -26,6 +28,13 @@ $register_ids(GJGarageLayer) { "trail-button", "death-effect-button" ); + + menu->setContentSize({ 320.f, 50.f }); + menu->setLayout( + RowLayout::create() + ->setAxisAlignment(AxisAlignment::Start) + ->setGap(-4.f) + ); } setIDs( @@ -47,13 +56,99 @@ $register_ids(GJGarageLayer) { "color-selection-menu" ); - if (auto menu = getChildOfType<CCMenu>(this, 11)) { + if (auto menu = getChildOfType<CCMenu>(this, 1)) { menu->setID("top-left-menu"); setIDs(menu, 0, "back-button", "shop-button", "shards-button"); - detachAndCreateMenu( - menu, "shards-button-menu", ColumnLayout::create(), menu->getChildByID("shards-button") + auto backBtn = menu->getChildByID("back-button"); + auto backMenu = detachAndCreateMenu( + this, + "back-menu", + RowLayout::create() + ->setAxisAlignment(AxisAlignment::Start), + backBtn + ); + backMenu->setContentSize({ 100.f, 50.f }); + backMenu->setPositionX( + backMenu->getPositionX() + 100.f / 2 - + getSizeSafe(backBtn).width / 2 + ); + backMenu->updateLayout(); + + auto shardsBtn = menu->getChildByID("shards-button"); + auto shardsMenu = detachAndCreateMenu( + this, + "shards-menu", + ColumnLayout::create() + ->setAxisReverse(true) + ->setAxisAlignment(AxisAlignment::End), + shardsBtn + ); + shardsMenu->setContentSize({ 50.f, 100.f }); + shardsMenu->setPositionY( + shardsMenu->getPositionY() - 100.f / 2 + + getSizeSafe(shardsBtn).height / 2 + ); + shardsMenu->updateLayout(); + } + + auto bottomLeftMenu = CCMenu::create(); + bottomLeftMenu->setID("bottom-left-menu"); + bottomLeftMenu->setContentSize({ 50.f, 70.f }); + bottomLeftMenu->setPosition(30.f, 115.f); + bottomLeftMenu->setLayout( + ColumnLayout::create() + ->setAxisAlignment(AxisAlignment::Start) + ); + this->addChild(bottomLeftMenu); + + auto bottomRightMenu = CCMenu::create(); + bottomRightMenu->setID("bottom-right-menu"); + bottomRightMenu->setContentSize({ 50.f, 110.f }); + bottomRightMenu->setPosition(winSize.width - 30.f, 135.f); + bottomRightMenu->setLayout( + ColumnLayout::create() + ->setAxisAlignment(AxisAlignment::Start) + ); + this->addChild(bottomRightMenu); + + // aspect ratio responsiveness + if (winSize.width / winSize.height <= 5.1f / 3.f) { + bottomLeftMenu->setPosition(15.f, 115.f); + bottomRightMenu->setPosition(winSize.width - 15.f, 135.f); + + if (auto shardsMenu = this->getChildByID("shards-menu")) { + shardsMenu->setContentSize({ 110.f, 50.f }); + shardsMenu->setPosition( + shardsMenu->getPosition() + ccp(50.f, 30.f) + ); + shardsMenu->setLayout( + RowLayout::create() + ->setAxisAlignment(AxisAlignment::Start) + ); + } + } + if (winSize.width / winSize.height <= 4.1f / 3.f) { + bottomLeftMenu->setContentSize({ 90.f, 50.f }); + bottomLeftMenu->setPosition( + 15.f + 110.f / 2, + 85.f + ); + bottomLeftMenu->setLayout( + RowLayout::create() + ->setAxisAlignment(AxisAlignment::Start) + ); + + bottomRightMenu->setContentSize({ 90.f, 50.f }); + bottomRightMenu->setPosition( + winSize.width - 15.f - 110.f / 2, + 85.f + ); + bottomRightMenu->setLayout( + RowLayout::create() + ->setAxisReverse(true) + ->setAxisAlignment(AxisAlignment::End) ); } } diff --git a/loader/src/ids/LevelBrowserLayer.cpp b/loader/src/ids/LevelBrowserLayer.cpp index c1c74149..bd7ffdc4 100644 --- a/loader/src/ids/LevelBrowserLayer.cpp +++ b/loader/src/ids/LevelBrowserLayer.cpp @@ -7,28 +7,153 @@ USE_GEODE_NAMESPACE(); $register_ids(LevelBrowserLayer) { + auto winSize = CCDirector::get()->getWinSize(); + if (auto menu = getChildOfType<CCMenu>(this, 0)) { - menu->setID("go-back-menu"); - setIDSafe(menu, 0, "back-button"); + menu->setID("back-menu"); + auto btn = setIDSafe(menu, 0, "back-button"); + menu->setContentSize({ 100.f, 50.f }); + menu->setPositionX( + menu->getPositionX() + 100.f / 2 - + getSizeSafe(btn).width / 2 + ); + menu->setLayout( + RowLayout::create() + ->setAxisAlignment(AxisAlignment::Start) + ); } if (m_searchObject->m_searchType == SearchType::MyLevels) { if (auto menu = getChildOfType<CCMenu>(this, 2)) { menu->setID("new-level-menu"); - setIDSafe(menu, 0, "new-level-button"); + auto newLvlBtn = setIDSafe(menu, 0, "new-level-button"); if (auto myLevelsBtn = setIDSafe(menu, 1, "my-levels-button")) { - detachAndCreateMenu( + auto menu = detachAndCreateMenu( this, "my-levels-menu", - ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::End), + ColumnLayout::create() + ->setAxisAlignment(AxisAlignment::Start), myLevelsBtn ); + menu->setPositionY( + menu->getPositionY() + 125.f / 2 - + myLevelsBtn->getScaledContentSize().height / 2 + ); + menu->setContentSize({ 50.f, 125.f }); + menu->updateLayout(); } - menu->setLayout(ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::End)); + menu->setLayout( + ColumnLayout::create() + ->setAxisAlignment(AxisAlignment::Start) + ); + menu->setPositionY( + menu->getPositionY() + 130.f / 2 - + getSizeSafe(newLvlBtn).height / 2 + ); + menu->setContentSize({ 50.f, 130.f }); + menu->updateLayout(); + } + + if (auto menu = getChildOfType<CCMenu>(this, 1)) { + if (auto searchBtn = setIDSafe(menu, 5, "search-button")) { + auto clearBtn = setIDSafe(menu, 6, "clear-search-button"); + // this is a hacky fix because for some reason adding children + // before the clear button is made visible is inconsistent + if (clearBtn) { + searchBtn->setZOrder(-1); + clearBtn->setZOrder(-1); + } + auto searchMenu = detachAndCreateMenu( + this, + "search-menu", + ColumnLayout::create() + ->setAxisReverse(true) + ->setCrossAxisReverse(true) + ->setGrowCrossAxis(true) + ->setCrossAxisOverflow(false) + ->setCrossAxisAlignment(AxisAlignment::Start) + ->setAxisAlignment(AxisAlignment::End), + searchBtn, + clearBtn + ); + auto width = 45.f * winSize.aspect(); + searchMenu->setPosition( + searchMenu->getPositionX() + width / 2 - + searchBtn->getScaledContentSize().width / 2, + searchMenu->getPositionY() - 80.f / 2 + + searchBtn->getScaledContentSize().height / 2 + ); + searchMenu->setContentSize({ width, 80.f }); + searchMenu->updateLayout(); + } + + if (auto pageBtn = setIDSafe(menu, 2, "page-button")) { + auto folderBtn = setIDSafe(menu, 3, "folder-button"); + auto lastPageBtn = setIDSafe(menu, 4, "last-page-button"); + auto pageMenu = detachAndCreateMenu( + this, + "page-menu", + ColumnLayout::create() + ->setAxisReverse(true) + ->setGrowCrossAxis(true) + ->setAxisAlignment(AxisAlignment::End), + pageBtn, + folderBtn, + lastPageBtn + ); + pageMenu->setContentSize({ 40.f, 110.f }); + pageMenu->setAnchorPoint({ 1.f, .5f }); + pageMenu->setPosition( + pageMenu->getPositionX() + 20.f, + pageMenu->getPositionY() - 110.f / 2 + 12.5f + ); + pageMenu->updateLayout(); + } + + auto navMenuWidth = 50.f * winSize.aspect(); + + if (auto prevPageBtn = setIDSafe(menu, 0, "prev-page-button")) { + auto navMenu = detachAndCreateMenu( + this, + "prev-page-menu", + RowLayout::create() + ->setAxisAlignment(AxisAlignment::Start), + prevPageBtn + ); + prevPageBtn->setZOrder(-1); + navMenu->setContentSize({ navMenuWidth, 40.f }); + navMenu->setPositionX( + navMenu->getPositionX() + navMenuWidth / 2 - + prevPageBtn->getScaledContentSize().width / 2 + ); + navMenu->updateLayout(); + } + + auto nextPageBtn = setIDSafe(menu, 0, "next-page-button"); + + menu->setID("next-page-menu"); + menu->setLayout( + RowLayout::create() + ->setAxisReverse(true) + ->setAxisAlignment(AxisAlignment::End) + ); + menu->setContentSize({ navMenuWidth, 40.f }); + menu->setPositionX( + winSize.width - navMenuWidth / 2 - 5.f + ); + menu->updateLayout(); } } + + auto bottomMenu = CCMenu::create(); + bottomMenu->setID("bottom-menu"); + bottomMenu->setContentSize({ 325.f + 20.f * winSize.aspect(), 50.f }); + bottomMenu->setPosition(winSize.width / 2, 28.f); + bottomMenu->setZOrder(15); + bottomMenu->setLayout(RowLayout::create()); + this->addChild(bottomMenu); } struct LevelBrowserLayerIDs : Modify<LevelBrowserLayerIDs, LevelBrowserLayer> { diff --git a/loader/src/ids/LevelInfoLayer.cpp b/loader/src/ids/LevelInfoLayer.cpp index 4a082700..d2f96629 100644 --- a/loader/src/ids/LevelInfoLayer.cpp +++ b/loader/src/ids/LevelInfoLayer.cpp @@ -35,26 +35,52 @@ $register_ids(LevelInfoLayer) { setIDSafe<CustomSongWidget>(this, 0, "custom-songs-widget"); if (auto menu = getChildOfType<CCMenu>(this, 0)) { - menu->setID("exit-menu"); - setIDSafe(menu, 0, "exit-button"); + menu->setID("play-menu"); + setIDSafe(menu, 0, "play-button"); + } + + if (auto menu = getChildOfType<CCMenu>(this, 2)) { + menu->setID("back-menu"); + auto backBtn = setIDSafe(menu, 0, "back-button"); + menu->setPositionX( + menu->getPositionX() + 100.f / 2 - + getSizeSafe(backBtn).width / 2 + ); + menu->setContentSize({ 100.f, 50.f }); + menu->setLayout( + RowLayout::create() + ->setAxisAlignment(AxisAlignment::Start) + ); } if (auto menu = getChildOfType<CCMenu>(this, 1)) { menu->setID("right-side-menu"); if (auto name = setIDSafe(menu, 0, "creator-name")) { - detachAndCreateMenu( - this, "creator-info-menu", ColumnLayout::create()->setAlignment(Alignment::Begin), name + auto menu = detachAndCreateMenu( + this, + "creator-info-menu", + ColumnLayout::create() + ->setAxisReverse(true) + ->setAxisAlignment(AxisAlignment::End), + name ); + menu->setPositionY( + menu->getPositionY() - 40.f / 2 + + name->getScaledContentSize().height / 2 + ); + menu->setContentSize({ 60.f, 40.f }); + menu->updateLayout(); } auto leftSideMenu = CCMenu::create(); - leftSideMenu->setPosition(winSize / 2 + ccp(-254.f, 30.f)); + leftSideMenu->setPosition(30.f, winSize.height / 2); leftSideMenu->setLayout(ColumnLayout::create()); leftSideMenu->setID("left-side-menu"); + leftSideMenu->setContentSize({ 50.f, 225.f }); this->addChild(leftSideMenu); - menu->setPosition(winSize / 2 + ccp(254.f, 0.f)); + menu->setPosition(winSize.width - 30.f, winSize.height / 2); for (auto child : CCArrayExt<CCNode>(menu->getChildren())) { if (child->getPositionX() < 0.f) { @@ -73,6 +99,20 @@ $register_ids(LevelInfoLayer) { setIDSafe(menu, 4, "like-button"); setIDSafe(menu, 5, "rate-button"); + menu->setPosition( + menu->getPositionX() + static_cast<CCNode*>( + menu->getChildren()->firstObject() + )->getPositionX(), + winSize.height / 2 + ); + menu->setContentSize({ 60.f, winSize.height - 15.f }); + menu->setLayout( + ColumnLayout::create() + ->setGap(3.f) + ->setAxisAlignment(AxisAlignment::End) + ->setAxisReverse(true) + ); + setIDSafe(leftSideMenu, 0, "copy-button"); menu->updateLayout(); diff --git a/loader/src/ids/LevelSettingsLayer.cpp b/loader/src/ids/LevelSettingsLayer.cpp index ce857a83..a898d013 100644 --- a/loader/src/ids/LevelSettingsLayer.cpp +++ b/loader/src/ids/LevelSettingsLayer.cpp @@ -162,12 +162,15 @@ $register_ids(LevelSettingsLayer) { menu->getChildByID("2-player-toggle") ); - detachAndCreateMenu( + auto fontButtonMenu = detachAndCreateMenu( this, "font-button-menu", - RowLayout::create()->setAlignment(Alignment::End), + RowLayout::create() + ->setAxisAlignment(AxisAlignment::End), menu->getChildByID("font-button") ); + fontButtonMenu->setPositionY(fontButtonMenu->getPositionY() - 100.f / 2); + fontButtonMenu->setContentSize({ 50.f, 100.f }); } } diff --git a/loader/src/ids/MenuLayer.cpp b/loader/src/ids/MenuLayer.cpp index a42364cb..8bc228ed 100644 --- a/loader/src/ids/MenuLayer.cpp +++ b/loader/src/ids/MenuLayer.cpp @@ -2,6 +2,7 @@ #include <Geode/modify/MenuLayer.hpp> #include <Geode/utils/cocos.hpp> +#include <Geode/ui/BasedButtonSprite.hpp> USE_GEODE_NAMESPACE(); @@ -10,6 +11,8 @@ $register_ids(MenuLayer) { setIDSafe(this, 0, "main-menu-bg"); setIDSafe<CCSprite>(this, 0, "main-title"); + auto winSize = CCDirector::get()->getWinSize(); + // controller if (PlatformToolbox::isControllerConnected()) { setIDSafe<CCSprite>(this, 1, "play-gamepad-icon"); @@ -28,24 +31,42 @@ $register_ids(MenuLayer) { else { setIDSafe<CCLabelBMFont>(this, 0, "player-username"); } + // main menu if (auto menu = getChildOfType<CCMenu>(this, 0)) { menu->setID("main-menu"); auto playBtn = setIDSafe(menu, 0, "play-button"); auto iconBtn = setIDSafe(menu, 1, "icon-kit-button"); + setIDSafe(menu, 2, "editor-button"); + + if (auto pfp = setIDSafe(menu, 3, "profile-button")) { + auto profileMenu = detachAndCreateMenu( + this, "profile-menu", + RowLayout::create() + ->setAxisAlignment(AxisAlignment::Start), + pfp + ); + profileMenu->setContentSize({ 150.f, 50.f }); + profileMenu->setPositionX( + profileMenu->getPositionX() + 150.f / 2 - + pfp->getScaledContentSize().height / 2 + ); + profileMenu->updateLayout(); + } + // the buttons are added in order play, icon, editor which doesn't work // well with setLayout that deals with children in order menu->swapChildIndices(playBtn, iconBtn); - setIDSafe(menu, 2, "editor-button"); - - if (auto pfp = setIDSafe(menu, 3, "profile-button")) { - pfp->setPositionHint(PositionHint::Absolute); - } - - menu->setLayout(RowLayout::create(18.f, 0.f)); + menu->setContentSize({ winSize.width - 140.f, 65.f }); + menu->setLayout( + RowLayout::create() + ->setGap(18.f) + ->setCrossAxisOverflow(true) + ); } + // bottom menu if (auto menu = getChildOfType<CCMenu>(this, 1)) { menu->setID("bottom-menu"); @@ -57,11 +78,22 @@ $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(0.f, 0.f), dailyChest); + auto menu = detachAndCreateMenu( + this, + "right-side-menu", + ColumnLayout::create(), + dailyChest + ); + menu->setContentSize({ 65.f, 180.f }); + menu->updateLayout(); } - menu->setLayout(RowLayout::create(5.f, ach->getPositionY())); + menu->setContentSize({ winSize.width - 220.f, 65.f }); + menu->setLayout( + RowLayout::create() + ); } + // social media menu if (auto menu = getChildOfType<CCMenu>(this, 2)) { menu->setID("social-media-menu"); @@ -70,19 +102,62 @@ $register_ids(MenuLayer) { setIDSafe(menu, 2, "twitter-button"); setIDSafe(menu, 3, "youtube-button"); } + // more games menu if (auto menu = getChildOfType<CCMenu>(this, 3)) { menu->setID("more-games-menu"); - setIDSafe(menu, 0, "more-games-button"); + auto moreGamesBtn = setIDSafe(menu, 0, "more-games-button"); // move close button to its own menu if (auto closeBtn = setIDSafe(menu, 1, "close-button")) { - detachAndCreateMenu( - this, "close-menu", RowLayout::create(5.f, 0.f)->setAlignment(Alignment::Begin), closeBtn + auto closeMenu = detachAndCreateMenu( + this, + "close-menu", + RowLayout::create() + ->setAxisAlignment(AxisAlignment::Start), + closeBtn ); + closeMenu->setContentSize({ 200.f, 50.f }); + closeMenu->setPositionX( + closeMenu->getPositionX() + 200.f / 2 - + closeBtn->getScaledContentSize().width / 2 + ); + closeMenu->updateLayout(); } + + menu->setContentSize({ 100.f, 50.f }); + menu->setPositionX( + menu->getPositionX() - 100.f / 2 + + getSizeSafe(moreGamesBtn).width / 2 + ); + menu->setLayout( + RowLayout::create() + ->setAxisAlignment(AxisAlignment::End) + ->setAxisReverse(true) + ); } + + // add a menu to the top right corner and middle left that are empty + // but prolly a place mods want to add stuff + + auto topRightMenu = CCMenu::create(); + topRightMenu->setPosition(winSize.width - 200.f / 2, winSize.height - 50.f / 2); + topRightMenu->setID("top-right-menu"); + topRightMenu->setContentSize({ 200.f, 50.f }); + topRightMenu->setLayout( + RowLayout::create() + ->setAxisReverse(true) + ->setAxisAlignment(AxisAlignment::End) + ); + this->addChild(topRightMenu); + + auto middleLeftMenu = CCMenu::create(); + middleLeftMenu->setPosition(25.f, 215.f); + middleLeftMenu->setID("side-menu"); + middleLeftMenu->setContentSize({ 50.f, 120.f }); + middleLeftMenu->setLayout(ColumnLayout::create()); + this->addChild(middleLeftMenu); } // MenuLayer::init is hooked in ../hooks/MenuLayer.cpp diff --git a/loader/src/internal/about.hpp.in b/loader/src/internal/about.hpp.in index a0cdc50e..6da36c01 100644 --- a/loader/src/internal/about.hpp.in +++ b/loader/src/internal/about.hpp.in @@ -11,6 +11,6 @@ static constexpr geode::VersionInfo LOADER_VERSION = { @PROJECT_VERSION_MAJOR@, @PROJECT_VERSION_MINOR@, @PROJECT_VERSION_PATCH@, - @PROJECT_VERSION_TYPE@, + @PROJECT_VERSION_TAG_CONSTR@, }; static constexpr const char* LOADER_MOD_JSON = R"JSON_SEPARATOR(@LOADER_MOD_JSON@)JSON_SEPARATOR"; diff --git a/loader/src/loader/ModInfo.cpp b/loader/src/loader/ModInfo.cpp index dbfc4802..af6200e0 100644 --- a/loader/src/loader/ModInfo.cpp +++ b/loader/src/loader/ModInfo.cpp @@ -97,8 +97,7 @@ Result<ModInfo> ModInfo::Impl::createFromSchemaV010(ModJson const& rawJson) { root.has("unloadable").into(impl->m_supportsUnloading); root.has("early-load").into(impl->m_needsEarlyLoad); if (root.has("api")) { - // TODO: figure out what got wiped with merge - // impl->isAPI = true; + impl->m_isAPI = true; } for (auto& dep : root.has("dependencies").iterate()) { @@ -194,11 +193,9 @@ Result<ModInfo> ModInfo::Impl::create(ModJson const& json) { return Err( "[mod.json] targets a version (" + schema.toString() + - ") that isn't " - "supported by this version (v" + + ") that isn't supported by this version (v" + LOADER_VERSION_STR + - ") of geode. " - "This is probably a bug; report it to " + ") of geode. This is probably a bug; report it to " "the Geode Development Team." ); } diff --git a/loader/src/ui/internal/list/ModListCell.cpp b/loader/src/ui/internal/list/ModListCell.cpp index a2b89bf1..b70c83eb 100644 --- a/loader/src/ui/internal/list/ModListCell.cpp +++ b/loader/src/ui/internal/list/ModListCell.cpp @@ -79,9 +79,7 @@ void ModListCell::setupInfo( this->addChild(versionLabel); if (auto tag = info.version().getTag()) { - auto tagLabel = TagNode::create( - versionTagToString(tag.value()).c_str() - ); + auto tagLabel = TagNode::create(tag.value().toString().c_str()); tagLabel->setAnchorPoint({ .0f, .5f }); tagLabel->setScale(.3f); tagLabel->setPosition( diff --git a/loader/src/utils/VersionInfo.cpp b/loader/src/utils/VersionInfo.cpp index 13f8aad7..23269a82 100644 --- a/loader/src/utils/VersionInfo.cpp +++ b/loader/src/utils/VersionInfo.cpp @@ -10,31 +10,57 @@ USE_GEODE_NAMESPACE(); // VersionTag -std::optional<VersionTag> geode::versionTagFromString(std::string const& str) { - switch (hash(str.c_str())) { - case hash("alpha"): return VersionTag::Alpha; - case hash("beta"): return VersionTag::Beta; - case hash("prerelease"): return VersionTag::Prerelease; - default: return std::nullopt; +Result<VersionTag> VersionTag::parse(std::stringstream& str) { + std::string iden; + while ('a' <= str.peek() && str.peek() <= 'z') { + iden += str.get(); } + if (str.fail()) { + return Err("Unable to parse tag"); + } + VersionTag tag = VersionTag::Alpha; + switch (hash(iden.c_str())) { + case hash("alpha"): tag = VersionTag::Alpha; break; + case hash("beta"): tag = VersionTag::Beta; break; + case hash("prerelease"): case hash("pr"): tag = VersionTag::Prerelease; break; + default: return Err("Invalid tag \"" + iden + "\""); + } + if (str.peek() == '.') { + str.get(); + size_t num; + str >> num; + if (str.fail()) { + return Err("Unable to parse tag number"); + } + tag.number = num; + } + return Ok(tag); } -std::string geode::versionTagToSuffixString(VersionTag tag) { - switch (tag) { - case VersionTag::Alpha: return "-alpha"; - case VersionTag::Beta: return "-beta"; - case VersionTag::Prerelease: return "-prerelease"; +std::string VersionTag::toSuffixString() const { + std::string res = ""; + switch (value) { + case Alpha: res += "-alpha"; break; + case Beta: res += "-beta"; break; + case Prerelease: res += "-prerelease"; break; } - return ""; + if (number) { + res += "." + std::to_string(number.value()); + } + return res; } -std::string geode::versionTagToString(VersionTag tag) { - switch (tag) { - case VersionTag::Alpha: return "Alpha"; - case VersionTag::Beta: return "Beta"; - case VersionTag::Prerelease: return "Prerelease"; +std::string VersionTag::toString() const { + std::string res = ""; + switch (value) { + case Alpha: res += "Alpha"; break; + case Beta: res += "Beta"; break; + case Prerelease: res += "Prerelease"; break; } - return ""; + if (number) { + res += " " + std::to_string(number.value()); + } + return res; } // VersionInfo @@ -77,18 +103,13 @@ Result<VersionInfo> VersionInfo::parse(std::string const& string) { std::optional<VersionTag> tag; if (str.peek() == '-') { str.get(); - std::string iden; - str >> iden; - if (str.fail()) { - return Err("Unable to parse tag"); - } - if (auto t = versionTagFromString(iden)) { - tag = t; - } - else { - return Err("Invalid tag \"" + iden + "\""); - } + GEODE_UNWRAP_INTO(tag, VersionTag::parse(str)); } + + if (!str.eof()) { + return Err("Expected end of version, found '" + std::string(1, str.get()) + "'"); + } + return Ok(VersionInfo(major, minor, patch, tag)); } @@ -97,7 +118,7 @@ std::string VersionInfo::toString(bool includeTag) const { return fmt::format( "v{}.{}.{}{}", m_major, m_minor, m_patch, - versionTagToSuffixString(m_tag.value()) + m_tag.value().toSuffixString() ); } return fmt::format("v{}.{}.{}", m_major, m_minor, m_patch);