Moved out logic to calculate size consumed on a line into seperate function
Reviewed By: emilsjolander Differential Revision: D6797640 fbshipit-source-id: ad9757e7d603c0ce57f452b1e5c404037605bed9
This commit is contained in:
Родитель
8235a49a33
Коммит
34b7ec82b5
|
@ -11,6 +11,41 @@
|
|||
#include "YGNode.h"
|
||||
#include "Yoga-internal.h"
|
||||
|
||||
// This struct is an helper model to hold the data for step 4 of flexbox
|
||||
// algo, which is collecting the flex items in a line.
|
||||
//
|
||||
// - itemsOnLine: Number of items which can fit in a line considering the
|
||||
// available Inner dimension, the flex items computed flexbasis and their
|
||||
// margin. It may be different than the difference between start and end
|
||||
// indicates because we skip over absolute-positioned items.
|
||||
//
|
||||
// - sizeConsumedOnCurrentLine: It is accumulation of the dimensions and margin
|
||||
// of all the children on the current line. This will be used in order to either
|
||||
// set the dimensions of the node if none already exist or to compute the
|
||||
// remaining space left for the flexible children.
|
||||
//
|
||||
// - totalFlexGrowFactors: total flex grow factors of flex items which are to be
|
||||
// layed in the current line
|
||||
//
|
||||
// - totalFlexShrinkFactors: total flex shrink factors of flex items which are
|
||||
// to be layed in the current line
|
||||
//
|
||||
// - endOfLineIndex: Its the end index of the last flex item which was examined
|
||||
// and it may or may not be part of the current line(as it may be absolutely
|
||||
// positioned or inculding it may have caused to overshoot availableInnerDim)
|
||||
//
|
||||
// - relativeChildren: Maintain a vector of the child nodes that can shrink
|
||||
// and/or grow.
|
||||
|
||||
struct YGCollectFlexItemsRowValues {
|
||||
uint32_t itemsOnLine;
|
||||
float sizeConsumedOnCurrentLine;
|
||||
float totalFlexGrowFactors;
|
||||
float totalFlexShrinkScaledFactors;
|
||||
float endOfLineIndex;
|
||||
std::vector<YGNodeRef> relativeChildren;
|
||||
};
|
||||
|
||||
bool YGValueEqual(const YGValue a, const YGValue b);
|
||||
|
||||
YGFlexDirection YGFlexDirectionCross(
|
||||
|
|
|
@ -59,6 +59,10 @@ YGVector YGNode::getChildren() const {
|
|||
return children_;
|
||||
}
|
||||
|
||||
uint32_t YGNode::getChildrenCount() const {
|
||||
return static_cast<uint32_t>(children_.size());
|
||||
}
|
||||
|
||||
YGNodeRef YGNode::getChild(uint32_t index) const {
|
||||
return children_.at(index);
|
||||
}
|
||||
|
|
|
@ -72,6 +72,7 @@ struct YGNode {
|
|||
uint32_t getLineIndex() const;
|
||||
YGNodeRef getParent() const;
|
||||
YGVector getChildren() const;
|
||||
uint32_t getChildrenCount() const;
|
||||
YGNodeRef getChild(uint32_t index) const;
|
||||
YGNodeRef getNextChild() const;
|
||||
YGConfigRef getConfig() const;
|
||||
|
|
|
@ -1619,6 +1619,88 @@ static void YGNodeComputeFlexBasisForChildren(
|
|||
}
|
||||
}
|
||||
|
||||
// This function assumes that all the children of node have their
|
||||
// computedFlexBasis properly computed(To do this use
|
||||
// YGNodeComputeFlexBasisForChildren function).
|
||||
// This function calculates YGCollectFlexItemsRowMeasurement
|
||||
static YGCollectFlexItemsRowValues YGCalculateCollectFlexItemsRowValues(
|
||||
const YGNodeRef& node,
|
||||
const YGDirection parentDirection,
|
||||
const float mainAxisParentSize,
|
||||
const float availableInnerWidth,
|
||||
const float availableInnerMainDim,
|
||||
const uint32_t startOfLineIndex,
|
||||
const uint32_t lineCount) {
|
||||
YGCollectFlexItemsRowValues flexAlgoRowMeasurement = {};
|
||||
flexAlgoRowMeasurement.relativeChildren.reserve(node->getChildren().size());
|
||||
|
||||
float sizeConsumedOnCurrentLineIncludingMinConstraint = 0;
|
||||
const YGFlexDirection mainAxis = YGResolveFlexDirection(
|
||||
node->getStyle().flexDirection, node->resolveDirection(parentDirection));
|
||||
const bool isNodeFlexWrap = node->getStyle().flexWrap != YGWrapNoWrap;
|
||||
|
||||
// Add items to the current line until it's full or we run out of items.
|
||||
uint32_t endOfLineIndex = startOfLineIndex;
|
||||
for (; endOfLineIndex < node->getChildrenCount(); endOfLineIndex++) {
|
||||
const YGNodeRef child = node->getChild(endOfLineIndex);
|
||||
if (child->getStyle().display == YGDisplayNone ||
|
||||
child->getStyle().positionType == YGPositionTypeAbsolute) {
|
||||
continue;
|
||||
}
|
||||
child->setLineIndex(lineCount);
|
||||
const float childMarginMainAxis =
|
||||
YGNodeMarginForAxis(child, mainAxis, availableInnerWidth);
|
||||
const float flexBasisWithMinAndMaxConstraints =
|
||||
YGNodeBoundAxisWithinMinAndMax(
|
||||
child,
|
||||
mainAxis,
|
||||
child->getLayout().computedFlexBasis,
|
||||
mainAxisParentSize);
|
||||
|
||||
// If this is a multi-line flow and this item pushes us over the
|
||||
// available size, we've
|
||||
// hit the end of the current line. Break out of the loop and lay out
|
||||
// the current line.
|
||||
if (sizeConsumedOnCurrentLineIncludingMinConstraint +
|
||||
flexBasisWithMinAndMaxConstraints + childMarginMainAxis >
|
||||
availableInnerMainDim &&
|
||||
isNodeFlexWrap && flexAlgoRowMeasurement.itemsOnLine > 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
sizeConsumedOnCurrentLineIncludingMinConstraint +=
|
||||
flexBasisWithMinAndMaxConstraints + childMarginMainAxis;
|
||||
flexAlgoRowMeasurement.sizeConsumedOnCurrentLine +=
|
||||
flexBasisWithMinAndMaxConstraints + childMarginMainAxis;
|
||||
flexAlgoRowMeasurement.itemsOnLine++;
|
||||
|
||||
if (child->isNodeFlexible()) {
|
||||
flexAlgoRowMeasurement.totalFlexGrowFactors += child->resolveFlexGrow();
|
||||
|
||||
// Unlike the grow factor, the shrink factor is scaled relative to the
|
||||
// child dimension.
|
||||
flexAlgoRowMeasurement.totalFlexShrinkScaledFactors +=
|
||||
-child->resolveFlexShrink() * child->getLayout().computedFlexBasis;
|
||||
}
|
||||
|
||||
flexAlgoRowMeasurement.relativeChildren.push_back(child);
|
||||
}
|
||||
|
||||
// The total flex factor needs to be floored to 1.
|
||||
if (flexAlgoRowMeasurement.totalFlexGrowFactors > 0 &&
|
||||
flexAlgoRowMeasurement.totalFlexGrowFactors < 1) {
|
||||
flexAlgoRowMeasurement.totalFlexGrowFactors = 1;
|
||||
}
|
||||
|
||||
// The total flex shrink factor needs to be floored to 1.
|
||||
if (flexAlgoRowMeasurement.totalFlexShrinkScaledFactors > 0 &&
|
||||
flexAlgoRowMeasurement.totalFlexShrinkScaledFactors < 1) {
|
||||
flexAlgoRowMeasurement.totalFlexShrinkScaledFactors = 1;
|
||||
}
|
||||
flexAlgoRowMeasurement.endOfLineIndex = endOfLineIndex;
|
||||
return flexAlgoRowMeasurement;
|
||||
}
|
||||
|
||||
//
|
||||
// This is the main routine that implements a subset of the flexbox layout
|
||||
// algorithm
|
||||
|
@ -1803,7 +1885,6 @@ static void YGNodelayoutImpl(const YGNodeRef node,
|
|||
YGResolveFlexDirection(node->getStyle().flexDirection, direction);
|
||||
const YGFlexDirection crossAxis = YGFlexDirectionCross(mainAxis, direction);
|
||||
const bool isMainAxisRow = YGFlexDirectionIsRow(mainAxis);
|
||||
const YGJustify justifyContent = node->getStyle().justifyContent;
|
||||
const bool isNodeFlexWrap = node->getStyle().flexWrap != YGWrapNoWrap;
|
||||
|
||||
const float mainAxisParentSize = isMainAxisRow ? parentWidth : parentHeight;
|
||||
|
@ -1879,9 +1960,10 @@ static void YGNodelayoutImpl(const YGNodeRef node,
|
|||
totalOuterFlexBasis);
|
||||
|
||||
const bool flexBasisOverflows = measureModeMainDim == YGMeasureModeUndefined
|
||||
? false
|
||||
: totalOuterFlexBasis > availableInnerMainDim;
|
||||
if (isNodeFlexWrap && flexBasisOverflows && measureModeMainDim == YGMeasureModeAtMost) {
|
||||
? false
|
||||
: totalOuterFlexBasis > availableInnerMainDim;
|
||||
if (isNodeFlexWrap && flexBasisOverflows &&
|
||||
measureModeMainDim == YGMeasureModeAtMost) {
|
||||
measureModeMainDim = YGMeasureModeExactly;
|
||||
}
|
||||
// STEP 4: COLLECT FLEX ITEMS INTO FLEX LINES
|
||||
|
@ -1898,93 +1980,23 @@ static void YGNodelayoutImpl(const YGNodeRef node,
|
|||
|
||||
// Max main dimension of all the lines.
|
||||
float maxLineMainDim = 0;
|
||||
|
||||
for (; endOfLineIndex < childCount; lineCount++, startOfLineIndex = endOfLineIndex) {
|
||||
// Number of items on the currently line. May be different than the
|
||||
// difference
|
||||
// between start and end indicates because we skip over absolute-positioned
|
||||
// items.
|
||||
uint32_t itemsOnLine = 0;
|
||||
|
||||
// sizeConsumedOnCurrentLine is accumulation of the dimensions and margin
|
||||
// of all the children on the current line. This will be used in order to
|
||||
// either set the dimensions of the node if none already exist or to compute
|
||||
// the remaining space left for the flexible children.
|
||||
float sizeConsumedOnCurrentLine = 0;
|
||||
float sizeConsumedOnCurrentLineIncludingMinConstraint = 0;
|
||||
|
||||
float totalFlexGrowFactors = 0;
|
||||
float totalFlexShrinkScaledFactors = 0;
|
||||
|
||||
// Maintain a vector of the child nodes that can shrink and/or grow.
|
||||
std::vector<YGNodeRef> relativeChildren;
|
||||
|
||||
// Add items to the current line until it's full or we run out of items.
|
||||
for (uint32_t i = startOfLineIndex; i < childCount; i++, endOfLineIndex++) {
|
||||
const YGNodeRef child = node->getChild(i);
|
||||
if (child->getStyle().display == YGDisplayNone ||
|
||||
child->getStyle().positionType == YGPositionTypeAbsolute) {
|
||||
continue;
|
||||
}
|
||||
child->setLineIndex(lineCount);
|
||||
const float childMarginMainAxis =
|
||||
YGNodeMarginForAxis(child, mainAxis, availableInnerWidth);
|
||||
const float flexBasisWithMinAndMaxConstraints =
|
||||
YGNodeBoundAxisWithinMinAndMax(
|
||||
child,
|
||||
mainAxis,
|
||||
child->getLayout().computedFlexBasis,
|
||||
mainAxisParentSize);
|
||||
|
||||
// If this is a multi-line flow and this item pushes us over the
|
||||
// available size, we've
|
||||
// hit the end of the current line. Break out of the loop and lay out
|
||||
// the current line.
|
||||
if (sizeConsumedOnCurrentLineIncludingMinConstraint +
|
||||
flexBasisWithMinAndMaxConstraints + childMarginMainAxis >
|
||||
availableInnerMainDim &&
|
||||
isNodeFlexWrap && itemsOnLine > 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
sizeConsumedOnCurrentLineIncludingMinConstraint +=
|
||||
flexBasisWithMinAndMaxConstraints + childMarginMainAxis;
|
||||
sizeConsumedOnCurrentLine +=
|
||||
flexBasisWithMinAndMaxConstraints + childMarginMainAxis;
|
||||
itemsOnLine++;
|
||||
|
||||
if (child->isNodeFlexible()) {
|
||||
totalFlexGrowFactors += child->resolveFlexGrow();
|
||||
|
||||
// Unlike the grow factor, the shrink factor is scaled relative to the
|
||||
// child dimension.
|
||||
totalFlexShrinkScaledFactors +=
|
||||
-child->resolveFlexShrink() * child->getLayout().computedFlexBasis;
|
||||
}
|
||||
|
||||
// Store a private linked list of children that need to be layed out.
|
||||
relativeChildren.push_back(child);
|
||||
}
|
||||
|
||||
// The total flex factor needs to be floored to 1.
|
||||
if (totalFlexGrowFactors > 0 && totalFlexGrowFactors < 1) {
|
||||
totalFlexGrowFactors = 1;
|
||||
}
|
||||
|
||||
// The total flex shrink factor needs to be floored to 1.
|
||||
if (totalFlexShrinkScaledFactors > 0 && totalFlexShrinkScaledFactors < 1) {
|
||||
totalFlexShrinkScaledFactors = 1;
|
||||
}
|
||||
YGCollectFlexItemsRowValues collectedFlexItemsValues;
|
||||
for (; endOfLineIndex < childCount;
|
||||
lineCount++, startOfLineIndex = endOfLineIndex) {
|
||||
collectedFlexItemsValues = YGCalculateCollectFlexItemsRowValues(
|
||||
node,
|
||||
parentDirection,
|
||||
mainAxisParentSize,
|
||||
availableInnerWidth,
|
||||
availableInnerMainDim,
|
||||
startOfLineIndex,
|
||||
lineCount);
|
||||
endOfLineIndex = collectedFlexItemsValues.endOfLineIndex;
|
||||
|
||||
// If we don't need to measure the cross axis, we can skip the entire flex
|
||||
// step.
|
||||
const bool canSkipFlex = !performLayout && measureModeCrossDim == YGMeasureModeExactly;
|
||||
|
||||
// In order to position the elements in the main axis, we have two
|
||||
// controls. The space between the beginning and the first element
|
||||
// and the space between each two elements.
|
||||
float leadingMainDim = 0;
|
||||
float betweenMainDim = 0;
|
||||
const bool canSkipFlex =
|
||||
!performLayout && measureModeCrossDim == YGMeasureModeExactly;
|
||||
|
||||
// STEP 5: RESOLVING FLEXIBLE LENGTHS ON MAIN AXIS
|
||||
// Calculate the remaining available space that needs to be allocated.
|
||||
|
@ -1994,18 +2006,24 @@ static void YGNodelayoutImpl(const YGNodeRef node,
|
|||
bool sizeBasedOnContent = false;
|
||||
// If we don't measure with exact main dimension we want to ensure we don't violate min and max
|
||||
if (measureModeMainDim != YGMeasureModeExactly) {
|
||||
if (!YGFloatIsUndefined(minInnerMainDim) && sizeConsumedOnCurrentLine < minInnerMainDim) {
|
||||
if (!YGFloatIsUndefined(minInnerMainDim) &&
|
||||
collectedFlexItemsValues.sizeConsumedOnCurrentLine <
|
||||
minInnerMainDim) {
|
||||
availableInnerMainDim = minInnerMainDim;
|
||||
} else if (!YGFloatIsUndefined(maxInnerMainDim) &&
|
||||
sizeConsumedOnCurrentLine > maxInnerMainDim) {
|
||||
} else if (
|
||||
!YGFloatIsUndefined(maxInnerMainDim) &&
|
||||
collectedFlexItemsValues.sizeConsumedOnCurrentLine >
|
||||
maxInnerMainDim) {
|
||||
availableInnerMainDim = maxInnerMainDim;
|
||||
} else {
|
||||
if (!node->getConfig()->useLegacyStretchBehaviour &&
|
||||
(totalFlexGrowFactors == 0 || node->resolveFlexGrow() == 0)) {
|
||||
(collectedFlexItemsValues.totalFlexGrowFactors == 0 ||
|
||||
node->resolveFlexGrow() == 0)) {
|
||||
// If we don't have any children to flex or we can't flex the node
|
||||
// itself, space we've used is all space we need. Root node also
|
||||
// should be shrunk to minimum
|
||||
availableInnerMainDim = sizeConsumedOnCurrentLine;
|
||||
availableInnerMainDim =
|
||||
collectedFlexItemsValues.sizeConsumedOnCurrentLine;
|
||||
}
|
||||
|
||||
if (node->getConfig()->useLegacyStretchBehaviour) {
|
||||
|
@ -2017,13 +2035,14 @@ static void YGNodelayoutImpl(const YGNodeRef node,
|
|||
|
||||
float remainingFreeSpace = 0;
|
||||
if (!sizeBasedOnContent && !YGFloatIsUndefined(availableInnerMainDim)) {
|
||||
remainingFreeSpace = availableInnerMainDim - sizeConsumedOnCurrentLine;
|
||||
} else if (sizeConsumedOnCurrentLine < 0) {
|
||||
remainingFreeSpace = availableInnerMainDim -
|
||||
collectedFlexItemsValues.sizeConsumedOnCurrentLine;
|
||||
} else if (collectedFlexItemsValues.sizeConsumedOnCurrentLine < 0) {
|
||||
// availableInnerMainDim is indefinite which means the node is being sized based on its
|
||||
// content.
|
||||
// sizeConsumedOnCurrentLine is negative which means the node will allocate 0 points for
|
||||
// its content. Consequently, remainingFreeSpace is 0 - sizeConsumedOnCurrentLine.
|
||||
remainingFreeSpace = -sizeConsumedOnCurrentLine;
|
||||
remainingFreeSpace = -collectedFlexItemsValues.sizeConsumedOnCurrentLine;
|
||||
}
|
||||
|
||||
const float originalRemainingFreeSpace = remainingFreeSpace;
|
||||
|
@ -2062,7 +2081,8 @@ static void YGNodelayoutImpl(const YGNodeRef node,
|
|||
float deltaFlexShrinkScaledFactors = 0;
|
||||
float deltaFlexGrowFactors = 0;
|
||||
|
||||
for (auto currentRelativeChild : relativeChildren) {
|
||||
for (auto currentRelativeChild :
|
||||
collectedFlexItemsValues.relativeChildren) {
|
||||
childFlexBasis = YGNodeBoundAxisWithinMinAndMax(
|
||||
currentRelativeChild,
|
||||
mainAxis,
|
||||
|
@ -2076,7 +2096,8 @@ static void YGNodelayoutImpl(const YGNodeRef node,
|
|||
// Is this child able to shrink?
|
||||
if (flexShrinkScaledFactor != 0) {
|
||||
baseMainSize = childFlexBasis +
|
||||
remainingFreeSpace / totalFlexShrinkScaledFactors *
|
||||
remainingFreeSpace /
|
||||
collectedFlexItemsValues.totalFlexShrinkScaledFactors *
|
||||
flexShrinkScaledFactor;
|
||||
boundMainSize = YGNodeBoundAxis(
|
||||
currentRelativeChild,
|
||||
|
@ -2101,7 +2122,9 @@ static void YGNodelayoutImpl(const YGNodeRef node,
|
|||
// Is this child able to grow?
|
||||
if (flexGrowFactor != 0) {
|
||||
baseMainSize = childFlexBasis +
|
||||
remainingFreeSpace / totalFlexGrowFactors * flexGrowFactor;
|
||||
remainingFreeSpace /
|
||||
collectedFlexItemsValues.totalFlexGrowFactors *
|
||||
flexGrowFactor;
|
||||
boundMainSize = YGNodeBoundAxis(
|
||||
currentRelativeChild,
|
||||
mainAxis,
|
||||
|
@ -2125,13 +2148,15 @@ static void YGNodelayoutImpl(const YGNodeRef node,
|
|||
currentRelativeChild = currentRelativeChild->getNextChild();
|
||||
}
|
||||
|
||||
totalFlexShrinkScaledFactors += deltaFlexShrinkScaledFactors;
|
||||
totalFlexGrowFactors += deltaFlexGrowFactors;
|
||||
collectedFlexItemsValues.totalFlexShrinkScaledFactors +=
|
||||
deltaFlexShrinkScaledFactors;
|
||||
collectedFlexItemsValues.totalFlexGrowFactors += deltaFlexGrowFactors;
|
||||
remainingFreeSpace += deltaFreeSpace;
|
||||
|
||||
// Second pass: resolve the sizes of the flexible items
|
||||
deltaFreeSpace = 0;
|
||||
for (auto currentRelativeChild : relativeChildren) {
|
||||
for (auto currentRelativeChild :
|
||||
collectedFlexItemsValues.relativeChildren) {
|
||||
childFlexBasis = YGNodeBoundAxisWithinMinAndMax(
|
||||
currentRelativeChild,
|
||||
mainAxis,
|
||||
|
@ -2146,11 +2171,12 @@ static void YGNodelayoutImpl(const YGNodeRef node,
|
|||
if (flexShrinkScaledFactor != 0) {
|
||||
float childSize;
|
||||
|
||||
if (totalFlexShrinkScaledFactors == 0) {
|
||||
if (collectedFlexItemsValues.totalFlexShrinkScaledFactors == 0) {
|
||||
childSize = childFlexBasis + flexShrinkScaledFactor;
|
||||
} else {
|
||||
childSize = childFlexBasis +
|
||||
(remainingFreeSpace / totalFlexShrinkScaledFactors) *
|
||||
(remainingFreeSpace /
|
||||
collectedFlexItemsValues.totalFlexShrinkScaledFactors) *
|
||||
flexShrinkScaledFactor;
|
||||
}
|
||||
|
||||
|
@ -2170,7 +2196,9 @@ static void YGNodelayoutImpl(const YGNodeRef node,
|
|||
currentRelativeChild,
|
||||
mainAxis,
|
||||
childFlexBasis +
|
||||
remainingFreeSpace / totalFlexGrowFactors * flexGrowFactor,
|
||||
remainingFreeSpace /
|
||||
collectedFlexItemsValues.totalFlexGrowFactors *
|
||||
flexGrowFactor,
|
||||
availableInnerMainDim,
|
||||
availableInnerWidth);
|
||||
}
|
||||
|
@ -2333,6 +2361,13 @@ static void YGNodelayoutImpl(const YGNodeRef node,
|
|||
}
|
||||
}
|
||||
|
||||
// In order to position the elements in the main axis, we have two
|
||||
// controls. The space between the beginning and the first element
|
||||
// and the space between each two elements.
|
||||
float leadingMainDim = 0;
|
||||
float betweenMainDim = 0;
|
||||
const YGJustify justifyContent = node->getStyle().justifyContent;
|
||||
|
||||
if (numberOfAutoMarginsOnCurrentLine == 0) {
|
||||
switch (justifyContent) {
|
||||
case YGJustifyCenter:
|
||||
|
@ -2342,20 +2377,23 @@ static void YGNodelayoutImpl(const YGNodeRef node,
|
|||
leadingMainDim = remainingFreeSpace;
|
||||
break;
|
||||
case YGJustifySpaceBetween:
|
||||
if (itemsOnLine > 1) {
|
||||
betweenMainDim = fmaxf(remainingFreeSpace, 0) / (itemsOnLine - 1);
|
||||
if (collectedFlexItemsValues.itemsOnLine > 1) {
|
||||
betweenMainDim = fmaxf(remainingFreeSpace, 0) /
|
||||
(collectedFlexItemsValues.itemsOnLine - 1);
|
||||
} else {
|
||||
betweenMainDim = 0;
|
||||
}
|
||||
break;
|
||||
case YGJustifySpaceEvenly:
|
||||
// Space is distributed evenly across all elements
|
||||
betweenMainDim = remainingFreeSpace / (itemsOnLine + 1);
|
||||
betweenMainDim =
|
||||
remainingFreeSpace / (collectedFlexItemsValues.itemsOnLine + 1);
|
||||
leadingMainDim = betweenMainDim;
|
||||
break;
|
||||
case YGJustifySpaceAround:
|
||||
// Space on the edges is half of the space between elements
|
||||
betweenMainDim = remainingFreeSpace / itemsOnLine;
|
||||
betweenMainDim =
|
||||
remainingFreeSpace / collectedFlexItemsValues.itemsOnLine;
|
||||
leadingMainDim = betweenMainDim / 2;
|
||||
break;
|
||||
case YGJustifyFlexStart:
|
||||
|
|
Загрузка…
Ссылка в новой задаче