JUCE-7.0.12-0-g4f43011b96 JUCE-7.0.12-0-g4f43011b96
JUCE — C++ application framework with suport for VST, VST3, LV2 audio plug-ins

« « « Anklang Documentation
Loading...
Searching...
No Matches
juce_Grid.cpp
Go to the documentation of this file.
1 /*
2 ==============================================================================
3
4 This file is part of the JUCE library.
5 Copyright (c) 2022 - Raw Material Software Limited
6
7 JUCE is an open source library subject to commercial or open-source
8 licensing.
9
10 By using JUCE, you agree to the terms of both the JUCE 7 End-User License
11 Agreement and JUCE Privacy Policy.
12
13 End User License Agreement: www.juce.com/juce-7-licence
14 Privacy Policy: www.juce.com/juce-privacy-policy
15
16 Or: You may also use this code under the terms of the GPL v3 (see
17 www.gnu.org/licenses).
18
19 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21 DISCLAIMED.
22
23 ==============================================================================
24*/
25
26namespace juce
27{
28
29template <typename Item>
30static Array<Item> operator+ (const Array<Item>& a, const Array<Item>& b)
31{
32 auto copy = a;
33 copy.addArray (b);
34 return copy;
35}
36
38{
39
41 {
42 Array<TrackInfo> items;
43 int numImplicitLeading; // The number of implicit items before the explicit items
44 };
45
46 struct Tracks
47 {
48 AllTracksIncludingImplicit columns, rows;
49 };
50
52 {
53 template <typename T>
54 T operator() (T t) const { return t; }
55 };
56
58 {
59 template <typename T>
60 T operator() (T t) const { return std::round (t); }
61 };
62
63 template <typename RoundingFunction>
65 {
66 float getTotalAbsoluteSize (const Array<TrackInfo>& tracks, Px gapSize) noexcept
67 {
68 float totalCellSize = 0.0f;
69
70 for (const auto& trackInfo : tracks)
71 if (! trackInfo.isFractional() || trackInfo.isAuto())
72 totalCellSize += roundingFunction (trackInfo.getSize());
73
74 float totalGap = tracks.size() > 1 ? (float) (tracks.size() - 1) * roundingFunction ((float) gapSize.pixels)
75 : 0.0f;
76
77 return totalCellSize + totalGap;
78 }
79
80 static float getRelativeUnitSize (float size, float totalAbsolute, const Array<TrackInfo>& tracks) noexcept
81 {
82 const float totalRelative = jlimit (0.0f, size, size - totalAbsolute);
83 float factorsSum = 0.0f;
84
85 for (const auto& trackInfo : tracks)
86 if (trackInfo.isFractional())
87 factorsSum += trackInfo.getSize();
88
91 }
92
93 //==============================================================================
94 float getTotalAbsoluteHeight (const Array<TrackInfo>& rowTracks, Px rowGapSize)
95 {
96 return getTotalAbsoluteSize (rowTracks, rowGapSize);
97 }
98
99 float getTotalAbsoluteWidth (const Array<TrackInfo>& columnTracks, Px columnGapSize)
100 {
101 return getTotalAbsoluteSize (columnTracks, columnGapSize);
102 }
103
104 float getRelativeWidthUnit (float gridWidth, Px columnGapSize, const Array<TrackInfo>& columnTracks)
105 {
106 return getRelativeUnitSize (gridWidth, getTotalAbsoluteWidth (columnTracks, columnGapSize), columnTracks);
107 }
108
109 float getRelativeHeightUnit (float gridHeight, Px rowGapSize, const Array<TrackInfo>& rowTracks)
110 {
111 return getRelativeUnitSize (gridHeight, getTotalAbsoluteHeight (rowTracks, rowGapSize), rowTracks);
112 }
113
114 //==============================================================================
115 static bool hasAnyFractions (const Array<TrackInfo>& tracks)
116 {
117 return std::any_of (tracks.begin(),
118 tracks.end(),
119 [] (const auto& t) { return t.isFractional(); });
120 }
121
122 void computeSizes (float gridWidth, float gridHeight,
124 const Tracks& tracks)
125 {
126 if (hasAnyFractions (tracks.columns.items))
127 {
128 relativeWidthUnit = getRelativeWidthUnit (gridWidth, columnGapToUse, tracks.columns.items);
129 fractionallyDividedWidth = gridWidth - getTotalAbsoluteSize (tracks.columns.items, columnGapToUse);
130 }
131 else
132 {
133 remainingWidth = gridWidth - getTotalAbsoluteSize (tracks.columns.items, columnGapToUse);
134 }
135
136 if (hasAnyFractions (tracks.rows.items))
137 {
138 relativeHeightUnit = getRelativeHeightUnit (gridHeight, rowGapToUse, tracks.rows.items);
139 fractionallyDividedHeight = gridHeight - getTotalAbsoluteSize (tracks.rows.items, rowGapToUse);
140 }
141 else
142 {
143 remainingHeight = gridHeight - getTotalAbsoluteSize (tracks.rows.items, rowGapToUse);
144 }
145
146 const auto calculateTrackBounds = [&] (auto& outBounds,
147 const auto& trackItems,
148 auto relativeUnit,
150 auto gap)
151 {
152 const auto lastFractionalIndex = [&]
153 {
154 for (int i = trackItems.size() - 1; 0 <= i; --i)
155 if (trackItems[i].isFractional())
156 return i;
157
158 return -1;
159 }();
160
161 float start = 0.0f;
162 float carriedError = 0.0f;
163
164 for (int i = 0; i < trackItems.size(); ++i)
165 {
166 const auto& currentItem = trackItems[i];
167
168 const auto currentTrackSize = [&]
169 {
170 if (i == lastFractionalIndex)
172
173 const auto absoluteSize = currentItem.getAbsoluteSize (relativeUnit);
174
175 if (! currentItem.isFractional())
176 return roundingFunction (absoluteSize);
177
178 const auto result = roundingFunction (absoluteSize - carriedError);
179 carriedError += result - absoluteSize;
180 return result;
181 }();
182
183 if (currentItem.isFractional())
185
186 const auto end = start + currentTrackSize;
187 outBounds.emplace_back (start, end);
188 start = end + roundingFunction (static_cast<float> (gap.pixels));
189 }
190 };
191
192 calculateTrackBounds (columnTrackBounds,
193 tracks.columns.items,
194 relativeWidthUnit,
195 fractionallyDividedWidth,
197
198 calculateTrackBounds (rowTrackBounds,
199 tracks.rows.items,
200 relativeHeightUnit,
201 fractionallyDividedHeight,
203 }
204
205 float relativeWidthUnit = 0.0f;
206 float relativeHeightUnit = 0.0f;
207 float fractionallyDividedWidth = 0.0f;
208 float fractionallyDividedHeight = 0.0f;
209 float remainingWidth = 0.0f;
210 float remainingHeight = 0.0f;
211
212 std::vector<Range<float>> columnTrackBounds;
213 std::vector<Range<float>> rowTrackBounds;
214 RoundingFunction roundingFunction;
215 };
216
217 //==============================================================================
219 {
220 enum { invalid = -999999 };
221 static constexpr auto emptyAreaCharacter = ".";
222
223 //==============================================================================
224 struct LineRange { int start, end; };
225 struct LineArea { LineRange column, row; };
226 struct LineInfo { StringArray lineNames; };
227
229 {
230 String name;
231 LineArea lines;
232 };
233
234 //==============================================================================
235 static Array<LineInfo> getArrayOfLinesFromTracks (const Array<TrackInfo>& tracks)
236 {
237 // fill line info array
238 Array<LineInfo> lines;
239
240 for (int i = 1; i <= tracks.size(); ++i)
241 {
242 const auto& currentTrack = tracks.getReference (i - 1);
243
244 if (i == 1) // start line
245 {
246 LineInfo li;
247 li.lineNames.add (currentTrack.getStartLineName());
248 lines.add (li);
249 }
250
251 if (i > 1 && i <= tracks.size()) // two lines in between tracks
252 {
253 const auto& prevTrack = tracks.getReference (i - 2);
254
255 LineInfo li;
256 li.lineNames.add (prevTrack.getEndLineName());
257 li.lineNames.add (currentTrack.getStartLineName());
258
259 lines.add (li);
260 }
261
262 if (i == tracks.size()) // end line
263 {
264 LineInfo li;
265 li.lineNames.add (currentTrack.getEndLineName());
266 lines.add (li);
267 }
268 }
269
270 jassert (lines.size() == tracks.size() + 1);
271
272 return lines;
273 }
274
275 //==============================================================================
276 static int deduceAbsoluteLineNumberFromLineName (GridItem::Property prop,
277 const Array<TrackInfo>& tracks)
278 {
279 jassert (prop.hasAbsolute());
280
281 const auto lines = getArrayOfLinesFromTracks (tracks);
282 int count = 0;
283
284 for (const auto [index, line] : enumerate (lines))
285 {
286 for (const auto& name : line.lineNames)
287 {
288 if (prop.getName() == name)
289 {
290 ++count;
291 break;
292 }
293 }
294
295 if (count == prop.getNumber())
296 return (int) index + 1;
297 }
298
300 return count;
301 }
302
303 static int deduceAbsoluteLineNumber (GridItem::Property prop,
304 const Array<TrackInfo>& tracks)
305 {
306 jassert (prop.hasAbsolute());
307
308 if (prop.hasName())
309 return deduceAbsoluteLineNumberFromLineName (prop, tracks);
310
311 if (prop.getNumber() > 0)
312 return prop.getNumber();
313
314 if (prop.getNumber() < 0)
315 return tracks.size() + 2 + prop.getNumber();
316
317 // An integer value of 0 is invalid
319 return 1;
320 }
321
322 static int deduceAbsoluteLineNumberFromNamedSpan (int startLineNumber,
323 GridItem::Property propertyWithSpan,
324 const Array<TrackInfo>& tracks)
325 {
326 jassert (propertyWithSpan.hasSpan());
327
328 const auto lines = getArrayOfLinesFromTracks (tracks);
329 int count = 0;
330
331 const auto enumerated = enumerate (lines);
332
333 for (const auto [index, line] : makeRange (enumerated.begin() + startLineNumber, enumerated.end()))
334 {
335 for (const auto& name : line.lineNames)
336 {
337 if (propertyWithSpan.getName() == name)
338 {
339 ++count;
340 break;
341 }
342 }
343
344 if (count == propertyWithSpan.getNumber())
345 return (int) index + 1;
346 }
347
349 return count;
350 }
351
352 static int deduceAbsoluteLineNumberBasedOnSpan (int startLineNumber,
353 GridItem::Property propertyWithSpan,
354 const Array<TrackInfo>& tracks)
355 {
356 jassert (propertyWithSpan.hasSpan());
357
358 if (propertyWithSpan.hasName())
359 return deduceAbsoluteLineNumberFromNamedSpan (startLineNumber, propertyWithSpan, tracks);
360
361 return startLineNumber + propertyWithSpan.getNumber();
362 }
363
364 //==============================================================================
365 static LineRange deduceLineRange (GridItem::StartAndEndProperty prop, const Array<TrackInfo>& tracks)
366 {
367 jassert (! (prop.start.hasAuto() && prop.end.hasAuto()));
368
369 if (prop.start.hasAbsolute() && prop.end.hasAuto())
370 {
371 prop.end = GridItem::Span (1);
372 }
373 else if (prop.start.hasAuto() && prop.end.hasAbsolute())
374 {
375 prop.start = GridItem::Span (1);
376 }
377
378 auto s = [&]() -> LineRange
379 {
380 if (prop.start.hasAbsolute() && prop.end.hasAbsolute())
381 {
382 return { deduceAbsoluteLineNumber (prop.start, tracks),
383 deduceAbsoluteLineNumber (prop.end, tracks) };
384 }
385
386 if (prop.start.hasAbsolute() && prop.end.hasSpan())
387 {
388 const auto start = deduceAbsoluteLineNumber (prop.start, tracks);
389 return { start, deduceAbsoluteLineNumberBasedOnSpan (start, prop.end, tracks) };
390 }
391
392 if (prop.start.hasSpan() && prop.end.hasAbsolute())
393 {
394 const auto start = deduceAbsoluteLineNumber (prop.end, tracks);
395 return { start, deduceAbsoluteLineNumberBasedOnSpan (start, prop.start, tracks) };
396 }
397
398 // Can't have an item with spans on both start and end.
400 return {};
401 }();
402
403 // swap if start overtakes end
404 if (s.start > s.end)
405 std::swap (s.start, s.end);
406 else if (s.start == s.end)
407 s.end = s.start + 1;
408
409 return s;
410 }
411
412 static LineArea deduceLineArea (const GridItem& item,
413 const Grid& grid,
415 {
416 if (item.area.isNotEmpty() && ! grid.templateAreas.isEmpty())
417 {
418 // Must be a named area!
419 jassert (namedAreas.count (item.area) != 0);
420
421 return namedAreas.at (item.area);
422 }
423
424 return { deduceLineRange (item.column, grid.templateColumns),
425 deduceLineRange (item.row, grid.templateRows) };
426 }
427
428 //==============================================================================
429 static Array<StringArray> parseAreasProperty (const StringArray& areasStrings)
430 {
431 Array<StringArray> strings;
432
433 for (const auto& areaString : areasStrings)
434 strings.add (StringArray::fromTokens (areaString, false));
435
436 if (strings.size() > 0)
437 {
438 for (auto s : strings)
439 {
440 jassert (s.size() == strings[0].size()); // all rows must have the same number of columns
441 }
442 }
443
444 return strings;
445 }
446
447 static NamedArea findArea (Array<StringArray>& stringsArrays)
448 {
449 NamedArea area;
450
451 for (auto& stringArray : stringsArrays)
452 {
453 for (auto& string : stringArray)
454 {
455 // find anchor
456 if (area.name.isEmpty())
457 {
458 if (string != emptyAreaCharacter)
459 {
460 area.name = string;
461 area.lines.row.start = stringsArrays.indexOf (stringArray) + 1; // non-zero indexed;
462 area.lines.column.start = stringArray.indexOf (string) + 1; // non-zero indexed;
463
464 area.lines.row.end = stringsArrays.indexOf (stringArray) + 2;
465 area.lines.column.end = stringArray.indexOf (string) + 2;
466
467 // mark as visited
468 string = emptyAreaCharacter;
469 }
470 }
471 else
472 {
473 if (string == area.name)
474 {
475 area.lines.row.end = stringsArrays.indexOf (stringArray) + 2;
476 area.lines.column.end = stringArray.indexOf (string) + 2;
477
478 // mark as visited
479 string = emptyAreaCharacter;
480 }
481 }
482 }
483 }
484
485 return area;
486 }
487
488 //==============================================================================
489 static std::map<String, LineArea> deduceNamedAreas (const StringArray& areasStrings)
490 {
491 auto stringsArrays = parseAreasProperty (areasStrings);
492
494
495 for (auto area = findArea (stringsArrays); area.name.isNotEmpty(); area = findArea (stringsArrays))
496 {
497 if (areas.count (area.name) == 0)
498 areas[area.name] = area.lines;
499 else
500 // Make sure your template-areas property only has one area with the same name and is well-formed
502 }
503
504 return areas;
505 }
506
507 //==============================================================================
508 template <typename RoundingFunction>
509 static Rectangle<float> getCellBounds (int columnNumber, int rowNumber,
510 const Tracks& tracks,
512 {
513 const auto correctedColumn = columnNumber - 1 + tracks.columns.numImplicitLeading;
514 const auto correctedRow = rowNumber - 1 + tracks.rows .numImplicitLeading;
515
516 jassert (isPositiveAndBelow (correctedColumn, tracks.columns.items.size()));
517 jassert (isPositiveAndBelow (correctedRow, tracks.rows .items.size()));
518
519 return
520 {
521 calculation.columnTrackBounds[(size_t) correctedColumn].getStart(),
522 calculation.rowTrackBounds[(size_t) correctedRow].getStart(),
523 calculation.columnTrackBounds[(size_t) correctedColumn].getEnd() - calculation.columnTrackBounds[(size_t) correctedColumn].getStart(),
524 calculation.rowTrackBounds[(size_t) correctedRow].getEnd() - calculation.rowTrackBounds[(size_t) correctedRow].getStart()
525 };
526 }
527
528 template <typename RoundingFunction>
529 static Rectangle<float> alignCell (Rectangle<float> area,
530 int columnNumber, int rowNumber,
531 int numberOfColumns, int numberOfRows,
535 {
537 area.setY (area.getY() + calculation.remainingHeight);
538
540 area.setX (area.getX() + calculation.remainingWidth);
541
543 area.setY (area.getY() + calculation.remainingHeight / 2);
544
546 area.setX (area.getX() + calculation.remainingWidth / 2);
547
549 {
550 const auto shift = ((float) (rowNumber - 1) * (calculation.remainingHeight / float (numberOfRows - 1)));
551 area.setY (area.getY() + shift);
552 }
553
555 {
556 const auto shift = ((float) (columnNumber - 1) * (calculation.remainingWidth / float (numberOfColumns - 1)));
557 area.setX (area.getX() + shift);
558 }
559
561 {
562 const auto shift = ((float) rowNumber * (calculation.remainingHeight / float (numberOfRows + 1)));
563 area.setY (area.getY() + shift);
564 }
565
567 {
568 const auto shift = ((float) columnNumber * (calculation.remainingWidth / float (numberOfColumns + 1)));
569 area.setX (area.getX() + shift);
570 }
571
573 {
574 const auto inbetweenShift = calculation.remainingHeight / float (numberOfRows);
575 const auto sidesShift = inbetweenShift / 2;
576 auto shift = (float) (rowNumber - 1) * inbetweenShift + sidesShift;
577
578 area.setY (area.getY() + shift);
579 }
580
582 {
583 const auto inbetweenShift = calculation.remainingWidth / float (numberOfColumns);
584 const auto sidesShift = inbetweenShift / 2;
585 auto shift = (float) (columnNumber - 1) * inbetweenShift + sidesShift;
586
587 area.setX (area.getX() + shift);
588 }
589
590 return area;
591 }
592
593 template <typename RoundingFunction>
594 static Rectangle<float> getAreaBounds (PlacementHelpers::LineRange columnRange,
595 PlacementHelpers::LineRange rowRange,
596 const Tracks& tracks,
600 {
601 const auto findAlignedCell = [&] (int column, int row)
602 {
603 const auto cell = getCellBounds (column, row, tracks, calculation);
604 return alignCell (cell,
605 column,
606 row,
607 tracks.columns.items.size(),
608 tracks.rows.items.size(),
612 };
613
614 const auto startCell = findAlignedCell (columnRange.start, rowRange.start);
615 const auto endCell = findAlignedCell (columnRange.end - 1, rowRange.end - 1);
616
617 const auto horizontalRange = startCell.getHorizontalRange().getUnionWith (endCell.getHorizontalRange());
618 const auto verticalRange = startCell.getVerticalRange() .getUnionWith (endCell.getVerticalRange());
619 return { horizontalRange.getStart(), verticalRange.getStart(),
620 horizontalRange.getLength(), verticalRange.getLength() };
621 }
622 };
623
624 //==============================================================================
626 {
628
629 //==============================================================================
631 {
632 struct Cell { int column, row; };
633
635 : highestCrossDimension (isColumnFirst ? highestRowToUse : highestColumnToUse),
636 columnFirst (isColumnFirst)
637 {}
638
639 PlacementHelpers::LineArea setCell (Cell cell, int columnSpan, int rowSpan)
640 {
641 for (int i = 0; i < columnSpan; i++)
642 for (int j = 0; j < rowSpan; j++)
643 setCell (cell.column + i, cell.row + j);
644
645 return { { cell.column, cell.column + columnSpan }, { cell.row, cell.row + rowSpan } };
646 }
647
648 PlacementHelpers::LineArea setCell (Cell start, Cell end)
649 {
650 return setCell (start, std::abs (end.column - start.column),
651 std::abs (end.row - start.row));
652 }
653
654 Cell nextAvailable (Cell referenceCell, int columnSpan, int rowSpan)
655 {
656 while (isOccupied (referenceCell, columnSpan, rowSpan) || isOutOfBounds (referenceCell, columnSpan, rowSpan))
657 referenceCell = advance (referenceCell);
658
659 return referenceCell;
660 }
661
662 Cell nextAvailableOnRow (Cell referenceCell, int columnSpan, int rowSpan, int rowNumber)
663 {
664 if (columnFirst && (rowNumber + rowSpan) > highestCrossDimension)
665 highestCrossDimension = rowNumber + rowSpan;
666
667 while (isOccupied (referenceCell, columnSpan, rowSpan)
668 || (referenceCell.row != rowNumber))
669 referenceCell = advance (referenceCell);
670
671 return referenceCell;
672 }
673
674 Cell nextAvailableOnColumn (Cell referenceCell, int columnSpan, int rowSpan, int columnNumber)
675 {
676 if (! columnFirst && (columnNumber + columnSpan) > highestCrossDimension)
677 highestCrossDimension = columnNumber + columnSpan;
678
679 while (isOccupied (referenceCell, columnSpan, rowSpan)
680 || (referenceCell.column != columnNumber))
681 referenceCell = advance (referenceCell);
682
683 return referenceCell;
684 }
685
686 void updateMaxCrossDimensionFromAutoPlacementItem (int columnSpan, int rowSpan)
687 {
688 highestCrossDimension = jmax (highestCrossDimension, 1 + getCrossDimension ({ columnSpan, rowSpan }));
689 }
690
691 private:
692 struct SortableCell
693 {
694 int column, row;
695 bool columnFirst;
696
697 bool operator< (const SortableCell& other) const
698 {
699 if (columnFirst)
700 {
701 if (row == other.row)
702 return column < other.column;
703
704 return row < other.row;
705 }
706
707 if (row == other.row)
708 return column < other.column;
709
710 return row < other.row;
711 }
712 };
713
714 void setCell (int column, int row)
715 {
716 occupiedCells.insert ({ column, row, columnFirst });
717 }
718
719 bool isOccupied (Cell cell) const
720 {
721 return occupiedCells.count ({ cell.column, cell.row, columnFirst }) > 0;
722 }
723
724 bool isOccupied (Cell cell, int columnSpan, int rowSpan) const
725 {
726 for (int i = 0; i < columnSpan; i++)
727 for (int j = 0; j < rowSpan; j++)
728 if (isOccupied ({ cell.column + i, cell.row + j }))
729 return true;
730
731 return false;
732 }
733
734 bool isOutOfBounds (Cell cell, int columnSpan, int rowSpan) const
735 {
736 const auto highestIndexOfCell = getCrossDimension (cell) + getCrossDimension ({ columnSpan, rowSpan });
737 const auto highestIndexOfGrid = getHighestCrossDimension();
738
740 }
741
742 int getHighestCrossDimension() const
743 {
744 Cell cell { 1, 1 };
745
746 if (occupiedCells.size() > 0)
747 cell = { occupiedCells.crbegin()->column, occupiedCells.crbegin()->row };
748
749 return std::max (getCrossDimension (cell), highestCrossDimension);
750 }
751
752 Cell advance (Cell cell) const
753 {
754 if ((getCrossDimension (cell) + 1) >= getHighestCrossDimension())
755 return fromDimensions (getMainDimension (cell) + 1, 1);
756
757 return fromDimensions (getMainDimension (cell), getCrossDimension (cell) + 1);
758 }
759
760 int getMainDimension (Cell cell) const { return columnFirst ? cell.column : cell.row; }
761 int getCrossDimension (Cell cell) const { return columnFirst ? cell.row : cell.column; }
762
763 Cell fromDimensions (int mainDimension, int crossDimension) const
764 {
765 if (columnFirst)
766 return { mainDimension, crossDimension };
767
768 return { crossDimension, mainDimension };
769 }
770
771 int highestCrossDimension;
772 bool columnFirst;
773 std::set<SortableCell> occupiedCells;
774 };
775
776 //==============================================================================
777 static bool isFixed (GridItem::StartAndEndProperty prop)
778 {
779 return prop.start.hasName() || prop.start.hasAbsolute() || prop.end.hasName() || prop.end.hasAbsolute();
780 }
781
782 static bool hasFullyFixedPlacement (const GridItem& item)
783 {
784 if (item.area.isNotEmpty())
785 return true;
786
787 if (isFixed (item.column) && isFixed (item.row))
788 return true;
789
790 return false;
791 }
792
793 static bool hasPartialFixedPlacement (const GridItem& item)
794 {
795 if (item.area.isNotEmpty())
796 return false;
797
798 if (isFixed (item.column) ^ isFixed (item.row))
799 return true;
800
801 return false;
802 }
803
804 static bool hasAutoPlacement (const GridItem& item)
805 {
806 return ! hasFullyFixedPlacement (item) && ! hasPartialFixedPlacement (item);
807 }
808
809 //==============================================================================
810 static bool hasDenseAutoFlow (AutoFlow autoFlow)
811 {
814 }
815
816 static bool isColumnAutoFlow (AutoFlow autoFlow)
817 {
818 return autoFlow == AutoFlow::column
820 }
821
822 //==============================================================================
823 static int getSpanFromAuto (GridItem::StartAndEndProperty prop)
824 {
825 if (prop.end.hasSpan())
826 return prop.end.getNumber();
827
828 if (prop.start.hasSpan())
829 return prop.start.getNumber();
830
831 return 1;
832 }
833
834 //==============================================================================
835 ItemPlacementArray deduceAllItems (Grid& grid) const
836 {
837 const auto namedAreas = PlacementHelpers::deduceNamedAreas (grid.templateAreas);
838
839 OccupancyPlane plane (jmax (grid.templateColumns.size() + 1, 2),
840 jmax (grid.templateRows.size() + 1, 2),
841 isColumnAutoFlow (grid.autoFlow));
842
843 ItemPlacementArray itemPlacementArray;
845
846 for (auto& item : grid.items)
847 sortedItems.add (&item);
848
850 [] (const GridItem* i1, const GridItem* i2) { return i1->order < i2->order; });
851
852 // place fixed items first
853 for (auto* item : sortedItems)
854 {
855 if (hasFullyFixedPlacement (*item))
856 {
857 const auto a = PlacementHelpers::deduceLineArea (*item, grid, namedAreas);
858 plane.setCell ({ a.column.start, a.row.start }, { a.column.end, a.row.end });
859 itemPlacementArray.add ({ item, a });
860 }
861 }
862
863 OccupancyPlane::Cell lastInsertionCell = { 1, 1 };
864
865 for (auto* item : sortedItems)
866 {
867 if (hasPartialFixedPlacement (*item))
868 {
869 if (isFixed (item->column))
870 {
871 const auto p = PlacementHelpers::deduceLineRange (item->column, grid.templateColumns);
872 const auto columnSpan = std::abs (p.start - p.end);
873 const auto rowSpan = getSpanFromAuto (item->row);
874
875 const auto insertionCell = hasDenseAutoFlow (grid.autoFlow) ? OccupancyPlane::Cell { p.start, 1 }
877 const auto nextAvailableCell = plane.nextAvailableOnColumn (insertionCell, columnSpan, rowSpan, p.start);
878 const auto lineArea = plane.setCell (nextAvailableCell, columnSpan, rowSpan);
880
881 itemPlacementArray.add ({ item, lineArea });
882 }
883 else if (isFixed (item->row))
884 {
885 const auto p = PlacementHelpers::deduceLineRange (item->row, grid.templateRows);
886 const auto columnSpan = getSpanFromAuto (item->column);
887 const auto rowSpan = std::abs (p.start - p.end);
888
889 const auto insertionCell = hasDenseAutoFlow (grid.autoFlow) ? OccupancyPlane::Cell { 1, p.start }
891
892 const auto nextAvailableCell = plane.nextAvailableOnRow (insertionCell, columnSpan, rowSpan, p.start);
893 const auto lineArea = plane.setCell (nextAvailableCell, columnSpan, rowSpan);
894
896
897 itemPlacementArray.add ({ item, lineArea });
898 }
899 }
900 }
901
902 // https://www.w3.org/TR/css-grid-1/#auto-placement-algo step 3.3
903 for (auto* item : sortedItems)
904 if (hasAutoPlacement (*item))
905 plane.updateMaxCrossDimensionFromAutoPlacementItem (getSpanFromAuto (item->column), getSpanFromAuto (item->row));
906
907 lastInsertionCell = { 1, 1 };
908
909 for (auto* item : sortedItems)
910 {
911 if (hasAutoPlacement (*item))
912 {
913 const auto columnSpan = getSpanFromAuto (item->column);
914 const auto rowSpan = getSpanFromAuto (item->row);
915
916 const auto nextAvailableCell = plane.nextAvailable (lastInsertionCell, columnSpan, rowSpan);
917 const auto lineArea = plane.setCell (nextAvailableCell, columnSpan, rowSpan);
918
919 if (! hasDenseAutoFlow (grid.autoFlow))
921
922 itemPlacementArray.add ({ item, lineArea });
923 }
924 }
925
926 return itemPlacementArray;
927 }
928
929 //==============================================================================
930 template <typename Accessor>
931 static PlacementHelpers::LineRange findFullLineRange (const ItemPlacementArray& items, Accessor&& accessor)
932 {
933 if (items.isEmpty())
934 return { 1, 1 };
935
936 const auto combine = [&accessor] (const auto& acc, const auto& item)
937 {
938 const auto newRange = accessor (item);
939 return PlacementHelpers::LineRange { std::min (acc.start, newRange.start),
940 std::max (acc.end, newRange.end) };
941 };
942
943 return std::accumulate (std::next (items.begin()), items.end(), accessor (*items.begin()), combine);
944 }
945
946 static PlacementHelpers::LineArea findFullLineArea (const ItemPlacementArray& items)
947 {
948 return { findFullLineRange (items, [] (const auto& item) { return item.second.column; }),
949 findFullLineRange (items, [] (const auto& item) { return item.second.row; }) };
950 }
951
952 template <typename Item>
953 static Array<Item> repeated (int repeats, const Item& item)
954 {
955 Array<Item> result;
956 result.insertMultiple (-1, item, repeats);
957 return result;
958 }
959
960 static Tracks createImplicitTracks (const Grid& grid, const ItemPlacementArray& items)
961 {
962 const auto fullArea = findFullLineArea (items);
963
964 const auto leadingColumns = std::max (0, 1 - fullArea.column.start);
965 const auto leadingRows = std::max (0, 1 - fullArea.row.start);
966
967 const auto trailingColumns = std::max (0, fullArea.column.end - grid.templateColumns.size() - 1);
968 const auto trailingRows = std::max (0, fullArea.row .end - grid.templateRows .size() - 1);
969
970 return { { repeated (leadingColumns, grid.autoColumns) + grid.templateColumns + repeated (trailingColumns, grid.autoColumns),
972 { repeated (leadingRows, grid.autoRows) + grid.templateRows + repeated (trailingRows, grid.autoRows),
973 leadingRows } };
974 }
975
976 //==============================================================================
977 static void applySizeForAutoTracks (Tracks& tracks, const ItemPlacementArray& placements)
978 {
979 const auto setSizes = [&placements] (auto& tracksInDirection, const auto& getItem, const auto& getItemSize)
980 {
981 auto& array = tracksInDirection.items;
982
983 for (int index = 0; index < array.size(); ++index)
984 {
985 if (array.getReference (index).isAuto())
986 {
987 const auto combiner = [&] (const auto acc, const auto& element)
988 {
989 const auto item = getItem (element.second);
990 const auto isNotSpan = std::abs (item.end - item.start) <= 1;
991 return isNotSpan && item.start == index + 1 - tracksInDirection.numImplicitLeading
992 ? std::max (acc, getItemSize (*element.first))
993 : acc;
994 };
995
996 array.getReference (index).size = std::accumulate (placements.begin(), placements.end(), 0.0f, combiner);
997 }
998 }
999 };
1000
1001 setSizes (tracks.rows,
1002 [] (const auto& i) { return i.row; },
1003 [] (const auto& i) { return i.height + i.margin.top + i.margin.bottom; });
1004
1005 setSizes (tracks.columns,
1006 [] (const auto& i) { return i.column; },
1007 [] (const auto& i) { return i.width + i.margin.left + i.margin.right; });
1008 }
1009 };
1010
1011 //==============================================================================
1013 {
1014 static Rectangle<float> alignItem (const GridItem& item, const Grid& grid, Rectangle<float> area)
1015 {
1016 // if item align is auto, inherit value from grid
1018 ? grid.alignItems
1019 : static_cast<AlignItems> (item.alignSelf);
1020
1022 ? grid.justifyItems
1023 : static_cast<JustifyItems> (item.justifySelf);
1024
1025 // subtract margin from area
1026 area = BorderSize<float> (item.margin.top, item.margin.left, item.margin.bottom, item.margin.right)
1027 .subtractedFrom (area);
1028
1029 // align and justify
1030 auto r = area;
1031
1032 if (! approximatelyEqual (item.width, (float) GridItem::notAssigned)) r.setWidth (item.width);
1033 if (! approximatelyEqual (item.height, (float) GridItem::notAssigned)) r.setHeight (item.height);
1034 if (! approximatelyEqual (item.maxWidth, (float) GridItem::notAssigned)) r.setWidth (jmin (item.maxWidth, r.getWidth()));
1035 if (item.minWidth > 0.0f) r.setWidth (jmax (item.minWidth, r.getWidth()));
1036 if (! approximatelyEqual (item.maxHeight, (float) GridItem::notAssigned)) r.setHeight (jmin (item.maxHeight, r.getHeight()));
1037 if (item.minHeight > 0.0f) r.setHeight (jmax (item.minHeight, r.getHeight()));
1038
1040 return r;
1041
1042 if (alignType == AlignItems::end) r.setY (r.getY() + (area.getHeight() - r.getHeight()));
1043 if (justifyType == JustifyItems::end) r.setX (r.getX() + (area.getWidth() - r.getWidth()));
1044 if (alignType == AlignItems::center) r.setCentre (r.getCentreX(), area.getCentreY());
1045 if (justifyType == JustifyItems::center) r.setCentre (area.getCentreX(), r.getCentreY());
1046
1047 return r;
1048 }
1049 };
1050
1051};
1052
1053//==============================================================================
1054Grid::TrackInfo::TrackInfo() noexcept : hasKeyword (true) {}
1055
1057 : size (static_cast<float> (sizeInPixels.pixels)), isFraction (false) {}
1058
1060 : size ((float)fractionOfFreeSpace.fraction), isFraction (true) {}
1061
1062Grid::TrackInfo::TrackInfo (Px sizeInPixels, const String& endLineNameToUse) noexcept
1063 : TrackInfo (sizeInPixels)
1064{
1065 endLineName = endLineNameToUse;
1066}
1067
1069 : TrackInfo (fractionOfFreeSpace)
1070{
1071 endLineName = endLineNameToUse;
1072}
1073
1075 : TrackInfo (sizeInPixels)
1076{
1077 startLineName = startLineNameToUse;
1078}
1079
1081 : TrackInfo (fractionOfFreeSpace)
1082{
1083 startLineName = startLineNameToUse;
1084}
1085
1086Grid::TrackInfo::TrackInfo (const String& startLineNameToUse, Px sizeInPixels, const String& endLineNameToUse) noexcept
1087 : TrackInfo (startLineNameToUse, sizeInPixels)
1088{
1089 endLineName = endLineNameToUse;
1090}
1091
1094{
1095 endLineName = endLineNameToUse;
1096}
1097
1098float Grid::TrackInfo::getAbsoluteSize (float relativeFractionalUnit) const
1099{
1100 return isFractional() ? size * relativeFractionalUnit : size;
1101}
1102
1103//==============================================================================
1105{
1106 const auto itemsAndAreas = Helpers::AutoPlacement().deduceAllItems (*this);
1107
1108 auto implicitTracks = Helpers::AutoPlacement::createImplicitTracks (*this, itemsAndAreas);
1109
1110 Helpers::AutoPlacement::applySizeForAutoTracks (implicitTracks, itemsAndAreas);
1111
1114
1115 const auto doComputeSizes = [&] (auto& sizeCalculation)
1116 {
1117 sizeCalculation.computeSizes (targetArea.toFloat().getWidth(),
1118 targetArea.toFloat().getHeight(),
1119 columnGap,
1120 rowGap,
1122 };
1123
1126
1127 for (auto& itemAndArea : itemsAndAreas)
1128 {
1129 auto* item = itemAndArea.first;
1130
1131 const auto getBounds = [&] (const auto& sizeCalculation)
1132 {
1133 const auto a = itemAndArea.second;
1134
1135 const auto areaBounds = Helpers::PlacementHelpers::getAreaBounds (a.column,
1136 a.row,
1141
1142 const auto rounded = [&] (auto rect) -> decltype (rect)
1143 {
1144 return { sizeCalculation.roundingFunction (rect.getX()),
1145 sizeCalculation.roundingFunction (rect.getY()),
1146 sizeCalculation.roundingFunction (rect.getWidth()),
1147 sizeCalculation.roundingFunction (rect.getHeight()) };
1148 };
1149
1150 return rounded (Helpers::BoxAlignment::alignItem (*item, *this, areaBounds));
1151 };
1152
1153 item->currentBounds = getBounds (calculation) + targetArea.toFloat().getPosition();
1154
1155 if (auto* c = item->associatedComponent)
1156 c->setBounds (getBounds (roundedCalculation).toNearestIntEdges() + targetArea.getPosition());
1157 }
1158}
1159
1160//==============================================================================
1161#if JUCE_UNIT_TESTS
1162
1163struct GridTests final : public UnitTest
1164{
1165 GridTests()
1166 : UnitTest ("Grid", UnitTestCategories::gui)
1167 {}
1168
1169 void runTest() override
1170 {
1171 using Fr = Grid::Fr;
1172 using Tr = Grid::TrackInfo;
1173 using Rect = Rectangle<float>;
1174
1175 beginTest ("Layout calculation of an empty grid is a no-op");
1176 {
1177 const Rectangle<int> bounds { 100, 200 };
1178 Grid grid;
1179 grid.performLayout (bounds);
1180 }
1181
1182 {
1183 Grid grid;
1184
1185 grid.templateColumns.add (Tr (1_fr));
1186 grid.templateRows.addArray ({ Tr (20_px), Tr (1_fr) });
1187
1188 grid.items.addArray ({ GridItem().withArea (1, 1),
1189 GridItem().withArea (2, 1) });
1190
1191 grid.performLayout (Rectangle<int> (200, 400));
1192
1193 beginTest ("Layout calculation test: 1 column x 2 rows: no gap");
1194 expect (grid.items[0].currentBounds == Rect (0.0f, 0.0f, 200.f, 20.0f));
1195 expect (grid.items[1].currentBounds == Rect (0.0f, 20.0f, 200.f, 380.0f));
1196
1197 grid.templateColumns.add (Tr (50_px));
1198 grid.templateRows.add (Tr (2_fr));
1199
1200 grid.items.addArray ( { GridItem().withArea (1, 2),
1201 GridItem().withArea (2, 2),
1202 GridItem().withArea (3, 1),
1203 GridItem().withArea (3, 2) });
1204
1205 grid.performLayout (Rectangle<int> (150, 170));
1206
1207 beginTest ("Layout calculation test: 2 columns x 3 rows: no gap");
1208 expect (grid.items[0].currentBounds == Rect (0.0f, 0.0f, 100.0f, 20.0f));
1209 expect (grid.items[1].currentBounds == Rect (0.0f, 20.0f, 100.0f, 50.0f));
1210 expect (grid.items[2].currentBounds == Rect (100.0f, 0.0f, 50.0f, 20.0f));
1211 expect (grid.items[3].currentBounds == Rect (100.0f, 20.0f, 50.0f, 50.0f));
1212 expect (grid.items[4].currentBounds == Rect (0.0f, 70.0f, 100.0f, 100.0f));
1213 expect (grid.items[5].currentBounds == Rect (100.0f, 70.0f, 50.0f, 100.0f));
1214
1215 grid.columnGap = 20_px;
1216 grid.rowGap = 10_px;
1217
1218 grid.performLayout (Rectangle<int> (200, 310));
1219
1220 beginTest ("Layout calculation test: 2 columns x 3 rows: rowGap of 10 and columnGap of 20");
1221 expect (grid.items[0].currentBounds == Rect (0.0f, 0.0f, 130.0f, 20.0f));
1222 expect (grid.items[1].currentBounds == Rect (0.0f, 30.0f, 130.0f, 90.0f));
1223 expect (grid.items[2].currentBounds == Rect (150.0f, 0.0f, 50.0f, 20.0f));
1224 expect (grid.items[3].currentBounds == Rect (150.0f, 30.0f, 50.0f, 90.0f));
1225 expect (grid.items[4].currentBounds == Rect (0.0f, 130.0f, 130.0f, 180.0f));
1226 expect (grid.items[5].currentBounds == Rect (150.0f, 130.0f, 50.0f, 180.0f));
1227 }
1228
1229 {
1230 Grid grid;
1231
1232 grid.templateColumns.addArray ({ Tr ("first", 20_px, "in"), Tr ("in", 1_fr, "in"), Tr (20_px, "last") });
1233 grid.templateRows.addArray ({ Tr (1_fr),
1234 Tr (20_px)});
1235
1236 {
1237 beginTest ("Grid items placement tests: integer and custom ident, counting forward");
1238
1239 GridItem i1, i2, i3, i4, i5;
1240 i1.column = { 1, 4 };
1241 i1.row = { 1, 2 };
1242
1243 i2.column = { 1, 3 };
1244 i2.row = { 1, 3 };
1245
1246 i3.column = { "first", "in" };
1247 i3.row = { 2, 3 };
1248
1249 i4.column = { "first", { 2, "in" } };
1250 i4.row = { 1, 2 };
1251
1252 i5.column = { "first", "last" };
1253 i5.row = { 1, 2 };
1254
1255 grid.items.addArray ({ i1, i2, i3, i4, i5 });
1256
1257 grid.performLayout ({ 140, 100 });
1258
1259 expect (grid.items[0].currentBounds == Rect (0.0f, 0.0f, 140.0f, 80.0f));
1260 expect (grid.items[1].currentBounds == Rect (0.0f, 0.0f, 120.0f, 100.0f));
1261 expect (grid.items[2].currentBounds == Rect (0.0f, 80.0f, 20.0f, 20.0f));
1262 expect (grid.items[3].currentBounds == Rect (0.0f, 0.0f, 120.0f, 80.0f));
1263 expect (grid.items[4].currentBounds == Rect (0.0f, 0.0f, 140.0f, 80.0f));
1264 }
1265 }
1266
1267 {
1268 Grid grid;
1269
1270 grid.templateColumns.addArray ({ Tr ("first", 20_px, "in"), Tr ("in", 1_fr, "in"), Tr (20_px, "last") });
1271 grid.templateRows.addArray ({ Tr (1_fr),
1272 Tr (20_px)});
1273
1274 beginTest ("Grid items placement tests: integer and custom ident, counting forward, reversed end and start");
1275
1276 GridItem i1, i2, i3, i4, i5;
1277 i1.column = { 4, 1 };
1278 i1.row = { 2, 1 };
1279
1280 i2.column = { 3, 1 };
1281 i2.row = { 3, 1 };
1282
1283 i3.column = { "in", "first" };
1284 i3.row = { 3, 2 };
1285
1286 i4.column = { "first", { 2, "in" } };
1287 i4.row = { 1, 2 };
1288
1289 i5.column = { "last", "first" };
1290 i5.row = { 1, 2 };
1291
1292 grid.items.addArray ({ i1, i2, i3, i4, i5 });
1293
1294 grid.performLayout ({ 140, 100 });
1295
1296 expect (grid.items[0].currentBounds == Rect (0.0f, 0.0f, 140.0f, 80.0f));
1297 expect (grid.items[1].currentBounds == Rect (0.0f, 0.0f, 120.0f, 100.0f));
1298 expect (grid.items[2].currentBounds == Rect (0.0f, 80.0f, 20.0f, 20.0f));
1299 expect (grid.items[3].currentBounds == Rect (0.0f, 0.0f, 120.0f, 80.0f));
1300 expect (grid.items[4].currentBounds == Rect (0.0f, 0.0f, 140.0f, 80.0f));
1301 }
1302
1303 {
1304 Grid grid;
1305
1306 grid.templateColumns = { Tr ("first", 20_px, "in"), Tr ("in", 1_fr, "in"), Tr (20_px, "last") };
1307 grid.templateRows = { Tr (1_fr), Tr (20_px) };
1308
1309 beginTest ("Grid items placement tests: integer, counting backward");
1310
1311 grid.items = { GridItem{}.withColumn ({ -2, -1 }).withRow ({ 1, 3 }),
1312 GridItem{}.withColumn ({ -10, -1 }).withRow ({ 1, -1 }) };
1313
1314 grid.performLayout ({ 140, 100 });
1315
1316 expect (grid.items[0].currentBounds == Rect (120.0f, 0.0f, 20.0f, 100.0f));
1317 expect (grid.items[1].currentBounds == Rect (0.0f, 0.0f, 140.0f, 100.0f));
1318 }
1319
1320 {
1321 beginTest ("Grid items placement tests: areas");
1322
1323 Grid grid;
1324
1325 grid.templateColumns = { Tr (50_px), Tr (100_px), Tr (Fr (1_fr)), Tr (50_px) };
1326 grid.templateRows = { Tr (50_px),
1327 Tr (1_fr),
1328 Tr (50_px) };
1329
1330 grid.templateAreas = { "header header header header",
1331 "main main . sidebar",
1332 "footer footer footer footer" };
1333
1334 grid.items.addArray ({ GridItem().withArea ("header"),
1335 GridItem().withArea ("main"),
1336 GridItem().withArea ("sidebar"),
1337 GridItem().withArea ("footer"),
1338 });
1339
1340 grid.performLayout ({ 300, 150 });
1341
1342 expect (grid.items[0].currentBounds == Rect (0.f, 0.f, 300.f, 50.f));
1343 expect (grid.items[1].currentBounds == Rect (0.f, 50.f, 150.f, 50.f));
1344 expect (grid.items[2].currentBounds == Rect (250.f, 50.f, 50.f, 50.f));
1345 expect (grid.items[3].currentBounds == Rect (0.f, 100.f, 300.f, 50.f));
1346 }
1347
1348 {
1349 beginTest ("Grid implicit rows and columns: triggered by areas");
1350
1351 Grid grid;
1352
1353 grid.templateColumns = { Tr (50_px), Tr (100_px), Tr (1_fr), Tr (50_px) };
1354 grid.templateRows = { Tr (50_px),
1355 Tr (1_fr),
1356 Tr (50_px) };
1357
1358 grid.autoRows = Tr (30_px);
1359 grid.autoColumns = Tr (30_px);
1360
1361 grid.templateAreas = { "header header header header header",
1362 "main main . sidebar sidebar",
1363 "footer footer footer footer footer",
1364 "sub sub sub sub sub"};
1365
1366 grid.items.addArray ({ GridItem().withArea ("header"),
1367 GridItem().withArea ("main"),
1368 GridItem().withArea ("sidebar"),
1369 GridItem().withArea ("footer"),
1370 GridItem().withArea ("sub"),
1371 });
1372
1373 grid.performLayout ({ 330, 180 });
1374
1375 expect (grid.items[0].currentBounds == Rect (0.f, 0.f, 330.f, 50.f));
1376 expect (grid.items[1].currentBounds == Rect (0.f, 50.f, 150.f, 50.f));
1377 expect (grid.items[2].currentBounds == Rect (250.f, 50.f, 80.f, 50.f));
1378 expect (grid.items[3].currentBounds == Rect (0.f, 100.f, 330.f, 50.f));
1379 expect (grid.items[4].currentBounds == Rect (0.f, 150.f, 330.f, 30.f));
1380 }
1381
1382 {
1383 beginTest ("Grid implicit rows and columns: triggered by areas");
1384
1385 Grid grid;
1386
1387 grid.templateColumns = { Tr (50_px), Tr (100_px), Tr (1_fr), Tr (50_px) };
1388 grid.templateRows = { Tr (50_px),
1389 Tr (1_fr),
1390 Tr (50_px) };
1391
1392 grid.autoRows = Tr (1_fr);
1393 grid.autoColumns = Tr (1_fr);
1394
1395 grid.templateAreas = { "header header header header",
1396 "main main . sidebar",
1397 "footer footer footer footer" };
1398
1399 grid.items.addArray ({ GridItem().withArea ("header"),
1400 GridItem().withArea ("main"),
1401 GridItem().withArea ("sidebar"),
1402 GridItem().withArea ("footer"),
1403 GridItem().withArea (4, 5, 6, 7)
1404 });
1405
1406 grid.performLayout ({ 350, 250 });
1407
1408 expect (grid.items[0].currentBounds == Rect (0.f, 0.f, 250.f, 50.f));
1409 expect (grid.items[1].currentBounds == Rect (0.f, 50.f, 150.f, 50.f));
1410 expect (grid.items[2].currentBounds == Rect (200.f, 50.f, 50.f, 50.f));
1411 expect (grid.items[3].currentBounds == Rect (0.f, 100.f, 250.f, 50.f));
1412 expect (grid.items[4].currentBounds == Rect (250.f, 150.f, 100.f, 100.f));
1413 }
1414
1415 {
1416 beginTest ("Grid implicit rows and columns: triggered by out-of-bounds indices");
1417
1418 Grid grid;
1419
1420 grid.templateColumns = { Tr (1_fr), Tr (1_fr) };
1421 grid.templateRows = { Tr (60_px), Tr (60_px) };
1422
1423 grid.autoColumns = Tr (20_px);
1424 grid.autoRows = Tr (1_fr);
1425
1426 grid.items = { GridItem{}.withColumn ({ 5, 8 }).withRow ({ -5, -4 }),
1427 GridItem{}.withColumn ({ 4, 7 }).withRow ({ -4, -3 }),
1428 GridItem{}.withColumn ({ -2, -1 }).withRow ({ 4, 5 }) };
1429
1430 grid.performLayout ({ 500, 400 });
1431
1432 // -3 -2 -1
1433 // 1 2 3 4 5 6 7 8
1434 // -5 +---+---+---+---+---+---+---+ 0
1435 // | | | | | 0 | 0 | 0 |
1436 // -4 +---+---+---+---+---+---+---+ 70
1437 // | | | | 1 | 1 | 1 | |
1438 // -3 1 +---+---+---+---+---+---+---+ 140
1439 // | x | x | | | | | |
1440 // -2 2 +---+---+---+---+---+---+---+ 200 y positions
1441 // | x | x | | | | | |
1442 // -1 3 +---+---+---+---+---+---+---+ 260
1443 // | | | | | | | |
1444 // 4 +---+---+---+---+---+---+---+ 330
1445 // | | 2 | | | | | |
1446 // 5 +---+---+---+---+---+---+---+ 400
1447 //
1448 // 0 200 400 420 440 460 480 500
1449 // x positions
1450 //
1451 // The cells marked "x" are the explicit cells specified by the template rows
1452 // and columns.
1453 //
1454 // The cells marked 0/1/2 correspond to the GridItems at those indices in the
1455 // items array.
1456 //
1457 // Note that negative indices count back from the last explicit line
1458 // number in that direction, so "2" and "-2" both correspond to the same line.
1459
1460 expect (grid.items[0].currentBounds == Rect (440.0f, 0.0f, 60.0f, 70.0f));
1461 expect (grid.items[1].currentBounds == Rect (420.0f, 70.0f, 60.0f, 70.0f));
1462 expect (grid.items[2].currentBounds == Rect (200.0f, 330.0f, 200.0f, 70.0f));
1463 }
1464
1465 {
1466 beginTest ("Items with specified sizes should translate to correctly rounded Component dimensions");
1467
1468 static constexpr int targetSize = 100;
1469
1470 juce::Component component;
1471 juce::GridItem item { component };
1472 item.alignSelf = juce::GridItem::AlignSelf::center;
1473 item.justifySelf = juce::GridItem::JustifySelf::center;
1474 item.width = (float) targetSize;
1475 item.height = (float) targetSize;
1476
1477 juce::Grid grid;
1478 grid.templateColumns = { juce::Grid::Fr { 1 } };
1479 grid.templateRows = { juce::Grid::Fr { 1 } };
1480 grid.items = { item };
1481
1482 for (int totalSize = 100 - 20; totalSize < 100 + 20; ++totalSize)
1483 {
1484 Rectangle<int> bounds { 0, 0, totalSize, totalSize };
1485 grid.performLayout (bounds);
1486
1487 expectEquals (component.getWidth(), targetSize);
1488 expectEquals (component.getHeight(), targetSize);
1489 }
1490 }
1491
1492 {
1493 beginTest ("Track sizes specified in Px should translate to correctly rounded Component dimensions");
1494
1495 static constexpr int targetSize = 100;
1496
1497 juce::Component component;
1498 juce::GridItem item { component };
1499 item.alignSelf = juce::GridItem::AlignSelf::center;
1500 item.justifySelf = juce::GridItem::JustifySelf::center;
1501 item.setArea (1, 3);
1502
1503 juce::Grid grid;
1504 grid.templateColumns = { juce::Grid::Fr { 1 },
1505 juce::Grid::Fr { 1 },
1506 juce::Grid::Px { targetSize },
1507 juce::Grid::Fr { 1 } };
1508 grid.templateRows = { juce::Grid::Fr { 1 } };
1509 grid.items = { item };
1510
1511 for (int totalSize = 100 - 20; totalSize < 100 + 20; ++totalSize)
1512 {
1513 Rectangle<int> bounds { 0, 0, totalSize, totalSize };
1514 grid.performLayout (bounds);
1515
1516 expectEquals (component.getWidth(), targetSize);
1517 }
1518 }
1519
1520 {
1521 beginTest ("Evaluate invariants on randomised Grid layouts");
1522
1523 struct Solution
1524 {
1525 Grid grid;
1526 std::deque<Component> components;
1527 int absoluteWidth;
1528 Rectangle<int> bounds;
1529 };
1530
1531 auto createSolution = [this] (int numColumns,
1532 float probabilityOfFractionalColumn,
1533 Rectangle<int> bounds) -> Solution
1534 {
1535 auto random = getRandom();
1536
1537 Grid grid;
1538 grid.templateRows = { Grid::Fr { 1 } };
1539
1540 // Ensuring that the sum of absolute item widths never exceed total width
1541 const auto widthOfAbsolute = (int) ((float) bounds.getWidth() / (float) (numColumns + 1));
1542
1543 for (int i = 0; i < numColumns; ++i)
1544 {
1545 if (random.nextFloat() < probabilityOfFractionalColumn)
1546 grid.templateColumns.add (Grid::Fr { 1 });
1547 else
1548 grid.templateColumns.add (Grid::Px { widthOfAbsolute });
1549 }
1550
1551 std::deque<Component> itemComponents (static_cast<size_t> (grid.templateColumns.size()));
1552
1553 for (auto& c : itemComponents)
1554 grid.items.add (GridItem { c });
1555
1556 grid.performLayout (bounds);
1557
1558 return { std::move (grid), std::move (itemComponents), widthOfAbsolute, bounds };
1559 };
1560
1561 const auto getFractionalComponentWidths = [] (const Solution& solution)
1562 {
1563 std::vector<int> result;
1564
1565 for (int i = 0; i < solution.grid.templateColumns.size(); ++i)
1566 if (solution.grid.templateColumns[i].isFractional())
1567 result.push_back (solution.components[(size_t) i].getWidth());
1568
1569 return result;
1570 };
1571
1572 const auto getAbsoluteComponentWidths = [] (const Solution& solution)
1573 {
1574 std::vector<int> result;
1575
1576 for (int i = 0; i < solution.grid.templateColumns.size(); ++i)
1577 if (! solution.grid.templateColumns[i].isFractional())
1578 result.push_back (solution.components[(size_t) i].getWidth());
1579
1580 return result;
1581 };
1582
1583 const auto evaluateInvariants = [&] (const Solution& solution)
1584 {
1585 const auto fractionalWidths = getFractionalComponentWidths (solution);
1586
1587 if (! fractionalWidths.empty())
1588 {
1589 const auto [min, max] = std::minmax_element (fractionalWidths.begin(),
1590 fractionalWidths.end());
1591 expectLessOrEqual (*max - *min, 1, "Fr { 1 } items are expected to share the "
1592 "rounding errors equally and hence couldn't "
1593 "deviate in size by more than 1 px");
1594 }
1595
1596 const auto absoluteWidths = getAbsoluteComponentWidths (solution);
1597
1598 for (const auto& w : absoluteWidths)
1599 expectEquals (w, solution.absoluteWidth, "Sizes specified in absolute dimensions should "
1600 "be preserved");
1601
1602 Rectangle<int> unionOfComponentBounds;
1603
1604 for (const auto& c : solution.components)
1605 unionOfComponentBounds = unionOfComponentBounds.getUnion (c.getBoundsInParent());
1606
1607 if ((size_t) solution.grid.templateColumns.size() == absoluteWidths.size())
1608 expect (solution.bounds.contains (unionOfComponentBounds), "Non-oversized absolute Components "
1609 "should never be placed outside the "
1610 "provided bounds.");
1611 else
1612 expect (unionOfComponentBounds == solution.bounds, "With fractional items, positioned items "
1613 "should cover the provided bounds exactly");
1614 };
1615
1616 const auto knownPreviousBad = createSolution (5, 1.0f, Rectangle<int> { 0, 0, 600, 200 }.reduced (16));
1617 evaluateInvariants (knownPreviousBad);
1618
1619 auto random = getRandom();
1620
1621 for (int i = 0; i < 1000; ++i)
1622 {
1623 const auto numColumns = random.nextInt (Range<int> { 1, 26 });
1624 const auto probabilityOfFractionalColumn = random.nextFloat();
1625 const auto bounds = Rectangle<int> { random.nextInt (Range<int> { 0, 3 }),
1626 random.nextInt (Range<int> { 0, 3 }),
1627 random.nextInt (Range<int> { 300, 1200 }),
1628 random.nextInt (Range<int> { 100, 500 }) }
1629 .reduced (random.nextInt (Range<int> { 0, 16 }));
1630
1631 const auto randomSolution = createSolution (numColumns, probabilityOfFractionalColumn, bounds);
1632 evaluateInvariants (randomSolution);
1633 }
1634 }
1635 }
1636};
1637
1639
1640#endif
1641
1642} // namespace juce
T accumulate(T... args)
T any_of(T... args)
Holds a resizable array of primitive or copy-by-value objects.
Definition juce_Array.h:56
int size() const noexcept
Returns the current number of elements in the array.
Definition juce_Array.h:215
ElementType * begin() noexcept
Returns a pointer to the first element in the array.
Definition juce_Array.h:328
ElementType * end() noexcept
Returns a pointer to the element which follows the last element in the array.
Definition juce_Array.h:344
void add(const ElementType &newElement)
Appends a new element at the end of the array.
Definition juce_Array.h:418
ElementType & getReference(int index) noexcept
Returns a direct reference to one of the elements in the array, without checking the index passed in.
Definition juce_Array.h:267
The base class for all JUCE user-interface objects.
int getHeight() const noexcept
Returns the component's height in pixels.
int getWidth() const noexcept
Returns the component's width in pixels.
Defines an item in a Grid.
@ autoValue
Follows the Grid container's alignItems property.
@ center
Content inside the item is aligned towards the center.
Margin margin
The margin to leave around this item.
JustifySelf justifySelf
This is the justify-self property of the item.
@ autoValue
Follows the Grid container's justifyItems property.
@ center
Content inside the item is justified towards the center.
AlignSelf alignSelf
This is the align-self property of the item.
Container that handles geometry for grid layouts (fixed columns and rows) using a set of declarative ...
Definition juce_Grid.h:40
void performLayout(Rectangle< int >)
Lays-out the grid's items within the given rectangle.
AutoFlow autoFlow
Specifies how the auto-placement algorithm places items.
Definition juce_Grid.h:173
Grid()=default
Creates an empty Grid container with default parameters.
JustifyItems
Possible values for the justifyItems property.
Definition juce_Grid.h:105
@ end
Content inside the item is justified towards the right.
@ center
Content inside the item is justified towards the center.
@ start
Content inside the item is justified towards the left.
Array< TrackInfo > templateRows
The set of row tracks to lay out.
Definition juce_Grid.h:181
Px columnGap
The gap in pixels between columns.
Definition juce_Grid.h:193
JustifyContent justifyContent
Specifies the alignment of items along the rows.
Definition juce_Grid.h:167
JustifyContent
Possible values for the justifyContent property.
Definition juce_Grid.h:123
@ end
Items are justified towards the right of the container.
@ center
Items are justified towards the center of the container.
@ spaceBetween
Items are evenly spaced along the row with spaces around them.
@ spaceAround
Items are evenly spaced along the row with spaces between them.
@ spaceEvenly
Items are evenly spaced along the row with even amount of spaces between them.
AlignContent
Possible values for the alignContent property.
Definition juce_Grid.h:135
@ end
Items are aligned towards the bottom of the container.
@ center
Items are aligned towards the center of the container.
@ spaceBetween
Items are evenly spaced along the column with spaces around them.
@ spaceAround
Items are evenly spaced along the column with spaces between them.
@ spaceEvenly
Items are evenly spaced along the column with even amount of spaces between them.
Array< TrackInfo > templateColumns
The set of column tracks to lay out.
Definition juce_Grid.h:178
AlignContent alignContent
Specifies the alignment of items along the columns.
Definition juce_Grid.h:170
Px rowGap
The gap in pixels between rows.
Definition juce_Grid.h:195
AlignItems
Possible values for the alignItems property.
Definition juce_Grid.h:114
@ end
Content inside the item is aligned towards the bottom.
@ center
Content inside the item is aligned towards the center.
@ start
Content inside the item is aligned towards the top.
AutoFlow
Possible values for the autoFlow property.
Definition juce_Grid.h:147
@ column
Fills the grid by adding columns of items.
@ rowDense
Fills the grid by adding rows of items and attempts to fill in gaps.
@ columnDense
Fills the grid by adding columns of items and attempts to fill in gaps.
@ row
Fills the grid by adding rows of items.
Array< GridItem > items
The set of items to lay-out.
Definition juce_Grid.h:202
Manages a rectangle and allows geometric operations to be performed on it.
Rectangle< float > toFloat() const noexcept
Casts this rectangle to a Rectangle<float>.
Point< ValueType > getPosition() const noexcept
Returns the rectangle's top-left position as a Point.
ValueType getCentreX() const noexcept
Returns the x coordinate of the rectangle's centre.
ValueType getCentreY() const noexcept
Returns the y coordinate of the rectangle's centre.
ValueType getWidth() const noexcept
Returns the width of the rectangle.
void setWidth(ValueType newWidth) noexcept
Changes the rectangle's width.
ValueType getHeight() const noexcept
Returns the height of the rectangle.
A special array for holding a list of strings.
void add(String stringToAdd)
Appends a string at the end of the array.
The JUCE String class!
Definition juce_String.h:53
This is a base class for classes that perform a unit test.
T count(T... args)
T end(T... args)
random
T insert(T... args)
#define jassert(expression)
Platform-independent assertion macro.
#define jassertfalse
This will always cause an assertion failure.
typedef int
typedef float
T max(T... args)
T min(T... args)
T minmax_element(T... args)
@ copy
The command ID that should be used to send a "Copy to clipboard" command.
JUCE Namespace.
constexpr auto enumerate(Range &&range, Index startingValue={})
Given a range and an optional starting offset, returns an IteratorPair that holds EnumerateIterators ...
constexpr bool approximatelyEqual(Type a, Type b, Tolerance< Type > tolerance=Tolerance< Type >{} .withAbsolute(std::numeric_limits< Type >::min()) .withRelative(std::numeric_limits< Type >::epsilon()))
Returns true if the two floating-point numbers are approximately equal.
constexpr Type jmin(Type a, Type b)
Returns the smaller of two values.
constexpr Type jmax(Type a, Type b)
Returns the larger of two values.
Type jlimit(Type lowerLimit, Type upperLimit, Type valueToConstrain) noexcept
Constrains a value to keep it within a given range.
constexpr auto makeRange(Begin begin, End end)
Given two iterators "begin" and "end", returns an IteratorPair with a member begin() and end() functi...
Type unalignedPointerCast(void *ptr) noexcept
Casts a pointer to another type via void*, which suppresses the cast-align warning which sometimes ar...
Definition juce_Memory.h:88
bool isPositiveAndBelow(Type1 valueToTest, Type2 upperLimit) noexcept
Returns true if a value is at least zero, and also below a specified upper limit.
RangedDirectoryIterator begin(const RangedDirectoryIterator &it)
Returns the iterator that was passed in.
T next(T... args)
T push_back(T... args)
T crbegin(T... args)
T round(T... args)
T size(T... args)
T stable_sort(T... args)
A fractional ratio integer.
Definition juce_Grid.h:56
A size in pixels.
Definition juce_Grid.h:45
TrackInfo() noexcept
Creates a track with auto dimension.
T swap(T... args)
typedef size_t