From 8939bcb96db74e671b71fce3ec036dc4883bab14 Mon Sep 17 00:00:00 2001 From: Emil Sjolander Date: Wed, 12 Oct 2016 09:32:40 -0700 Subject: [PATCH] UIView category Summary: Add flexbox support to UIViews via a category Reviewed By: dshahidehpour Differential Revision: D4002873 fbshipit-source-id: f89de3acdf8fd89c7801918dcad34ba9011dd025 --- uikit/CSSLayout/BUCK | 18 ++ uikit/CSSLayout/UIView+CSSLayout.h | 51 ++++++ uikit/CSSLayout/UIView+CSSLayout.m | 281 +++++++++++++++++++++++++++++ 3 files changed, 350 insertions(+) create mode 100644 uikit/CSSLayout/BUCK create mode 100644 uikit/CSSLayout/UIView+CSSLayout.h create mode 100644 uikit/CSSLayout/UIView+CSSLayout.m diff --git a/uikit/CSSLayout/BUCK b/uikit/CSSLayout/BUCK new file mode 100644 index 00000000..1167a8b6 --- /dev/null +++ b/uikit/CSSLayout/BUCK @@ -0,0 +1,18 @@ +# 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. + +include_defs('//CSSLAYOUT_DEFS') + +apple_library( + name = 'CSSLayout', + srcs = glob(['*.m']), + exported_headers = glob(['*.h']), + deps = [ + csslayout_dep(':CSSLayout'), + ], + visibility = ['PUBLIC'], +) diff --git a/uikit/CSSLayout/UIView+CSSLayout.h b/uikit/CSSLayout/UIView+CSSLayout.h new file mode 100644 index 00000000..e17948b5 --- /dev/null +++ b/uikit/CSSLayout/UIView+CSSLayout.h @@ -0,0 +1,51 @@ +/** + * 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 +#import + +@interface UIView (CSSLayout) + +- (void)css_setUsesFlexbox:(BOOL)enabled; +- (BOOL)css_usesFlexbox; + +- (void)css_setDirection:(CSSDirection)direction; +- (void)css_setFlexDirection:(CSSFlexDirection)flexDirection; +- (void)css_setJustifyContent:(CSSJustify)justifyContent; +- (void)css_setAlignContent:(CSSAlign)alignContent; +- (void)css_setAlignItems:(CSSAlign)alignItems; +- (void)css_setAlignSelf:(CSSAlign)alignSelf; +- (void)css_setPositionType:(CSSPositionType)positionType; +- (void)css_setFlexWrap:(CSSWrapType)flexWrap; +- (void)css_setOverflow:(CSSOverflow)overflow; + +- (void)css_setFlex:(CGFloat)flex; +- (void)css_setFlexGrow:(CGFloat)flexGrow; +- (void)css_setFlexShrink:(CGFloat)flexShrink; +- (void)css_setFlexBasis:(CGFloat)flexBasis; + +- (void)css_setPosition:(CGFloat)position forEdge:(CSSEdge)edge; +- (void)css_setMargin:(CGFloat)margin forEdge:(CSSEdge)edge; +- (void)css_setPadding:(CGFloat)padding forEdge:(CSSEdge)edge; +- (void)css_setBorder:(CGFloat)border forEdge:(CSSEdge)edge; + +- (void)css_setWidth:(CGFloat)width; +- (void)css_setHeight:(CGFloat)height; +- (void)css_setMinWidth:(CGFloat)minWidth; +- (void)css_setMinHeight:(CGFloat)minHeight; +- (void)css_setMaxWidth:(CGFloat)maxWidth; +- (void)css_setMaxHeight:(CGFloat)maxHeight; + +// Get the resolved direction of this node. This won't be CSSDirectionInherit +- (CSSDirection)css_resolvedDirection; + +// Perform a layout calculation and update the frames of the views in the hierarchy with th results +- (void)css_applyLayout; + +@end diff --git a/uikit/CSSLayout/UIView+CSSLayout.m b/uikit/CSSLayout/UIView+CSSLayout.m new file mode 100644 index 00000000..194e08ca --- /dev/null +++ b/uikit/CSSLayout/UIView+CSSLayout.m @@ -0,0 +1,281 @@ +/** + * 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 "UIView+CSSLayout.h" + +#import + +static CSSSize _measure( + void *context, + float width, + CSSMeasureMode widthMode, + float height, + CSSMeasureMode heightMode) +{ + UIView *view = (__bridge UIView*) context; + CGSize sizeThatFits = [view sizeThatFits:(CGSize) { + .width = widthMode == CSSMeasureModeUndefined ? CGFLOAT_MAX : width, + .height = heightMode == CSSMeasureModeUndefined ? CGFLOAT_MAX : height, + }]; + + return (CSSSize) { + .width = widthMode == CSSMeasureModeExactly ? width : sizeThatFits.width, + .height = heightMode == CSSMeasureModeExactly ? height : sizeThatFits.height, + }; +} + +static void _attachNodesRecursive(UIView *view); +static void _updateFrameRecursive(UIView *view); + +@interface CSSNodeBridge : NSObject +@property (nonatomic, assign) CSSNodeRef cnode; +@end + +@implementation CSSNodeBridge +- (instancetype)init +{ + if ([super init]) { + _cnode = CSSNodeNew(); + } + + return self; +} + +- (void)dealloc +{ + [super dealloc]; + CSSNodeFree(_cnode); +} +@end + +@implementation UIView (CSSLayout) + +- (CSSNodeRef)cssNode +{ + CSSNodeBridge *node = objc_getAssociatedObject(self, @selector(cssNode)); + if (!node) { + node = [CSSNodeBridge new]; + CSSNodeSetContext(node.cnode, (__bridge void *) self); + objc_setAssociatedObject(self, @selector(cssNode), node, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + + return node.cnode; +} + +- (void)css_setUsesFlexbox:(BOOL)enabled +{ + objc_setAssociatedObject( + self, + @selector(css_usesFlexbox), + @(enabled), + OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (BOOL)css_usesFlexbox +{ + if (objc_getAssociatedObject(self, @selector(css_usesFlexbox))) { + return YES; + } else { + return NO; + } +} + +- (void)css_setDirection:(CSSDirection)direction +{ + CSSNodeStyleSetDirection([self cssNode], direction); +} + +- (void)css_setFlexDirection:(CSSFlexDirection)flexDirection +{ + CSSNodeStyleSetFlexDirection([self cssNode], flexDirection); +} + +- (void)css_setJustifyContent:(CSSJustify)justifyContent +{ + CSSNodeStyleSetJustifyContent([self cssNode], justifyContent); +} + +- (void)css_setAlignContent:(CSSAlign)alignContent +{ + CSSNodeStyleSetAlignContent([self cssNode], alignContent); +} + +- (void)css_setAlignItems:(CSSAlign)alignItems +{ + CSSNodeStyleSetAlignItems([self cssNode], alignItems); +} + +- (void)css_setAlignSelf:(CSSAlign)alignSelf +{ + CSSNodeStyleSetAlignSelf([self cssNode], alignSelf); +} + +- (void)css_setPositionType:(CSSPositionType)positionType +{ + CSSNodeStyleSetPositionType([self cssNode], positionType); +} + +- (void)css_setFlexWrap:(CSSWrapType)flexWrap +{ + CSSNodeStyleSetFlexWrap([self cssNode], flexWrap); +} + +- (void)css_setOverflow:(CSSOverflow)overflow +{ + CSSNodeStyleSetOverflow([self cssNode], overflow); +} + +- (void)css_setFlex:(CGFloat)flex +{ + CSSNodeStyleSetFlex([self cssNode], flex); +} + +- (void)css_setFlexGrow:(CGFloat)flexGrow +{ + CSSNodeStyleSetFlexGrow([self cssNode], flexGrow); +} + +- (void)css_setFlexShrink:(CGFloat)flexShrink +{ + CSSNodeStyleSetFlexShrink([self cssNode], flexShrink); +} + +- (void)css_setFlexBasis:(CGFloat)flexBasis +{ + CSSNodeStyleSetFlexBasis([self cssNode], flexBasis); +} + +- (void)css_setPosition:(CGFloat)position forEdge:(CSSEdge)edge +{ + CSSNodeStyleSetPosition([self cssNode], edge, position); +} + +- (void)css_setMargin:(CGFloat)margin forEdge:(CSSEdge)edge +{ + CSSNodeStyleSetMargin([self cssNode], edge, margin); +} + +- (void)css_setPadding:(CGFloat)padding forEdge:(CSSEdge)edge +{ + CSSNodeStyleSetPadding([self cssNode], edge, padding); +} + +- (void)css_setBorder:(CGFloat)border forEdge:(CSSEdge)edge +{ + CSSNodeStyleSetBorder([self cssNode], edge, border); +} + +- (void)css_setWidth:(CGFloat)width +{ + CSSNodeStyleSetWidth([self cssNode], width); +} + +- (void)css_setHeight:(CGFloat)height +{ + CSSNodeStyleSetHeight([self cssNode], height); +} + +- (void)css_setMinWidth:(CGFloat)minWidth +{ + CSSNodeStyleSetMinWidth([self cssNode], minWidth); +} + +- (void)css_setMinHeight:(CGFloat)minHeight +{ + CSSNodeStyleSetMinHeight([self cssNode], minHeight); +} + +- (void)css_setMaxWidth:(CGFloat)maxWidth +{ + CSSNodeStyleSetMaxWidth([self cssNode], maxWidth); +} + +- (void)css_setMaxHeight:(CGFloat)maxHeight +{ + CSSNodeStyleSetMaxHeight([self cssNode], maxHeight); +} + +- (CSSDirection)css_resolvedDirection +{ + return CSSNodeLayoutGetDirection([self cssNode]); +} + +- (void)css_applyLayout +{ + NSAssert([NSThread isMainThread], @"Method called using a thread other than main!"); + NSAssert([self css_usesFlexbox], @"css_applyLayout must be called on a node using css styling!"); + + _attachNodesRecursive(self); + + CSSNodeRef node = [self cssNode]; + CSSNodeCalculateLayout( + [self cssNode], + CSSNodeStyleGetWidth(node), + CSSNodeStyleGetHeight(node), + CSSNodeStyleGetDirection(node)); + + _updateFrameRecursive(self); +} + +@end + +static void _attachNodesRecursive(UIView *view) { + CSSNodeRef node = [view cssNode]; + const BOOL usesFlexbox = [view css_usesFlexbox]; + const BOOL isLeaf = !usesFlexbox || view.subviews.count == 0; + + // Only leaf nodes should have a measure function + if (isLeaf) { + CSSNodeSetMeasureFunc(node, _measure); + + // Clear any children + while (CSSNodeChildCount(node) > 0) { + CSSNodeRemoveChild(node, CSSNodeGetChild(node, 0)); + } + } else { + CSSNodeSetMeasureFunc(node, NULL); + + // Add any children which were added since the last call to css_applyLayout + for (NSUInteger i = 0; i < view.subviews.count; i++) { + CSSNodeRef childNode = [view.subviews[i] cssNode]; + if (CSSNodeGetChild(node, i) != childNode) { + CSSNodeInsertChild(node, childNode, i); + } + _attachNodesRecursive(view.subviews[i]); + } + + // Remove any children which were removed since the last call to css_applyLayout + while (view.subviews.count < CSSNodeChildCount(node)) { + CSSNodeRemoveChild(node, CSSNodeGetChild(node, CSSNodeChildCount(node) - 1)); + } + } +} + +static void _updateFrameRecursive(UIView *view) { + CSSNodeRef node = [view cssNode]; + const BOOL usesFlexbox = [view css_usesFlexbox]; + const BOOL isLeaf = !usesFlexbox || view.subviews.count == 0; + + view.frame = (CGRect) { + .origin = { + .x = CSSNodeLayoutGetLeft(node), + .y = CSSNodeLayoutGetTop(node), + }, + .size = { + .width = CSSNodeLayoutGetWidth(node), + .height = CSSNodeLayoutGetHeight(node), + }, + }; + + if (!isLeaf) { + for (NSUInteger i = 0; i < view.subviews.count; i++) { + _updateFrameRecursive(view.subviews[i]); + } + } +}