rework layouts

This commit is contained in:
HJfod 2023-02-04 15:58:10 +02:00
parent d09b74eb34
commit e36a5aea35
10 changed files with 287 additions and 218 deletions

View file

@ -56,29 +56,28 @@ enum class Alignment {
End,
};
/**
* Simple layout for arranging nodes in a row (horizontal line)
*/
class GEODE_DLL RowLayout : public Layout {
class GEODE_DLL AxisLayout : public Layout {
public:
enum Axis : bool {
Row,
Column,
};
protected:
std::optional<Alignment> m_vAlignment = Alignment::Center;
float m_gap;
std::optional<float> m_maxAutoScale = std::nullopt;
Axis m_axis;
Alignment m_axisAlignment = Alignment::Center;
Alignment m_crossAlignment = Alignment::Center;
float m_gap = 5.f;
bool m_autoScale = true;
bool m_shrinkCrossAxis = true;
bool m_reverse = false;
bool m_fitInside = false;
AxisLayout(Axis);
public:
void apply(CCNode* on) override;
/**
* Create a new RowLayout. Note that this class is not automatically
* managed by default, so you must assign it to a CCNode or manually
* manage the memory yourself. See the chainable setters on RowLayout for
* what options you can customize for the layout
* @returns Created RowLayout
*/
static RowLayout* create();
/**
* Sets where to align nodes on the Y-axis. If nullopt, the
* nodes' Y-position will not be affected, and the height of the node this
@ -89,16 +88,17 @@ public:
* @param align Value
* @returns The same RowLayout this was applied on
*/
RowLayout* setVAlignment(std::optional<Alignment> align);
AxisLayout* setCrossAxisAlignment(Alignment align);
AxisLayout* setAxisAlignment(Alignment align);
/**
* The spacing between the children of the node this layout applies to.
* Measured as the space between their edges, not centres
*/
RowLayout* setGap(float gap);
AxisLayout* setGap(float gap);
/**
* Whether to reverse the direction of the children in this layout or not
*/
RowLayout* setReverse(bool reverse);
AxisLayout* setReverse(bool reverse);
/**
* If a value is provided, then the node this layout applies to may be
* automatically rescaled to fit its contents better. By default the value
@ -109,30 +109,43 @@ public:
* inside of, and scales to fit that space. If the value is nullopt, the
* unscaled content size is used instead
*/
RowLayout* setMaxAutoScale(std::optional<float> scale);
AxisLayout* setAutoScale(bool enable);
/**
* If true, the children of the node this layout is applied to will be
* contained entirely within the bounds of the node's content size. If
* false, the children's positions will be within the bounds, but they may
* visually overflow depending on their anchor point
*/
RowLayout* setFitInside(bool fit);
AxisLayout* setFitInside(bool fit);
AxisLayout* setShrinkCrossAxis(bool shrink);
};
/**
* Simple layout for arranging nodes in a row (horizontal line)
*/
class GEODE_DLL RowLayout : public AxisLayout {
protected:
RowLayout();
public:
/**
* Create a new RowLayout. Note that this class is not automatically
* managed by default, so you must assign it to a CCNode or manually
* manage the memory yourself. See the chainable setters on RowLayout for
* what options you can customize for the layout
* @returns Created RowLayout
*/
static RowLayout* create();
};
/**
* Simple layout for arranging nodes in a column (vertical line)
*/
class GEODE_DLL ColumnLayout : public Layout {
class GEODE_DLL ColumnLayout : public AxisLayout {
protected:
std::optional<Alignment> m_hAlignment = Alignment::Center;
float m_gap;
std::optional<float> m_maxAutoScale = std::nullopt;
bool m_reverse = false;
bool m_fitInside = false;
ColumnLayout();
public:
void apply(CCNode* on) override;
/**
* Create a new ColumnLayout. Note that this class is not automatically
* managed by default, so you must assign it to a CCNode or manually
@ -141,42 +154,6 @@ public:
* @returns Created ColumnLayout
*/
static ColumnLayout* create();
/**
* Sets where to align nodes on the X-axis. If nullopt, the
* nodes' X-position will not be affected, and the width of the node this
* layout applies to isn't altered. If an alignment is given, the width
* of the node this layout applies to is shrunk to fit the width of the
* nodes and no more. Any nodes that don't fit inside this space are
* aligned based on the value
* @param align Value
* @returns The same RowLayout this was applied on
*/
ColumnLayout* setHAlignment(std::optional<Alignment> align);
/**
* The spacing between the children of the node this layout applies to.
* Measured as the space between their edges, not centres
*/
ColumnLayout* setGap(float gap);
/**
* Whether to reverse the direction of the children in this layout or not
*/
ColumnLayout* setReverse(bool reverse);
/**
* If a value is provided, then the node this layout applies to may be
* automatically rescaled to fit its contents better. By default the value
* is nullopt, which means that the layout doesn't affect the node's scale
* in any way, and any nodes that might overflow will be squished using
* other methods
*/
ColumnLayout* setMaxAutoScale(std::optional<float> scale);
/**
* If true, the children of the node this layout is applied to will be
* contained entirely within the bounds of the node's content size. If
* false, the children's positions will be within the bounds, but they may
* visually overflow depending on their anchor point
*/
ColumnLayout* setFitInside(bool fit);
};
/**

View file

@ -2,6 +2,7 @@
#include <Geode/utils/cocos.hpp>
#include <Geode/utils/ranges.hpp>
#include <Geode/loader/Log.hpp>
#include <Geode/binding/CCMenuItemSpriteExtra.hpp>
USE_GEODE_NAMESPACE();
@ -21,182 +22,230 @@ CCArray* Layout::getNodesToPosition(CCNode* on) {
return filtered;
}
void RowLayout::apply(CCNode* on) {
void AxisLayout::apply(CCNode* on) {
auto nodes = getNodesToPosition(on);
if (m_reverse) {
nodes->reverseObjects();
}
auto availableWidth = m_maxAutoScale.has_value() ?
on->getScaledContentSize().width :
on->getContentSize().width;
float availableAxisLength;
float originalCrossHeight;
if (m_axis == Axis::Row) {
availableAxisLength = on->getContentSize().width;
originalCrossHeight = on->getContentSize().height;
}
else {
availableAxisLength = on->getContentSize().height;
originalCrossHeight = on->getContentSize().width;
}
size_t ix = 0;
float totalWidth = .0f;
float totalAxisLength = .0f;
float maxCrossLength = 0.f;
for (auto& node : CCArrayExt<CCNode*>(nodes)) {
float axisLength;
float axisAnchor;
float crossLength;
if (m_axis == Axis::Row) {
axisLength = node->getScaledContentSize().width;
axisAnchor = node->getAnchorPoint().x;
crossLength = node->getScaledContentSize().height;
}
else {
axisLength = node->getScaledContentSize().height;
axisAnchor = node->getAnchorPoint().y;
crossLength = node->getScaledContentSize().width;
}
// if no need to fit fully inside and only one item exists, the total
// width taken up is 0
if (nodes->count() == 1 && !m_fitInside) {
totalAxisLength = 0;
}
// if no need to fit fully inside, figure out what part may overflow
// for first item
if (ix == 0 && !m_fitInside) {
totalWidth += node->getScaledContentSize().width
* (1.f - node->getAnchorPoint().x);
else if (ix == 0 && !m_fitInside) {
totalAxisLength += axisLength * (1.f - axisAnchor);
}
// if no need to fit fully inside, figure out what part may overflow
// for last item
else if (ix == nodes->count() - 1 && !m_fitInside) {
totalWidth += node->getScaledContentSize().width
* node->getAnchorPoint().x;
totalAxisLength += axisLength * axisAnchor;
}
// otherwise either we need to fit fully inside or this node is not
// at the start or end
else {
totalWidth += node->getScaledContentSize().width;
totalAxisLength += axisLength;
}
if (ix) {
totalWidth += m_gap;
totalAxisLength += m_gap;
}
if (crossLength > maxCrossLength) {
maxCrossLength = crossLength;
}
ix++;
}
auto squeeze = availableSize.width / totalWidth;
if (squeeze > 1.f) {
squeeze = 1.f;
const auto minScale = .65f;
// assume intended scale is 1x
auto setScale = 1.f;
auto squeeze = 1.f;
// check for overflow
// first try to make the node smaller
if (totalAxisLength > availableAxisLength && m_autoScale) {
setScale = availableAxisLength / totalAxisLength;
if (setScale < minScale) {
setScale = minScale;
}
totalAxisLength *= setScale;
}
// if we're still overflowing, squeeze nodes closer together
if (totalAxisLength > availableAxisLength) {
squeeze = availableAxisLength / totalAxisLength;
totalAxisLength = availableAxisLength;
}
// resize target to match settings
if (m_shrinkCrossAxis) {
if (m_axis == Axis::Row) {
on->setContentSize({
availableAxisLength,
maxCrossLength,
});
}
else {
on->setContentSize({
maxCrossLength,
availableAxisLength,
});
}
}
float pos;
switch (m_alignment) {
default:
case Alignment::Center: pos = -totalWidth / 2; break;
case Alignment::Begin: pos = -totalWidth; break;
case Alignment::End: pos = 0.f; break;
switch (m_axisAlignment) {
case Alignment::Begin: {
pos = 0.f;
} break;
case Alignment::Center: {
pos = availableAxisLength / 2 - totalAxisLength / 2;
} break;
case Alignment::End: {
pos = availableAxisLength - totalAxisLength;
} break;
}
ix = 0;
for (auto& node : CCArrayExt<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;
// rescale node if overflowing
if (m_autoScale) {
// CCMenuItemSpriteExtra is quirky af
if (auto btn = typeinfo_cast<CCMenuItemSpriteExtra*>(node)) {
btn->m_baseScale = setScale;
}
node->setScale(setScale);
}
node->setPositionX(pos + disp);
if (m_alignVertically) {
node->setPositionY(m_alignVertically.value());
float axisLength;
float axisAnchor;
float crossLength;
float crossAnchor;
if (m_axis == Axis::Row) {
axisLength = node->getScaledContentSize().width;
axisAnchor = node->getAnchorPoint().x;
crossLength = node->getScaledContentSize().height;
crossAnchor = node->getAnchorPoint().y;
}
pos += (sw + m_gap) * squeeze;
else {
axisLength = node->getScaledContentSize().height;
axisAnchor = node->getAnchorPoint().y;
crossLength = node->getScaledContentSize().width;
crossAnchor = node->getAnchorPoint().x;
}
float axisPos;
if (ix == 0 && !m_fitInside) {
axisPos = pos;
pos += (axisLength * (1.f - axisAnchor) + m_gap * setScale) * squeeze;
}
else {
axisPos = pos + axisLength * axisAnchor * squeeze;
pos += (axisLength + m_gap * setScale) * squeeze;
}
float crossPos;
switch (m_crossAlignment) {
case Alignment::Begin: {
crossPos = crossLength * crossAnchor;
} break;
case Alignment::Center: {
crossPos = maxCrossLength / 2 - crossLength * (.5f - crossAnchor);
} break;
case Alignment::End: {
crossPos = maxCrossLength - crossLength * (1.f - crossAnchor);
} break;
}
if (m_axis == Axis::Row) {
node->setPosition(axisPos, crossPos);
}
else {
node->setPosition(crossPos, axisPos);
}
ix++;
}
}
AxisLayout::AxisLayout(Axis axis) : m_axis(axis) {}
AxisLayout* AxisLayout::setCrossAxisAlignment(Alignment align) {
m_crossAlignment = align;
return this;
}
AxisLayout* AxisLayout::setAxisAlignment(Alignment align) {
m_axisAlignment = align;
return this;
}
AxisLayout* AxisLayout::setGap(float gap) {
m_gap = gap;
return this;
}
AxisLayout* AxisLayout::setReverse(bool reverse) {
m_reverse = reverse;
return this;
}
AxisLayout* AxisLayout::setAutoScale(bool scale) {
m_autoScale = scale;
return this;
}
AxisLayout* AxisLayout::setFitInside(bool fit) {
m_fitInside = fit;
return this;
}
AxisLayout* AxisLayout::setShrinkCrossAxis(bool shrink) {
m_shrinkCrossAxis = shrink;
return this;
}
RowLayout::RowLayout() : AxisLayout(AxisLayout::Row) {}
RowLayout* RowLayout::create() {
return new RowLayout();
}
RowLayout* RowLayout::setVAlignment(std::optional<Alignment> align) {
m_vAlignment = align;
return this;
}
RowLayout* RowLayout::setGap(float gap) {
m_gap = gap;
return this;
}
RowLayout* RowLayout::setReverse(bool reverse) {
m_reverse = reverse;
return this;
}
RowLayout* RowLayout::setMaxAutoScale(std::optional<float> scale) {
m_maxAutoScale = scale;
return this;
}
RowLayout* RowLayout::setFitInside(bool fit) {
m_fitInside = fit;
return this;
}
void ColumnLayout::apply(CCNode* on) {
float totalHeight = .0f;
size_t ix = 0;
auto nodes = getNodesToPosition(on);
auto availableSize = on->getScaledContentSize();
for (auto& node : CCArrayExt<CCNode*>(nodes)) {
totalHeight += node->getScaledContentSize().height;
if (ix) {
totalHeight += m_gap;
}
}
auto squeeze = availableSize.height / totalHeight;
if (squeeze > 1.f) {
squeeze = 1.f;
}
if (totalHeight > availableSize.height) {
totalHeight = availableSize.height;
}
float pos;
switch (m_alignment) {
default:
case Alignment::Center: pos = -totalHeight / 2; break;
case Alignment::Begin: pos = -totalHeight; break;
case Alignment::End: pos = 0.f; break;
}
if (m_reverse) {
nodes->reverseObjects();
}
log::debug("start pos: {}", pos);
log::debug("squeeze: {}", squeeze);
for (auto& node : CCArrayExt<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 = (m_reverse ? 0.f : sh); break;
case Alignment::End: disp = (m_reverse ? sh : 0.f); break;
}
log::debug("positioning at: {}", pos + disp);
node->setPositionY(pos + disp);
if (m_alignHorizontally) {
node->setPositionX(m_alignHorizontally.value());
}
auto opos = pos;
pos += (sh + m_gap) * squeeze;
log::debug("pos: {} -> {}", opos, pos);
}
}
ColumnLayout::ColumnLayout() : AxisLayout(AxisLayout::Column) {}
ColumnLayout* ColumnLayout::create() {
return new ColumnLayout();
}
ColumnLayout* ColumnLayout::setHAlignment(std::optional<Alignment> align) {
m_hAlignment = align;
return this;
}
ColumnLayout* ColumnLayout::setGap(float gap) {
m_gap = gap;
return this;
}
ColumnLayout* ColumnLayout::setReverse(bool reverse) {
m_reverse = reverse;
return this;
}
ColumnLayout* ColumnLayout::setMaxAutoScale(std::optional<float> scale) {
m_maxAutoScale = scale;
return this;
}
ColumnLayout* ColumnLayout::setFitInside(bool fit) {
m_fitInside = fit;
return this;
}
void GridLayout::apply(CCNode* on) {
// todo
}

View file

@ -64,12 +64,13 @@ static CCMenu* detachAndCreateMenu(CCNode* parent, const char* menuID, Layout* l
newMenu->setPosition(parent->convertToNodeSpace(oldMenu->convertToWorldSpace(first->getPosition())));
newMenu->setID(menuID);
newMenu->setZOrder(oldMenu->getZOrder());
newMenu->setLayout(layout);
parent->addChild(newMenu);
first->setPosition(0, 0);
newMenu->addChild(first);
first->release();
newMenu->setLayout(layout);
(switchToMenu(args, newMenu), ...);

View file

@ -29,9 +29,11 @@ $register_ids(CreatorLayer) {
detachAndCreateMenu(
this,
"top-right-menu",
ColumnLayout::create(),
ColumnLayout::create()
->setFitInside(false)
->setAxisAlignment(Alignment::Begin),
lockBtn
)->setAnchorPoint({ 0.f, 0.f });
)->setAnchorPoint({ .5f, 0.f });
}
// move treasure room button to its own menu
@ -39,9 +41,11 @@ $register_ids(CreatorLayer) {
detachAndCreateMenu(
this,
"bottom-right-menu",
ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::End),
ColumnLayout::create()
->setFitInside(false)
->setAxisAlignment(Alignment::End),
roomBtn
);
)->setAnchorPoint({ .5f, 1.f });
}
}

View file

@ -57,10 +57,11 @@ $register_ids(EditLevelLayer) {
menu->setLayout(
ColumnLayout::create()
->setGap(7.f)
->setFitInside(false)
->setAxisAlignment(Alignment::Begin)
->setReverse(true)
->setAlignment(Alignment::Begin)
->setAlignHorizontally(0.f)
);
menu->setAnchorPoint({ .5f, 0.f });
menu->setZOrder(1);
for (int i = 0; i < rand() % 4; i++) {

View file

@ -177,10 +177,12 @@ $register_ids(EditorUI) {
detachAndCreateMenu(
this,
"top-right-menu",
RowLayout::create()->setAlignment(Alignment::End),
RowLayout::create()
->setFitInside(false)
->setAxisAlignment(Alignment::End),
menu->getChildByID("pause-button"),
menu->getChildByID("settings-button")
);
)->setAnchorPoint({ 1.f, .5f });
detachAndCreateMenu(
this,

View file

@ -21,12 +21,19 @@ $register_ids(LevelBrowserLayer) {
detachAndCreateMenu(
this,
"my-levels-menu",
ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::End),
ColumnLayout::create()
->setFitInside(false)
->setAxisAlignment(Alignment::End),
myLevelsBtn
);
)->setAnchorPoint({ .5f, 1.f });
}
menu->setLayout(ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::End));
menu->setLayout(
ColumnLayout::create()
->setFitInside(false)
->setAxisAlignment(Alignment::End)
);
menu->setAnchorPoint({ .5f, 1.f });
}
}
}

View file

@ -44,8 +44,13 @@ $register_ids(LevelInfoLayer) {
if (auto name = setIDSafe(menu, 0, "creator-name")) {
detachAndCreateMenu(
this, "creator-info-menu", ColumnLayout::create()->setAlignment(Alignment::Begin), name
);
this,
"creator-info-menu",
ColumnLayout::create()
->setFitInside(false)
->setAxisAlignment(Alignment::Begin),
name
)->setAnchorPoint({ .5f, 0.f });
}
auto leftSideMenu = CCMenu::create();

View file

@ -165,9 +165,11 @@ $register_ids(LevelSettingsLayer) {
detachAndCreateMenu(
this,
"font-button-menu",
RowLayout::create()->setAlignment(Alignment::End),
RowLayout::create()
->setFitInside(false)
->setAxisAlignment(Alignment::End),
menu->getChildByID("font-button")
);
)->setAnchorPoint({ .5f, 1.f });
}
}

View file

@ -41,9 +41,17 @@ $register_ids(MenuLayer) {
setIDSafe(menu, 2, "editor-button");
if (auto pfp = setIDSafe(menu, 3, "profile-button")) {
pfp->setPositionHint(PositionHint::Absolute);
detachAndCreateMenu(
this, "profile-menu",
ColumnLayout::create()
->setFitInside(false)
->setAxisAlignment(Alignment::Begin)
->setReverse(true),
pfp
)->setAnchorPoint({ .5f, .0f });
}
menu->setContentSize({ 400.f, 65.f });
menu->setLayout(RowLayout::create()->setGap(18.f));
}
// bottom menu
@ -57,10 +65,18 @@ $register_ids(MenuLayer) {
// move daily chest to its own menu
if (auto dailyChest = setIDSafe(menu, -1, "daily-chest-button")) {
detachAndCreateMenu(this, "right-side-menu", ColumnLayout::create(), dailyChest);
auto menu = detachAndCreateMenu(
this,
"right-side-menu",
ColumnLayout::create()->setFitInside(true),
dailyChest
);
menu->setContentSize({ 65.f, 180.f });
menu->updateLayout();
}
menu->setLayout(RowLayout::create());
menu->setContentSize({ 360.f, 65.f });
menu->setLayout(RowLayout::create()->setFitInside(true));
}
// social media menu
if (auto menu = getChildOfType<CCMenu>(this, 2)) {
@ -79,7 +95,12 @@ $register_ids(MenuLayer) {
if (auto closeBtn = setIDSafe(menu, 1, "close-button")) {
detachAndCreateMenu(
this, "close-menu", RowLayout::create(), closeBtn
this,
"close-menu",
RowLayout::create()
->setFitInside(false)
->setAxisAlignment(Alignment::Begin),
closeBtn
)->setAnchorPoint({ 0.f, .5f });
}
}