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 | 1948 ++++++++++-------- modules/juce_gui_basics/layout/juce_Grid.h | 5 +- 3 files changed, 1084 insertions(+), 898 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,507 +26,6 @@ namespace juce { -struct AllTracksIncludingImplicit -{ - Array items; - int numImplicitLeading; // The number of implicit items before the explicit items -}; - -struct Tracks -{ - AllTracksIncludingImplicit columns, rows; -}; - -struct Grid::SizeCalculation -{ - static float getTotalAbsoluteSize (const Array& tracks, Px gapSize) noexcept - { - 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; - } - - static float getRelativeUnitSize (float size, float totalAbsolute, const Array& tracks) noexcept - { - 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; - } - - //============================================================================== - static float getTotalAbsoluteHeight (const Array& rowTracks, Px rowGap) - { - return getTotalAbsoluteSize (rowTracks, rowGap); - } - - static float getTotalAbsoluteWidth (const Array& columnTracks, Px columnGap) - { - return getTotalAbsoluteSize (columnTracks, columnGap); - } - - static float getRelativeWidthUnit (float gridWidth, Px columnGap, const Array& columnTracks) - { - return getRelativeUnitSize (gridWidth, getTotalAbsoluteWidth (columnTracks, columnGap), columnTracks); - } - - static float getRelativeHeightUnit (float gridHeight, Px rowGap, const Array& rowTracks) - { - return getRelativeUnitSize (gridHeight, getTotalAbsoluteHeight (rowTracks, rowGap), rowTracks); - } - - //============================================================================== - static bool hasAnyFractions (const Array& tracks) - { - return std::any_of (tracks.begin(), - tracks.end(), - [] (const auto& t) { return t.isFractional(); }); - } - - 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); - } - - float relativeWidthUnit = 0.0f; - float relativeHeightUnit = 0.0f; - float remainingWidth = 0.0f; - float remainingHeight = 0.0f; -}; - -//============================================================================== -struct Grid::PlacementHelpers -{ - enum { invalid = -999999 }; - static constexpr auto emptyAreaCharacter = "."; - - //============================================================================== - struct LineRange { int start, end; }; - struct LineArea { LineRange column, row; }; - struct LineInfo { StringArray lineNames; }; - - struct NamedArea - { - String name; - LineArea lines; - }; - - //============================================================================== - static Array getArrayOfLinesFromTracks (const Array& tracks) - { - // fill line info array - Array lines; - - for (int i = 1; i <= tracks.size(); ++i) - { - const auto& currentTrack = tracks.getReference (i - 1); - - if (i == 1) // start line - { - LineInfo li; - li.lineNames.add (currentTrack.getStartLineName()); - lines.add (li); - } - - if (i > 1 && i <= tracks.size()) // two lines in between tracks - { - const auto& prevTrack = tracks.getReference (i - 2); - - LineInfo li; - li.lineNames.add (prevTrack.getEndLineName()); - li.lineNames.add (currentTrack.getStartLineName()); - - lines.add (li); - } - - if (i == tracks.size()) // end line - { - LineInfo li; - li.lineNames.add (currentTrack.getEndLineName()); - lines.add (li); - } - } - - jassert (lines.size() == tracks.size() + 1); - - return lines; - } - - //============================================================================== - static int deduceAbsoluteLineNumberFromLineName (GridItem::Property prop, - const Array& tracks) - { - jassert (prop.hasAbsolute()); - - const auto lines = getArrayOfLinesFromTracks (tracks); - int count = 0; - - for (int i = 0; i < lines.size(); i++) - { - for (const auto& name : lines.getReference (i).lineNames) - { - if (prop.getName() == name) - { - ++count; - break; - } - } - - if (count == prop.getNumber()) - return i + 1; - } - - jassertfalse; - return count; - } - - static int deduceAbsoluteLineNumber (GridItem::Property prop, - const Array& tracks) - { - jassert (prop.hasAbsolute()); - - if (prop.hasName()) - return deduceAbsoluteLineNumberFromLineName (prop, tracks); - - if (prop.getNumber() > 0) - return prop.getNumber(); - - if (prop.getNumber() < 0) - return tracks.size() + 2 + prop.getNumber(); - - // An integer value of 0 is invalid - jassertfalse; - return 1; - } - - static int deduceAbsoluteLineNumberFromNamedSpan (int startLineNumber, - GridItem::Property propertyWithSpan, - const Array& tracks) - { - 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; - } - - static int deduceAbsoluteLineNumberBasedOnSpan (int startLineNumber, - GridItem::Property propertyWithSpan, - const Array& tracks) - { - jassert (propertyWithSpan.hasSpan()); - - if (propertyWithSpan.hasName()) - return deduceAbsoluteLineNumberFromNamedSpan (startLineNumber, propertyWithSpan, tracks); - - return startLineNumber + propertyWithSpan.getNumber(); - } - - //============================================================================== - static LineRange deduceLineRange (GridItem::StartAndEndProperty prop, const Array& tracks) - { - jassert (! (prop.start.hasAuto() && prop.end.hasAuto())); - - if (prop.start.hasAbsolute() && prop.end.hasAuto()) - { - prop.end = GridItem::Span (1); - } - else if (prop.start.hasAuto() && prop.end.hasAbsolute()) - { - prop.start = GridItem::Span (1); - } - - auto s = [&]() -> LineRange - { - if (prop.start.hasAbsolute() && prop.end.hasAbsolute()) - { - return { deduceAbsoluteLineNumber (prop.start, tracks), - deduceAbsoluteLineNumber (prop.end, tracks) }; - } - - if (prop.start.hasAbsolute() && prop.end.hasSpan()) - { - const auto start = deduceAbsoluteLineNumber (prop.start, tracks); - return { start, deduceAbsoluteLineNumberBasedOnSpan (start, prop.end, tracks) }; - } - - if (prop.start.hasSpan() && prop.end.hasAbsolute()) - { - const auto start = deduceAbsoluteLineNumber (prop.end, tracks); - return { start, deduceAbsoluteLineNumberBasedOnSpan (start, prop.start, tracks) }; - } - - // Can't have an item with spans on both start and end. - jassertfalse; - return {}; - }(); - - // 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 s; - } - - 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); - - return namedAreas.at (item.area); - } - - return { deduceLineRange (item.column, grid.templateColumns), - deduceLineRange (item.row, grid.templateRows) }; - } - - //============================================================================== - static Array parseAreasProperty (const StringArray& areasStrings) - { - Array strings; - - for (const auto& areaString : areasStrings) - strings.add (StringArray::fromTokens (areaString, false)); - - if (strings.size() > 0) - { - 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; - - for (auto& stringArray : stringsArrays) - { - for (auto& string : stringArray) - { - // find anchor - if (area.name.isEmpty()) - { - 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) - { - area.lines.row.end = stringsArrays.indexOf (stringArray) + 2; - area.lines.column.end = stringArray.indexOf (string) + 2; - - // mark as visited - string = emptyAreaCharacter; - } - } - } - } - - return area; - } - - //============================================================================== - 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; - } - - return areas; - } - - //============================================================================== - static float getCoord (int trackNumber, float relativeUnit, Px gap, const Array& tracks) - { - float c = 0; - - for (const auto* it = tracks.begin(); it != tracks.begin() + trackNumber; ++it) - c += it->getAbsoluteSize (relativeUnit) + static_cast (gap.pixels); - - return c; - } - - 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; - - jassert (isPositiveAndBelow (correctedColumn, tracks.columns.items.size())); - jassert (isPositiveAndBelow (correctedRow, tracks.rows .items.size())); - - 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) }; - } - - 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::end) - area.setX (area.getX() + calculation.remainingWidth); - - if (alignContent == AlignContent::center) - area.setY (area.getY() + calculation.remainingHeight / 2); - - if (justifyContent == JustifyContent::center) - area.setX (area.getX() + calculation.remainingWidth / 2); - - if (alignContent == AlignContent::spaceBetween) - { - const auto shift = ((float) (rowNumber - 1) * (calculation.remainingHeight / float(numberOfRows - 1))); - area.setY (area.getY() + shift); - } - - if (justifyContent == JustifyContent::spaceBetween) - { - const auto shift = ((float) (columnNumber - 1) * (calculation.remainingWidth / float(numberOfColumns - 1))); - area.setX (area.getX() + shift); - } - - if (alignContent == AlignContent::spaceEvenly) - { - const auto shift = ((float) rowNumber * (calculation.remainingHeight / float(numberOfRows + 1))); - area.setY (area.getY() + shift); - } - - if (justifyContent == JustifyContent::spaceEvenly) - { - const auto shift = ((float) columnNumber * (calculation.remainingWidth / float(numberOfColumns + 1))); - area.setX (area.getX() + shift); - } - - if (alignContent == AlignContent::spaceAround) - { - const auto inbetweenShift = calculation.remainingHeight / float(numberOfRows); - const auto sidesShift = inbetweenShift / 2; - auto shift = (float) (rowNumber - 1) * inbetweenShift + sidesShift; - - area.setY (area.getY() + shift); - } - - if (justifyContent == JustifyContent::spaceAround) - { - const auto inbetweenShift = calculation.remainingWidth / float(numberOfColumns); - const auto sidesShift = inbetweenShift / 2; - auto shift = (float) (columnNumber - 1) * inbetweenShift + sidesShift; - - area.setX (area.getX() + shift); - } - - return area; - } - - 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); - }; - - 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() }; - } -}; - template static Array operator+ (const Array& a, const Array& b) { @@ -535,433 +34,1018 @@ static Array operator+ (const Array& a, const Array& b) return copy; } -//============================================================================== -struct Grid::AutoPlacement +struct Grid::Helpers { - using ItemPlacementArray = Array>; + + struct AllTracksIncludingImplicit + { + Array items; + int numImplicitLeading; // The number of implicit items before the explicit items + }; + + struct Tracks + { + AllTracksIncludingImplicit columns, rows; + }; + + struct NoRounding + { + template + T operator() (T t) const { return t; } + }; + + struct StandardRounding + { + template + T operator() (T t) const { return std::round (t); } + }; + + template + struct SizeCalculation + { + float getTotalAbsoluteSize (const Array& tracks, Px gapSize) noexcept + { + float totalCellSize = 0.0f; + + for (const auto& trackInfo : tracks) + if (! trackInfo.isFractional() || trackInfo.isAuto()) + totalCellSize += roundingFunction (trackInfo.getSize()); + + float totalGap = tracks.size() > 1 ? (float) (tracks.size() - 1) * roundingFunction ((float) gapSize.pixels) + : 0.0f; + + return totalCellSize + totalGap; + } + + static float getRelativeUnitSize (float size, float totalAbsolute, const Array& tracks) noexcept + { + 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; + } + + //============================================================================== + float getTotalAbsoluteHeight (const Array& rowTracks, Px rowGapSize) + { + return getTotalAbsoluteSize (rowTracks, rowGapSize); + } + + float getTotalAbsoluteWidth (const Array& columnTracks, Px columnGapSize) + { + return getTotalAbsoluteSize (columnTracks, columnGapSize); + } + + float getRelativeWidthUnit (float gridWidth, Px columnGapSize, const Array& columnTracks) + { + 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(); }); + } + + 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); + fractionallyDividedWidth = gridWidth - getTotalAbsoluteSize (tracks.columns.items, columnGapToUse); + } + else + { + remainingWidth = gridWidth - getTotalAbsoluteSize (tracks.columns.items, columnGapToUse); + } + + if (hasAnyFractions (tracks.rows.items)) + { + relativeHeightUnit = getRelativeHeightUnit (gridHeight, rowGapToUse, tracks.rows.items); + fractionallyDividedHeight = gridHeight - getTotalAbsoluteSize (tracks.rows.items, rowGapToUse); + } + else + { + 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); + } + + float relativeWidthUnit = 0.0f; + float relativeHeightUnit = 0.0f; + float fractionallyDividedWidth = 0.0f; + float fractionallyDividedHeight = 0.0f; + float remainingWidth = 0.0f; + float remainingHeight = 0.0f; + + std::vector> columnTrackBounds; + std::vector> rowTrackBounds; + RoundingFunction roundingFunction; + }; //============================================================================== - struct OccupancyPlane + struct PlacementHelpers { - struct Cell { int column, row; }; + enum { invalid = -999999 }; + static constexpr auto emptyAreaCharacter = "."; - OccupancyPlane (int highestColumnToUse, int highestRowToUse, bool isColumnFirst) - : highestCrossDimension (isColumnFirst ? highestRowToUse : highestColumnToUse), - columnFirst (isColumnFirst) - {} + //============================================================================== + struct LineRange { int start, end; }; + struct LineArea { LineRange column, row; }; + struct LineInfo { StringArray lineNames; }; - PlacementHelpers::LineArea setCell (Cell cell, int columnSpan, int rowSpan) + struct NamedArea { - for (int i = 0; i < columnSpan; i++) - for (int j = 0; j < rowSpan; j++) - setCell (cell.column + i, cell.row + j); + String name; + LineArea lines; + }; - return { { cell.column, cell.column + columnSpan }, { cell.row, cell.row + rowSpan } }; - } - - PlacementHelpers::LineArea setCell (Cell start, Cell end) + //============================================================================== + static Array getArrayOfLinesFromTracks (const Array& tracks) { - return setCell (start, std::abs (end.column - start.column), - std::abs (end.row - start.row)); - } + // fill line info array + Array lines; - Cell nextAvailable (Cell referenceCell, int columnSpan, int rowSpan) - { - while (isOccupied (referenceCell, columnSpan, rowSpan) || isOutOfBounds (referenceCell, columnSpan, rowSpan)) - referenceCell = advance (referenceCell); - - return referenceCell; - } - - Cell nextAvailableOnRow (Cell referenceCell, int columnSpan, int rowSpan, int rowNumber) - { - if (columnFirst && (rowNumber + rowSpan) > highestCrossDimension) - highestCrossDimension = rowNumber + rowSpan; - - while (isOccupied (referenceCell, columnSpan, rowSpan) - || (referenceCell.row != rowNumber)) - referenceCell = advance (referenceCell); - - return referenceCell; - } - - Cell nextAvailableOnColumn (Cell referenceCell, int columnSpan, int rowSpan, int columnNumber) - { - if (! columnFirst && (columnNumber + columnSpan) > highestCrossDimension) - highestCrossDimension = columnNumber + columnSpan; - - while (isOccupied (referenceCell, columnSpan, rowSpan) - || (referenceCell.column != columnNumber)) - referenceCell = advance (referenceCell); - - return referenceCell; - } - - void updateMaxCrossDimensionFromAutoPlacementItem (int columnSpan, int rowSpan) - { - highestCrossDimension = jmax (highestCrossDimension, 1 + getCrossDimension ({ columnSpan, rowSpan })); - } - - private: - struct SortableCell - { - int column, row; - bool columnFirst; - - bool operator< (const SortableCell& other) const + for (int i = 1; i <= tracks.size(); ++i) { - if (columnFirst) + const auto& currentTrack = tracks.getReference (i - 1); + + if (i == 1) // start line { + LineInfo li; + li.lineNames.add (currentTrack.getStartLineName()); + lines.add (li); + } + + if (i > 1 && i <= tracks.size()) // two lines in between tracks + { + const auto& prevTrack = tracks.getReference (i - 2); + + LineInfo li; + li.lineNames.add (prevTrack.getEndLineName()); + li.lineNames.add (currentTrack.getStartLineName()); + + lines.add (li); + } + + if (i == tracks.size()) // end line + { + LineInfo li; + li.lineNames.add (currentTrack.getEndLineName()); + lines.add (li); + } + } + + jassert (lines.size() == tracks.size() + 1); + + return lines; + } + + //============================================================================== + static int deduceAbsoluteLineNumberFromLineName (GridItem::Property prop, + const Array& tracks) + { + jassert (prop.hasAbsolute()); + + const auto lines = getArrayOfLinesFromTracks (tracks); + int count = 0; + + for (int i = 0; i < lines.size(); i++) + { + for (const auto& name : lines.getReference (i).lineNames) + { + if (prop.getName() == name) + { + ++count; + break; + } + } + + if (count == prop.getNumber()) + return i + 1; + } + + jassertfalse; + return count; + } + + static int deduceAbsoluteLineNumber (GridItem::Property prop, + const Array& tracks) + { + jassert (prop.hasAbsolute()); + + if (prop.hasName()) + return deduceAbsoluteLineNumberFromLineName (prop, tracks); + + if (prop.getNumber() > 0) + return prop.getNumber(); + + if (prop.getNumber() < 0) + return tracks.size() + 2 + prop.getNumber(); + + // An integer value of 0 is invalid + jassertfalse; + return 1; + } + + static int deduceAbsoluteLineNumberFromNamedSpan (int startLineNumber, + GridItem::Property propertyWithSpan, + const Array& tracks) + { + 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; + } + + static int deduceAbsoluteLineNumberBasedOnSpan (int startLineNumber, + GridItem::Property propertyWithSpan, + const Array& tracks) + { + jassert (propertyWithSpan.hasSpan()); + + if (propertyWithSpan.hasName()) + return deduceAbsoluteLineNumberFromNamedSpan (startLineNumber, propertyWithSpan, tracks); + + return startLineNumber + propertyWithSpan.getNumber(); + } + + //============================================================================== + static LineRange deduceLineRange (GridItem::StartAndEndProperty prop, const Array& tracks) + { + jassert (! (prop.start.hasAuto() && prop.end.hasAuto())); + + if (prop.start.hasAbsolute() && prop.end.hasAuto()) + { + prop.end = GridItem::Span (1); + } + else if (prop.start.hasAuto() && prop.end.hasAbsolute()) + { + prop.start = GridItem::Span (1); + } + + auto s = [&]() -> LineRange + { + if (prop.start.hasAbsolute() && prop.end.hasAbsolute()) + { + return { deduceAbsoluteLineNumber (prop.start, tracks), + deduceAbsoluteLineNumber (prop.end, tracks) }; + } + + if (prop.start.hasAbsolute() && prop.end.hasSpan()) + { + const auto start = deduceAbsoluteLineNumber (prop.start, tracks); + return { start, deduceAbsoluteLineNumberBasedOnSpan (start, prop.end, tracks) }; + } + + if (prop.start.hasSpan() && prop.end.hasAbsolute()) + { + const auto start = deduceAbsoluteLineNumber (prop.end, tracks); + return { start, deduceAbsoluteLineNumberBasedOnSpan (start, prop.start, tracks) }; + } + + // Can't have an item with spans on both start and end. + jassertfalse; + return {}; + }(); + + // 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 s; + } + + 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); + + return namedAreas.at (item.area); + } + + return { deduceLineRange (item.column, grid.templateColumns), + deduceLineRange (item.row, grid.templateRows) }; + } + + //============================================================================== + static Array parseAreasProperty (const StringArray& areasStrings) + { + Array strings; + + for (const auto& areaString : areasStrings) + strings.add (StringArray::fromTokens (areaString, false)); + + if (strings.size() > 0) + { + 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; + + for (auto& stringArray : stringsArrays) + { + for (auto& string : stringArray) + { + // find anchor + if (area.name.isEmpty()) + { + 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) + { + area.lines.row.end = stringsArrays.indexOf (stringArray) + 2; + area.lines.column.end = stringArray.indexOf (string) + 2; + + // mark as visited + string = emptyAreaCharacter; + } + } + } + } + + return area; + } + + //============================================================================== + 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; + } + + return areas; + } + + //============================================================================== + template + static Rectangle getCellBounds (int columnNumber, int rowNumber, + const Tracks& tracks, + const SizeCalculation& calculation) + { + 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() + }; + } + + 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); + + if (justifyContent == JustifyContent::end) + area.setX (area.getX() + calculation.remainingWidth); + + if (alignContent == AlignContent::center) + area.setY (area.getY() + calculation.remainingHeight / 2); + + if (justifyContent == JustifyContent::center) + area.setX (area.getX() + calculation.remainingWidth / 2); + + if (alignContent == AlignContent::spaceBetween) + { + const auto shift = ((float) (rowNumber - 1) * (calculation.remainingHeight / float(numberOfRows - 1))); + area.setY (area.getY() + shift); + } + + if (justifyContent == JustifyContent::spaceBetween) + { + const auto shift = ((float) (columnNumber - 1) * (calculation.remainingWidth / float(numberOfColumns - 1))); + area.setX (area.getX() + shift); + } + + if (alignContent == AlignContent::spaceEvenly) + { + const auto shift = ((float) rowNumber * (calculation.remainingHeight / float(numberOfRows + 1))); + area.setY (area.getY() + shift); + } + + if (justifyContent == JustifyContent::spaceEvenly) + { + const auto shift = ((float) columnNumber * (calculation.remainingWidth / float(numberOfColumns + 1))); + area.setX (area.getX() + shift); + } + + if (alignContent == AlignContent::spaceAround) + { + const auto inbetweenShift = calculation.remainingHeight / float(numberOfRows); + const auto sidesShift = inbetweenShift / 2; + auto shift = (float) (rowNumber - 1) * inbetweenShift + sidesShift; + + area.setY (area.getY() + shift); + } + + if (justifyContent == JustifyContent::spaceAround) + { + const auto inbetweenShift = calculation.remainingWidth / float(numberOfColumns); + const auto sidesShift = inbetweenShift / 2; + auto shift = (float) (columnNumber - 1) * inbetweenShift + sidesShift; + + area.setX (area.getX() + shift); + } + + return area; + } + + template + static Rectangle getAreaBounds (PlacementHelpers::LineRange columnRange, + PlacementHelpers::LineRange rowRange, + const Tracks& tracks, + const SizeCalculation& calculation, + AlignContent alignContent, + JustifyContent justifyContent) + { + 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() }; + } + }; + + //============================================================================== + struct AutoPlacement + { + using ItemPlacementArray = Array>; + + //============================================================================== + struct OccupancyPlane + { + struct Cell { int column, row; }; + + OccupancyPlane (int highestColumnToUse, int highestRowToUse, bool isColumnFirst) + : highestCrossDimension (isColumnFirst ? highestRowToUse : highestColumnToUse), + columnFirst (isColumnFirst) + {} + + 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 { { cell.column, cell.column + columnSpan }, { cell.row, cell.row + rowSpan } }; + } + + PlacementHelpers::LineArea setCell (Cell start, Cell end) + { + return setCell (start, std::abs (end.column - start.column), + std::abs (end.row - start.row)); + } + + Cell nextAvailable (Cell referenceCell, int columnSpan, int rowSpan) + { + while (isOccupied (referenceCell, columnSpan, rowSpan) || isOutOfBounds (referenceCell, columnSpan, rowSpan)) + referenceCell = advance (referenceCell); + + return referenceCell; + } + + Cell nextAvailableOnRow (Cell referenceCell, int columnSpan, int rowSpan, int rowNumber) + { + if (columnFirst && (rowNumber + rowSpan) > highestCrossDimension) + highestCrossDimension = rowNumber + rowSpan; + + while (isOccupied (referenceCell, columnSpan, rowSpan) + || (referenceCell.row != rowNumber)) + referenceCell = advance (referenceCell); + + return referenceCell; + } + + Cell nextAvailableOnColumn (Cell referenceCell, int columnSpan, int rowSpan, int columnNumber) + { + if (! columnFirst && (columnNumber + columnSpan) > highestCrossDimension) + highestCrossDimension = columnNumber + columnSpan; + + while (isOccupied (referenceCell, columnSpan, rowSpan) + || (referenceCell.column != columnNumber)) + referenceCell = advance (referenceCell); + + return referenceCell; + } + + void updateMaxCrossDimensionFromAutoPlacementItem (int columnSpan, int rowSpan) + { + highestCrossDimension = jmax (highestCrossDimension, 1 + getCrossDimension ({ columnSpan, rowSpan })); + } + + private: + struct SortableCell + { + int column, row; + bool columnFirst; + + bool operator< (const SortableCell& other) const + { + if (columnFirst) + { + if (row == other.row) + return column < other.column; + + return row < other.row; + } + if (row == other.row) return column < other.column; return row < other.row; } + }; - if (row == other.row) - return column < other.column; - - return row < other.row; + void setCell (int column, int row) + { + occupiedCells.insert ({ column, row, columnFirst }); } + + bool isOccupied (Cell cell) const + { + return occupiedCells.count ({ cell.column, cell.row, columnFirst }) > 0; + } + + 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 false; + } + + bool isOutOfBounds (Cell cell, int columnSpan, int rowSpan) const + { + const auto highestIndexOfCell = getCrossDimension (cell) + getCrossDimension ({ columnSpan, rowSpan }); + const auto highestIndexOfGrid = getHighestCrossDimension(); + + return highestIndexOfGrid < highestIndexOfCell; + } + + int getHighestCrossDimension() const + { + Cell cell { 1, 1 }; + + if (occupiedCells.size() > 0) + cell = { occupiedCells.crbegin()->column, occupiedCells.crbegin()->row }; + + return std::max (getCrossDimension (cell), highestCrossDimension); + } + + Cell advance (Cell cell) const + { + if ((getCrossDimension (cell) + 1) >= getHighestCrossDimension()) + return fromDimensions (getMainDimension (cell) + 1, 1); + + 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 int getSpanFromAuto (GridItem::StartAndEndProperty prop) + { + if (prop.end.hasSpan()) + return prop.end.getNumber(); + + if (prop.start.hasSpan()) + return prop.start.getNumber(); + + return 1; + } + + //============================================================================== + ItemPlacementArray deduceAllItems (Grid& grid) const + { + const auto namedAreas = PlacementHelpers::deduceNamedAreas (grid.templateAreas); + + OccupancyPlane plane (jmax (grid.templateColumns.size() + 1, 2), + jmax (grid.templateRows.size() + 1, 2), + isColumnAutoFlow (grid.autoFlow)); + + ItemPlacementArray itemPlacementArray; + Array sortedItems; + + for (auto& item : grid.items) + sortedItems.add (&item); + + std::stable_sort (sortedItems.begin(), sortedItems.end(), + [] (const GridItem* i1, const GridItem* i2) { return i1->order < i2->order; }); + + // 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 }); + } + } + + OccupancyPlane::Cell lastInsertionCell = { 1, 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); + + 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)) + { + 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 nextAvailableCell = plane.nextAvailableOnRow (insertionCell, columnSpan, rowSpan, p.start); + const auto lineArea = plane.setCell (nextAvailableCell, columnSpan, rowSpan); + + lastInsertionCell = nextAvailableCell; + + 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 }; + + for (auto* item : sortedItems) + { + 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 (! hasDenseAutoFlow (grid.autoFlow)) + lastInsertionCell = nextAvailableCell; + + itemPlacementArray.add ({ item, lineArea }); + } + } + + return itemPlacementArray; + } + + //============================================================================== + template + static PlacementHelpers::LineRange findFullLineRange (const ItemPlacementArray& items, Accessor&& accessor) + { + if (items.isEmpty()) + return { 1, 1 }; + + 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) }; + }; + + return std::accumulate (std::next (items.begin()), items.end(), accessor (*items.begin()), combine); + } + + 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 Array repeated (int repeats, const Item& item) + { + Array result; + result.insertMultiple (-1, item, repeats); + return result; + } + + static Tracks createImplicitTracks (const Grid& grid, const ItemPlacementArray& items) + { + const auto fullArea = findFullLineArea (items); + + const auto leadingColumns = std::max (0, 1 - fullArea.column.start); + const auto leadingRows = std::max (0, 1 - fullArea.row.start); + + 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); + + return { { repeated (leadingColumns, grid.autoColumns) + grid.templateColumns + repeated (trailingColumns, grid.autoColumns), + leadingColumns }, + { repeated (leadingRows, grid.autoRows) + grid.templateRows + repeated (trailingRows, grid.autoRows), + leadingRows } }; + } + + //============================================================================== + static void applySizeForAutoTracks (Tracks& tracks, const ItemPlacementArray& placements) + { + const auto setSizes = [&placements] (auto& tracksInDirection, const auto& getItem, const auto& getItemSize) + { + auto& array = tracksInDirection.items; + + 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); + } + } + }; + + setSizes (tracks.rows, + [] (const auto& i) { return i.row; }, + [] (const auto& i) { return i.height + i.margin.top + i.margin.bottom; }); + + setSizes (tracks.columns, + [] (const auto& i) { return i.column; }, + [] (const auto& i) { return i.width + i.margin.left + i.margin.right; }); + } }; //============================================================================== - static bool isFixed (GridItem::StartAndEndProperty prop) + struct BoxAlignment { - 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; - - return false; - } - - static bool hasPartialFixedPlacement (const GridItem& item) - { - if (item.area.isNotEmpty()) - return false; - - if (isFixed (item.column) ^ isFixed (item.row)) - return true; - - return false; - } - - static bool hasAutoPlacement (const GridItem& item) - { - return ! hasFullyFixedPlacement (item) && ! hasPartialFixedPlacement (item); - } - - //============================================================================== - static bool hasDenseAutoFlow (AutoFlow autoFlow) - { - return autoFlow == AutoFlow::columnDense - || autoFlow == AutoFlow::rowDense; - } - - static bool isColumnAutoFlow (AutoFlow autoFlow) - { - return autoFlow == AutoFlow::column - || autoFlow == AutoFlow::columnDense; - } - - //============================================================================== - static int getSpanFromAuto (GridItem::StartAndEndProperty prop) - { - if (prop.end.hasSpan()) - return prop.end.getNumber(); - - if (prop.start.hasSpan()) - return prop.start.getNumber(); - - return 1; - } - - //============================================================================== - ItemPlacementArray deduceAllItems (Grid& grid) const - { - const auto namedAreas = PlacementHelpers::deduceNamedAreas (grid.templateAreas); - - OccupancyPlane plane (jmax (grid.templateColumns.size() + 1, 2), - jmax (grid.templateRows.size() + 1, 2), - isColumnAutoFlow (grid.autoFlow)); - - ItemPlacementArray itemPlacementArray; - Array sortedItems; - - for (auto& item : grid.items) - sortedItems.add (&item); - - std::stable_sort (sortedItems.begin(), sortedItems.end(), - [] (const GridItem* i1, const GridItem* i2) { return i1->order < i2->order; }); - - // place fixed items first - for (auto* item : sortedItems) + static Rectangle alignItem (const GridItem& item, const Grid& grid, Rectangle area) { - 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 item align is auto, inherit value from grid + const auto alignType = item.alignSelf == GridItem::AlignSelf::autoValue + ? grid.alignItems + : static_cast (item.alignSelf); - OccupancyPlane::Cell lastInsertionCell = { 1, 1 }; + const auto justifyType = item.justifySelf == GridItem::JustifySelf::autoValue + ? grid.justifyItems + : static_cast (item.justifySelf); - 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); + // subtract margin from area + area = BorderSize (item.margin.top, item.margin.left, item.margin.bottom, item.margin.right) + .subtractedFrom (area); - 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; + // align and justify + auto r = area; - 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); + 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())); - const auto insertionCell = hasDenseAutoFlow (grid.autoFlow) ? OccupancyPlane::Cell { 1, p.start } - : lastInsertionCell; + if (alignType == AlignItems::start && justifyType == JustifyItems::start) + return r; - const auto nextAvailableCell = plane.nextAvailableOnRow (insertionCell, columnSpan, rowSpan, p.start); - const auto lineArea = plane.setCell (nextAvailableCell, columnSpan, rowSpan); + 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()); - lastInsertionCell = nextAvailableCell; - - 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 }; - - for (auto* item : sortedItems) - { - 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 (! hasDenseAutoFlow (grid.autoFlow)) - lastInsertionCell = nextAvailableCell; - - itemPlacementArray.add ({ item, lineArea }); - } - } - - return itemPlacementArray; - } - - //============================================================================== - template - static PlacementHelpers::LineRange findFullLineRange (const ItemPlacementArray& items, Accessor&& accessor) - { - if (items.isEmpty()) - return { 1, 1 }; - - 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) }; - }; - - return std::accumulate (std::next (items.begin()), items.end(), accessor (*items.begin()), combine); - } - - 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 Array repeated (int repeats, const Item& item) - { - Array result; - result.insertMultiple (-1, item, repeats); - return result; - } - - static Tracks createImplicitTracks (const Grid& grid, const ItemPlacementArray& items) - { - const auto fullArea = findFullLineArea (items); - - const auto leadingColumns = std::max (0, 1 - fullArea.column.start); - const auto leadingRows = std::max (0, 1 - fullArea.row.start); - - 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); - - return { { repeated (leadingColumns, grid.autoColumns) + grid.templateColumns + repeated (trailingColumns, grid.autoColumns), - leadingColumns }, - { repeated (leadingRows, grid.autoRows) + grid.templateRows + repeated (trailingRows, grid.autoRows), - leadingRows } }; - } - - //============================================================================== - static void applySizeForAutoTracks (Tracks& tracks, const ItemPlacementArray& placements) - { - const auto setSizes = [&placements] (auto& tracksInDirection, const auto& getItem, const auto& getItemSize) - { - auto& array = tracksInDirection.items; - - 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); - } - } - }; - - setSizes (tracks.rows, - [] (const auto& i) { return i.row; }, - [] (const auto& i) { return i.height + i.margin.top + i.margin.bottom; }); - - setSizes (tracks.columns, - [] (const auto& i) { return i.column; }, - [] (const auto& i) { return i.width + i.margin.left + i.margin.right; }); - } -}; - -//============================================================================== -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 (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; - } }; //============================================================================== @@ -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; + + const auto doComputeSizes = [&] (auto& sizeCalculation) + { + sizeCalculation.computeSizes (targetArea.toFloat().getWidth(), + targetArea.toFloat().getHeight(), + columnGap, + rowGap, + implicitTracks); + }; + + doComputeSizes (calculation); + doComputeSizes (roundedCalculation); for (auto& itemAndArea : itemsAndAreas) { - const auto a = itemAndArea.second; - const auto areaBounds = PlacementHelpers::getAreaBounds (a.column, - a.row, - implicitTracks, - calculation, - alignContent, - justifyContent, - columnGap, - rowGap); - 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 }; }