diff --git a/BREAKING-CHANGES.txt b/BREAKING-CHANGES.txt index 91ccc1f1ac..f31de9b142 100644 --- a/BREAKING-CHANGES.txt +++ b/BREAKING-CHANGES.txt @@ -4,6 +4,35 @@ JUCE breaking changes develop ======= +Change +------ +The Grid layout algorithm has been slightly altered to provide more consistent +behaviour. The new approach guarantees that dimensions specified using the +absolute Px quantity will always be correctly rounded when applied to the +integer dimensions of Components. + +Possible Issues +--------------- +Components laid out using Grid can observe a size or position change of +/- 1px +along each dimension compared with the result of the previous algorithm. + +Workaround +---------- +If the Grid based graphical layout is sensitive to changes of +/- 1px, then the +UI layout code may have to be adjusted to the new algorithm. + +Rationale +--------- +The old Grid layout algorithm could exhibit surprising and difficult to control +single pixel artifacts, where an item with a specified absolute size of +e.g. 100px could end up with a layout size of 101px. The new approach +guarantees that such items will have a layout size exactly as specified, and +this new behaviour is also in line with CSS behaviour in browsers. The new +approach makes necessary corrections easier as adding 1px to the size of an +item with absolute dimensions is guaranteed to translate into an observable 1px +increase in the layout size. + + Change ------ The k91_4 and k90_4 VST3 layouts are now mapped to the canonical JUCE 9.1.4 and diff --git a/modules/juce_gui_basics/layout/juce_Grid.cpp b/modules/juce_gui_basics/layout/juce_Grid.cpp index 4ef0454ca5..5c1e426bfb 100644 --- a/modules/juce_gui_basics/layout/juce_Grid.cpp +++ b/modules/juce_gui_basics/layout/juce_Grid.cpp @@ -26,942 +26,1026 @@ namespace juce { -struct AllTracksIncludingImplicit +template +static Array operator+ (const Array& a, const Array& b) { - Array items; - int numImplicitLeading; // The number of implicit items before the explicit items -}; + auto copy = a; + copy.addArray (b); + return copy; +} -struct Tracks +struct Grid::Helpers { - AllTracksIncludingImplicit columns, rows; -}; -struct Grid::SizeCalculation -{ - static float getTotalAbsoluteSize (const Array& tracks, Px gapSize) noexcept + struct AllTracksIncludingImplicit { - float totalCellSize = 0.0f; - - for (const auto& trackInfo : tracks) - if (! trackInfo.isFractional() || trackInfo.isAuto()) - totalCellSize += trackInfo.getSize(); - - float totalGap = tracks.size() > 1 ? static_cast ((tracks.size() - 1) * gapSize.pixels) - : 0.0f; - - return totalCellSize + totalGap; - } + Array items; + int numImplicitLeading; // The number of implicit items before the explicit items + }; - static float getRelativeUnitSize (float size, float totalAbsolute, const Array& tracks) noexcept + struct Tracks { - const float totalRelative = jlimit (0.0f, size, size - totalAbsolute); - float factorsSum = 0.0f; - - for (const auto& trackInfo : tracks) - if (trackInfo.isFractional()) - factorsSum += trackInfo.getSize(); - - jassert (! approximatelyEqual (factorsSum, 0.0f)); - return totalRelative / factorsSum; - } + AllTracksIncludingImplicit columns, rows; + }; - //============================================================================== - static float getTotalAbsoluteHeight (const Array& rowTracks, Px rowGap) + struct NoRounding { - return getTotalAbsoluteSize (rowTracks, rowGap); - } + template + T operator() (T t) const { return t; } + }; - static float getTotalAbsoluteWidth (const Array& columnTracks, Px columnGap) + struct StandardRounding { - return getTotalAbsoluteSize (columnTracks, columnGap); - } + template + T operator() (T t) const { return std::round (t); } + }; - static float getRelativeWidthUnit (float gridWidth, Px columnGap, const Array& columnTracks) + template + struct SizeCalculation { - return getRelativeUnitSize (gridWidth, getTotalAbsoluteWidth (columnTracks, columnGap), columnTracks); - } + float getTotalAbsoluteSize (const Array& tracks, Px gapSize) noexcept + { + float totalCellSize = 0.0f; - static float getRelativeHeightUnit (float gridHeight, Px rowGap, const Array& rowTracks) - { - return getRelativeUnitSize (gridHeight, getTotalAbsoluteHeight (rowTracks, rowGap), rowTracks); - } + for (const auto& trackInfo : tracks) + if (! trackInfo.isFractional() || trackInfo.isAuto()) + totalCellSize += roundingFunction (trackInfo.getSize()); - //============================================================================== - static bool hasAnyFractions (const Array& tracks) - { - return std::any_of (tracks.begin(), - tracks.end(), - [] (const auto& t) { return t.isFractional(); }); - } + float totalGap = tracks.size() > 1 ? (float) (tracks.size() - 1) * roundingFunction ((float) gapSize.pixels) + : 0.0f; - void computeSizes (float gridWidth, float gridHeight, - Px columnGapToUse, Px rowGapToUse, - const Tracks& tracks) - { - if (hasAnyFractions (tracks.columns.items)) - relativeWidthUnit = getRelativeWidthUnit (gridWidth, columnGapToUse, tracks.columns.items); - else - remainingWidth = gridWidth - getTotalAbsoluteSize (tracks.columns.items, columnGapToUse); - - if (hasAnyFractions (tracks.rows.items)) - relativeHeightUnit = getRelativeHeightUnit (gridHeight, rowGapToUse, tracks.rows.items); - else - remainingHeight = gridHeight - getTotalAbsoluteSize (tracks.rows.items, rowGapToUse); - } + return totalCellSize + totalGap; + } - float relativeWidthUnit = 0.0f; - float relativeHeightUnit = 0.0f; - float remainingWidth = 0.0f; - float remainingHeight = 0.0f; -}; + static float getRelativeUnitSize (float size, float totalAbsolute, const Array& tracks) noexcept + { + const float totalRelative = jlimit (0.0f, size, size - totalAbsolute); + float factorsSum = 0.0f; -//============================================================================== -struct Grid::PlacementHelpers -{ - enum { invalid = -999999 }; - static constexpr auto emptyAreaCharacter = "."; + for (const auto& trackInfo : tracks) + if (trackInfo.isFractional()) + factorsSum += trackInfo.getSize(); - //============================================================================== - struct LineRange { int start, end; }; - struct LineArea { LineRange column, row; }; - struct LineInfo { StringArray lineNames; }; + jassert (! approximatelyEqual (factorsSum, 0.0f)); + return totalRelative / factorsSum; + } - struct NamedArea - { - String name; - LineArea lines; - }; + //============================================================================== + float getTotalAbsoluteHeight (const Array& rowTracks, Px rowGapSize) + { + return getTotalAbsoluteSize (rowTracks, rowGapSize); + } - //============================================================================== - static Array getArrayOfLinesFromTracks (const Array& tracks) - { - // fill line info array - Array lines; + float getTotalAbsoluteWidth (const Array& columnTracks, Px columnGapSize) + { + return getTotalAbsoluteSize (columnTracks, columnGapSize); + } - for (int i = 1; i <= tracks.size(); ++i) + float getRelativeWidthUnit (float gridWidth, Px columnGapSize, const Array& columnTracks) { - const auto& currentTrack = tracks.getReference (i - 1); + return getRelativeUnitSize (gridWidth, getTotalAbsoluteWidth (columnTracks, columnGapSize), columnTracks); + } + + float getRelativeHeightUnit (float gridHeight, Px rowGapSize, const Array& rowTracks) + { + return getRelativeUnitSize (gridHeight, getTotalAbsoluteHeight (rowTracks, rowGapSize), rowTracks); + } + + //============================================================================== + static bool hasAnyFractions (const Array& tracks) + { + return std::any_of (tracks.begin(), + tracks.end(), + [] (const auto& t) { return t.isFractional(); }); + } - if (i == 1) // start line + void computeSizes (float gridWidth, float gridHeight, + Px columnGapToUse, Px rowGapToUse, + const Tracks& tracks) + { + if (hasAnyFractions (tracks.columns.items)) { - LineInfo li; - li.lineNames.add (currentTrack.getStartLineName()); - lines.add (li); + relativeWidthUnit = getRelativeWidthUnit (gridWidth, columnGapToUse, tracks.columns.items); + fractionallyDividedWidth = gridWidth - getTotalAbsoluteSize (tracks.columns.items, columnGapToUse); } - - if (i > 1 && i <= tracks.size()) // two lines in between tracks + else { - const auto& prevTrack = tracks.getReference (i - 2); - - LineInfo li; - li.lineNames.add (prevTrack.getEndLineName()); - li.lineNames.add (currentTrack.getStartLineName()); - - lines.add (li); + remainingWidth = gridWidth - getTotalAbsoluteSize (tracks.columns.items, columnGapToUse); } - if (i == tracks.size()) // end line + if (hasAnyFractions (tracks.rows.items)) + { + relativeHeightUnit = getRelativeHeightUnit (gridHeight, rowGapToUse, tracks.rows.items); + fractionallyDividedHeight = gridHeight - getTotalAbsoluteSize (tracks.rows.items, rowGapToUse); + } + else { - LineInfo li; - li.lineNames.add (currentTrack.getEndLineName()); - lines.add (li); + remainingHeight = gridHeight - getTotalAbsoluteSize (tracks.rows.items, rowGapToUse); } + + const auto calculateTrackBounds = [&] (auto& outBounds, + const auto& trackItems, + auto relativeUnit, + auto totalSizeForFractionalItems, + auto gap) + { + const auto lastFractionalIndex = [&] + { + for (int i = trackItems.size() - 1; 0 <= i; --i) + if (trackItems[i].isFractional()) + return i; + + return -1; + }(); + + float start = 0.0f; + float carriedError = 0.0f; + + for (int i = 0; i < trackItems.size(); ++i) + { + const auto& currentItem = trackItems[i]; + + const auto currentTrackSize = [&] + { + if (i == lastFractionalIndex) + return totalSizeForFractionalItems; + + const auto absoluteSize = currentItem.getAbsoluteSize (relativeUnit); + + if (! currentItem.isFractional()) + return roundingFunction (absoluteSize); + + const auto result = roundingFunction (absoluteSize + carriedError); + carriedError = result - absoluteSize; + return result; + }(); + + if (currentItem.isFractional()) + totalSizeForFractionalItems -= currentTrackSize; + + const auto end = start + currentTrackSize; + outBounds.emplace_back (start, end); + start = end + roundingFunction (static_cast (gap.pixels)); + } + }; + + calculateTrackBounds (columnTrackBounds, + tracks.columns.items, + relativeWidthUnit, + fractionallyDividedWidth, + columnGapToUse); + + calculateTrackBounds (rowTrackBounds, + tracks.rows.items, + relativeHeightUnit, + fractionallyDividedHeight, + rowGapToUse); } - jassert (lines.size() == tracks.size() + 1); + float relativeWidthUnit = 0.0f; + float relativeHeightUnit = 0.0f; + float fractionallyDividedWidth = 0.0f; + float fractionallyDividedHeight = 0.0f; + float remainingWidth = 0.0f; + float remainingHeight = 0.0f; - return lines; - } + std::vector> columnTrackBounds; + std::vector> rowTrackBounds; + RoundingFunction roundingFunction; + }; //============================================================================== - static int deduceAbsoluteLineNumberFromLineName (GridItem::Property prop, - const Array& tracks) + struct PlacementHelpers { - jassert (prop.hasAbsolute()); + enum { invalid = -999999 }; + static constexpr auto emptyAreaCharacter = "."; - const auto lines = getArrayOfLinesFromTracks (tracks); - int count = 0; + //============================================================================== + struct LineRange { int start, end; }; + struct LineArea { LineRange column, row; }; + struct LineInfo { StringArray lineNames; }; - for (int i = 0; i < lines.size(); i++) + struct NamedArea { - for (const auto& name : lines.getReference (i).lineNames) + String name; + LineArea lines; + }; + + //============================================================================== + static Array getArrayOfLinesFromTracks (const Array& tracks) + { + // fill line info array + Array lines; + + for (int i = 1; i <= tracks.size(); ++i) { - if (prop.getName() == name) + const auto& currentTrack = tracks.getReference (i - 1); + + if (i == 1) // start line { - ++count; - break; + LineInfo li; + li.lineNames.add (currentTrack.getStartLineName()); + lines.add (li); } - } - if (count == prop.getNumber()) - return i + 1; - } - - jassertfalse; - return count; - } + if (i > 1 && i <= tracks.size()) // two lines in between tracks + { + const auto& prevTrack = tracks.getReference (i - 2); - static int deduceAbsoluteLineNumber (GridItem::Property prop, - const Array& tracks) - { - jassert (prop.hasAbsolute()); + LineInfo li; + li.lineNames.add (prevTrack.getEndLineName()); + li.lineNames.add (currentTrack.getStartLineName()); - if (prop.hasName()) - return deduceAbsoluteLineNumberFromLineName (prop, tracks); + lines.add (li); + } - if (prop.getNumber() > 0) - return prop.getNumber(); + if (i == tracks.size()) // end line + { + LineInfo li; + li.lineNames.add (currentTrack.getEndLineName()); + lines.add (li); + } + } - if (prop.getNumber() < 0) - return tracks.size() + 2 + prop.getNumber(); + jassert (lines.size() == tracks.size() + 1); - // An integer value of 0 is invalid - jassertfalse; - return 1; - } + return lines; + } - static int deduceAbsoluteLineNumberFromNamedSpan (int startLineNumber, - GridItem::Property propertyWithSpan, - const Array& tracks) - { - jassert (propertyWithSpan.hasSpan()); + //============================================================================== + static int deduceAbsoluteLineNumberFromLineName (GridItem::Property prop, + const Array& tracks) + { + jassert (prop.hasAbsolute()); - const auto lines = getArrayOfLinesFromTracks (tracks); - int count = 0; + const auto lines = getArrayOfLinesFromTracks (tracks); + int count = 0; - for (int i = startLineNumber; i < lines.size(); i++) - { - for (const auto& name : lines.getReference (i).lineNames) + for (int i = 0; i < lines.size(); i++) { - if (propertyWithSpan.getName() == name) + for (const auto& name : lines.getReference (i).lineNames) { - ++count; - break; + if (prop.getName() == name) + { + ++count; + break; + } } + + if (count == prop.getNumber()) + return i + 1; } - if (count == propertyWithSpan.getNumber()) - return i + 1; + jassertfalse; + return count; } - jassertfalse; - return count; - } + static int deduceAbsoluteLineNumber (GridItem::Property prop, + const Array& tracks) + { + jassert (prop.hasAbsolute()); - static int deduceAbsoluteLineNumberBasedOnSpan (int startLineNumber, - GridItem::Property propertyWithSpan, - const Array& tracks) - { - jassert (propertyWithSpan.hasSpan()); + if (prop.hasName()) + return deduceAbsoluteLineNumberFromLineName (prop, tracks); - if (propertyWithSpan.hasName()) - return deduceAbsoluteLineNumberFromNamedSpan (startLineNumber, propertyWithSpan, tracks); + if (prop.getNumber() > 0) + return prop.getNumber(); - return startLineNumber + propertyWithSpan.getNumber(); - } + if (prop.getNumber() < 0) + return tracks.size() + 2 + prop.getNumber(); - //============================================================================== - static LineRange deduceLineRange (GridItem::StartAndEndProperty prop, const Array& tracks) - { - jassert (! (prop.start.hasAuto() && prop.end.hasAuto())); + // An integer value of 0 is invalid + jassertfalse; + return 1; + } - if (prop.start.hasAbsolute() && prop.end.hasAuto()) + static int deduceAbsoluteLineNumberFromNamedSpan (int startLineNumber, + GridItem::Property propertyWithSpan, + const Array& tracks) { - prop.end = GridItem::Span (1); + jassert (propertyWithSpan.hasSpan()); + + const auto lines = getArrayOfLinesFromTracks (tracks); + int count = 0; + + for (int i = startLineNumber; i < lines.size(); i++) + { + for (const auto& name : lines.getReference (i).lineNames) + { + if (propertyWithSpan.getName() == name) + { + ++count; + break; + } + } + + if (count == propertyWithSpan.getNumber()) + return i + 1; + } + + jassertfalse; + return count; } - else if (prop.start.hasAuto() && prop.end.hasAbsolute()) + + static int deduceAbsoluteLineNumberBasedOnSpan (int startLineNumber, + GridItem::Property propertyWithSpan, + const Array& tracks) { - prop.start = GridItem::Span (1); + jassert (propertyWithSpan.hasSpan()); + + if (propertyWithSpan.hasName()) + return deduceAbsoluteLineNumberFromNamedSpan (startLineNumber, propertyWithSpan, tracks); + + return startLineNumber + propertyWithSpan.getNumber(); } - auto s = [&]() -> LineRange + //============================================================================== + static LineRange deduceLineRange (GridItem::StartAndEndProperty prop, const Array& tracks) { - if (prop.start.hasAbsolute() && prop.end.hasAbsolute()) + jassert (! (prop.start.hasAuto() && prop.end.hasAuto())); + + if (prop.start.hasAbsolute() && prop.end.hasAuto()) { - return { deduceAbsoluteLineNumber (prop.start, tracks), - deduceAbsoluteLineNumber (prop.end, tracks) }; + prop.end = GridItem::Span (1); } - - if (prop.start.hasAbsolute() && prop.end.hasSpan()) + else if (prop.start.hasAuto() && prop.end.hasAbsolute()) { - const auto start = deduceAbsoluteLineNumber (prop.start, tracks); - return { start, deduceAbsoluteLineNumberBasedOnSpan (start, prop.end, tracks) }; + prop.start = GridItem::Span (1); } - if (prop.start.hasSpan() && prop.end.hasAbsolute()) + auto s = [&]() -> LineRange { - const auto start = deduceAbsoluteLineNumber (prop.end, tracks); - return { start, deduceAbsoluteLineNumberBasedOnSpan (start, prop.start, tracks) }; - } + if (prop.start.hasAbsolute() && prop.end.hasAbsolute()) + { + return { deduceAbsoluteLineNumber (prop.start, tracks), + deduceAbsoluteLineNumber (prop.end, tracks) }; + } - // Can't have an item with spans on both start and end. - jassertfalse; - return {}; - }(); + if (prop.start.hasAbsolute() && prop.end.hasSpan()) + { + const auto start = deduceAbsoluteLineNumber (prop.start, tracks); + return { start, deduceAbsoluteLineNumberBasedOnSpan (start, prop.end, tracks) }; + } - // swap if start overtakes end - if (s.start > s.end) - std::swap (s.start, s.end); - else if (s.start == s.end) - s.end = s.start + 1; + if (prop.start.hasSpan() && prop.end.hasAbsolute()) + { + const auto start = deduceAbsoluteLineNumber (prop.end, tracks); + return { start, deduceAbsoluteLineNumberBasedOnSpan (start, prop.start, tracks) }; + } - return s; - } + // Can't have an item with spans on both start and end. + jassertfalse; + return {}; + }(); - static LineArea deduceLineArea (const GridItem& item, - const Grid& grid, - const std::map& namedAreas) - { - if (item.area.isNotEmpty() && ! grid.templateAreas.isEmpty()) - { - // Must be a named area! - jassert (namedAreas.count (item.area) != 0); + // swap if start overtakes end + if (s.start > s.end) + std::swap (s.start, s.end); + else if (s.start == s.end) + s.end = s.start + 1; - return namedAreas.at (item.area); + return s; } - return { deduceLineRange (item.column, grid.templateColumns), - deduceLineRange (item.row, grid.templateRows) }; - } + static LineArea deduceLineArea (const GridItem& item, + const Grid& grid, + const std::map& namedAreas) + { + if (item.area.isNotEmpty() && ! grid.templateAreas.isEmpty()) + { + // Must be a named area! + jassert (namedAreas.count (item.area) != 0); - //============================================================================== - static Array parseAreasProperty (const StringArray& areasStrings) - { - Array strings; + return namedAreas.at (item.area); + } - for (const auto& areaString : areasStrings) - strings.add (StringArray::fromTokens (areaString, false)); + return { deduceLineRange (item.column, grid.templateColumns), + deduceLineRange (item.row, grid.templateRows) }; + } - if (strings.size() > 0) + //============================================================================== + static Array parseAreasProperty (const StringArray& areasStrings) { - for (auto s : strings) + Array strings; + + for (const auto& areaString : areasStrings) + strings.add (StringArray::fromTokens (areaString, false)); + + if (strings.size() > 0) { - jassert (s.size() == strings[0].size()); // all rows must have the same number of columns + for (auto s : strings) + { + jassert (s.size() == strings[0].size()); // all rows must have the same number of columns + } } - } - return strings; - } - - static NamedArea findArea (Array& stringsArrays) - { - NamedArea area; + return strings; + } - for (auto& stringArray : stringsArrays) + static NamedArea findArea (Array& stringsArrays) { - for (auto& string : stringArray) + NamedArea area; + + for (auto& stringArray : stringsArrays) { - // find anchor - if (area.name.isEmpty()) + for (auto& string : stringArray) { - if (string != emptyAreaCharacter) + // find anchor + if (area.name.isEmpty()) { - area.name = string; - area.lines.row.start = stringsArrays.indexOf (stringArray) + 1; // non-zero indexed; - area.lines.column.start = stringArray.indexOf (string) + 1; // non-zero indexed; - - area.lines.row.end = stringsArrays.indexOf (stringArray) + 2; - area.lines.column.end = stringArray.indexOf (string) + 2; - - // mark as visited - string = emptyAreaCharacter; + if (string != emptyAreaCharacter) + { + area.name = string; + area.lines.row.start = stringsArrays.indexOf (stringArray) + 1; // non-zero indexed; + area.lines.column.start = stringArray.indexOf (string) + 1; // non-zero indexed; + + area.lines.row.end = stringsArrays.indexOf (stringArray) + 2; + area.lines.column.end = stringArray.indexOf (string) + 2; + + // mark as visited + string = emptyAreaCharacter; + } } - } - else - { - if (string == area.name) + else { - area.lines.row.end = stringsArrays.indexOf (stringArray) + 2; - area.lines.column.end = stringArray.indexOf (string) + 2; - - // mark as visited - string = emptyAreaCharacter; + if (string == area.name) + { + area.lines.row.end = stringsArrays.indexOf (stringArray) + 2; + area.lines.column.end = stringArray.indexOf (string) + 2; + + // mark as visited + string = emptyAreaCharacter; + } } } } + + return area; } - return area; - } + //============================================================================== + static std::map deduceNamedAreas (const StringArray& areasStrings) + { + auto stringsArrays = parseAreasProperty (areasStrings); - //============================================================================== - static std::map deduceNamedAreas (const StringArray& areasStrings) - { - auto stringsArrays = parseAreasProperty (areasStrings); + std::map areas; + + for (auto area = findArea (stringsArrays); area.name.isNotEmpty(); area = findArea (stringsArrays)) + { + if (areas.count (area.name) == 0) + areas[area.name] = area.lines; + else + // Make sure your template-areas property only has one area with the same name and is well-formed + jassertfalse; + } - std::map areas; + return areas; + } - for (auto area = findArea (stringsArrays); area.name.isNotEmpty(); area = findArea (stringsArrays)) + //============================================================================== + template + static Rectangle getCellBounds (int columnNumber, int rowNumber, + const Tracks& tracks, + const SizeCalculation& calculation) { - if (areas.count (area.name) == 0) - areas[area.name] = area.lines; - else - // Make sure your template-areas property only has one area with the same name and is well-formed - jassertfalse; + const auto correctedColumn = columnNumber - 1 + tracks.columns.numImplicitLeading; + const auto correctedRow = rowNumber - 1 + tracks.rows .numImplicitLeading; + + jassert (isPositiveAndBelow (correctedColumn, tracks.columns.items.size())); + jassert (isPositiveAndBelow (correctedRow, tracks.rows .items.size())); + + return + { + calculation.columnTrackBounds[(size_t) correctedColumn].getStart(), + calculation.rowTrackBounds[(size_t) correctedRow].getStart(), + calculation.columnTrackBounds[(size_t) correctedColumn].getEnd() - calculation.columnTrackBounds[(size_t) correctedColumn].getStart(), + calculation.rowTrackBounds[(size_t) correctedRow].getEnd() - calculation.rowTrackBounds[(size_t) correctedRow].getStart() + }; } - return areas; - } + template + static Rectangle alignCell (Rectangle area, + int columnNumber, int rowNumber, + int numberOfColumns, int numberOfRows, + const SizeCalculation& calculation, + AlignContent alignContent, + JustifyContent justifyContent) + { + if (alignContent == AlignContent::end) + area.setY (area.getY() + calculation.remainingHeight); - //============================================================================== - static float getCoord (int trackNumber, float relativeUnit, Px gap, const Array& tracks) - { - float c = 0; + if (justifyContent == JustifyContent::end) + area.setX (area.getX() + calculation.remainingWidth); - for (const auto* it = tracks.begin(); it != tracks.begin() + trackNumber; ++it) - c += it->getAbsoluteSize (relativeUnit) + static_cast (gap.pixels); + if (alignContent == AlignContent::center) + area.setY (area.getY() + calculation.remainingHeight / 2); - return c; - } + if (justifyContent == JustifyContent::center) + area.setX (area.getX() + calculation.remainingWidth / 2); - static Rectangle getCellBounds (int columnNumber, int rowNumber, - const Tracks& tracks, - SizeCalculation calculation, - Px columnGap, Px rowGap) - { - const auto correctedColumn = columnNumber - 1 + tracks.columns.numImplicitLeading; - const auto correctedRow = rowNumber - 1 + tracks.rows .numImplicitLeading; + if (alignContent == AlignContent::spaceBetween) + { + const auto shift = ((float) (rowNumber - 1) * (calculation.remainingHeight / float(numberOfRows - 1))); + area.setY (area.getY() + shift); + } - jassert (isPositiveAndBelow (correctedColumn, tracks.columns.items.size())); - jassert (isPositiveAndBelow (correctedRow, tracks.rows .items.size())); + if (justifyContent == JustifyContent::spaceBetween) + { + const auto shift = ((float) (columnNumber - 1) * (calculation.remainingWidth / float(numberOfColumns - 1))); + area.setX (area.getX() + shift); + } - return { getCoord (correctedColumn, calculation.relativeWidthUnit, columnGap, tracks.columns.items), - getCoord (correctedRow, calculation.relativeHeightUnit, rowGap, tracks.rows .items), - tracks.columns.items.getReference (correctedColumn).getAbsoluteSize (calculation.relativeWidthUnit), - tracks.rows .items.getReference (correctedRow) .getAbsoluteSize (calculation.relativeHeightUnit) }; - } + if (alignContent == AlignContent::spaceEvenly) + { + const auto shift = ((float) rowNumber * (calculation.remainingHeight / float(numberOfRows + 1))); + area.setY (area.getY() + shift); + } - static Rectangle alignCell (Rectangle area, - int columnNumber, int rowNumber, - int numberOfColumns, int numberOfRows, - SizeCalculation calculation, - AlignContent alignContent, - JustifyContent justifyContent) - { - if (alignContent == AlignContent::end) - area.setY (area.getY() + calculation.remainingHeight); + if (justifyContent == JustifyContent::spaceEvenly) + { + const auto shift = ((float) columnNumber * (calculation.remainingWidth / float(numberOfColumns + 1))); + area.setX (area.getX() + shift); + } - if (justifyContent == JustifyContent::end) - area.setX (area.getX() + calculation.remainingWidth); + if (alignContent == AlignContent::spaceAround) + { + const auto inbetweenShift = calculation.remainingHeight / float(numberOfRows); + const auto sidesShift = inbetweenShift / 2; + auto shift = (float) (rowNumber - 1) * inbetweenShift + sidesShift; - if (alignContent == AlignContent::center) - area.setY (area.getY() + calculation.remainingHeight / 2); + area.setY (area.getY() + shift); + } - if (justifyContent == JustifyContent::center) - area.setX (area.getX() + calculation.remainingWidth / 2); + if (justifyContent == JustifyContent::spaceAround) + { + const auto inbetweenShift = calculation.remainingWidth / float(numberOfColumns); + const auto sidesShift = inbetweenShift / 2; + auto shift = (float) (columnNumber - 1) * inbetweenShift + sidesShift; - if (alignContent == AlignContent::spaceBetween) - { - const auto shift = ((float) (rowNumber - 1) * (calculation.remainingHeight / float(numberOfRows - 1))); - area.setY (area.getY() + shift); - } + area.setX (area.getX() + shift); + } - if (justifyContent == JustifyContent::spaceBetween) - { - const auto shift = ((float) (columnNumber - 1) * (calculation.remainingWidth / float(numberOfColumns - 1))); - area.setX (area.getX() + shift); + return area; } - if (alignContent == AlignContent::spaceEvenly) + template + static Rectangle getAreaBounds (PlacementHelpers::LineRange columnRange, + PlacementHelpers::LineRange rowRange, + const Tracks& tracks, + const SizeCalculation& calculation, + AlignContent alignContent, + JustifyContent justifyContent) { - const auto shift = ((float) rowNumber * (calculation.remainingHeight / float(numberOfRows + 1))); - area.setY (area.getY() + shift); + const auto findAlignedCell = [&] (int column, int row) + { + const auto cell = getCellBounds (column, row, tracks, calculation); + return alignCell (cell, + column, + row, + tracks.columns.items.size(), + tracks.rows.items.size(), + calculation, + alignContent, + justifyContent); + }; + + const auto startCell = findAlignedCell (columnRange.start, rowRange.start); + const auto endCell = findAlignedCell (columnRange.end - 1, rowRange.end - 1); + + const auto horizontalRange = startCell.getHorizontalRange().getUnionWith (endCell.getHorizontalRange()); + const auto verticalRange = startCell.getVerticalRange() .getUnionWith (endCell.getVerticalRange()); + return { horizontalRange.getStart(), verticalRange.getStart(), + horizontalRange.getLength(), verticalRange.getLength() }; } + }; - if (justifyContent == JustifyContent::spaceEvenly) - { - const auto shift = ((float) columnNumber * (calculation.remainingWidth / float(numberOfColumns + 1))); - area.setX (area.getX() + shift); - } + //============================================================================== + struct AutoPlacement + { + using ItemPlacementArray = Array>; - if (alignContent == AlignContent::spaceAround) + //============================================================================== + struct OccupancyPlane { - const auto inbetweenShift = calculation.remainingHeight / float(numberOfRows); - const auto sidesShift = inbetweenShift / 2; - auto shift = (float) (rowNumber - 1) * inbetweenShift + sidesShift; + struct Cell { int column, row; }; - area.setY (area.getY() + shift); - } + OccupancyPlane (int highestColumnToUse, int highestRowToUse, bool isColumnFirst) + : highestCrossDimension (isColumnFirst ? highestRowToUse : highestColumnToUse), + columnFirst (isColumnFirst) + {} - if (justifyContent == JustifyContent::spaceAround) - { - const auto inbetweenShift = calculation.remainingWidth / float(numberOfColumns); - const auto sidesShift = inbetweenShift / 2; - auto shift = (float) (columnNumber - 1) * inbetweenShift + sidesShift; + PlacementHelpers::LineArea setCell (Cell cell, int columnSpan, int rowSpan) + { + for (int i = 0; i < columnSpan; i++) + for (int j = 0; j < rowSpan; j++) + setCell (cell.column + i, cell.row + j); - area.setX (area.getX() + shift); - } + return { { cell.column, cell.column + columnSpan }, { cell.row, cell.row + rowSpan } }; + } - return area; - } + PlacementHelpers::LineArea setCell (Cell start, Cell end) + { + return setCell (start, std::abs (end.column - start.column), + std::abs (end.row - start.row)); + } - static Rectangle getAreaBounds (PlacementHelpers::LineRange columnRange, - PlacementHelpers::LineRange rowRange, - const Tracks& tracks, - SizeCalculation calculation, - AlignContent alignContent, - JustifyContent justifyContent, - Px columnGap, Px rowGap) - { - const auto findAlignedCell = [&] (int column, int row) - { - const auto cell = getCellBounds (column, row, tracks, calculation, columnGap, rowGap); - return alignCell (cell, - column, - row, - tracks.columns.items.size(), - tracks.rows.items.size(), - calculation, - alignContent, - justifyContent); - }; + Cell nextAvailable (Cell referenceCell, int columnSpan, int rowSpan) + { + while (isOccupied (referenceCell, columnSpan, rowSpan) || isOutOfBounds (referenceCell, columnSpan, rowSpan)) + referenceCell = advance (referenceCell); - const auto startCell = findAlignedCell (columnRange.start, rowRange.start); - const auto endCell = findAlignedCell (columnRange.end - 1, rowRange.end - 1); + return referenceCell; + } - const auto horizontalRange = startCell.getHorizontalRange().getUnionWith (endCell.getHorizontalRange()); - const auto verticalRange = startCell.getVerticalRange() .getUnionWith (endCell.getVerticalRange()); - return { horizontalRange.getStart(), verticalRange.getStart(), - horizontalRange.getLength(), verticalRange.getLength() }; - } -}; + Cell nextAvailableOnRow (Cell referenceCell, int columnSpan, int rowSpan, int rowNumber) + { + if (columnFirst && (rowNumber + rowSpan) > highestCrossDimension) + highestCrossDimension = rowNumber + rowSpan; -template -static Array operator+ (const Array& a, const Array& b) -{ - auto copy = a; - copy.addArray (b); - return copy; -} + while (isOccupied (referenceCell, columnSpan, rowSpan) + || (referenceCell.row != rowNumber)) + referenceCell = advance (referenceCell); -//============================================================================== -struct Grid::AutoPlacement -{ - using ItemPlacementArray = Array>; + return referenceCell; + } - //============================================================================== - struct OccupancyPlane - { - struct Cell { int column, row; }; + Cell nextAvailableOnColumn (Cell referenceCell, int columnSpan, int rowSpan, int columnNumber) + { + if (! columnFirst && (columnNumber + columnSpan) > highestCrossDimension) + highestCrossDimension = columnNumber + columnSpan; - OccupancyPlane (int highestColumnToUse, int highestRowToUse, bool isColumnFirst) - : highestCrossDimension (isColumnFirst ? highestRowToUse : highestColumnToUse), - columnFirst (isColumnFirst) - {} + while (isOccupied (referenceCell, columnSpan, rowSpan) + || (referenceCell.column != columnNumber)) + referenceCell = advance (referenceCell); - PlacementHelpers::LineArea setCell (Cell cell, int columnSpan, int rowSpan) - { - for (int i = 0; i < columnSpan; i++) - for (int j = 0; j < rowSpan; j++) - setCell (cell.column + i, cell.row + j); + return referenceCell; + } - return { { cell.column, cell.column + columnSpan }, { cell.row, cell.row + rowSpan } }; - } + void updateMaxCrossDimensionFromAutoPlacementItem (int columnSpan, int rowSpan) + { + highestCrossDimension = jmax (highestCrossDimension, 1 + getCrossDimension ({ columnSpan, rowSpan })); + } - PlacementHelpers::LineArea setCell (Cell start, Cell end) - { - return setCell (start, std::abs (end.column - start.column), - std::abs (end.row - start.row)); - } + private: + struct SortableCell + { + int column, row; + bool columnFirst; - Cell nextAvailable (Cell referenceCell, int columnSpan, int rowSpan) - { - while (isOccupied (referenceCell, columnSpan, rowSpan) || isOutOfBounds (referenceCell, columnSpan, rowSpan)) - referenceCell = advance (referenceCell); + bool operator< (const SortableCell& other) const + { + if (columnFirst) + { + if (row == other.row) + return column < other.column; - return referenceCell; - } + return row < other.row; + } - Cell nextAvailableOnRow (Cell referenceCell, int columnSpan, int rowSpan, int rowNumber) - { - if (columnFirst && (rowNumber + rowSpan) > highestCrossDimension) - highestCrossDimension = rowNumber + rowSpan; + if (row == other.row) + return column < other.column; - while (isOccupied (referenceCell, columnSpan, rowSpan) - || (referenceCell.row != rowNumber)) - referenceCell = advance (referenceCell); + return row < other.row; + } + }; - return referenceCell; - } + void setCell (int column, int row) + { + occupiedCells.insert ({ column, row, columnFirst }); + } - Cell nextAvailableOnColumn (Cell referenceCell, int columnSpan, int rowSpan, int columnNumber) - { - if (! columnFirst && (columnNumber + columnSpan) > highestCrossDimension) - highestCrossDimension = columnNumber + columnSpan; + bool isOccupied (Cell cell) const + { + return occupiedCells.count ({ cell.column, cell.row, columnFirst }) > 0; + } - while (isOccupied (referenceCell, columnSpan, rowSpan) - || (referenceCell.column != columnNumber)) - referenceCell = advance (referenceCell); + bool isOccupied (Cell cell, int columnSpan, int rowSpan) const + { + for (int i = 0; i < columnSpan; i++) + for (int j = 0; j < rowSpan; j++) + if (isOccupied ({ cell.column + i, cell.row + j })) + return true; - return referenceCell; - } + return false; + } - void updateMaxCrossDimensionFromAutoPlacementItem (int columnSpan, int rowSpan) - { - highestCrossDimension = jmax (highestCrossDimension, 1 + getCrossDimension ({ columnSpan, rowSpan })); - } + bool isOutOfBounds (Cell cell, int columnSpan, int rowSpan) const + { + const auto highestIndexOfCell = getCrossDimension (cell) + getCrossDimension ({ columnSpan, rowSpan }); + const auto highestIndexOfGrid = getHighestCrossDimension(); - private: - struct SortableCell - { - int column, row; - bool columnFirst; + return highestIndexOfGrid < highestIndexOfCell; + } - bool operator< (const SortableCell& other) const + int getHighestCrossDimension() const { - if (columnFirst) - { - if (row == other.row) - return column < other.column; + Cell cell { 1, 1 }; - return row < other.row; - } + if (occupiedCells.size() > 0) + cell = { occupiedCells.crbegin()->column, occupiedCells.crbegin()->row }; + + return std::max (getCrossDimension (cell), highestCrossDimension); + } - if (row == other.row) - return column < other.column; + Cell advance (Cell cell) const + { + if ((getCrossDimension (cell) + 1) >= getHighestCrossDimension()) + return fromDimensions (getMainDimension (cell) + 1, 1); - return row < other.row; + return fromDimensions (getMainDimension (cell), getCrossDimension (cell) + 1); } + + int getMainDimension (Cell cell) const { return columnFirst ? cell.column : cell.row; } + int getCrossDimension (Cell cell) const { return columnFirst ? cell.row : cell.column; } + + Cell fromDimensions (int mainDimension, int crossDimension) const + { + if (columnFirst) + return { mainDimension, crossDimension }; + + return { crossDimension, mainDimension }; + } + + int highestCrossDimension; + bool columnFirst; + std::set occupiedCells; }; - void setCell (int column, int row) + //============================================================================== + static bool isFixed (GridItem::StartAndEndProperty prop) { - occupiedCells.insert ({ column, row, columnFirst }); + return prop.start.hasName() || prop.start.hasAbsolute() || prop.end.hasName() || prop.end.hasAbsolute(); } - bool isOccupied (Cell cell) const + static bool hasFullyFixedPlacement (const GridItem& item) { - return occupiedCells.count ({ cell.column, cell.row, columnFirst }) > 0; - } + if (item.area.isNotEmpty()) + return true; - bool isOccupied (Cell cell, int columnSpan, int rowSpan) const - { - for (int i = 0; i < columnSpan; i++) - for (int j = 0; j < rowSpan; j++) - if (isOccupied ({ cell.column + i, cell.row + j })) - return true; + if (isFixed (item.column) && isFixed (item.row)) + return true; return false; } - bool isOutOfBounds (Cell cell, int columnSpan, int rowSpan) const + static bool hasPartialFixedPlacement (const GridItem& item) { - const auto highestIndexOfCell = getCrossDimension (cell) + getCrossDimension ({ columnSpan, rowSpan }); - const auto highestIndexOfGrid = getHighestCrossDimension(); + if (item.area.isNotEmpty()) + return false; - return highestIndexOfGrid < highestIndexOfCell; + if (isFixed (item.column) ^ isFixed (item.row)) + return true; + + return false; } - int getHighestCrossDimension() const + static bool hasAutoPlacement (const GridItem& item) { - Cell cell { 1, 1 }; - - if (occupiedCells.size() > 0) - cell = { occupiedCells.crbegin()->column, occupiedCells.crbegin()->row }; - - return std::max (getCrossDimension (cell), highestCrossDimension); + return ! hasFullyFixedPlacement (item) && ! hasPartialFixedPlacement (item); } - Cell advance (Cell cell) const + //============================================================================== + static bool hasDenseAutoFlow (AutoFlow autoFlow) { - if ((getCrossDimension (cell) + 1) >= getHighestCrossDimension()) - return fromDimensions (getMainDimension (cell) + 1, 1); - - return fromDimensions (getMainDimension (cell), getCrossDimension (cell) + 1); + return autoFlow == AutoFlow::columnDense + || autoFlow == AutoFlow::rowDense; } - int getMainDimension (Cell cell) const { return columnFirst ? cell.column : cell.row; } - int getCrossDimension (Cell cell) const { return columnFirst ? cell.row : cell.column; } - - Cell fromDimensions (int mainDimension, int crossDimension) const + static bool isColumnAutoFlow (AutoFlow autoFlow) { - if (columnFirst) - return { mainDimension, crossDimension }; - - return { crossDimension, mainDimension }; + return autoFlow == AutoFlow::column + || autoFlow == AutoFlow::columnDense; } - int highestCrossDimension; - bool columnFirst; - std::set occupiedCells; - }; - - //============================================================================== - static bool isFixed (GridItem::StartAndEndProperty prop) - { - return prop.start.hasName() || prop.start.hasAbsolute() || prop.end.hasName() || prop.end.hasAbsolute(); - } - - static bool hasFullyFixedPlacement (const GridItem& item) - { - if (item.area.isNotEmpty()) - return true; - - if (isFixed (item.column) && isFixed (item.row)) - return true; + //============================================================================== + static int getSpanFromAuto (GridItem::StartAndEndProperty prop) + { + if (prop.end.hasSpan()) + return prop.end.getNumber(); - return false; - } + if (prop.start.hasSpan()) + return prop.start.getNumber(); - static bool hasPartialFixedPlacement (const GridItem& item) - { - if (item.area.isNotEmpty()) - return false; + return 1; + } - if (isFixed (item.column) ^ isFixed (item.row)) - return true; + //============================================================================== + ItemPlacementArray deduceAllItems (Grid& grid) const + { + const auto namedAreas = PlacementHelpers::deduceNamedAreas (grid.templateAreas); - return false; - } + OccupancyPlane plane (jmax (grid.templateColumns.size() + 1, 2), + jmax (grid.templateRows.size() + 1, 2), + isColumnAutoFlow (grid.autoFlow)); - static bool hasAutoPlacement (const GridItem& item) - { - return ! hasFullyFixedPlacement (item) && ! hasPartialFixedPlacement (item); - } + ItemPlacementArray itemPlacementArray; + Array sortedItems; - //============================================================================== - static bool hasDenseAutoFlow (AutoFlow autoFlow) - { - return autoFlow == AutoFlow::columnDense - || autoFlow == AutoFlow::rowDense; - } + for (auto& item : grid.items) + sortedItems.add (&item); - static bool isColumnAutoFlow (AutoFlow autoFlow) - { - return autoFlow == AutoFlow::column - || autoFlow == AutoFlow::columnDense; - } + std::stable_sort (sortedItems.begin(), sortedItems.end(), + [] (const GridItem* i1, const GridItem* i2) { return i1->order < i2->order; }); - //============================================================================== - static int getSpanFromAuto (GridItem::StartAndEndProperty prop) - { - if (prop.end.hasSpan()) - return prop.end.getNumber(); + // place fixed items first + for (auto* item : sortedItems) + { + if (hasFullyFixedPlacement (*item)) + { + const auto a = PlacementHelpers::deduceLineArea (*item, grid, namedAreas); + plane.setCell ({ a.column.start, a.row.start }, { a.column.end, a.row.end }); + itemPlacementArray.add ({ item, a }); + } + } - if (prop.start.hasSpan()) - return prop.start.getNumber(); + OccupancyPlane::Cell lastInsertionCell = { 1, 1 }; - return 1; - } + for (auto* item : sortedItems) + { + if (hasPartialFixedPlacement (*item)) + { + if (isFixed (item->column)) + { + const auto p = PlacementHelpers::deduceLineRange (item->column, grid.templateColumns); + const auto columnSpan = std::abs (p.start - p.end); + const auto rowSpan = getSpanFromAuto (item->row); - //============================================================================== - ItemPlacementArray deduceAllItems (Grid& grid) const - { - const auto namedAreas = PlacementHelpers::deduceNamedAreas (grid.templateAreas); + const auto insertionCell = hasDenseAutoFlow (grid.autoFlow) ? OccupancyPlane::Cell { p.start, 1 } + : lastInsertionCell; + const auto nextAvailableCell = plane.nextAvailableOnColumn (insertionCell, columnSpan, rowSpan, p.start); + const auto lineArea = plane.setCell (nextAvailableCell, columnSpan, rowSpan); + lastInsertionCell = nextAvailableCell; - OccupancyPlane plane (jmax (grid.templateColumns.size() + 1, 2), - jmax (grid.templateRows.size() + 1, 2), - isColumnAutoFlow (grid.autoFlow)); + itemPlacementArray.add ({ item, lineArea }); + } + else if (isFixed (item->row)) + { + const auto p = PlacementHelpers::deduceLineRange (item->row, grid.templateRows); + const auto columnSpan = getSpanFromAuto (item->column); + const auto rowSpan = std::abs (p.start - p.end); - ItemPlacementArray itemPlacementArray; - Array sortedItems; + const auto insertionCell = hasDenseAutoFlow (grid.autoFlow) ? OccupancyPlane::Cell { 1, p.start } + : lastInsertionCell; - for (auto& item : grid.items) - sortedItems.add (&item); + const auto nextAvailableCell = plane.nextAvailableOnRow (insertionCell, columnSpan, rowSpan, p.start); + const auto lineArea = plane.setCell (nextAvailableCell, columnSpan, rowSpan); - std::stable_sort (sortedItems.begin(), sortedItems.end(), - [] (const GridItem* i1, const GridItem* i2) { return i1->order < i2->order; }); + lastInsertionCell = nextAvailableCell; - // place fixed items first - for (auto* item : sortedItems) - { - if (hasFullyFixedPlacement (*item)) - { - const auto a = PlacementHelpers::deduceLineArea (*item, grid, namedAreas); - plane.setCell ({ a.column.start, a.row.start }, { a.column.end, a.row.end }); - itemPlacementArray.add ({ item, a }); + itemPlacementArray.add ({ item, lineArea }); + } + } } - } - OccupancyPlane::Cell lastInsertionCell = { 1, 1 }; + // https://www.w3.org/TR/css-grid-1/#auto-placement-algo step 3.3 + for (auto* item : sortedItems) + if (hasAutoPlacement (*item)) + plane.updateMaxCrossDimensionFromAutoPlacementItem (getSpanFromAuto (item->column), getSpanFromAuto (item->row)); - for (auto* item : sortedItems) - { - if (hasPartialFixedPlacement (*item)) - { - if (isFixed (item->column)) - { - const auto p = PlacementHelpers::deduceLineRange (item->column, grid.templateColumns); - const auto columnSpan = std::abs (p.start - p.end); - const auto rowSpan = getSpanFromAuto (item->row); + lastInsertionCell = { 1, 1 }; - const auto insertionCell = hasDenseAutoFlow (grid.autoFlow) ? OccupancyPlane::Cell { p.start, 1 } - : lastInsertionCell; - const auto nextAvailableCell = plane.nextAvailableOnColumn (insertionCell, columnSpan, rowSpan, p.start); - const auto lineArea = plane.setCell (nextAvailableCell, columnSpan, rowSpan); - lastInsertionCell = nextAvailableCell; - - itemPlacementArray.add ({ item, lineArea }); - } - else if (isFixed (item->row)) + for (auto* item : sortedItems) + { + if (hasAutoPlacement (*item)) { - const auto p = PlacementHelpers::deduceLineRange (item->row, grid.templateRows); const auto columnSpan = getSpanFromAuto (item->column); - const auto rowSpan = std::abs (p.start - p.end); - - const auto insertionCell = hasDenseAutoFlow (grid.autoFlow) ? OccupancyPlane::Cell { 1, p.start } - : lastInsertionCell; + const auto rowSpan = getSpanFromAuto (item->row); - const auto nextAvailableCell = plane.nextAvailableOnRow (insertionCell, columnSpan, rowSpan, p.start); + const auto nextAvailableCell = plane.nextAvailable (lastInsertionCell, columnSpan, rowSpan); const auto lineArea = plane.setCell (nextAvailableCell, columnSpan, rowSpan); - lastInsertionCell = nextAvailableCell; + if (! hasDenseAutoFlow (grid.autoFlow)) + lastInsertionCell = nextAvailableCell; - itemPlacementArray.add ({ item, lineArea }); + itemPlacementArray.add ({ item, lineArea }); } } - } - // https://www.w3.org/TR/css-grid-1/#auto-placement-algo step 3.3 - for (auto* item : sortedItems) - if (hasAutoPlacement (*item)) - plane.updateMaxCrossDimensionFromAutoPlacementItem (getSpanFromAuto (item->column), getSpanFromAuto (item->row)); - - lastInsertionCell = { 1, 1 }; + return itemPlacementArray; + } - for (auto* item : sortedItems) + //============================================================================== + template + static PlacementHelpers::LineRange findFullLineRange (const ItemPlacementArray& items, Accessor&& accessor) { - if (hasAutoPlacement (*item)) - { - const auto columnSpan = getSpanFromAuto (item->column); - const auto rowSpan = getSpanFromAuto (item->row); - - const auto nextAvailableCell = plane.nextAvailable (lastInsertionCell, columnSpan, rowSpan); - const auto lineArea = plane.setCell (nextAvailableCell, columnSpan, rowSpan); + if (items.isEmpty()) + return { 1, 1 }; - if (! hasDenseAutoFlow (grid.autoFlow)) - lastInsertionCell = nextAvailableCell; + const auto combine = [&accessor] (const auto& acc, const auto& item) + { + const auto newRange = accessor (item); + return PlacementHelpers::LineRange { std::min (acc.start, newRange.start), + std::max (acc.end, newRange.end) }; + }; - itemPlacementArray.add ({ item, lineArea }); - } + return std::accumulate (std::next (items.begin()), items.end(), accessor (*items.begin()), combine); } - return itemPlacementArray; - } + static PlacementHelpers::LineArea findFullLineArea (const ItemPlacementArray& items) + { + return { findFullLineRange (items, [] (const auto& item) { return item.second.column; }), + findFullLineRange (items, [] (const auto& item) { return item.second.row; }) }; + } - //============================================================================== - template - static PlacementHelpers::LineRange findFullLineRange (const ItemPlacementArray& items, Accessor&& accessor) - { - if (items.isEmpty()) - return { 1, 1 }; + template + static Array repeated (int repeats, const Item& item) + { + Array result; + result.insertMultiple (-1, item, repeats); + return result; + } - const auto combine = [&accessor] (const auto& acc, const auto& item) + static Tracks createImplicitTracks (const Grid& grid, const ItemPlacementArray& items) { - const auto newRange = accessor (item); - return PlacementHelpers::LineRange { std::min (acc.start, newRange.start), - std::max (acc.end, newRange.end) }; - }; + const auto fullArea = findFullLineArea (items); - return std::accumulate (std::next (items.begin()), items.end(), accessor (*items.begin()), combine); - } + const auto leadingColumns = std::max (0, 1 - fullArea.column.start); + const auto leadingRows = std::max (0, 1 - fullArea.row.start); - static PlacementHelpers::LineArea findFullLineArea (const ItemPlacementArray& items) - { - return { findFullLineRange (items, [] (const auto& item) { return item.second.column; }), - findFullLineRange (items, [] (const auto& item) { return item.second.row; }) }; - } + const auto trailingColumns = std::max (0, fullArea.column.end - grid.templateColumns.size() - 1); + const auto trailingRows = std::max (0, fullArea.row .end - grid.templateRows .size() - 1); - template - static Array repeated (int repeats, const Item& item) - { - Array result; - result.insertMultiple (-1, item, repeats); - return result; - } + return { { repeated (leadingColumns, grid.autoColumns) + grid.templateColumns + repeated (trailingColumns, grid.autoColumns), + leadingColumns }, + { repeated (leadingRows, grid.autoRows) + grid.templateRows + repeated (trailingRows, grid.autoRows), + leadingRows } }; + } - static Tracks createImplicitTracks (const Grid& grid, const ItemPlacementArray& items) - { - const auto fullArea = findFullLineArea (items); + //============================================================================== + static void applySizeForAutoTracks (Tracks& tracks, const ItemPlacementArray& placements) + { + const auto setSizes = [&placements] (auto& tracksInDirection, const auto& getItem, const auto& getItemSize) + { + auto& array = tracksInDirection.items; - const auto leadingColumns = std::max (0, 1 - fullArea.column.start); - const auto leadingRows = std::max (0, 1 - fullArea.row.start); + for (int index = 0; index < array.size(); ++index) + { + if (array.getReference (index).isAuto()) + { + const auto combiner = [&] (const auto acc, const auto& element) + { + const auto item = getItem (element.second); + const auto isNotSpan = std::abs (item.end - item.start) <= 1; + return isNotSpan && item.start == index + 1 - tracksInDirection.numImplicitLeading + ? std::max (acc, getItemSize (*element.first)) + : acc; + }; + + array.getReference (index).size = std::accumulate (placements.begin(), placements.end(), 0.0f, combiner); + } + } + }; - const auto trailingColumns = std::max (0, fullArea.column.end - grid.templateColumns.size() - 1); - const auto trailingRows = std::max (0, fullArea.row .end - grid.templateRows .size() - 1); + setSizes (tracks.rows, + [] (const auto& i) { return i.row; }, + [] (const auto& i) { return i.height + i.margin.top + i.margin.bottom; }); - return { { repeated (leadingColumns, grid.autoColumns) + grid.templateColumns + repeated (trailingColumns, grid.autoColumns), - leadingColumns }, - { repeated (leadingRows, grid.autoRows) + grid.templateRows + repeated (trailingRows, grid.autoRows), - leadingRows } }; - } + setSizes (tracks.columns, + [] (const auto& i) { return i.column; }, + [] (const auto& i) { return i.width + i.margin.left + i.margin.right; }); + } + }; //============================================================================== - static void applySizeForAutoTracks (Tracks& tracks, const ItemPlacementArray& placements) + struct BoxAlignment { - const auto setSizes = [&placements] (auto& tracksInDirection, const auto& getItem, const auto& getItemSize) + static Rectangle alignItem (const GridItem& item, const Grid& grid, Rectangle area) { - auto& array = tracksInDirection.items; + // if item align is auto, inherit value from grid + const auto alignType = item.alignSelf == GridItem::AlignSelf::autoValue + ? grid.alignItems + : static_cast (item.alignSelf); - for (int index = 0; index < array.size(); ++index) - { - if (array.getReference (index).isAuto()) - { - const auto combiner = [&] (const auto acc, const auto& element) - { - const auto item = getItem (element.second); - const auto isNotSpan = std::abs (item.end - item.start) <= 1; - return isNotSpan && item.start == index + 1 - tracksInDirection.numImplicitLeading - ? std::max (acc, getItemSize (*element.first)) - : acc; - }; - - array.getReference (index).size = std::accumulate (placements.begin(), placements.end(), 0.0f, combiner); - } - } - }; + const auto justifyType = item.justifySelf == GridItem::JustifySelf::autoValue + ? grid.justifyItems + : static_cast (item.justifySelf); - setSizes (tracks.rows, - [] (const auto& i) { return i.row; }, - [] (const auto& i) { return i.height + i.margin.top + i.margin.bottom; }); + // subtract margin from area + area = BorderSize (item.margin.top, item.margin.left, item.margin.bottom, item.margin.right) + .subtractedFrom (area); - setSizes (tracks.columns, - [] (const auto& i) { return i.column; }, - [] (const auto& i) { return i.width + i.margin.left + i.margin.right; }); - } -}; + // align and justify + auto r = area; -//============================================================================== -struct Grid::BoxAlignment -{ - static Rectangle alignItem (const GridItem& item, - const Grid& grid, - Rectangle area) - { - // if item align is auto, inherit value from grid - const auto alignType = item.alignSelf == GridItem::AlignSelf::autoValue - ? grid.alignItems - : static_cast (item.alignSelf); - - const auto justifyType = item.justifySelf == GridItem::JustifySelf::autoValue - ? grid.justifyItems - : static_cast (item.justifySelf); - - // subtract margin from area - area = BorderSize (item.margin.top, item.margin.left, item.margin.bottom, item.margin.right) - .subtractedFrom (area); - - // align and justify - auto r = area; - - if (! approximatelyEqual (item.width, (float) GridItem::notAssigned)) r.setWidth (item.width); - if (! approximatelyEqual (item.height, (float) GridItem::notAssigned)) r.setHeight (item.height); - if (! approximatelyEqual (item.maxWidth, (float) GridItem::notAssigned)) r.setWidth (jmin (item.maxWidth, r.getWidth())); - if (item.minWidth > 0.0f) r.setWidth (jmax (item.minWidth, r.getWidth())); - if (! approximatelyEqual (item.maxHeight, (float) GridItem::notAssigned)) r.setHeight (jmin (item.maxHeight, r.getHeight())); - if (item.minHeight > 0.0f) r.setHeight (jmax (item.minHeight, r.getHeight())); - - if (alignType == AlignItems::start && justifyType == JustifyItems::start) - return r; + if (! approximatelyEqual (item.width, (float) GridItem::notAssigned)) r.setWidth (item.width); + if (! approximatelyEqual (item.height, (float) GridItem::notAssigned)) r.setHeight (item.height); + if (! approximatelyEqual (item.maxWidth, (float) GridItem::notAssigned)) r.setWidth (jmin (item.maxWidth, r.getWidth())); + if (item.minWidth > 0.0f) r.setWidth (jmax (item.minWidth, r.getWidth())); + if (! approximatelyEqual (item.maxHeight, (float) GridItem::notAssigned)) r.setHeight (jmin (item.maxHeight, r.getHeight())); + if (item.minHeight > 0.0f) r.setHeight (jmax (item.minHeight, r.getHeight())); - if (alignType == AlignItems::end) r.setY (r.getY() + (area.getHeight() - r.getHeight())); - if (justifyType == JustifyItems::end) r.setX (r.getX() + (area.getWidth() - r.getWidth())); - if (alignType == AlignItems::center) r.setCentre (r.getCentreX(), area.getCentreY()); - if (justifyType == JustifyItems::center) r.setCentre (area.getCentreX(), r.getCentreY()); + if (alignType == AlignItems::start && justifyType == JustifyItems::start) + return r; + + if (alignType == AlignItems::end) r.setY (r.getY() + (area.getHeight() - r.getHeight())); + if (justifyType == JustifyItems::end) r.setX (r.getX() + (area.getWidth() - r.getWidth())); + if (alignType == AlignItems::center) r.setCentre (r.getCentreX(), area.getCentreY()); + if (justifyType == JustifyItems::center) r.setCentre (area.getCentreX(), r.getCentreY()); + + return r; + } + }; - return r; - } }; //============================================================================== @@ -1004,7 +1088,7 @@ Grid::TrackInfo::TrackInfo (const String& startLineNameToUse, Px sizeInPixels, c } Grid::TrackInfo::TrackInfo (const String& startLineNameToUse, Fr fractionOfFreeSpace, const String& endLineNameToUse) noexcept - : TrackInfo (startLineNameToUse, fractionOfFreeSpace) + : TrackInfo (startLineNameToUse, fractionOfFreeSpace) { endLineName = endLineNameToUse; } @@ -1017,37 +1101,58 @@ float Grid::TrackInfo::getAbsoluteSize (float relativeFractionalUnit) const //============================================================================== void Grid::performLayout (Rectangle targetArea) { - const auto itemsAndAreas = AutoPlacement().deduceAllItems (*this); + const auto itemsAndAreas = Helpers::AutoPlacement().deduceAllItems (*this); - auto implicitTracks = AutoPlacement::createImplicitTracks (*this, itemsAndAreas); + auto implicitTracks = Helpers::AutoPlacement::createImplicitTracks (*this, itemsAndAreas); - AutoPlacement::applySizeForAutoTracks (implicitTracks, itemsAndAreas); + Helpers::AutoPlacement::applySizeForAutoTracks (implicitTracks, itemsAndAreas); - SizeCalculation calculation; - calculation.computeSizes (targetArea.toFloat().getWidth(), - targetArea.toFloat().getHeight(), - columnGap, - rowGap, - implicitTracks); + Helpers::SizeCalculation calculation; + Helpers::SizeCalculation roundedCalculation; - for (auto& itemAndArea : itemsAndAreas) + const auto doComputeSizes = [&] (auto& sizeCalculation) { - const auto a = itemAndArea.second; - const auto areaBounds = PlacementHelpers::getAreaBounds (a.column, - a.row, - implicitTracks, - calculation, - alignContent, - justifyContent, - columnGap, - rowGap); + sizeCalculation.computeSizes (targetArea.toFloat().getWidth(), + targetArea.toFloat().getHeight(), + columnGap, + rowGap, + implicitTracks); + }; + + doComputeSizes (calculation); + doComputeSizes (roundedCalculation); + for (auto& itemAndArea : itemsAndAreas) + { auto* item = itemAndArea.first; - item->currentBounds = BoxAlignment::alignItem (*item, *this, areaBounds) - + targetArea.toFloat().getPosition(); + + const auto getBounds = [&] (const auto& sizeCalculation) + { + const auto a = itemAndArea.second; + + const auto areaBounds = Helpers::PlacementHelpers::getAreaBounds (a.column, + a.row, + implicitTracks, + sizeCalculation, + alignContent, + justifyContent); + + const auto rounded = [&] (auto rect) -> decltype (rect) + { + return { sizeCalculation.roundingFunction (rect.getX()), + sizeCalculation.roundingFunction (rect.getY()), + sizeCalculation.roundingFunction (rect.getWidth()), + sizeCalculation.roundingFunction (rect.getHeight()) }; + }; + + return rounded (Helpers::BoxAlignment::alignItem (*item, *this, areaBounds)) + + targetArea.toFloat().getPosition(); + }; + + item->currentBounds = getBounds (calculation) + targetArea.toFloat().getPosition(); if (auto* c = item->associatedComponent) - c->setBounds (item->currentBounds.getSmallestIntegerContainer()); + c->setBounds (getBounds (roundedCalculation).toNearestIntEdges() + targetArea.getPosition()); } } @@ -1355,6 +1460,61 @@ struct GridTests : public UnitTest expect (grid.items[1].currentBounds == Rect (420.0f, 70.0f, 60.0f, 70.0f)); expect (grid.items[2].currentBounds == Rect (200.0f, 330.0f, 200.0f, 70.0f)); } + + { + beginTest ("Items with specified sizes should translate to correctly rounded Component dimensions"); + + static constexpr int targetSize = 100; + + juce::Component component; + juce::GridItem item { component }; + item.alignSelf = juce::GridItem::AlignSelf::center; + item.justifySelf = juce::GridItem::JustifySelf::center; + item.width = (float) targetSize; + item.height = (float) targetSize; + + juce::Grid grid; + grid.templateColumns = { juce::Grid::Fr { 1 } }; + grid.templateRows = { juce::Grid::Fr { 1 } }; + grid.items = { item }; + + for (int totalSize = 100 - 20; totalSize < 100 + 20; ++totalSize) + { + Rectangle bounds { 0, 0, totalSize, totalSize }; + grid.performLayout (bounds); + + expectEquals (component.getWidth(), targetSize); + expectEquals (component.getHeight(), targetSize); + } + } + + { + beginTest ("Track sizes specified in Px should translate to correctly rounded Component dimensions"); + + static constexpr int targetSize = 100; + + juce::Component component; + juce::GridItem item { component }; + item.alignSelf = juce::GridItem::AlignSelf::center; + item.justifySelf = juce::GridItem::JustifySelf::center; + item.setArea (1, 3); + + juce::Grid grid; + grid.templateColumns = { juce::Grid::Fr { 1 }, + juce::Grid::Fr { 1 }, + juce::Grid::Px { targetSize }, + juce::Grid::Fr { 1 } }; + grid.templateRows = { juce::Grid::Fr { 1 } }; + grid.items = { item }; + + for (int totalSize = 100 - 20; totalSize < 100 + 20; ++totalSize) + { + Rectangle bounds { 0, 0, totalSize, totalSize }; + grid.performLayout (bounds); + + expectEquals (component.getWidth(), targetSize); + } + } } }; diff --git a/modules/juce_gui_basics/layout/juce_Grid.h b/modules/juce_gui_basics/layout/juce_Grid.h index ce261d2199..25af8f32c6 100644 --- a/modules/juce_gui_basics/layout/juce_Grid.h +++ b/modules/juce_gui_basics/layout/juce_Grid.h @@ -216,10 +216,7 @@ public: private: //============================================================================== - struct SizeCalculation; - struct PlacementHelpers; - struct AutoPlacement; - struct BoxAlignment; + struct Helpers; }; constexpr Grid::Px operator"" _px (long double px) { return Grid::Px { px }; }