Summary: Pull Request resolved: https://github.com/facebook/yoga/pull/1261 YogaKit integrates Yoga with UIKit as a higher level framework. It does not expose config setting to users. We set YGErrataClassic for now to prioritize compatibility instead of conformance (YogaKit is relatively used in fbsource as well). I'm also tempted to remove the usage of ExperimentalWebFlexBasis since last I heard rozelle thought it was generally broken, but I am a bit afraid to if it has been enabled so long, and is used in many cases in Meta. Reviewed By: yungsters Differential Revision: D45298803 fbshipit-source-id: 92f72148fafbdaffba4589c18c8b46591ca7c364
512 lines
20 KiB
Objective-C
512 lines
20 KiB
Objective-C
/*
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#import "UIView+Yoga.h"
|
|
#import "YGLayout+Private.h"
|
|
|
|
#define YG_PROPERTY(type, lowercased_name, capitalized_name) \
|
|
-(type)lowercased_name { \
|
|
return YGNodeStyleGet##capitalized_name(self.node); \
|
|
} \
|
|
\
|
|
-(void)set##capitalized_name : (type)lowercased_name { \
|
|
YGNodeStyleSet##capitalized_name(self.node, lowercased_name); \
|
|
}
|
|
|
|
#define YG_VALUE_PROPERTY(lowercased_name, capitalized_name) \
|
|
-(YGValue)lowercased_name { \
|
|
return YGNodeStyleGet##capitalized_name(self.node); \
|
|
} \
|
|
\
|
|
-(void)set##capitalized_name : (YGValue)lowercased_name { \
|
|
switch (lowercased_name.unit) { \
|
|
case YGUnitUndefined: \
|
|
YGNodeStyleSet##capitalized_name(self.node, lowercased_name.value); \
|
|
break; \
|
|
case YGUnitPoint: \
|
|
YGNodeStyleSet##capitalized_name(self.node, lowercased_name.value); \
|
|
break; \
|
|
case YGUnitPercent: \
|
|
YGNodeStyleSet##capitalized_name##Percent( \
|
|
self.node, lowercased_name.value); \
|
|
break; \
|
|
default: \
|
|
NSAssert(NO, @"Not implemented"); \
|
|
} \
|
|
}
|
|
|
|
#define YG_AUTO_VALUE_PROPERTY(lowercased_name, capitalized_name) \
|
|
-(YGValue)lowercased_name { \
|
|
return YGNodeStyleGet##capitalized_name(self.node); \
|
|
} \
|
|
\
|
|
-(void)set##capitalized_name : (YGValue)lowercased_name { \
|
|
switch (lowercased_name.unit) { \
|
|
case YGUnitPoint: \
|
|
YGNodeStyleSet##capitalized_name(self.node, lowercased_name.value); \
|
|
break; \
|
|
case YGUnitPercent: \
|
|
YGNodeStyleSet##capitalized_name##Percent( \
|
|
self.node, lowercased_name.value); \
|
|
break; \
|
|
case YGUnitAuto: \
|
|
YGNodeStyleSet##capitalized_name##Auto(self.node); \
|
|
break; \
|
|
default: \
|
|
NSAssert(NO, @"Not implemented"); \
|
|
} \
|
|
}
|
|
|
|
#define YG_EDGE_PROPERTY_GETTER( \
|
|
type, lowercased_name, capitalized_name, property, edge) \
|
|
-(type)lowercased_name { \
|
|
return YGNodeStyleGet##property(self.node, edge); \
|
|
}
|
|
|
|
#define YG_EDGE_PROPERTY_SETTER( \
|
|
lowercased_name, capitalized_name, property, edge) \
|
|
-(void)set##capitalized_name : (CGFloat)lowercased_name { \
|
|
YGNodeStyleSet##property(self.node, edge, lowercased_name); \
|
|
}
|
|
|
|
#define YG_EDGE_PROPERTY(lowercased_name, capitalized_name, property, edge) \
|
|
YG_EDGE_PROPERTY_GETTER( \
|
|
CGFloat, lowercased_name, capitalized_name, property, edge) \
|
|
YG_EDGE_PROPERTY_SETTER(lowercased_name, capitalized_name, property, edge)
|
|
|
|
#define YG_VALUE_EDGE_PROPERTY_SETTER( \
|
|
objc_lowercased_name, objc_capitalized_name, c_name, edge) \
|
|
-(void)set##objc_capitalized_name : (YGValue)objc_lowercased_name { \
|
|
switch (objc_lowercased_name.unit) { \
|
|
case YGUnitUndefined: \
|
|
YGNodeStyleSet##c_name(self.node, edge, objc_lowercased_name.value); \
|
|
break; \
|
|
case YGUnitPoint: \
|
|
YGNodeStyleSet##c_name(self.node, edge, objc_lowercased_name.value); \
|
|
break; \
|
|
case YGUnitPercent: \
|
|
YGNodeStyleSet##c_name##Percent( \
|
|
self.node, edge, objc_lowercased_name.value); \
|
|
break; \
|
|
default: \
|
|
NSAssert(NO, @"Not implemented"); \
|
|
} \
|
|
}
|
|
|
|
#define YG_VALUE_EDGE_PROPERTY( \
|
|
lowercased_name, capitalized_name, property, edge) \
|
|
YG_EDGE_PROPERTY_GETTER( \
|
|
YGValue, lowercased_name, capitalized_name, property, edge) \
|
|
YG_VALUE_EDGE_PROPERTY_SETTER( \
|
|
lowercased_name, capitalized_name, property, edge)
|
|
|
|
#define YG_VALUE_EDGES_PROPERTIES(lowercased_name, capitalized_name) \
|
|
YG_VALUE_EDGE_PROPERTY( \
|
|
lowercased_name##Left, \
|
|
capitalized_name##Left, \
|
|
capitalized_name, \
|
|
YGEdgeLeft) \
|
|
YG_VALUE_EDGE_PROPERTY( \
|
|
lowercased_name##Top, \
|
|
capitalized_name##Top, \
|
|
capitalized_name, \
|
|
YGEdgeTop) \
|
|
YG_VALUE_EDGE_PROPERTY( \
|
|
lowercased_name##Right, \
|
|
capitalized_name##Right, \
|
|
capitalized_name, \
|
|
YGEdgeRight) \
|
|
YG_VALUE_EDGE_PROPERTY( \
|
|
lowercased_name##Bottom, \
|
|
capitalized_name##Bottom, \
|
|
capitalized_name, \
|
|
YGEdgeBottom) \
|
|
YG_VALUE_EDGE_PROPERTY( \
|
|
lowercased_name##Start, \
|
|
capitalized_name##Start, \
|
|
capitalized_name, \
|
|
YGEdgeStart) \
|
|
YG_VALUE_EDGE_PROPERTY( \
|
|
lowercased_name##End, \
|
|
capitalized_name##End, \
|
|
capitalized_name, \
|
|
YGEdgeEnd) \
|
|
YG_VALUE_EDGE_PROPERTY( \
|
|
lowercased_name##Horizontal, \
|
|
capitalized_name##Horizontal, \
|
|
capitalized_name, \
|
|
YGEdgeHorizontal) \
|
|
YG_VALUE_EDGE_PROPERTY( \
|
|
lowercased_name##Vertical, \
|
|
capitalized_name##Vertical, \
|
|
capitalized_name, \
|
|
YGEdgeVertical) \
|
|
YG_VALUE_EDGE_PROPERTY( \
|
|
lowercased_name, capitalized_name, capitalized_name, YGEdgeAll)
|
|
|
|
YGValue YGPointValue(CGFloat value) {
|
|
return (YGValue){.value = value, .unit = YGUnitPoint};
|
|
}
|
|
|
|
YGValue YGPercentValue(CGFloat value) {
|
|
return (YGValue){.value = value, .unit = YGUnitPercent};
|
|
}
|
|
|
|
static YGConfigRef globalConfig;
|
|
|
|
@interface YGLayout ()
|
|
|
|
@property(nonatomic, weak, readonly) UIView* view;
|
|
@property(nonatomic, assign, readonly) BOOL isUIView;
|
|
|
|
@end
|
|
|
|
@implementation YGLayout
|
|
|
|
@synthesize isEnabled = _isEnabled;
|
|
@synthesize isIncludedInLayout = _isIncludedInLayout;
|
|
@synthesize node = _node;
|
|
|
|
+ (void)initialize {
|
|
globalConfig = YGConfigNew();
|
|
YGConfigSetExperimentalFeatureEnabled(
|
|
globalConfig, YGExperimentalFeatureWebFlexBasis, true);
|
|
YGConfigSetErrata(globalConfig, YGErrataClassic);
|
|
YGConfigSetPointScaleFactor(globalConfig, [UIScreen mainScreen].scale);
|
|
}
|
|
|
|
- (instancetype)initWithView:(UIView*)view {
|
|
if (self = [super init]) {
|
|
_view = view;
|
|
_node = YGNodeNewWithConfig(globalConfig);
|
|
YGNodeSetContext(_node, (__bridge void*)view);
|
|
_isEnabled = NO;
|
|
_isIncludedInLayout = YES;
|
|
_isUIView = [view isMemberOfClass:[UIView class]];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
YGNodeFree(self.node);
|
|
}
|
|
|
|
- (BOOL)isDirty {
|
|
return YGNodeIsDirty(self.node);
|
|
}
|
|
|
|
- (void)markDirty {
|
|
if (self.isDirty || !self.isLeaf) {
|
|
return;
|
|
}
|
|
|
|
// Yoga is not happy if we try to mark a node as "dirty" before we have set
|
|
// the measure function. Since we already know that this is a leaf,
|
|
// this *should* be fine. Forgive me Hack Gods.
|
|
const YGNodeRef node = self.node;
|
|
if (!YGNodeHasMeasureFunc(node)) {
|
|
YGNodeSetMeasureFunc(node, YGMeasureView);
|
|
}
|
|
|
|
YGNodeMarkDirty(node);
|
|
}
|
|
|
|
- (NSUInteger)numberOfChildren {
|
|
return YGNodeGetChildCount(self.node);
|
|
}
|
|
|
|
- (BOOL)isLeaf {
|
|
NSAssert(
|
|
[NSThread isMainThread],
|
|
@"This method must be called on the main thread.");
|
|
if (self.isEnabled) {
|
|
for (UIView* subview in self.view.subviews) {
|
|
YGLayout* const yoga = subview.yoga;
|
|
if (yoga.isEnabled && yoga.isIncludedInLayout) {
|
|
return NO;
|
|
}
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
#pragma mark - Style
|
|
|
|
- (YGPositionType)position {
|
|
return YGNodeStyleGetPositionType(self.node);
|
|
}
|
|
|
|
- (void)setPosition:(YGPositionType)position {
|
|
YGNodeStyleSetPositionType(self.node, position);
|
|
}
|
|
|
|
YG_PROPERTY(YGDirection, direction, Direction)
|
|
YG_PROPERTY(YGFlexDirection, flexDirection, FlexDirection)
|
|
YG_PROPERTY(YGJustify, justifyContent, JustifyContent)
|
|
YG_PROPERTY(YGAlign, alignContent, AlignContent)
|
|
YG_PROPERTY(YGAlign, alignItems, AlignItems)
|
|
YG_PROPERTY(YGAlign, alignSelf, AlignSelf)
|
|
YG_PROPERTY(YGWrap, flexWrap, FlexWrap)
|
|
YG_PROPERTY(YGOverflow, overflow, Overflow)
|
|
YG_PROPERTY(YGDisplay, display, Display)
|
|
|
|
YG_PROPERTY(CGFloat, flex, Flex)
|
|
YG_PROPERTY(CGFloat, flexGrow, FlexGrow)
|
|
YG_PROPERTY(CGFloat, flexShrink, FlexShrink)
|
|
YG_AUTO_VALUE_PROPERTY(flexBasis, FlexBasis)
|
|
|
|
YG_VALUE_EDGE_PROPERTY(left, Left, Position, YGEdgeLeft)
|
|
YG_VALUE_EDGE_PROPERTY(top, Top, Position, YGEdgeTop)
|
|
YG_VALUE_EDGE_PROPERTY(right, Right, Position, YGEdgeRight)
|
|
YG_VALUE_EDGE_PROPERTY(bottom, Bottom, Position, YGEdgeBottom)
|
|
YG_VALUE_EDGE_PROPERTY(start, Start, Position, YGEdgeStart)
|
|
YG_VALUE_EDGE_PROPERTY(end, End, Position, YGEdgeEnd)
|
|
YG_VALUE_EDGES_PROPERTIES(margin, Margin)
|
|
YG_VALUE_EDGES_PROPERTIES(padding, Padding)
|
|
|
|
YG_EDGE_PROPERTY(borderLeftWidth, BorderLeftWidth, Border, YGEdgeLeft)
|
|
YG_EDGE_PROPERTY(borderTopWidth, BorderTopWidth, Border, YGEdgeTop)
|
|
YG_EDGE_PROPERTY(borderRightWidth, BorderRightWidth, Border, YGEdgeRight)
|
|
YG_EDGE_PROPERTY(borderBottomWidth, BorderBottomWidth, Border, YGEdgeBottom)
|
|
YG_EDGE_PROPERTY(borderStartWidth, BorderStartWidth, Border, YGEdgeStart)
|
|
YG_EDGE_PROPERTY(borderEndWidth, BorderEndWidth, Border, YGEdgeEnd)
|
|
YG_EDGE_PROPERTY(borderWidth, BorderWidth, Border, YGEdgeAll)
|
|
|
|
YG_AUTO_VALUE_PROPERTY(width, Width)
|
|
YG_AUTO_VALUE_PROPERTY(height, Height)
|
|
YG_VALUE_PROPERTY(minWidth, MinWidth)
|
|
YG_VALUE_PROPERTY(minHeight, MinHeight)
|
|
YG_VALUE_PROPERTY(maxWidth, MaxWidth)
|
|
YG_VALUE_PROPERTY(maxHeight, MaxHeight)
|
|
YG_PROPERTY(CGFloat, aspectRatio, AspectRatio)
|
|
|
|
YG_EDGE_PROPERTY(columnGap, ColumnGap, Gap, YGGutterColumn)
|
|
YG_EDGE_PROPERTY(rowGap, RowGap, Gap, YGGutterRow)
|
|
YG_EDGE_PROPERTY(gap, Gap, Gap, YGGutterAll)
|
|
|
|
#pragma mark - Layout and Sizing
|
|
|
|
- (YGDirection)resolvedDirection {
|
|
return YGNodeLayoutGetDirection(self.node);
|
|
}
|
|
|
|
- (void)applyLayout {
|
|
[self calculateLayoutWithSize:self.view.bounds.size];
|
|
YGApplyLayoutToViewHierarchy(self.view, NO);
|
|
}
|
|
|
|
- (void)applyLayoutPreservingOrigin:(BOOL)preserveOrigin {
|
|
[self calculateLayoutWithSize:self.view.bounds.size];
|
|
YGApplyLayoutToViewHierarchy(self.view, preserveOrigin);
|
|
}
|
|
|
|
- (void)applyLayoutPreservingOrigin:(BOOL)preserveOrigin
|
|
dimensionFlexibility:
|
|
(YGDimensionFlexibility)dimensionFlexibility {
|
|
CGSize size = self.view.bounds.size;
|
|
if (dimensionFlexibility & YGDimensionFlexibilityFlexibleWidth) {
|
|
size.width = YGUndefined;
|
|
}
|
|
if (dimensionFlexibility & YGDimensionFlexibilityFlexibleHeight) {
|
|
size.height = YGUndefined;
|
|
}
|
|
[self calculateLayoutWithSize:size];
|
|
YGApplyLayoutToViewHierarchy(self.view, preserveOrigin);
|
|
}
|
|
|
|
- (CGSize)intrinsicSize {
|
|
const CGSize constrainedSize = {
|
|
.width = YGUndefined,
|
|
.height = YGUndefined,
|
|
};
|
|
return [self calculateLayoutWithSize:constrainedSize];
|
|
}
|
|
|
|
- (CGSize)calculateLayoutWithSize:(CGSize)size {
|
|
NSAssert([NSThread isMainThread], @"Yoga calculation must be done on main.");
|
|
NSAssert(self.isEnabled, @"Yoga is not enabled for this view.");
|
|
|
|
YGAttachNodesFromViewHierachy(self.view);
|
|
|
|
const YGNodeRef node = self.node;
|
|
YGNodeCalculateLayout(
|
|
node, size.width, size.height, YGNodeStyleGetDirection(node));
|
|
|
|
return (CGSize){
|
|
.width = YGNodeLayoutGetWidth(node),
|
|
.height = YGNodeLayoutGetHeight(node),
|
|
};
|
|
}
|
|
|
|
#pragma mark - Private
|
|
|
|
static YGSize YGMeasureView(
|
|
YGNodeRef node,
|
|
float width,
|
|
YGMeasureMode widthMode,
|
|
float height,
|
|
YGMeasureMode heightMode) {
|
|
const CGFloat constrainedWidth =
|
|
(widthMode == YGMeasureModeUndefined) ? CGFLOAT_MAX : width;
|
|
const CGFloat constrainedHeight =
|
|
(heightMode == YGMeasureModeUndefined) ? CGFLOAT_MAX : height;
|
|
|
|
UIView* view = (__bridge UIView*)YGNodeGetContext(node);
|
|
CGSize sizeThatFits = CGSizeZero;
|
|
|
|
// The default implementation of sizeThatFits: returns the existing size of
|
|
// the view. That means that if we want to layout an empty UIView, which
|
|
// already has got a frame set, its measured size should be CGSizeZero, but
|
|
// UIKit returns the existing size.
|
|
//
|
|
// See https://github.com/facebook/yoga/issues/606 for more information.
|
|
if (!view.yoga.isUIView || [view.subviews count] > 0) {
|
|
sizeThatFits = [view sizeThatFits:(CGSize){
|
|
.width = constrainedWidth,
|
|
.height = constrainedHeight,
|
|
}];
|
|
}
|
|
|
|
return (YGSize){
|
|
.width = YGSanitizeMeasurement(
|
|
constrainedWidth, sizeThatFits.width, widthMode),
|
|
.height = YGSanitizeMeasurement(
|
|
constrainedHeight, sizeThatFits.height, heightMode),
|
|
};
|
|
}
|
|
|
|
static CGFloat YGSanitizeMeasurement(
|
|
CGFloat constrainedSize,
|
|
CGFloat measuredSize,
|
|
YGMeasureMode measureMode) {
|
|
CGFloat result;
|
|
if (measureMode == YGMeasureModeExactly) {
|
|
result = constrainedSize;
|
|
} else if (measureMode == YGMeasureModeAtMost) {
|
|
result = MIN(constrainedSize, measuredSize);
|
|
} else {
|
|
result = measuredSize;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static BOOL YGNodeHasExactSameChildren(
|
|
const YGNodeRef node,
|
|
NSArray<UIView*>* subviews) {
|
|
if (YGNodeGetChildCount(node) != subviews.count) {
|
|
return NO;
|
|
}
|
|
|
|
for (int i = 0; i < subviews.count; i++) {
|
|
if (YGNodeGetChild(node, i) != subviews[i].yoga.node) {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
static void YGAttachNodesFromViewHierachy(UIView* const view) {
|
|
YGLayout* const yoga = view.yoga;
|
|
const YGNodeRef node = yoga.node;
|
|
|
|
// Only leaf nodes should have a measure function
|
|
if (yoga.isLeaf) {
|
|
YGRemoveAllChildren(node);
|
|
YGNodeSetMeasureFunc(node, YGMeasureView);
|
|
} else {
|
|
YGNodeSetMeasureFunc(node, NULL);
|
|
|
|
NSMutableArray<UIView*>* subviewsToInclude =
|
|
[[NSMutableArray alloc] initWithCapacity:view.subviews.count];
|
|
for (UIView* subview in view.subviews) {
|
|
if (subview.yoga.isEnabled && subview.yoga.isIncludedInLayout) {
|
|
[subviewsToInclude addObject:subview];
|
|
}
|
|
}
|
|
|
|
if (!YGNodeHasExactSameChildren(node, subviewsToInclude)) {
|
|
YGRemoveAllChildren(node);
|
|
for (int i = 0; i < subviewsToInclude.count; i++) {
|
|
YGNodeInsertChild(node, subviewsToInclude[i].yoga.node, i);
|
|
}
|
|
}
|
|
|
|
for (UIView* const subview in subviewsToInclude) {
|
|
YGAttachNodesFromViewHierachy(subview);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void YGRemoveAllChildren(const YGNodeRef node) {
|
|
if (node == NULL) {
|
|
return;
|
|
}
|
|
|
|
YGNodeRemoveAllChildren(node);
|
|
}
|
|
|
|
static CGFloat YGRoundPixelValue(CGFloat value) {
|
|
static CGFloat scale;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^() {
|
|
scale = [UIScreen mainScreen].scale;
|
|
});
|
|
|
|
return roundf(value * scale) / scale;
|
|
}
|
|
|
|
static void YGApplyLayoutToViewHierarchy(UIView* view, BOOL preserveOrigin) {
|
|
NSCAssert(
|
|
[NSThread isMainThread],
|
|
@"Framesetting should only be done on the main thread.");
|
|
|
|
const YGLayout* yoga = view.yoga;
|
|
|
|
if (!yoga.isIncludedInLayout) {
|
|
return;
|
|
}
|
|
|
|
YGNodeRef node = yoga.node;
|
|
const CGPoint topLeft = {
|
|
YGNodeLayoutGetLeft(node),
|
|
YGNodeLayoutGetTop(node),
|
|
};
|
|
|
|
const CGPoint bottomRight = {
|
|
topLeft.x + YGNodeLayoutGetWidth(node),
|
|
topLeft.y + YGNodeLayoutGetHeight(node),
|
|
};
|
|
|
|
const CGPoint origin = preserveOrigin ? view.frame.origin : CGPointZero;
|
|
view.frame = (CGRect){
|
|
.origin =
|
|
{
|
|
.x = YGRoundPixelValue(topLeft.x + origin.x),
|
|
.y = YGRoundPixelValue(topLeft.y + origin.y),
|
|
},
|
|
.size =
|
|
{
|
|
.width = YGRoundPixelValue(bottomRight.x) -
|
|
YGRoundPixelValue(topLeft.x),
|
|
.height = YGRoundPixelValue(bottomRight.y) -
|
|
YGRoundPixelValue(topLeft.y),
|
|
},
|
|
};
|
|
|
|
if (!yoga.isLeaf) {
|
|
for (NSUInteger i = 0; i < view.subviews.count; i++) {
|
|
YGApplyLayoutToViewHierarchy(view.subviews[i], NO);
|
|
}
|
|
}
|
|
}
|
|
|
|
@end
|