/* ============================================================================== This file is part of the JUCE 7 technical preview. Copyright (c) 2022 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For the technical preview this file cannot be licensed commercially. 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