ComponentKit/LayoutComponents/CKFlexboxComponent.mm (758 lines of code) (raw):
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "CKFlexboxComponent.h"
#import <ComponentKit/CKComponentPerfScope.h>
#import <ComponentKit/CKExceptionInfoScopedValue.h>
#import <ComponentKit/CKGlobalConfig.h>
#import <ComponentKit/CKMacros.h>
#import <ComponentKit/CKInternalHelpers.h>
#import <RenderCore/RCAssert.h>
#import <ComponentKit/CKFunctionalHelpers.h>
#import <ComponentKit/CKWritingDirection.h>
#import <ComponentKit/CKSizeAssert.h>
#import "yoga/Yoga.h"
#import "CKComponent+Yoga.h"
#import "CKComponentInternal.h"
#import "CKComponentLayout.h"
#import "CKComponentLayoutBaseline.h"
#import "CKComponentSubclass.h"
#import "CKCompositeComponent.h"
#import "CKThreadLocalComponentScope.h"
#import "CKComponentViewConfiguration_SwiftBridge+Internal.h"
#import "RCComponentSize_SwiftBridge+Internal.h"
#import "RCDimension_SwiftBridge+Internal.h"
const struct CKStackComponentLayoutExtraKeys CKStackComponentLayoutExtraKeys = {
.hadOverflow = @"hadOverflow"
};
/*
This class contains information about cached layout for FlexboxComponent child
*/
@interface CKFlexboxChildCachedLayout : NSObject
@property (nonatomic) CKComponent *component;
@property (nonatomic) RCLayout componentLayout;
@property (nonatomic) float width;
@property (nonatomic) float height;
@property (nonatomic) YGMeasureMode widthMode;
@property (nonatomic) YGMeasureMode heightMode;
@property (nonatomic) CGSize parentSize;
@property (nonatomic) NSInteger zIndex;
@end
template class std::vector<CKFlexboxComponentChild>;
@implementation CKFlexboxChildCachedLayout
@end
@implementation CKFlexboxChild_SwiftBridge {
@package
CKComponent *_component;
CGFloat _spacingBefore;
CGFloat _spacingAfter;
// TODO: Support margin
// TODO: Support padding
CGFloat _flexGrow;
CGFloat _flexShrink;
RCDimension_SwiftBridge *_swiftFlexBasis;
CKFlexboxAlignSelf _alignSelf;
// TODO: Support position
NSInteger _zIndex;
// TODO: Support aspectRatio
RCComponentSize _sizeConstraints;
BOOL _useTextRounding;
BOOL _useHeightAsBaseline;
}
- (instancetype)initWithComponent:(CKComponent *)component
spacingBefore:(CGFloat)spacingBefore
spacingAfter:(CGFloat)spacingAfter
flexGrow:(CGFloat)flexGrow
flexShrink:(CGFloat)flexShrink
swiftFlexBasis:(RCDimension_SwiftBridge *)swiftFlexBasis
alignSelf:(CKFlexboxAlignSelf)alignSelf
zIndex:(NSInteger)zIndex
sizeConstraints:(RCComponentSize_SwiftBridge *)sizeConstraints
useTextRounding:(BOOL)useTextRounding
useHeightAsBaseline:(BOOL)useHeightAsBaseline
{
if (self = [super init]) {
_component = component;
_spacingBefore = spacingBefore;
_spacingAfter = spacingAfter;
_flexGrow = flexGrow;
_flexShrink = flexShrink;
_swiftFlexBasis = swiftFlexBasis;
_alignSelf = alignSelf;
_zIndex = zIndex;
if (sizeConstraints != nil) {
_sizeConstraints = sizeConstraints.componentSize;
}
_useTextRounding = useTextRounding;
_useHeightAsBaseline = useHeightAsBaseline;
}
return self;
}
- (CKFlexboxComponentChild)child
{
const auto flexBasis = _swiftFlexBasis != nil ? _swiftFlexBasis.dimension : RCRelativeDimension{};
return {
.component = _component,
.spacingBefore = _spacingBefore,
.spacingAfter = _spacingAfter,
// TODO: Support for .margin
// TODO: Support for .padding
.flexGrow = _flexGrow,
.flexShrink = _flexShrink,
.flexBasis = flexBasis,
.alignSelf = _alignSelf,
.zIndex = _zIndex,
.sizeConstraints = _sizeConstraints,
.useTextRounding = _useTextRounding,
.useHeightAsBaseline = _useHeightAsBaseline,
};
}
@end
@implementation CKFlexboxComponentStyle_SwiftBridge {
CKFlexboxDirection _direction;
CGFloat _spacing;
CKFlexboxJustifyContent _justifyContent;
CKFlexboxAlignItems _alignItems;
CKFlexboxAlignContent _alignContent;
CKFlexboxWrap _wrap;
// TODO: Support padding
// TODO: Support _border;
CKLayoutDirection _layoutDirection;
BOOL _useDeepYogaTrees;
}
- (instancetype)initWithDirection:(CKFlexboxDirection)direction
spacing:(CGFloat)spacing
justifyContent:(CKFlexboxJustifyContent)justifyContent
alignItems:(CKFlexboxAlignItems)alignItems
alignContent:(CKFlexboxAlignContent)alignContent
wrap:(CKFlexboxWrap)wrap
layoutDirection:(CKLayoutDirection)layoutDirection
useDeepYogaTrees:(BOOL)useDeepYogaTrees
{
if (self = [super init]) {
_direction = direction;
_spacing = spacing;
_justifyContent = justifyContent;
_alignItems = alignItems;
_alignContent = alignContent;
_wrap = wrap;
_layoutDirection = layoutDirection;
_useDeepYogaTrees = useDeepYogaTrees;
}
return self;
}
- (CKFlexboxComponentStyle)style
{
return {
.direction = _direction,
.spacing = _spacing,
.justifyContent = _justifyContent,
.alignItems = _alignItems,
.alignContent = _alignContent,
.wrap = _wrap,
.layoutDirection = _layoutDirection,
.useDeepYogaTrees = _useDeepYogaTrees,
};
}
@end
@implementation CKFlexboxComponent {
CKFlexboxComponentStyle _style;
std::vector<CKFlexboxComponentChild> _children;
}
- (instancetype)initWithView:(const CKComponentViewConfiguration &)view
size:(const RCComponentSize &)size
style:(const CKFlexboxComponentStyle &)style
children:(std::vector<CKFlexboxComponentChild>)children
{
CKComponentPerfScope perfScope(self.class);
if (self = [super initWithView:view size:size]) {
_style = style;
_children = std::move(children);
#if CK_ASSERTIONS_ENABLED
for (const auto &child : _children) {
if (child.component) {
RCAssertWithCategory(child.component.typeName != nullptr,
@"non_prod_validation_T79773577",
@"Expected `child.component` to be a valid object.");
}
}
#endif
}
return self;
}
- (instancetype)initWithSwiftView:(CKComponentViewConfiguration_SwiftBridge *)swiftView
swiftStyle:(CKFlexboxComponentStyle_SwiftBridge *)swiftStyle
swiftSize:(RCComponentSize_SwiftBridge *)swiftSize
swiftChildren:(NSArray<CKFlexboxChild_SwiftBridge *> *)swiftChildren
{
const auto view = swiftView != nil ? swiftView.viewConfig : CKComponentViewConfiguration{};
const auto size = swiftSize != nil ? swiftSize.componentSize : RCComponentSize{};
const auto style = swiftStyle != nil ? [swiftStyle style] : CKFlexboxComponentStyle{};
return [self initWithView:view size:size style:style children:CK::map(swiftChildren, [](const CKFlexboxChild_SwiftBridge *swiftChild){
return swiftChild.child;
})];
}
+ (instancetype)newWithView:(const CKComponentViewConfiguration &)view
size:(const RCComponentSize &)size
style:(const CKFlexboxComponentStyle &)style
children:(RCContainerWrapper<std::vector<CKFlexboxComponentChild>> &&)children
{
return [[self alloc] initWithView:view size:size style:style children:children.take()];
}
static bool setPercentOnChildNode(const CKFlexboxComponentStyle &style) {
return style.useDeepYogaTrees || CKReadGlobalConfig().setPercentOnChildNode;
}
static float convertFloatToYogaRepresentation(const float& value) {
return isnan(value) || isinf(value) ? YGUndefined : value;
}
static float convertCGFloatToYogaRepresentation(const CGFloat& value) {
return isnan(value) || isinf(value) ? YGUndefined : static_cast<float>(value);
}
static CGSize convertCGSizeToYogaRepresentation(const CGSize& size) {
return {static_cast<CGFloat>(convertCGFloatToYogaRepresentation(size.width)), static_cast<CGFloat>(convertCGFloatToYogaRepresentation(size.height))};
}
static CKSizeRange convertCKSizeRangeToYogaRepresentation(const CKSizeRange& size) {
auto range = CKSizeRange{};
range.min = convertCGSizeToYogaRepresentation(size.min);
range.max = convertCGSizeToYogaRepresentation(size.max);
return range;
}
static float convertFloatToCKRepresentation(const float& value) {
return YGFloatIsUndefined(value) ? INFINITY : value;
}
static CGFloat convertCGFloatToCKRepresentation(const CGFloat& value) {
return YGFloatIsUndefined(static_cast<float>(value)) ? INFINITY : value;
}
static CGSize convertCGSizeToCKRepresentation(const CGSize& size) {
return {convertCGFloatToCKRepresentation(size.width), convertCGFloatToCKRepresentation(size.height)};
}
static CKSizeRange convertCKSizeRangeToCKRepresentation(const CKSizeRange& size) {
return CKSizeRange(convertCGSizeToCKRepresentation(size.min), convertCGSizeToCKRepresentation(size.max));
}
static bool CKYogaNodeCanUseCachedMeasurement(const YGMeasureMode widthMode,
const float width,
const YGMeasureMode heightMode,
const float height,
const YGMeasureMode lastWidthMode,
const float lastWidth,
const YGMeasureMode lastHeightMode,
const float lastHeight,
const float lastComputedWidth,
const float lastComputedHeight,
const float marginRow,
const float marginColumn,
const YGConfigRef config) {
return YGNodeCanUseCachedMeasurement(widthMode, convertFloatToYogaRepresentation(width), heightMode, convertFloatToYogaRepresentation(height), lastWidthMode, convertFloatToYogaRepresentation(lastWidth), lastHeightMode, convertFloatToYogaRepresentation(lastHeight), convertFloatToYogaRepresentation(lastComputedWidth), convertFloatToYogaRepresentation(lastComputedHeight), convertFloatToYogaRepresentation(marginRow), convertFloatToYogaRepresentation(marginColumn), config);
}
static YGSize measureYGComponent(YGNodeRef node,
float width,
YGMeasureMode widthMode,
float height,
YGMeasureMode heightMode)
{
CKFlexboxChildCachedLayout *cachedLayout = (__bridge CKFlexboxChildCachedLayout *)YGNodeGetContext(node);
const CGSize minSize = {
.width = (widthMode == YGMeasureModeExactly) ? width : 0,
.height = (heightMode == YGMeasureModeExactly) ? height : 0
};
const CGSize maxSize = {
.width = (widthMode == YGMeasureModeExactly || widthMode == YGMeasureModeAtMost) ? width : INFINITY,
.height = (heightMode == YGMeasureModeExactly || heightMode == YGMeasureModeAtMost) ? height : INFINITY
};
// We cache measurements for the duration of single layout calculation of FlexboxComponent
// ComponentKit and Yoga handle caching between calculations
// We don't have any guarantees about when and how this will be called,
// so we just cache the results to try to reuse them during final layout
if (!CKYogaNodeCanUseCachedMeasurement(widthMode, width, heightMode, height, cachedLayout.widthMode, cachedLayout.width, cachedLayout.heightMode, cachedLayout.height, static_cast<float>(cachedLayout.componentLayout.size.width), static_cast<float>(cachedLayout.componentLayout.size.height), 0, 0, ckYogaDefaultConfig())) {
CKComponent *component = cachedLayout.component;
cachedLayout.componentLayout = CKComputeComponentLayout(component, convertCKSizeRangeToCKRepresentation(CKSizeRange(minSize, maxSize)), convertCGSizeToCKRepresentation(cachedLayout.parentSize));
cachedLayout.width = width;
cachedLayout.height = height;
cachedLayout.widthMode = widthMode;
cachedLayout.heightMode = heightMode;
}
const float componentLayoutWidth = static_cast<float>(cachedLayout.componentLayout.size.width);
const float componentLayoutHeight = static_cast<float>(cachedLayout.componentLayout.size.height);
const float measuredWidth = convertFloatToYogaRepresentation(componentLayoutWidth);
const float measuredHeight = convertFloatToYogaRepresentation(componentLayoutHeight);
return {measuredWidth, measuredHeight};
}
static float computeBaseline(YGNodeRef node, const float width, const float height)
{
CKFlexboxChildCachedLayout *const cachedLayout = getCKFlexboxChildCachedLayoutFromYogaNode(node, width, height);
if ([cachedLayout.componentLayout.extra objectForKey:kCKComponentLayoutExtraBaselineKey]) {
RCCAssert([[cachedLayout.componentLayout.extra objectForKey:kCKComponentLayoutExtraBaselineKey] isKindOfClass:[NSNumber class]], @"You must set a NSNumber for kCKComponentLayoutExtraBaselineKey");
return [[cachedLayout.componentLayout.extra objectForKey:kCKComponentLayoutExtraBaselineKey] floatValue];
}
return height;
}
static float useHeightAsBaselineFunction(YGNodeRef node, const float width, const float height)
{
return height;
}
static CKFlexboxChildCachedLayout* getCKFlexboxChildCachedLayoutFromYogaNode(YGNodeRef node, const float width, const float height)
{
CKFlexboxChildCachedLayout *const cachedLayout = (__bridge CKFlexboxChildCachedLayout *)YGNodeGetContext(node);
if (!CKYogaNodeCanUseCachedMeasurement(YGMeasureModeExactly, width, YGMeasureModeExactly, height, cachedLayout.widthMode, cachedLayout.width, cachedLayout.heightMode, cachedLayout.height, static_cast<float>(cachedLayout.componentLayout.size.width), static_cast<float>(cachedLayout.componentLayout.size.height), 0, 0, ckYogaDefaultConfig())) {
const CGSize fixedSize = {width, height};
const RCLayout componentLayout = CKComputeComponentLayout(cachedLayout.component, convertCKSizeRangeToCKRepresentation(CKSizeRange(fixedSize, fixedSize)), convertCGSizeToCKRepresentation(cachedLayout.parentSize));
cachedLayout.componentLayout = componentLayout;
cachedLayout.width = width;
cachedLayout.height = height;
cachedLayout.widthMode = YGMeasureModeExactly;
cachedLayout.heightMode = YGMeasureModeExactly;
}
return cachedLayout;
}
static YGDirection ygApplicationDirection()
{
switch (CKGetWritingDirection()) {
case CKWritingDirection::Natural:
case CKWritingDirection::LeftToRight:
return YGDirectionLTR;
case CKWritingDirection::RightToLeft:
return YGDirectionRTL;
}
}
static YGDirection ygDirectionFromStackStyle(const CKFlexboxComponentStyle &style)
{
switch (style.layoutDirection) {
case CKLayoutDirectionApplicationDirection:
return ygApplicationDirection();
case CKLayoutDirectionLTR:
return YGDirectionLTR;
case CKLayoutDirectionRTL:
return YGDirectionRTL;
}
}
static YGFlexDirection ygFlexDirectionFromStackStyle(const CKFlexboxComponentStyle &style)
{
switch (style.direction) {
case CKFlexboxDirectionRow:
return YGFlexDirectionRow;
case CKFlexboxDirectionColumn:
return YGFlexDirectionColumn;
case CKFlexboxDirectionRowReverse:
return YGFlexDirectionRowReverse;
case CKFlexboxDirectionColumnReverse:
return YGFlexDirectionColumnReverse;
}
}
static YGJustify ygJustifyFromStackStyle(const CKFlexboxComponentStyle &style)
{
switch (style.justifyContent) {
case CKFlexboxJustifyContentCenter:
return YGJustifyCenter;
case CKFlexboxJustifyContentEnd:
return YGJustifyFlexEnd;
case CKFlexboxJustifyContentStart:
return YGJustifyFlexStart;
case CKFlexboxJustifyContentSpaceBetween:
return YGJustifySpaceBetween;
case CKFlexboxJustifyContentSpaceAround:
return YGJustifySpaceAround;
case CKFlexboxJustifyContentSpaceEvenly:
return YGJustifySpaceEvenly;
}
}
static YGAlign ygAlignItemsFromStackStyle(const CKFlexboxComponentStyle &style)
{
switch (style.alignItems) {
case CKFlexboxAlignItemsEnd:
return YGAlignFlexEnd;
case CKFlexboxAlignItemsCenter:
return YGAlignCenter;
case CKFlexboxAlignItemsStretch:
return YGAlignStretch;
case CKFlexboxAlignItemsBaseline:
return YGAlignBaseline;
case CKFlexboxAlignItemsStart:
return YGAlignFlexStart;
}
}
static YGAlign ygAlignContentFromStackStyle(const CKFlexboxComponentStyle &style)
{
switch (style.alignContent) {
case CKFlexboxAlignContentEnd:
return YGAlignFlexEnd;
case CKFlexboxAlignContentCenter:
return YGAlignCenter;
case CKFlexboxAlignContentStretch:
return YGAlignStretch;
case CKFlexboxAlignContentStart:
return YGAlignFlexStart;
case CKFlexboxAlignContentSpaceAround:
return YGAlignSpaceAround;
case CKFlexboxAlignContentSpaceBetween:
return YGAlignSpaceBetween;
}
}
static YGAlign ygAlignFromChild(const CKFlexboxComponentChild &child)
{
switch (child.alignSelf) {
case CKFlexboxAlignSelfStart:
return YGAlignFlexStart;
case CKFlexboxAlignSelfEnd:
return YGAlignFlexEnd;
case CKFlexboxAlignSelfCenter:
return YGAlignCenter;
case CKFlexboxAlignSelfBaseline:
return YGAlignBaseline;
case CKFlexboxAlignSelfStretch:
return YGAlignStretch;
case CKFlexboxAlignSelfAuto:
return YGAlignAuto;
}
}
static YGWrap ygWrapFromStackStyle(const CKFlexboxComponentStyle &style)
{
switch (style.wrap) {
case CKFlexboxWrapNoWrap:
return YGWrapNoWrap;
case CKFlexboxWrapWrap:
return YGWrapWrap;
case CKFlexboxWrapWrapReverse:
return YGWrapWrapReverse;
}
}
static YGEdge ygSpacingEdgeFromDirection(const CKFlexboxDirection &direction, BOOL reverse = NO)
{
switch (direction) {
case CKFlexboxDirectionColumn:
return reverse ? YGEdgeBottom : YGEdgeTop;
case CKFlexboxDirectionColumnReverse:
return reverse ? YGEdgeTop : YGEdgeBottom;
case CKFlexboxDirectionRow:
return reverse ? YGEdgeEnd : YGEdgeStart;
case CKFlexboxDirectionRowReverse:
return reverse ? YGEdgeStart : YGEdgeEnd;
}
}
static BOOL isHorizontalFlexboxDirection(const CKFlexboxDirection &direction)
{
switch (direction) {
case CKFlexboxDirectionColumn:
case CKFlexboxDirectionColumnReverse:
return NO;
case CKFlexboxDirectionRow:
case CKFlexboxDirectionRowReverse:
return YES;
}
}
static bool hasChildWithRelativePositioning(const CKFlexboxComponentChild &child) {
return
(child.component != nil
&& child.position.type == CKFlexboxPositionTypeRelative);
}
/*
layoutCache is passed by reference so that we are able to allocate it in one thread
and mutate it within that thread
Layout cache shouldn't be exposed publicly
*/
- (YGNodeRef)ygStackLayoutNode:(CKSizeRange)constrainedSize
{
const YGNodeRef stackNode = YGNodeNewWithConfig(ckYogaDefaultConfig());
YGEdge spacingEdge = ygSpacingEdgeFromDirection(_style.direction);
CGFloat savedSpacing = 0;
// We need this to resolve RCRelativeDimension with percentage bases
CGFloat parentWidth = (constrainedSize.min.width == constrainedSize.max.width) ? constrainedSize.min.width : kCKComponentParentDimensionUndefined;
CGFloat parentHeight = (constrainedSize.min.height == constrainedSize.max.height) ? constrainedSize.min.height : kCKComponentParentDimensionUndefined;
CGFloat parentMainDimension = isHorizontalFlexboxDirection(_style.direction) ? parentWidth : parentHeight;
CGSize parentSize = CGSizeMake(parentWidth, parentHeight);
// Find the first and last relatively-positioned children,
// as we need to know them when we apply spacing as margin.
const auto firstRelativeChild = std::find_if(_children.cbegin(), _children.cend(), hasChildWithRelativePositioning);
const auto lastRelativeChild = ([&firstRelativeChild, children = &self->_children]() {
if (firstRelativeChild == children->cend()) {
return children->cend();
}
// We know we'll find a valid result here because we found firstRelativeChild.
const auto rFirstRelativeChild = std::make_reverse_iterator(firstRelativeChild);
const auto rLastRelativeChild = std::find_if(children->crbegin(), rFirstRelativeChild, hasChildWithRelativePositioning);
// Convert back to forward iterator
return rLastRelativeChild.base() - 1;
})();
for (auto iterator = _children.cbegin(); iterator != _children.cend(); ++iterator) {
const CKFlexboxComponentChild &child = *iterator;
if (!child.component) {
continue;
}
const YGNodeRef childNode = _style.useDeepYogaTrees ? [child.component ygNode:constrainedSize] : YGNodeNewWithConfig(ckYogaDefaultConfig());
// We add object only if there is actual used element
CKFlexboxChildCachedLayout *childLayout = [CKFlexboxChildCachedLayout new];
childLayout.component = child.component;
childLayout.componentLayout = {child.component, {0, 0}};
childLayout.widthMode = (YGMeasureMode) -1;
childLayout.heightMode = (YGMeasureMode) -1;
childLayout.parentSize = parentSize;
childLayout.zIndex = child.zIndex;
if (child.aspectRatio.isDefined()) {
YGNodeStyleSetAspectRatio(childNode, child.aspectRatio.aspectRatio());
}
// We pass the pointer ownership to context to release it later.
// We want cachedLayout to be alive until we've finished calculations
YGNodeSetContext(childNode, (__bridge_retained void *)childLayout);
if (YGNodeGetChildCount(childNode) == 0) {
YGNodeSetMeasureFunc(childNode, measureYGComponent);
}
if (_style.alignItems == CKFlexboxAlignItemsBaseline && [childLayout.component usesCustomBaseline]) {
YGNodeSetBaselineFunc(childNode, computeBaseline);
} else if (child.useHeightAsBaseline) {
YGNodeSetBaselineFunc(childNode, useHeightAsBaselineFunction);
}
// We need to make sure we do not include CKCompositeComponent
// size as node size, as it will always we equal to {} and
// use its child size instead
const auto nodeSize = [child.component nodeSize];
applySizeAttributes(childNode, child, nodeSize, parentWidth, parentHeight, setPercentOnChildNode(_style));
YGNodeStyleSetFlexGrow(childNode, child.flexGrow);
YGNodeStyleSetFlexShrink(childNode, child.flexShrink);
YGNodeStyleSetAlignSelf(childNode, ygAlignFromChild(child));
YGNodeStyleSetFlexBasis(childNode, child.flexBasis.resolve(YGUndefined, parentMainDimension));
// TODO: t18095186 Remove explicit opt-out when Yoga is going to move to opt-in for text rounding
YGNodeSetNodeType(childNode, child.useTextRounding ? YGNodeTypeText : YGNodeTypeDefault);
applyPositionToEdge(childNode, YGEdgeStart, child.position.start);
applyPositionToEdge(childNode, YGEdgeEnd, child.position.end);
applyPositionToEdge(childNode, YGEdgeTop, child.position.top);
applyPositionToEdge(childNode, YGEdgeBottom, child.position.bottom);
applyPositionToEdge(childNode, YGEdgeLeft, child.position.left);
applyPositionToEdge(childNode, YGEdgeRight, child.position.right);
applyPaddingToEdge(childNode, YGEdgeTop, child.padding.top);
applyPaddingToEdge(childNode, YGEdgeBottom, child.padding.bottom);
applyPaddingToEdge(childNode, YGEdgeStart, child.padding.start);
applyPaddingToEdge(childNode, YGEdgeEnd, child.padding.end);
YGNodeStyleSetPositionType(childNode, (child.position.type == CKFlexboxPositionTypeAbsolute) ? YGPositionTypeAbsolute : YGPositionTypeRelative);
// TODO: In odrer to keep the the logic consistent, we are resetting all
// the margins that were potentially set from the child's style in
// recursion. We will have to decide on the convention afterwards.
if (_style.useDeepYogaTrees) {
applyMarginToEdge(childNode, YGEdgeTop, convertFloatToYogaRepresentation(0));
applyMarginToEdge(childNode, YGEdgeBottom, convertFloatToYogaRepresentation(0));
applyMarginToEdge(childNode, YGEdgeStart, convertFloatToYogaRepresentation(0));
applyMarginToEdge(childNode, YGEdgeEnd, convertFloatToYogaRepresentation(0));
}
// Spacing emulation
// Stack layout defines spacing in terms of parent Spacing (used only between children) and
// spacingAfter / spacingBefore for every children
// Yoga defines spacing in terms of Parent padding and Child margin
// To avoid confusion for all children spacing is emulated with Start Margin
// We only use End Margin for the last child to emulate space between it and parent
if (child.position.type == CKFlexboxPositionTypeRelative) {
if (iterator != firstRelativeChild) {
// Children in the middle have margin = spacingBefore + spacingAfter of previous + spacing of parent
YGNodeStyleSetMargin(childNode, spacingEdge, convertFloatToYogaRepresentation(child.spacingBefore + _style.spacing + savedSpacing));
} else {
// For the space between parent and first child we just use spacingBefore
YGNodeStyleSetMargin(childNode, spacingEdge, convertFloatToYogaRepresentation(child.spacingBefore));
}
}
YGNodeInsertChild(stackNode, childNode, YGNodeGetChildCount(stackNode));
if (child.position.type == CKFlexboxPositionTypeRelative) {
savedSpacing = child.spacingAfter;
if (iterator == lastRelativeChild) {
// For the space between parent and last child we use only spacingAfter
YGNodeStyleSetMargin(childNode, ygSpacingEdgeFromDirection(_style.direction, YES), convertFloatToYogaRepresentation(savedSpacing));
}
}
/** The margins will override any spacing we applied earlier */
applyMarginToEdge(childNode, YGEdgeTop, child.margin.top);
applyMarginToEdge(childNode, YGEdgeBottom, child.margin.bottom);
applyMarginToEdge(childNode, YGEdgeStart, child.margin.start);
applyMarginToEdge(childNode, YGEdgeEnd, child.margin.end);
}
YGNodeStyleSetDirection(stackNode, ygDirectionFromStackStyle(_style));
YGNodeStyleSetFlexDirection(stackNode, ygFlexDirectionFromStackStyle(_style));
YGNodeStyleSetJustifyContent(stackNode, ygJustifyFromStackStyle(_style));
YGNodeStyleSetAlignItems(stackNode, ygAlignItemsFromStackStyle(_style));
YGNodeStyleSetAlignContent(stackNode, ygAlignContentFromStackStyle(_style));
YGNodeStyleSetFlexWrap(stackNode, ygWrapFromStackStyle(_style));
// TODO: t18095186 Remove explicit opt-out when Yoga is going to move to opt-in for text rounding
YGNodeSetNodeType(stackNode, YGNodeTypeDefault);
applyPaddingToEdge(stackNode, YGEdgeTop, _style.padding.top);
applyPaddingToEdge(stackNode, YGEdgeBottom, _style.padding.bottom);
applyPaddingToEdge(stackNode, YGEdgeStart, _style.padding.start);
applyPaddingToEdge(stackNode, YGEdgeEnd, _style.padding.end);
applyBorderToEdge(stackNode, YGEdgeTop, _style.border.top);
applyBorderToEdge(stackNode, YGEdgeBottom, _style.border.bottom);
applyBorderToEdge(stackNode, YGEdgeStart, _style.border.start);
applyBorderToEdge(stackNode, YGEdgeEnd, _style.border.end);
return stackNode;
}
static void applySizeAttribute(YGNodeRef node,
void(*percentFunc)(YGNodeRef, float),
void(*pointFunc)(YGNodeRef, float),
const RCRelativeDimension &childAttribute,
const RCRelativeDimension &nodeAttribute,
CGFloat parentValue,
BOOL setPercentOnChildNode)
{
switch (childAttribute.type()) {
case RCRelativeDimension::Type::PERCENT:
percentFunc(node, convertFloatToYogaRepresentation(childAttribute.value() * 100));
break;
case RCRelativeDimension::Type::POINTS:
pointFunc(node, convertFloatToYogaRepresentation(childAttribute.value()));
break;
case RCRelativeDimension::Type::AUTO:
if (setPercentOnChildNode) {
switch (nodeAttribute.type()) {
case RCRelativeDimension::Type::PERCENT:
percentFunc(node, convertFloatToYogaRepresentation(nodeAttribute.value() * 100));
break;
case RCRelativeDimension::Type::POINTS:
pointFunc(node, convertFloatToYogaRepresentation(nodeAttribute.value()));
break;
case RCRelativeDimension::Type::AUTO:
// Fall back to the component's size
const CGFloat value = nodeAttribute.resolve(YGUndefined, parentValue);
pointFunc(node, convertFloatToYogaRepresentation(value));
break;
}
} else {
// Fall back to the component's size
const CGFloat value = nodeAttribute.resolve(YGUndefined, parentValue);
pointFunc(node, convertFloatToYogaRepresentation(value));
}
break;
}
}
static void applySizeAttributes(YGNodeRef node,
const CKFlexboxComponentChild &child,
const RCComponentSize &nodeSize,
CGFloat parentWidth,
CGFloat parentHeight,
BOOL setPercentOnChildNode)
{
const RCComponentSize childSize = child.sizeConstraints;
applySizeAttribute(node, &YGNodeStyleSetWidthPercent, &YGNodeStyleSetWidth, childSize.width, nodeSize.width, parentWidth, setPercentOnChildNode);
applySizeAttribute(node, &YGNodeStyleSetHeightPercent, &YGNodeStyleSetHeight, childSize.height, nodeSize.height, parentHeight, setPercentOnChildNode);
applySizeAttribute(node, &YGNodeStyleSetMinWidthPercent, &YGNodeStyleSetMinWidth, childSize.minWidth, nodeSize.minWidth, parentWidth, setPercentOnChildNode);
applySizeAttribute(node, &YGNodeStyleSetMaxWidthPercent, &YGNodeStyleSetMaxWidth, childSize.maxWidth, nodeSize.maxWidth, parentWidth, setPercentOnChildNode);
applySizeAttribute(node, &YGNodeStyleSetMinHeightPercent, &YGNodeStyleSetMinHeight, childSize.minHeight, nodeSize.minHeight, parentHeight, setPercentOnChildNode);
applySizeAttribute(node, &YGNodeStyleSetMaxHeightPercent, &YGNodeStyleSetMaxHeight, childSize.maxHeight, nodeSize.maxHeight, parentHeight, setPercentOnChildNode);
}
static void applyPositionToEdge(YGNodeRef node, YGEdge edge, CKFlexboxDimension value)
{
RCRelativeDimension dimension = value.dimension();
switch (dimension.type()) {
case RCRelativeDimension::Type::PERCENT:
YGNodeStyleSetPositionPercent(node, edge, convertFloatToYogaRepresentation(dimension.value() * 100));
break;
case RCRelativeDimension::Type::POINTS:
YGNodeStyleSetPosition(node, edge, convertFloatToYogaRepresentation(dimension.value()));
break;
case RCRelativeDimension::Type::AUTO:
// no-op
break;
}
}
static void applyPaddingToEdge(YGNodeRef node, YGEdge edge, CKFlexboxDimension value)
{
if (value.isDefined() == false) {
return;
}
RCRelativeDimension dimension = value.dimension();
switch (dimension.type()) {
case RCRelativeDimension::Type::PERCENT:
YGNodeStyleSetPaddingPercent(node, edge, convertFloatToYogaRepresentation(dimension.value() * 100));
break;
case RCRelativeDimension::Type::POINTS:
YGNodeStyleSetPadding(node, edge, convertFloatToYogaRepresentation(dimension.value()));
break;
case RCRelativeDimension::Type::AUTO:
// no-op
break;
}
}
static void applyMarginToEdge(YGNodeRef node, YGEdge edge, CKFlexboxDimension value)
{
if (value.isDefined() == false) {
return;
}
RCRelativeDimension relativeDimension = value.dimension();
switch (relativeDimension.type()) {
case RCRelativeDimension::Type::PERCENT:
YGNodeStyleSetMarginPercent(node, edge, convertFloatToYogaRepresentation(relativeDimension.value() * 100));
break;
case RCRelativeDimension::Type::POINTS:
YGNodeStyleSetMargin(node, edge, convertFloatToYogaRepresentation(relativeDimension.value()));
break;
case RCRelativeDimension::Type::AUTO:
YGNodeStyleSetMarginAuto(node, edge);
break;
}
}
static void applyBorderToEdge(YGNodeRef node, YGEdge edge, CKFlexboxBorderDimension value)
{
if (value.isDefined() == false) {
return;
}
YGNodeStyleSetBorder(node, edge, convertFloatToYogaRepresentation(value.value()));
}
- (RCLayout)computeLayoutThatFits:(CKSizeRange)constrainedSize
{
const CKSizeRange sanitizedSizeRange = convertCKSizeRangeToYogaRepresentation(constrainedSize);
// We create cache for the duration of single calculation, so it is used only on one thread
// The cache is strictly internal and shouldn't be exposed in any way
// The purpose of the cache is to save calculations done in measure() function in Yoga to reuse
// for final layout
YGNodeRef layoutNode = [self ygNode:sanitizedSizeRange];
YGNodeCalculateLayout(layoutNode, YGUndefined, YGUndefined, YGDirectionLTR);
return [self layoutFromYgNode:layoutNode thatFits:constrainedSize];
}
- (RCLayout)layoutFromYgNode:(YGNodeRef)layoutNode thatFits:(CKSizeRange)constrainedSize
{
// Before we finalize layout we want to sort children according to their z-order
// We want children with higher z-order to be closer to the end of list
// They should be mounted later and thus shown on top of children with lower z-order const NSInteger childCount = YGNodeGetChildCount(layoutNode);
const NSInteger childCount = YGNodeGetChildCount(layoutNode);
std::vector<YGNodeRef> sortedChildNodes(childCount);
for (uint32_t i = 0; i < childCount; i++) {
sortedChildNodes[i] = YGNodeGetChild(layoutNode, i);
}
std::sort(sortedChildNodes.begin(), sortedChildNodes.end(),
[] (YGNodeRef const& a, YGNodeRef const& b) {
CKFlexboxChildCachedLayout *aCachedContext = (__bridge CKFlexboxChildCachedLayout *)YGNodeGetContext(a);
CKFlexboxChildCachedLayout *bCachedContext = (__bridge CKFlexboxChildCachedLayout *)YGNodeGetContext(b);
return aCachedContext.zIndex < bCachedContext.zIndex;
});
std::vector<RCLayoutChild> childrenLayout(childCount);
const float width = convertFloatToCKRepresentation(YGNodeLayoutGetWidth(layoutNode));
const float height = convertFloatToCKRepresentation(YGNodeLayoutGetHeight(layoutNode));
const CGSize size = {width, height};
for (NSUInteger i = 0; i < childCount; i++) {
// Get the layout for every child
const YGNodeRef childNode = sortedChildNodes[i];
const CGFloat childX = convertFloatToCKRepresentation(YGNodeLayoutGetLeft(childNode));
const CGFloat childY = convertFloatToCKRepresentation(YGNodeLayoutGetTop(childNode));
const CGFloat childWidth = convertFloatToCKRepresentation(YGNodeLayoutGetWidth(childNode));
const CGFloat childHeight = convertFloatToCKRepresentation(YGNodeLayoutGetHeight(childNode));
// Now we take back pointer ownership to be released, as we won't need it anymore
CKFlexboxChildCachedLayout *childCachedLayout = (__bridge_transfer CKFlexboxChildCachedLayout *)YGNodeGetContext(childNode);
childrenLayout[i].position = CGPointMake(childX, childY);
const CGSize childSize = CGSizeMake(childWidth, childHeight);
// We cache measurements for the duration of single layout calculation of FlexboxComponent
// ComponentKit and Yoga handle caching between calculations
if (_style.useDeepYogaTrees && [childCachedLayout.component isYogaBasedLayout]) {
// If the child component isYogaBasedLayout we don't call layoutThatFits:parentSize:
// because it will create another Yoga tree. Instead, we call layoutFromYgNode:thatFits:
// to reuse the already created yoga Node.
const CKSizeRange childRange = {childSize, childSize};
CKAssertSizeRange(childRange);
const CKSizeRange resolvedSizeRange = childCachedLayout.component.size.resolve(size);
CKAssertSizeRange(resolvedSizeRange);
const CKSizeRange childConstraintSize = childRange.intersect(resolvedSizeRange);
CKAssertSizeRange(childConstraintSize);
childrenLayout[i].layout = [childCachedLayout.component layoutFromYgNode:childNode thatFits:childConstraintSize];
} else if ([self canReuseCachedLayout:childCachedLayout forChildWithExactSize:childSize]) {
childrenLayout[i].layout = childCachedLayout.componentLayout;
} else {
const CKSizeRange childRange = {childSize, childSize};
CKAssertSizeRange(childRange);
childrenLayout[i].layout = CKComputeComponentLayout(childCachedLayout.component, childRange, size);
}
childrenLayout[i].layout.size = childSize;
}
YGNodeFreeRecursive(layoutNode);
// width/height should already be within constrainedSize, but we're just clamping to correct for roundoff error
return {self, constrainedSize.clamp(size), childrenLayout};
}
- (BOOL)canReuseCachedLayout:(const CKFlexboxChildCachedLayout * const)childCachedLayout
forChildWithExactSize:(const CGSize)childSize
{
return CKYogaNodeCanUseCachedMeasurement(YGMeasureModeExactly, static_cast<float>(childSize.width), YGMeasureModeExactly, static_cast<float>(childSize.height), childCachedLayout.widthMode, childCachedLayout.width, childCachedLayout.heightMode, childCachedLayout.height, static_cast<float>(childCachedLayout.componentLayout.size.width), static_cast<float>(childCachedLayout.componentLayout.size.height), 0, 0, ckYogaDefaultConfig()) ||
childSize.width == 0 ||
childSize.height == 0;
}
- (BOOL)isYogaBasedLayout
{
return YES;
}
- (YGNodeRef)ygNode:(CKSizeRange)constrainedSize
{
const YGNodeRef node = [self ygStackLayoutNode:constrainedSize];
// At the moment Yoga does not optimise minWidth == maxWidth, so we want to do it here
// ComponentKit and Yoga use different constants for +Inf, so we need to make sure the don't interfere
if (constrainedSize.min.width == constrainedSize.max.width) {
YGNodeStyleSetWidth(node, constrainedSize.min.width);
} else {
YGNodeStyleSetMinWidth(node, constrainedSize.min.width);
YGNodeStyleSetMaxWidth(node, constrainedSize.max.width);
}
if (constrainedSize.min.height == constrainedSize.max.height) {
YGNodeStyleSetHeight(node, constrainedSize.min.height);
} else {
YGNodeStyleSetMinHeight(node, constrainedSize.min.height);
YGNodeStyleSetMaxHeight(node, constrainedSize.max.height);
}
return node;
}
#pragma mark - CKMountable
- (unsigned int)numberOfChildren
{
return (unsigned int)_children.size();
}
- (id<CKMountable>)childAtIndex:(unsigned int)index
{
if (index < _children.size()) {
return _children[index].component;
}
RCFailAssertWithCategory(self.className, @"Index %u is out of bounds %lu", index, _children.size());
return nil;
}
- (NSArray<NSObject *> *)accessibilityChildren
{
std::vector<CKFlexboxComponentChild> sortedChildren;
// Use correct order
switch(_style.direction) {
case CKFlexboxDirectionRow:
case CKFlexboxDirectionColumn:
sortedChildren.insert(sortedChildren.end(), _children.begin(), _children.end());
break;
case CKFlexboxDirectionRowReverse:
case CKFlexboxDirectionColumnReverse:
sortedChildren.insert(sortedChildren.end(), _children.rbegin(), _children.rend());
break;
}
// Put higher z-index first
std::stable_sort(sortedChildren.begin(), sortedChildren.end(),
[](const CKFlexboxComponentChild &c1, const CKFlexboxComponentChild &c2){
return c1.accessibilitySortPriority > c2.accessibilitySortPriority ||
(c1.accessibilitySortPriority == c2.accessibilitySortPriority && c1.zIndex > c2.zIndex);
});
std::vector<CKComponent *> elements = CK::map(CK::filter(sortedChildren, [](const CKFlexboxComponentChild &c){
return c.component != nil;
}), [](const CKFlexboxComponentChild &c) -> CKComponent * {
return c.component;
});
return [[NSArray alloc] initWithObjects:&elements[0] count:elements.size()];
}
@end