From a6761f9eb839e56cbaf8bef7d8e89260c5a89c48 Mon Sep 17 00:00:00 2001 From: attila Date: Thu, 13 Apr 2023 20:14:19 +0200 Subject: [PATCH] Grid: Ensure that items with absolute sizes will maintain correctly rounded dimensions Prior to this commit all Grid calculations were carried out using floating point numbers. The dimensions of all items would then be rounded with the same function to calculate the integer dimensions used for Component layout. This resulted in layout solutions where the width or height of items with dimensions specified using the absolute Px quantity could differ from the correctly rounded value of these values. This commit ensures that the width and height of these items are always correct and their cumulative error in size is distributed among items with fractional dimensions. --- BREAKING-CHANGES.txt | 29 + modules/juce_gui_basics/layout/juce_Grid.cpp | 1606 ++++++++++-------- modules/juce_gui_basics/layout/juce_Grid.h | 5 +- 3 files changed, 913 insertions(+), 727 deletions(-) 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 }; }