/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { struct FlexBoxLayoutCalculation { using Coord = double; enum class Axis { main, cross }; FlexBoxLayoutCalculation (FlexBox& fb, Coord w, Coord h) : owner (fb), parentWidth (w), parentHeight (h), numItems (owner.items.size()), isRowDirection (fb.flexDirection == FlexBox::Direction::row || fb.flexDirection == FlexBox::Direction::rowReverse), containerLineLength (getContainerSize (Axis::main)) { lineItems.calloc (numItems * numItems); lineInfo.calloc (numItems); } struct ItemWithState { ItemWithState (FlexItem& source) noexcept : item (&source) {} FlexItem* item; Coord lockedWidth = 0, lockedHeight = 0; Coord lockedMarginLeft = 0, lockedMarginRight = 0, lockedMarginTop = 0, lockedMarginBottom = 0; Coord preferredWidth = 0, preferredHeight = 0; bool locked = false; void resetItemLockedSize() noexcept { lockedWidth = preferredWidth; lockedHeight = preferredHeight; lockedMarginLeft = getValueOrZeroIfAuto (item->margin.left); lockedMarginRight = getValueOrZeroIfAuto (item->margin.right); lockedMarginTop = getValueOrZeroIfAuto (item->margin.top); lockedMarginBottom = getValueOrZeroIfAuto (item->margin.bottom); } }; struct RowInfo { int numItems; Coord crossSize, lineY, totalLength; }; FlexBox& owner; const Coord parentWidth, parentHeight; const int numItems; const bool isRowDirection; const Coord containerLineLength; int numberOfRows = 1; Coord containerCrossLength = 0; HeapBlock lineItems; HeapBlock lineInfo; Array itemStates; ItemWithState& getItem (int x, int y) const noexcept { return *lineItems[y * numItems + x]; } static bool isAuto (Coord value) noexcept { return value == FlexItem::autoValue; } static bool isAssigned (Coord value) noexcept { return value != FlexItem::notAssigned; } static Coord getValueOrZeroIfAuto (Coord value) noexcept { return isAuto (value) ? Coord() : value; } //============================================================================== bool isSingleLine() const { return owner.flexWrap == FlexBox::Wrap::noWrap; } template Value& pickForAxis (Axis axis, Value& x, Value& y) const { return (isRowDirection ? axis == Axis::main : axis == Axis::cross) ? x : y; } auto& getStartMargin (Axis axis, ItemWithState& item) const { return pickForAxis (axis, item.item->margin.left, item.item->margin.top); } auto& getEndMargin (Axis axis, ItemWithState& item) const { return pickForAxis (axis, item.item->margin.right, item.item->margin.bottom); } auto& getStartLockedMargin (Axis axis, ItemWithState& item) const { return pickForAxis (axis, item.lockedMarginLeft, item.lockedMarginTop); } auto& getEndLockedMargin (Axis axis, ItemWithState& item) const { return pickForAxis (axis, item.lockedMarginRight, item.lockedMarginBottom); } auto& getLockedSize (Axis axis, ItemWithState& item) const { return pickForAxis (axis, item.lockedWidth, item.lockedHeight); } auto& getPreferredSize (Axis axis, ItemWithState& item) const { return pickForAxis (axis, item.preferredWidth, item.preferredHeight); } Coord getContainerSize (Axis axis) const { return pickForAxis (axis, parentWidth, parentHeight); } auto& getItemSize (Axis axis, ItemWithState& item) const { return pickForAxis (axis, item.item->width, item.item->height); } auto& getMinSize (Axis axis, ItemWithState& item) const { return pickForAxis (axis, item.item->minWidth, item.item->minHeight); } auto& getMaxSize (Axis axis, ItemWithState& item) const { return pickForAxis (axis, item.item->maxWidth, item.item->maxHeight); } //============================================================================== void createStates() { itemStates.ensureStorageAllocated (numItems); for (auto& item : owner.items) itemStates.add (item); std::stable_sort (itemStates.begin(), itemStates.end(), [] (const ItemWithState& i1, const ItemWithState& i2) { return i1.item->order < i2.item->order; }); for (auto& item : itemStates) { for (auto& axis : { Axis::main, Axis::cross }) getPreferredSize (axis, item) = computePreferredSize (axis, item); } } void initialiseItems() noexcept { if (isSingleLine()) // for single-line, all items go in line 1 { lineInfo[0].numItems = numItems; int i = 0; for (auto& item : itemStates) { item.resetItemLockedSize(); lineItems[i++] = &item; } } else // if multi-line, group the flexbox items into multiple lines { auto currentLength = containerLineLength; int column = 0, row = 0; bool firstRow = true; for (auto& item : itemStates) { item.resetItemLockedSize(); const auto flexitemLength = getItemMainSize (item); if (flexitemLength > currentLength) { if (! firstRow) row++; if (row >= numItems) break; column = 0; currentLength = containerLineLength; numberOfRows = jmax (numberOfRows, row + 1); } currentLength -= flexitemLength; lineItems[row * numItems + column] = &item; ++column; lineInfo[row].numItems = jmax (lineInfo[row].numItems, column); firstRow = false; } } } void resolveFlexibleLengths() noexcept { for (int row = 0; row < numberOfRows; ++row) { resetRowItems (row); for (int maxLoops = numItems; --maxLoops >= 0;) { resetUnlockedRowItems (row); if (layoutRowItems (row)) break; } } } void resolveAutoMarginsOnMainAxis() noexcept { for (int row = 0; row < numberOfRows; ++row) { Coord allFlexGrow = 0; const auto numColumns = lineInfo[row].numItems; const auto remainingLength = containerLineLength - lineInfo[row].totalLength; for (int column = 0; column < numColumns; ++column) { auto& item = getItem (column, row); if (isAuto (getStartMargin (Axis::main, item))) ++allFlexGrow; if (isAuto (getEndMargin (Axis::main, item))) ++allFlexGrow; } const auto changeUnitWidth = remainingLength / allFlexGrow; if (changeUnitWidth > 0) { for (int column = 0; column < numColumns; ++column) { auto& item = getItem (column, row); if (isAuto (getStartMargin (Axis::main, item))) getStartLockedMargin (Axis::main, item) = changeUnitWidth; if (isAuto (getEndMargin (Axis::main, item))) getEndLockedMargin (Axis::main, item) = changeUnitWidth; } } } } void calculateCrossSizesByLine() noexcept { // https://www.w3.org/TR/css-flexbox-1/#algo-cross-line // If the flex container is single-line and has a definite cross size, the cross size of the // flex line is the flex container’s inner cross size. if (isSingleLine()) { lineInfo[0].crossSize = getContainerSize (Axis::cross); } else { for (int row = 0; row < numberOfRows; ++row) { Coord maxSize = 0; const auto numColumns = lineInfo[row].numItems; for (int column = 0; column < numColumns; ++column) maxSize = jmax (maxSize, getItemCrossSize (getItem (column, row))); lineInfo[row].crossSize = maxSize; } } } void calculateCrossSizeOfAllItems() noexcept { for (int row = 0; row < numberOfRows; ++row) { const auto numColumns = lineInfo[row].numItems; for (int column = 0; column < numColumns; ++column) { auto& item = getItem (column, row); if (isAssigned (item.item->maxHeight) && item.lockedHeight > item.item->maxHeight) item.lockedHeight = item.item->maxHeight; if (isAssigned (item.item->maxWidth) && item.lockedWidth > item.item->maxWidth) item.lockedWidth = item.item->maxWidth; } } } void alignLinesPerAlignContent() noexcept { containerCrossLength = getContainerSize (Axis::cross); if (owner.alignContent == FlexBox::AlignContent::flexStart) { for (int row = 0; row < numberOfRows; ++row) for (int row2 = row; row2 < numberOfRows; ++row2) lineInfo[row].lineY = row == 0 ? 0 : lineInfo[row - 1].lineY + lineInfo[row - 1].crossSize; } else if (owner.alignContent == FlexBox::AlignContent::flexEnd) { for (int row = 0; row < numberOfRows; ++row) { Coord crossHeights = 0; for (int row2 = row; row2 < numberOfRows; ++row2) crossHeights += lineInfo[row2].crossSize; lineInfo[row].lineY = containerCrossLength - crossHeights; } } else { Coord totalHeight = 0; for (int row = 0; row < numberOfRows; ++row) totalHeight += lineInfo[row].crossSize; if (owner.alignContent == FlexBox::AlignContent::stretch) { const auto difference = jmax (Coord(), (containerCrossLength - totalHeight) / numberOfRows); for (int row = 0; row < numberOfRows; ++row) { lineInfo[row].crossSize += difference; lineInfo[row].lineY = row == 0 ? 0 : lineInfo[row - 1].lineY + lineInfo[row - 1].crossSize; } } else if (owner.alignContent == FlexBox::AlignContent::center) { const auto additionalength = (containerCrossLength - totalHeight) / 2; for (int row = 0; row < numberOfRows; ++row) lineInfo[row].lineY = row == 0 ? additionalength : lineInfo[row - 1].lineY + lineInfo[row - 1].crossSize; } else if (owner.alignContent == FlexBox::AlignContent::spaceBetween) { const auto additionalength = numberOfRows <= 1 ? Coord() : jmax (Coord(), (containerCrossLength - totalHeight) / static_cast (numberOfRows - 1)); lineInfo[0].lineY = 0; for (int row = 1; row < numberOfRows; ++row) lineInfo[row].lineY += additionalength + lineInfo[row - 1].lineY + lineInfo[row - 1].crossSize; } else if (owner.alignContent == FlexBox::AlignContent::spaceAround) { const auto additionalength = numberOfRows <= 1 ? Coord() : jmax (Coord(), (containerCrossLength - totalHeight) / static_cast (2 + (2 * (numberOfRows - 1)))); lineInfo[0].lineY = additionalength; for (int row = 1; row < numberOfRows; ++row) lineInfo[row].lineY += (2 * additionalength) + lineInfo[row - 1].lineY + lineInfo[row - 1].crossSize; } } } void resolveAutoMarginsOnCrossAxis() noexcept { for (int row = 0; row < numberOfRows; ++row) { const auto numColumns = lineInfo[row].numItems; const auto crossSizeForLine = lineInfo[row].crossSize; for (int column = 0; column < numColumns; ++column) { auto& item = getItem (column, row); getStartLockedMargin (Axis::cross, item) = [&] { if (isAuto (getStartMargin (Axis::cross, item)) && isAuto (getEndMargin (Axis::cross, item))) return (crossSizeForLine - getLockedSize (Axis::cross, item)) / 2; if (isAuto (getStartMargin (Axis::cross, item))) return crossSizeForLine - getLockedSize (Axis::cross, item) - getEndMargin (Axis::cross, item); return getStartLockedMargin (Axis::cross, item); }(); } } } // Align all flex items along the cross-axis per align-self, if neither of the item’s cross-axis margins are auto. void alignItemsInCrossAxisInLinesPerAlignSelf() noexcept { for (int row = 0; row < numberOfRows; ++row) { const auto numColumns = lineInfo[row].numItems; const auto lineSize = lineInfo[row].crossSize; for (int column = 0; column < numColumns; ++column) { auto& item = getItem (column, row); if (isAuto (getStartMargin (Axis::cross, item)) || isAuto (getEndMargin (Axis::cross, item))) continue; const auto alignment = [&] { switch (item.item->alignSelf) { case FlexItem::AlignSelf::stretch: return FlexBox::AlignItems::stretch; case FlexItem::AlignSelf::flexStart: return FlexBox::AlignItems::flexStart; case FlexItem::AlignSelf::flexEnd: return FlexBox::AlignItems::flexEnd; case FlexItem::AlignSelf::center: return FlexBox::AlignItems::center; case FlexItem::AlignSelf::autoAlign: break; } return owner.alignItems; }(); getStartLockedMargin (Axis::cross, item) = [&] { switch (alignment) { // https://www.w3.org/TR/css-flexbox-1/#valdef-align-items-flex-start // The cross-start margin edge of the flex item is placed flush with the // cross-start edge of the line. case FlexBox::AlignItems::flexStart: return (Coord) getStartMargin (Axis::cross, item); // https://www.w3.org/TR/css-flexbox-1/#valdef-align-items-flex-end // The cross-end margin edge of the flex item is placed flush with the cross-end // edge of the line. case FlexBox::AlignItems::flexEnd: return lineSize - getLockedSize (Axis::cross, item) - getEndMargin (Axis::cross, item); // https://www.w3.org/TR/css-flexbox-1/#valdef-align-items-center // The flex item’s margin box is centered in the cross axis within the line. case FlexBox::AlignItems::center: return getStartMargin (Axis::cross, item) + (lineSize - getLockedSize (Axis::cross, item) - getStartMargin (Axis::cross, item) - getEndMargin (Axis::cross, item)) / 2; // https://www.w3.org/TR/css-flexbox-1/#valdef-align-items-stretch case FlexBox::AlignItems::stretch: return (Coord) getStartMargin (Axis::cross, item); } jassertfalse; return 0.0; }(); if (alignment == FlexBox::AlignItems::stretch) { auto newSize = isAssigned (getItemSize (Axis::cross, item)) ? computePreferredSize (Axis::cross, item) : lineSize - getStartMargin (Axis::cross, item) - getEndMargin (Axis::cross, item); if (isAssigned (getMaxSize (Axis::cross, item))) newSize = jmin (newSize, (Coord) getMaxSize (Axis::cross, item)); if (isAssigned (getMinSize (Axis::cross, item))) newSize = jmax (newSize, (Coord) getMinSize (Axis::cross, item)); getLockedSize (Axis::cross, item) = newSize; } } } } void alignItemsByJustifyContent() noexcept { Coord additionalMarginRight = 0, additionalMarginLeft = 0; recalculateTotalItemLengthPerLineArray(); for (int row = 0; row < numberOfRows; ++row) { const auto numColumns = lineInfo[row].numItems; Coord x = 0; if (owner.justifyContent == FlexBox::JustifyContent::flexEnd) { x = containerLineLength - lineInfo[row].totalLength; } else if (owner.justifyContent == FlexBox::JustifyContent::center) { x = (containerLineLength - lineInfo[row].totalLength) / 2; } else if (owner.justifyContent == FlexBox::JustifyContent::spaceBetween) { additionalMarginRight = jmax (Coord(), (containerLineLength - lineInfo[row].totalLength) / jmax (1, numColumns - 1)); } else if (owner.justifyContent == FlexBox::JustifyContent::spaceAround) { additionalMarginLeft = additionalMarginRight = jmax (Coord(), (containerLineLength - lineInfo[row].totalLength) / jmax (1, 2 * numColumns)); } for (int column = 0; column < numColumns; ++column) { auto& item = getItem (column, row); getStartLockedMargin (Axis::main, item) += additionalMarginLeft; getEndLockedMargin (Axis::main, item) += additionalMarginRight; item.item->currentBounds.setPosition (isRowDirection ? (float) (x + item.lockedMarginLeft) : (float) item.lockedMarginLeft, isRowDirection ? (float) item.lockedMarginTop : (float) (x + item.lockedMarginTop)); x += getItemMainSize (item); } } } void layoutAllItems() noexcept { for (int row = 0; row < numberOfRows; ++row) { const auto lineY = lineInfo[row].lineY; const auto numColumns = lineInfo[row].numItems; for (int column = 0; column < numColumns; ++column) { auto& item = getItem (column, row); if (isRowDirection) item.item->currentBounds.setY ((float) (lineY + item.lockedMarginTop)); else item.item->currentBounds.setX ((float) (lineY + item.lockedMarginLeft)); item.item->currentBounds.setSize ((float) item.lockedWidth, (float) item.lockedHeight); } } reverseLocations(); reverseWrap(); } private: void resetRowItems (const int row) noexcept { const auto numColumns = lineInfo[row].numItems; for (int column = 0; column < numColumns; ++column) resetItem (getItem (column, row)); } void resetUnlockedRowItems (const int row) noexcept { const auto numColumns = lineInfo[row].numItems; for (int column = 0; column < numColumns; ++column) { auto& item = getItem (column, row); if (! item.locked) resetItem (item); } } void resetItem (ItemWithState& item) noexcept { item.locked = false; for (auto& axis : { Axis::main, Axis::cross }) getLockedSize (axis, item) = computePreferredSize (axis, item); } bool layoutRowItems (const int row) noexcept { const auto numColumns = lineInfo[row].numItems; auto flexContainerLength = containerLineLength; Coord totalItemsLength = 0, totalFlexGrow = 0, totalFlexShrink = 0; for (int column = 0; column < numColumns; ++column) { const auto& item = getItem (column, row); if (item.locked) { flexContainerLength -= getItemMainSize (item); } else { totalItemsLength += getItemMainSize (item); totalFlexGrow += item.item->flexGrow; totalFlexShrink += item.item->flexShrink; } } Coord changeUnit = 0; const auto difference = flexContainerLength - totalItemsLength; const bool positiveFlexibility = difference > 0; if (positiveFlexibility) { if (totalFlexGrow != 0.0) changeUnit = difference / totalFlexGrow; } else { if (totalFlexShrink != 0.0) changeUnit = difference / totalFlexShrink; } bool ok = true; for (int column = 0; column < numColumns; ++column) { auto& item = getItem (column, row); if (! item.locked) if (! addToItemLength (item, (positiveFlexibility ? item.item->flexGrow : item.item->flexShrink) * changeUnit, row)) ok = false; } return ok; } void recalculateTotalItemLengthPerLineArray() noexcept { for (int row = 0; row < numberOfRows; ++row) { lineInfo[row].totalLength = 0; const auto numColumns = lineInfo[row].numItems; for (int column = 0; column < numColumns; ++column) lineInfo[row].totalLength += getItemMainSize (getItem (column, row)); } } void reverseLocations() noexcept { if (owner.flexDirection == FlexBox::Direction::rowReverse) { for (auto& item : owner.items) item.currentBounds.setX ((float) (containerLineLength - item.currentBounds.getRight())); } else if (owner.flexDirection == FlexBox::Direction::columnReverse) { for (auto& item : owner.items) item.currentBounds.setY ((float) (containerLineLength - item.currentBounds.getBottom())); } } void reverseWrap() noexcept { if (owner.flexWrap == FlexBox::Wrap::wrapReverse) { if (isRowDirection) { for (auto& item : owner.items) item.currentBounds.setY ((float) (containerCrossLength - item.currentBounds.getBottom())); } else { for (auto& item : owner.items) item.currentBounds.setX ((float) (containerCrossLength - item.currentBounds.getRight())); } } } Coord getItemMainSize (const ItemWithState& item) const noexcept { return isRowDirection ? item.lockedWidth + item.lockedMarginLeft + item.lockedMarginRight : item.lockedHeight + item.lockedMarginTop + item.lockedMarginBottom; } Coord getItemCrossSize (const ItemWithState& item) const noexcept { return isRowDirection ? item.lockedHeight + item.lockedMarginTop + item.lockedMarginBottom : item.lockedWidth + item.lockedMarginLeft + item.lockedMarginRight; } bool addToItemLength (ItemWithState& item, const Coord length, int row) const noexcept { bool ok = false; const auto prefSize = computePreferredSize (Axis::main, item); const auto pickForMainAxis = [this] (auto& a, auto& b) -> auto& { return pickForAxis (Axis::main, a, b); }; if (isAssigned (pickForMainAxis (item.item->maxWidth, item.item->maxHeight)) && pickForMainAxis (item.item->maxWidth, item.item->maxHeight) < prefSize + length) { pickForMainAxis (item.lockedWidth, item.lockedHeight) = pickForMainAxis (item.item->maxWidth, item.item->maxHeight); item.locked = true; } else if (isAssigned (prefSize) && pickForMainAxis (item.item->minWidth, item.item->minHeight) > prefSize + length) { pickForMainAxis (item.lockedWidth, item.lockedHeight) = pickForMainAxis (item.item->minWidth, item.item->minHeight); item.locked = true; } else { ok = true; pickForMainAxis (item.lockedWidth, item.lockedHeight) = prefSize + length; } lineInfo[row].totalLength += pickForMainAxis (item.lockedWidth, item.lockedHeight) + pickForMainAxis (item.lockedMarginLeft, item.lockedMarginTop) + pickForMainAxis (item.lockedMarginRight, item.lockedMarginBottom); return ok; } Coord computePreferredSize (Axis axis, ItemWithState& itemWithState) const noexcept { const auto& item = *itemWithState.item; auto preferredSize = (item.flexBasis > 0 && axis == Axis::main) ? item.flexBasis : (isAssigned (getItemSize (axis, itemWithState)) ? getItemSize (axis, itemWithState) : getMinSize (axis, itemWithState)); const auto minSize = getMinSize (axis, itemWithState); if (isAssigned (minSize) && preferredSize < minSize) return minSize; const auto maxSize = getMaxSize (axis, itemWithState); if (isAssigned (maxSize) && maxSize < preferredSize) return maxSize; return preferredSize; } }; //============================================================================== FlexBox::FlexBox (JustifyContent jc) noexcept : justifyContent (jc) {} FlexBox::FlexBox (Direction d, Wrap w, AlignContent ac, AlignItems ai, JustifyContent jc) noexcept : flexDirection (d), flexWrap (w), alignContent (ac), alignItems (ai), justifyContent (jc) { } void FlexBox::performLayout (Rectangle targetArea) { if (! items.isEmpty()) { FlexBoxLayoutCalculation layout (*this, targetArea.getWidth(), targetArea.getHeight()); layout.createStates(); layout.initialiseItems(); layout.resolveFlexibleLengths(); layout.resolveAutoMarginsOnMainAxis(); layout.calculateCrossSizesByLine(); layout.calculateCrossSizeOfAllItems(); layout.alignLinesPerAlignContent(); layout.resolveAutoMarginsOnCrossAxis(); layout.alignItemsInCrossAxisInLinesPerAlignSelf(); layout.alignItemsByJustifyContent(); layout.layoutAllItems(); for (auto& item : items) { item.currentBounds += targetArea.getPosition(); if (auto* comp = item.associatedComponent) comp->setBounds (Rectangle::leftTopRightBottom ((int) item.currentBounds.getX(), (int) item.currentBounds.getY(), (int) item.currentBounds.getRight(), (int) item.currentBounds.getBottom())); if (auto* box = item.associatedFlexBox) box->performLayout (item.currentBounds); } } } void FlexBox::performLayout (Rectangle targetArea) { performLayout (targetArea.toFloat()); } //============================================================================== FlexItem::FlexItem() noexcept {} FlexItem::FlexItem (float w, float h) noexcept : currentBounds (w, h), minWidth (w), minHeight (h) {} FlexItem::FlexItem (float w, float h, Component& c) noexcept : FlexItem (w, h) { associatedComponent = &c; } FlexItem::FlexItem (float w, float h, FlexBox& fb) noexcept : FlexItem (w, h) { associatedFlexBox = &fb; } FlexItem::FlexItem (Component& c) noexcept : associatedComponent (&c) {} FlexItem::FlexItem (FlexBox& fb) noexcept : associatedFlexBox (&fb) {} FlexItem::Margin::Margin() noexcept : left(), right(), top(), bottom() {} FlexItem::Margin::Margin (float v) noexcept : left (v), right (v), top (v), bottom (v) {} FlexItem::Margin::Margin (float t, float r, float b, float l) noexcept : left (l), right (r), top (t), bottom (b) {} //============================================================================== FlexItem FlexItem::withFlex (float newFlexGrow) const noexcept { auto fi = *this; fi.flexGrow = newFlexGrow; return fi; } FlexItem FlexItem::withFlex (float newFlexGrow, float newFlexShrink) const noexcept { auto fi = withFlex (newFlexGrow); fi.flexShrink = newFlexShrink; return fi; } FlexItem FlexItem::withFlex (float newFlexGrow, float newFlexShrink, float newFlexBasis) const noexcept { auto fi = withFlex (newFlexGrow, newFlexShrink); fi.flexBasis = newFlexBasis; return fi; } FlexItem FlexItem::withWidth (float newWidth) const noexcept { auto fi = *this; fi.width = newWidth; return fi; } FlexItem FlexItem::withMinWidth (float newMinWidth) const noexcept { auto fi = *this; fi.minWidth = newMinWidth; return fi; } FlexItem FlexItem::withMaxWidth (float newMaxWidth) const noexcept { auto fi = *this; fi.maxWidth = newMaxWidth; return fi; } FlexItem FlexItem::withMinHeight (float newMinHeight) const noexcept { auto fi = *this; fi.minHeight = newMinHeight; return fi; } FlexItem FlexItem::withMaxHeight (float newMaxHeight) const noexcept { auto fi = *this; fi.maxHeight = newMaxHeight; return fi; } FlexItem FlexItem::withHeight (float newHeight) const noexcept { auto fi = *this; fi.height = newHeight; return fi; } FlexItem FlexItem::withMargin (Margin m) const noexcept { auto fi = *this; fi.margin = m; return fi; } FlexItem FlexItem::withOrder (int newOrder) const noexcept { auto fi = *this; fi.order = newOrder; return fi; } FlexItem FlexItem::withAlignSelf (AlignSelf a) const noexcept { auto fi = *this; fi.alignSelf = a; return fi; } //============================================================================== //============================================================================== #if JUCE_UNIT_TESTS class FlexBoxTests : public UnitTest { public: FlexBoxTests() : UnitTest ("FlexBox", UnitTestCategories::gui) {} void runTest() override { using AlignSelf = FlexItem::AlignSelf; using Direction = FlexBox::Direction; const Rectangle rect (10.0f, 20.0f, 300.0f, 200.0f); const auto doLayout = [&rect] (Direction direction, Array items) { juce::FlexBox flex; flex.flexDirection = direction; flex.items = std::move (items); flex.performLayout (rect); return flex; }; beginTest ("flex item with mostly auto properties"); { const auto test = [this, &doLayout] (Direction direction, AlignSelf alignment, Rectangle expectedBounds) { const auto flex = doLayout (direction, { juce::FlexItem{}.withAlignSelf (alignment) }); expect (flex.items.getFirst().currentBounds == expectedBounds); }; test (Direction::row, AlignSelf::autoAlign, { rect.getX(), rect.getY(), 0.0f, rect.getHeight() }); test (Direction::row, AlignSelf::stretch, { rect.getX(), rect.getY(), 0.0f, rect.getHeight() }); test (Direction::row, AlignSelf::flexStart, { rect.getX(), rect.getY(), 0.0f, 0.0f }); test (Direction::row, AlignSelf::flexEnd, { rect.getX(), rect.getBottom(), 0.0f, 0.0f }); test (Direction::row, AlignSelf::center, { rect.getX(), rect.getCentreY(), 0.0f, 0.0f }); test (Direction::column, AlignSelf::autoAlign, { rect.getX(), rect.getY(), rect.getWidth(), 0.0f }); test (Direction::column, AlignSelf::stretch, { rect.getX(), rect.getY(), rect.getWidth(), 0.0f }); test (Direction::column, AlignSelf::flexStart, { rect.getX(), rect.getY(), 0.0f, 0.0f }); test (Direction::column, AlignSelf::flexEnd, { rect.getRight(), rect.getY(), 0.0f, 0.0f }); test (Direction::column, AlignSelf::center, { rect.getCentreX(), rect.getY(), 0.0f, 0.0f }); } beginTest ("flex item with specified width and height"); { constexpr auto w = 50.0f; constexpr auto h = 60.0f; const auto test = [&] (Direction direction, AlignSelf alignment, Rectangle expectedBounds) { const auto flex = doLayout (direction, { juce::FlexItem().withAlignSelf (alignment) .withWidth (w) .withHeight (h) }); expect (flex.items.getFirst().currentBounds == expectedBounds); }; test (Direction::row, AlignSelf::autoAlign, { rect.getX(), rect.getY(), w, h }); test (Direction::row, AlignSelf::stretch, { rect.getX(), rect.getY(), w, h }); test (Direction::row, AlignSelf::flexStart, { rect.getX(), rect.getY(), w, h }); test (Direction::row, AlignSelf::flexEnd, { rect.getX(), rect.getBottom() - h, w, h }); test (Direction::row, AlignSelf::center, { rect.getX(), rect.getY() + (rect.getHeight() - h) * 0.5f, w, h }); test (Direction::column, AlignSelf::autoAlign, { rect.getX(), rect.getY(), w, h }); test (Direction::column, AlignSelf::stretch, { rect.getX(), rect.getY(), w, h }); test (Direction::column, AlignSelf::flexStart, { rect.getX(), rect.getY(), w, h }); test (Direction::column, AlignSelf::flexEnd, { rect.getRight() - w, rect.getY(), w, h }); test (Direction::column, AlignSelf::center, { rect.getX() + (rect.getWidth() - w) * 0.5f, rect.getY(), w, h }); } beginTest ("flex item with oversized width and height"); { const auto w = rect.getWidth() * 2; const auto h = rect.getHeight() * 2; const auto test = [this, &doLayout, &w, &h] (Direction direction, AlignSelf alignment, Rectangle expectedBounds) { const auto flex = doLayout (direction, { juce::FlexItem().withAlignSelf (alignment) .withWidth (w) .withHeight (h) }); expect (flex.items.getFirst().currentBounds == expectedBounds); }; const Rectangle baseRow (rect.getX(), rect.getY(), rect.getWidth(), h); test (Direction::row, AlignSelf::autoAlign, baseRow); test (Direction::row, AlignSelf::stretch, baseRow); test (Direction::row, AlignSelf::flexStart, baseRow); test (Direction::row, AlignSelf::flexEnd, baseRow.withBottomY (rect.getBottom())); test (Direction::row, AlignSelf::center, baseRow.withCentre (rect.getCentre())); const Rectangle baseColumn (rect.getX(), rect.getY(), w, rect.getHeight()); test (Direction::column, AlignSelf::autoAlign, baseColumn); test (Direction::column, AlignSelf::stretch, baseColumn); test (Direction::column, AlignSelf::flexStart, baseColumn); test (Direction::column, AlignSelf::flexEnd, baseColumn.withRightX (rect.getRight())); test (Direction::column, AlignSelf::center, baseColumn.withCentre (rect.getCentre())); } beginTest ("flex item with minimum width and height"); { constexpr auto w = 50.0f; constexpr auto h = 60.0f; const auto test = [&] (Direction direction, AlignSelf alignment, Rectangle expectedBounds) { const auto flex = doLayout (direction, { juce::FlexItem().withAlignSelf (alignment) .withMinWidth (w) .withMinHeight (h) }); expect (flex.items.getFirst().currentBounds == expectedBounds); }; test (Direction::row, AlignSelf::autoAlign, { rect.getX(), rect.getY(), w, rect.getHeight() }); test (Direction::row, AlignSelf::stretch, { rect.getX(), rect.getY(), w, rect.getHeight() }); test (Direction::row, AlignSelf::flexStart, { rect.getX(), rect.getY(), w, h }); test (Direction::row, AlignSelf::flexEnd, { rect.getX(), rect.getBottom() - h, w, h }); test (Direction::row, AlignSelf::center, { rect.getX(), rect.getY() + (rect.getHeight() - h) * 0.5f, w, h }); test (Direction::column, AlignSelf::autoAlign, { rect.getX(), rect.getY(), rect.getWidth(), h }); test (Direction::column, AlignSelf::stretch, { rect.getX(), rect.getY(), rect.getWidth(), h }); test (Direction::column, AlignSelf::flexStart, { rect.getX(), rect.getY(), w, h }); test (Direction::column, AlignSelf::flexEnd, { rect.getRight() - w, rect.getY(), w, h }); test (Direction::column, AlignSelf::center, { rect.getX() + (rect.getWidth() - w) * 0.5f, rect.getY(), w, h }); } beginTest ("flex item with maximum width and height"); { constexpr auto w = 50.0f; constexpr auto h = 60.0f; const auto test = [&] (Direction direction, AlignSelf alignment, Rectangle expectedBounds) { const auto flex = doLayout (direction, { juce::FlexItem().withAlignSelf (alignment) .withMaxWidth (w) .withMaxHeight (h) }); expect (flex.items.getFirst().currentBounds == expectedBounds); }; test (Direction::row, AlignSelf::autoAlign, { rect.getX(), rect.getY(), 0.0f, h }); test (Direction::row, AlignSelf::stretch, { rect.getX(), rect.getY(), 0.0f, h }); test (Direction::row, AlignSelf::flexStart, { rect.getX(), rect.getY(), 0.0f, 0.0f }); test (Direction::row, AlignSelf::flexEnd, { rect.getX(), rect.getBottom(), 0.0f, 0.0f }); test (Direction::row, AlignSelf::center, { rect.getX(), rect.getCentreY(), 0.0f, 0.0f }); test (Direction::column, AlignSelf::autoAlign, { rect.getX(), rect.getY(), w, 0.0f }); test (Direction::column, AlignSelf::stretch, { rect.getX(), rect.getY(), w, 0.0f }); test (Direction::column, AlignSelf::flexStart, { rect.getX(), rect.getY(), 0.0f, 0.0f }); test (Direction::column, AlignSelf::flexEnd, { rect.getRight(), rect.getY(), 0.0f, 0.0f }); test (Direction::column, AlignSelf::center, { rect.getCentreX(), rect.getY(), 0.0f, 0.0f }); } beginTest ("flex item with specified flex"); { const auto test = [this, &doLayout] (Direction direction, AlignSelf alignment, Rectangle expectedBounds) { const auto flex = doLayout (direction, { juce::FlexItem().withAlignSelf (alignment).withFlex (1.0f) }); expect (flex.items.getFirst().currentBounds == expectedBounds); }; test (Direction::row, AlignSelf::autoAlign, { rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight() }); test (Direction::row, AlignSelf::stretch, { rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight() }); test (Direction::row, AlignSelf::flexStart, { rect.getX(), rect.getY(), rect.getWidth(), 0.0f }); test (Direction::row, AlignSelf::flexEnd, { rect.getX(), rect.getBottom(), rect.getWidth(), 0.0f }); test (Direction::row, AlignSelf::center, { rect.getX(), rect.getCentreY(), rect.getWidth(), 0.0f }); test (Direction::column, AlignSelf::autoAlign, { rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight() }); test (Direction::column, AlignSelf::stretch, { rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight() }); test (Direction::column, AlignSelf::flexStart, { rect.getX(), rect.getY(), 0.0f, rect.getHeight() }); test (Direction::column, AlignSelf::flexEnd, { rect.getRight(), rect.getY(), 0.0f, rect.getHeight() }); test (Direction::column, AlignSelf::center, { rect.getCentreX(), rect.getY(), 0.0f, rect.getHeight() }); } beginTest ("flex item with margin"); { const FlexItem::Margin margin (10.0f, 20.0f, 30.0f, 40.0f); const auto test = [this, &doLayout, &margin] (Direction direction, AlignSelf alignment, Rectangle expectedBounds) { const auto flex = doLayout (direction, { juce::FlexItem().withAlignSelf (alignment).withMargin (margin) }); expect (flex.items.getFirst().currentBounds == expectedBounds); }; const auto remainingHeight = rect.getHeight() - margin.top - margin.bottom; const auto remainingWidth = rect.getWidth() - margin.left - margin.right; test (Direction::row, AlignSelf::autoAlign, { rect.getX() + margin.left, rect.getY() + margin.top, 0.0f, remainingHeight }); test (Direction::row, AlignSelf::stretch, { rect.getX() + margin.left, rect.getY() + margin.top, 0.0f, remainingHeight }); test (Direction::row, AlignSelf::flexStart, { rect.getX() + margin.left, rect.getY() + margin.top, 0.0f, 0.0f }); test (Direction::row, AlignSelf::flexEnd, { rect.getX() + margin.left, rect.getBottom() - margin.bottom, 0.0f, 0.0f }); test (Direction::row, AlignSelf::center, { rect.getX() + margin.left, rect.getY() + margin.top + remainingHeight * 0.5f, 0.0f, 0.0f }); test (Direction::column, AlignSelf::autoAlign, { rect.getX() + margin.left, rect.getY() + margin.top, remainingWidth, 0.0f }); test (Direction::column, AlignSelf::stretch, { rect.getX() + margin.left, rect.getY() + margin.top, remainingWidth, 0.0f }); test (Direction::column, AlignSelf::flexStart, { rect.getX() + margin.left, rect.getY() + margin.top, 0.0f, 0.0f }); test (Direction::column, AlignSelf::flexEnd, { rect.getRight() - margin.right, rect.getY() + margin.top, 0.0f, 0.0f }); test (Direction::column, AlignSelf::center, { rect.getX() + margin.left + remainingWidth * 0.5f, rect.getY() + margin.top, 0.0f, 0.0f }); } const AlignSelf alignments[] { AlignSelf::autoAlign, AlignSelf::stretch, AlignSelf::flexStart, AlignSelf::flexEnd, AlignSelf::center }; beginTest ("flex item with auto margin"); { for (const auto& alignment : alignments) { for (const auto& direction : { Direction::row, Direction::column }) { const auto flex = doLayout (direction, { juce::FlexItem().withAlignSelf (alignment) .withMargin ((float) FlexItem::autoValue) }); expect (flex.items.getFirst().currentBounds == Rectangle (rect.getCentre(), rect.getCentre())); } } const auto testTop = [this, &doLayout] (Direction direction, AlignSelf alignment, Rectangle expectedBounds) { const auto flex = doLayout (direction, { juce::FlexItem().withAlignSelf (alignment) .withMargin ({ (float) FlexItem::autoValue, 0.0f, 0.0f, 0.0f }) }); expect (flex.items.getFirst().currentBounds == expectedBounds); }; for (const auto& alignment : alignments) testTop (Direction::row, alignment, { rect.getX(), rect.getBottom(), 0.0f, 0.0f }); testTop (Direction::column, AlignSelf::autoAlign, { rect.getX(), rect.getBottom(), rect.getWidth(), 0.0f }); testTop (Direction::column, AlignSelf::stretch, { rect.getX(), rect.getBottom(), rect.getWidth(), 0.0f }); testTop (Direction::column, AlignSelf::flexStart, { rect.getX(), rect.getBottom(), 0.0f, 0.0f }); testTop (Direction::column, AlignSelf::flexEnd, { rect.getRight(), rect.getBottom(), 0.0f, 0.0f }); testTop (Direction::column, AlignSelf::center, { rect.getCentreX(), rect.getBottom(), 0.0f, 0.0f }); const auto testBottom = [this, &doLayout] (Direction direction, AlignSelf alignment, Rectangle expectedBounds) { const auto flex = doLayout (direction, { juce::FlexItem().withAlignSelf (alignment) .withMargin ({ 0.0f, 0.0f, (float) FlexItem::autoValue, 0.0f }) }); expect (flex.items.getFirst().currentBounds == expectedBounds); }; for (const auto& alignment : alignments) testBottom (Direction::row, alignment, { rect.getX(), rect.getY(), 0.0f, 0.0f }); testBottom (Direction::column, AlignSelf::autoAlign, { rect.getX(), rect.getY(), rect.getWidth(), 0.0f }); testBottom (Direction::column, AlignSelf::stretch, { rect.getX(), rect.getY(), rect.getWidth(), 0.0f }); testBottom (Direction::column, AlignSelf::flexStart, { rect.getX(), rect.getY(), 0.0f, 0.0f }); testBottom (Direction::column, AlignSelf::flexEnd, { rect.getRight(), rect.getY(), 0.0f, 0.0f }); testBottom (Direction::column, AlignSelf::center, { rect.getCentreX(), rect.getY(), 0.0f, 0.0f }); const auto testLeft = [this, &doLayout] (Direction direction, AlignSelf alignment, Rectangle expectedBounds) { const auto flex = doLayout (direction, { juce::FlexItem().withAlignSelf (alignment) .withMargin ({ 0.0f, 0.0f, 0.0f, (float) FlexItem::autoValue }) }); expect (flex.items.getFirst().currentBounds == expectedBounds); }; testLeft (Direction::row, AlignSelf::autoAlign, { rect.getRight(), rect.getY(), 0.0f, rect.getHeight() }); testLeft (Direction::row, AlignSelf::stretch, { rect.getRight(), rect.getY(), 0.0f, rect.getHeight() }); testLeft (Direction::row, AlignSelf::flexStart, { rect.getRight(), rect.getY(), 0.0f, 0.0f }); testLeft (Direction::row, AlignSelf::flexEnd, { rect.getRight(), rect.getBottom(), 0.0f, 0.0f }); testLeft (Direction::row, AlignSelf::center, { rect.getRight(), rect.getCentreY(), 0.0f, 0.0f }); for (const auto& alignment : alignments) testLeft (Direction::column, alignment, { rect.getRight(), rect.getY(), 0.0f, 0.0f }); const auto testRight = [this, &doLayout] (Direction direction, AlignSelf alignment, Rectangle expectedBounds) { const auto flex = doLayout (direction, { juce::FlexItem().withAlignSelf (alignment) .withMargin ({ 0.0f, (float) FlexItem::autoValue, 0.0f, 0.0f }) }); expect (flex.items.getFirst().currentBounds == expectedBounds); }; testRight (Direction::row, AlignSelf::autoAlign, { rect.getX(), rect.getY(), 0.0f, rect.getHeight() }); testRight (Direction::row, AlignSelf::stretch, { rect.getX(), rect.getY(), 0.0f, rect.getHeight() }); testRight (Direction::row, AlignSelf::flexStart, { rect.getX(), rect.getY(), 0.0f, 0.0f }); testRight (Direction::row, AlignSelf::flexEnd, { rect.getX(), rect.getBottom(), 0.0f, 0.0f }); testRight (Direction::row, AlignSelf::center, { rect.getX(), rect.getCentreY(), 0.0f, 0.0f }); for (const auto& alignment : alignments) testRight (Direction::column, alignment, { rect.getX(), rect.getY(), 0.0f, 0.0f }); } beginTest ("in a multiline layout, items too large to fit on the main axis are given a line to themselves"); { const auto spacer = 10.0f; for (const auto alignment : alignments) { juce::FlexBox flex; flex.flexWrap = FlexBox::Wrap::wrap; flex.items = { FlexItem().withAlignSelf (alignment) .withWidth (spacer) .withHeight (spacer), FlexItem().withAlignSelf (alignment) .withWidth (rect.getWidth() * 2) .withHeight (rect.getHeight()), FlexItem().withAlignSelf (alignment) .withWidth (spacer) .withHeight (spacer) }; flex.performLayout (rect); expect (flex.items[0].currentBounds == Rectangle (rect.getX(), rect.getY(), spacer, spacer)); expect (flex.items[1].currentBounds == Rectangle (rect.getX(), rect.getY() + spacer, rect.getWidth(), rect.getHeight())); expect (flex.items[2].currentBounds == Rectangle (rect.getX(), rect.getBottom() + spacer, 10.0f, 10.0f)); } } } }; static FlexBoxTests flexBoxTests; #endif } // namespace juce