519 lines
17 KiB
Objective-C
519 lines
17 KiB
Objective-C
// Copyright 2004-present Facebook. All Rights Reserved.
|
|
|
|
#import "RCTShadowView.h"
|
|
|
|
#import "RCTConvert.h"
|
|
#import "RCTLog.h"
|
|
#import "RCTSparseArray.h"
|
|
#import "RCTUtils.h"
|
|
|
|
typedef void (^RCTActionBlock)(RCTShadowView *shadowViewSelf, id value);
|
|
typedef void (^RCTResetActionBlock)(RCTShadowView *shadowViewSelf);
|
|
|
|
@interface RCTLayoutAction : NSObject
|
|
|
|
@property (nonatomic, readwrite, copy) RCTActionBlock block;
|
|
@property (nonatomic, readwrite, copy) RCTResetActionBlock resetBlock;
|
|
@property (nonatomic, readwrite, assign) NSInteger precedence;
|
|
|
|
@end
|
|
|
|
@implementation RCTLayoutAction @end
|
|
|
|
#define ACTION_FOR_KEY_DEFAULT(name, default, blockIn) \
|
|
do { \
|
|
RCTLayoutAction *action = [[RCTLayoutAction alloc] init]; \
|
|
action.block = blockIn; \
|
|
action.resetBlock = ^(id idSelf1) { \
|
|
blockIn(idSelf1, default); \
|
|
}; \
|
|
actions[@"" #name ""] = action; \
|
|
} while(0)
|
|
|
|
#define ACTION_FOR_KEY(name, blockIn) \
|
|
ACTION_FOR_KEY_DEFAULT(name, @([defaultShadowView name]), (blockIn))
|
|
|
|
#define ACTION_FOR_FLOAT_KEY_DEFAULT(name, default, blockIn) \
|
|
ACTION_FOR_KEY_DEFAULT(name, @(default), ^(id idSelf2, NSNumber *n) { \
|
|
if (isnan([n floatValue])) { \
|
|
RCTLogWarn(@"Got NaN for `"#name"` prop, ignoring"); \
|
|
return; \
|
|
} \
|
|
blockIn(idSelf2, RCTNumberToFloat(n)); \
|
|
});
|
|
|
|
#define ACTION_FOR_FLOAT_KEY(name, blockIn) \
|
|
ACTION_FOR_FLOAT_KEY_DEFAULT(name, [defaultShadowView name], (blockIn))
|
|
|
|
#define ACTION_FOR_DEFAULT_UNDEFINED_KEY(name, blockIn) \
|
|
ACTION_FOR_KEY_DEFAULT(name, nil, ^(id idSelf2, NSNumber *n) { \
|
|
blockIn(idSelf2, n == nil ? CSS_UNDEFINED : [n floatValue]); \
|
|
});
|
|
|
|
#define MAX_TREE_DEPTH 30
|
|
|
|
const NSString *const RCTBackgroundColorProp = @"backgroundColor";
|
|
|
|
typedef enum {
|
|
META_PROP_LEFT,
|
|
META_PROP_TOP,
|
|
META_PROP_RIGHT,
|
|
META_PROP_BOTTOM,
|
|
META_PROP_HORIZONTAL,
|
|
META_PROP_VERTICAL,
|
|
META_PROP_ALL,
|
|
META_PROP_COUNT,
|
|
} meta_prop_t;
|
|
|
|
@interface RCTShadowView()
|
|
{
|
|
float _paddingMetaProps[META_PROP_COUNT];
|
|
float _marginMetaProps[META_PROP_COUNT];
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation RCTShadowView
|
|
{
|
|
RCTPropagationLifecycle _propagationLifecycle;
|
|
RCTTextLifecycle _textLifecycle;
|
|
NSDictionary *_lastParentProperties;
|
|
NSMutableArray *_reactSubviews;
|
|
BOOL _recomputePadding;
|
|
BOOL _recomputeMargin;
|
|
}
|
|
|
|
@synthesize reactTag = _reactTag;
|
|
|
|
// css_node api
|
|
|
|
static void RCTPrint(void *context)
|
|
{
|
|
RCTShadowView *shadowView = (__bridge RCTShadowView *)context;
|
|
printf("%s(%zd), ", [[shadowView moduleName] UTF8String], [[shadowView reactTag] integerValue]);
|
|
}
|
|
|
|
static css_node_t *RCTGetChild(void *context, int i)
|
|
{
|
|
RCTShadowView *shadowView = (__bridge RCTShadowView *)context;
|
|
RCTShadowView *child = [shadowView reactSubviews][i];
|
|
return child->_cssNode;
|
|
}
|
|
|
|
static bool RCTIsDirty(void *context)
|
|
{
|
|
RCTShadowView *shadowView = (__bridge RCTShadowView *)context;
|
|
return shadowView.layoutLifecycle != RCTLayoutLifecycleComputed;
|
|
}
|
|
|
|
// Enforces precedence rules, e.g. marginLeft > marginHorizontal > margin.
|
|
static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float style[CSS_POSITION_COUNT]) {
|
|
style[CSS_LEFT] = !isUndefined(metaProps[META_PROP_LEFT]) ? metaProps[META_PROP_LEFT]
|
|
: !isUndefined(metaProps[META_PROP_HORIZONTAL]) ? metaProps[META_PROP_HORIZONTAL]
|
|
: !isUndefined(metaProps[META_PROP_ALL]) ? metaProps[META_PROP_ALL]
|
|
: 0;
|
|
style[CSS_RIGHT] = !isUndefined(metaProps[META_PROP_RIGHT]) ? metaProps[META_PROP_RIGHT]
|
|
: !isUndefined(metaProps[META_PROP_HORIZONTAL]) ? metaProps[META_PROP_HORIZONTAL]
|
|
: !isUndefined(metaProps[META_PROP_ALL]) ? metaProps[META_PROP_ALL]
|
|
: 0;
|
|
style[CSS_TOP] = !isUndefined(metaProps[META_PROP_TOP]) ? metaProps[META_PROP_TOP]
|
|
: !isUndefined(metaProps[META_PROP_VERTICAL]) ? metaProps[META_PROP_VERTICAL]
|
|
: !isUndefined(metaProps[META_PROP_ALL]) ? metaProps[META_PROP_ALL]
|
|
: 0;
|
|
style[CSS_BOTTOM] = !isUndefined(metaProps[META_PROP_BOTTOM]) ? metaProps[META_PROP_BOTTOM]
|
|
: !isUndefined(metaProps[META_PROP_VERTICAL]) ? metaProps[META_PROP_VERTICAL]
|
|
: !isUndefined(metaProps[META_PROP_ALL]) ? metaProps[META_PROP_ALL]
|
|
: 0;
|
|
}
|
|
|
|
- (UIEdgeInsets)paddingAsInsets
|
|
{
|
|
return (UIEdgeInsets){
|
|
_cssNode->style.padding[CSS_TOP],
|
|
_cssNode->style.padding[CSS_LEFT],
|
|
_cssNode->style.padding[CSS_BOTTOM],
|
|
_cssNode->style.padding[CSS_RIGHT]
|
|
};
|
|
}
|
|
|
|
- (void)fillCSSNode:(css_node_t *)node
|
|
{
|
|
node->children_count = (int)_reactSubviews.count;
|
|
}
|
|
|
|
- (void)applyLayoutNode:(css_node_t *)node viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame
|
|
{
|
|
[self _applyLayoutNode:node viewsWithNewFrame:viewsWithNewFrame absolutePosition:CGPointZero];
|
|
}
|
|
|
|
// The absolute stuff is so that we can take into account our absolute position when rounding in order to
|
|
// snap to the pixel grid. For example, say you have the following structure:
|
|
//
|
|
// +--------+---------+--------+
|
|
// | |+-------+| |
|
|
// | || || |
|
|
// | |+-------+| |
|
|
// +--------+---------+--------+
|
|
//
|
|
// Say the screen width is 320 pts so the three big views will get the following x bounds from our layout system:
|
|
// {0, 106.667}, {106.667, 213.333}, {213.333, 320}
|
|
//
|
|
// Assuming screen scale is 2, these numbers must be rounded to the nearest 0.5 to fit the pixel grid:
|
|
// {0, 106.5}, {106.5, 213.5}, {213.5, 320}
|
|
// You'll notice that the three widths are 106.5, 107, 106.5.
|
|
//
|
|
// This is great for the parent views but it gets trickier when we consider rounding for the subview.
|
|
//
|
|
// When we go to round the bounds for the subview in the middle, it's relative bounds are {0, 106.667}
|
|
// which gets rounded to {0, 106.5}. This will cause the subview to be one pixel smaller than it should be.
|
|
// this is why we need to pass in the absolute position in order to do the rounding relative to the screen's
|
|
// grid rather than the view's grid.
|
|
//
|
|
// After passing in the absolutePosition of {106.667, y}, we do the following calculations:
|
|
// absoluteLeft = round(absolutePosition.x + viewPosition.left) = round(106.667 + 0) = 106.5
|
|
// absoluteRight = round(absolutePosition.x + viewPosition.left + viewSize.left) + round(106.667 + 0 + 106.667) = 213.5
|
|
// width = 213.5 - 106.5 = 107
|
|
// You'll notice that this is the same width we calculated for the parent view because we've taken its position into account.
|
|
|
|
- (void)_applyLayoutNode:(css_node_t *)node viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame absolutePosition:(CGPoint)absolutePosition
|
|
{
|
|
if (!node->layout.should_update) {
|
|
return;
|
|
}
|
|
node->layout.should_update = false;
|
|
_layoutLifecycle = RCTLayoutLifecycleComputed;
|
|
|
|
CGPoint absoluteTopLeft = {
|
|
RCTRoundPixelValue(absolutePosition.x + node->layout.position[CSS_LEFT]),
|
|
RCTRoundPixelValue(absolutePosition.y + node->layout.position[CSS_TOP])
|
|
};
|
|
|
|
CGPoint absoluteBottomRight = {
|
|
RCTRoundPixelValue(absolutePosition.x + node->layout.position[CSS_LEFT] + node->layout.dimensions[CSS_WIDTH]),
|
|
RCTRoundPixelValue(absolutePosition.y + node->layout.position[CSS_TOP] + node->layout.dimensions[CSS_HEIGHT])
|
|
};
|
|
|
|
CGRect frame = {
|
|
RCTRoundPixelValue(node->layout.position[CSS_LEFT]),
|
|
RCTRoundPixelValue(node->layout.position[CSS_TOP]),
|
|
RCTRoundPixelValue(absoluteBottomRight.x - absoluteTopLeft.x),
|
|
RCTRoundPixelValue(absoluteBottomRight.y - absoluteTopLeft.y)
|
|
};
|
|
|
|
if (!CGRectEqualToRect(frame, _frame)) {
|
|
_frame = frame;
|
|
[viewsWithNewFrame addObject:self];
|
|
}
|
|
|
|
absolutePosition.x += node->layout.position[CSS_LEFT];
|
|
absolutePosition.y += node->layout.position[CSS_TOP];
|
|
|
|
node->layout.dimensions[CSS_WIDTH] = CSS_UNDEFINED;
|
|
node->layout.dimensions[CSS_HEIGHT] = CSS_UNDEFINED;
|
|
node->layout.position[CSS_LEFT] = 0;
|
|
node->layout.position[CSS_TOP] = 0;
|
|
|
|
for (int i = 0; i < node->children_count; ++i) {
|
|
RCTShadowView *child = (RCTShadowView *)_reactSubviews[i];
|
|
[child _applyLayoutNode:node->get_child(node->context, i) viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition];
|
|
}
|
|
}
|
|
|
|
- (NSDictionary *)processBackgroundColor:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties
|
|
{
|
|
if (!_isBGColorExplicitlySet) {
|
|
UIColor *parentBackgroundColor = parentProperties[RCTBackgroundColorProp];
|
|
if (parentBackgroundColor && ![_backgroundColor isEqual:parentBackgroundColor]) {
|
|
_backgroundColor = parentBackgroundColor;
|
|
[applierBlocks addObject:^(RCTSparseArray *viewRegistry) {
|
|
UIView *view = viewRegistry[_reactTag];
|
|
view.backgroundColor = parentBackgroundColor;
|
|
}];
|
|
}
|
|
}
|
|
if (_isBGColorExplicitlySet) {
|
|
// Update parent properties for children
|
|
NSMutableDictionary *properties = [NSMutableDictionary dictionaryWithDictionary:parentProperties];
|
|
CGFloat alpha = CGColorGetAlpha(_backgroundColor.CGColor);
|
|
if (alpha < 1.0 && alpha > 0.0) {
|
|
// If we see partial transparency, start propagating full transparency
|
|
properties[RCTBackgroundColorProp] = [UIColor clearColor];
|
|
} else {
|
|
properties[RCTBackgroundColorProp] = _backgroundColor;
|
|
}
|
|
return properties;
|
|
}
|
|
return parentProperties;
|
|
}
|
|
|
|
- (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties
|
|
{
|
|
if (_propagationLifecycle == RCTPropagationLifecycleComputed && [parentProperties isEqualToDictionary:_lastParentProperties]) {
|
|
return;
|
|
}
|
|
_propagationLifecycle = RCTPropagationLifecycleComputed;
|
|
_lastParentProperties = parentProperties;
|
|
NSDictionary *nextProps = [self processBackgroundColor:applierBlocks parentProperties:parentProperties];
|
|
for (RCTShadowView *child in _reactSubviews) {
|
|
[child collectUpdatedProperties:applierBlocks parentProperties:nextProps];
|
|
}
|
|
}
|
|
|
|
- (void)collectRootUpdatedFrames:(NSMutableSet *)viewsWithNewFrame parentConstraint:(CGSize)parentConstraint
|
|
{
|
|
[self fillCSSNode:_cssNode];
|
|
layoutNode(_cssNode, CSS_UNDEFINED);
|
|
[self applyLayoutNode:_cssNode viewsWithNewFrame:viewsWithNewFrame];
|
|
}
|
|
|
|
+ (CGRect)measureLayout:(RCTShadowView *)shadowView relativeTo:(RCTShadowView *)ancestor
|
|
{
|
|
CGFloat totalOffsetTop = 0.0;
|
|
CGFloat totalOffsetLeft = 0.0;
|
|
CGSize size = shadowView.frame.size;
|
|
NSInteger depth = 0;
|
|
while (depth < MAX_TREE_DEPTH && shadowView && shadowView != ancestor) {
|
|
totalOffsetTop += shadowView.frame.origin.y;
|
|
totalOffsetLeft += shadowView.frame.origin.x;
|
|
shadowView = shadowView->_superview;
|
|
depth++;
|
|
}
|
|
if (ancestor != shadowView) {
|
|
return CGRectNull;
|
|
}
|
|
return (CGRect){{totalOffsetLeft, totalOffsetTop}, size};
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
if ((self = [super init])) {
|
|
|
|
_frame = CGRectMake(0, 0, CSS_UNDEFINED, CSS_UNDEFINED);
|
|
|
|
for (int ii = 0; ii < META_PROP_COUNT; ii++) {
|
|
_paddingMetaProps[ii] = CSS_UNDEFINED;
|
|
_marginMetaProps[ii] = CSS_UNDEFINED;
|
|
}
|
|
|
|
_newView = YES;
|
|
_layoutLifecycle = RCTLayoutLifecycleUninitialized;
|
|
_propagationLifecycle = RCTPropagationLifecycleUninitialized;
|
|
_textLifecycle = RCTTextLifecycleUninitialized;
|
|
|
|
_reactSubviews = [NSMutableArray array];
|
|
|
|
_cssNode = new_css_node();
|
|
_cssNode->context = (__bridge void *)self;
|
|
_cssNode->print = RCTPrint;
|
|
_cssNode->get_child = RCTGetChild;
|
|
_cssNode->is_dirty = RCTIsDirty;
|
|
[self fillCSSNode:_cssNode];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
free_css_node(_cssNode);
|
|
}
|
|
|
|
- (void)dirtyLayout
|
|
{
|
|
if (_layoutLifecycle != RCTLayoutLifecycleDirtied) {
|
|
_layoutLifecycle = RCTLayoutLifecycleDirtied;
|
|
[_superview dirtyLayout];
|
|
}
|
|
}
|
|
|
|
- (void)dirtyPropagation
|
|
{
|
|
if (_propagationLifecycle != RCTPropagationLifecycleDirtied) {
|
|
_propagationLifecycle = RCTPropagationLifecycleDirtied;
|
|
[_superview dirtyPropagation];
|
|
}
|
|
}
|
|
|
|
- (void)dirtyText
|
|
{
|
|
if (_textLifecycle != RCTTextLifecycleDirtied) {
|
|
_textLifecycle = RCTTextLifecycleDirtied;
|
|
[_superview dirtyText];
|
|
}
|
|
}
|
|
|
|
- (BOOL)isTextDirty
|
|
{
|
|
return _textLifecycle != RCTTextLifecycleComputed;
|
|
}
|
|
|
|
- (void)setTextComputed
|
|
{
|
|
_textLifecycle = RCTTextLifecycleComputed;
|
|
}
|
|
|
|
- (void)insertReactSubview:(RCTShadowView *)subview atIndex:(NSInteger)atIndex
|
|
{
|
|
[_reactSubviews insertObject:subview atIndex:atIndex];
|
|
_cssNode->children_count = (int)[_reactSubviews count];
|
|
subview->_superview = self;
|
|
[self dirtyText];
|
|
[self dirtyLayout];
|
|
[self dirtyPropagation];
|
|
}
|
|
|
|
- (void)removeReactSubview:(RCTShadowView *)subview
|
|
{
|
|
[subview dirtyText];
|
|
[subview dirtyLayout];
|
|
[subview dirtyPropagation];
|
|
subview->_superview = nil;
|
|
[_reactSubviews removeObject:subview];
|
|
_cssNode->children_count = (int)[_reactSubviews count];
|
|
}
|
|
|
|
- (NSArray *)reactSubviews
|
|
{
|
|
return _reactSubviews;
|
|
}
|
|
|
|
- (void)updateShadowViewLayout
|
|
{
|
|
if (_recomputePadding) {
|
|
RCTProcessMetaProps(_paddingMetaProps, _cssNode->style.padding);
|
|
}
|
|
if (_recomputeMargin) {
|
|
RCTProcessMetaProps(_marginMetaProps, _cssNode->style.margin);
|
|
}
|
|
if (_recomputePadding || _recomputeMargin) {
|
|
[self dirtyLayout];
|
|
}
|
|
[self fillCSSNode:_cssNode];
|
|
_recomputeMargin = NO;
|
|
_recomputePadding = NO;
|
|
}
|
|
|
|
|
|
// Margin
|
|
|
|
#define RCT_MARGIN_PROPERTY(prop, metaProp) \
|
|
- (void)setMargin##prop:(CGFloat)value \
|
|
{ \
|
|
_marginMetaProps[META_PROP_##metaProp] = value; \
|
|
_recomputeMargin = YES; \
|
|
} \
|
|
- (CGFloat)margin##prop \
|
|
{ \
|
|
return _marginMetaProps[META_PROP_##metaProp]; \
|
|
}
|
|
|
|
RCT_MARGIN_PROPERTY(, ALL)
|
|
RCT_MARGIN_PROPERTY(Vertical, VERTICAL)
|
|
RCT_MARGIN_PROPERTY(Horizontal, HORIZONTAL)
|
|
RCT_MARGIN_PROPERTY(Top, TOP)
|
|
RCT_MARGIN_PROPERTY(Left, LEFT)
|
|
RCT_MARGIN_PROPERTY(Bottom, BOTTOM)
|
|
RCT_MARGIN_PROPERTY(Right, RIGHT)
|
|
|
|
// Padding
|
|
|
|
#define RCT_PADDING_PROPERTY(prop, metaProp) \
|
|
- (void)setPadding##prop:(CGFloat)value \
|
|
{ \
|
|
_paddingMetaProps[META_PROP_##metaProp] = value; \
|
|
_recomputePadding = YES; \
|
|
} \
|
|
- (CGFloat)padding##prop \
|
|
{ \
|
|
return _marginMetaProps[META_PROP_##metaProp]; \
|
|
}
|
|
|
|
RCT_PADDING_PROPERTY(, ALL)
|
|
RCT_PADDING_PROPERTY(Vertical, VERTICAL)
|
|
RCT_PADDING_PROPERTY(Horizontal, HORIZONTAL)
|
|
RCT_PADDING_PROPERTY(Top, TOP)
|
|
RCT_PADDING_PROPERTY(Left, LEFT)
|
|
RCT_PADDING_PROPERTY(Bottom, BOTTOM)
|
|
RCT_PADDING_PROPERTY(Right, RIGHT)
|
|
|
|
// Border
|
|
|
|
- (void)setBorderWidth:(CGFloat)value
|
|
{
|
|
for (int i = 0; i < 4; i++) {
|
|
_cssNode->style.border[i] = value;
|
|
}
|
|
[self dirtyLayout];
|
|
}
|
|
|
|
// Dimensions
|
|
|
|
#define RCT_DIMENSIONS_PROPERTY(setProp, getProp, cssProp) \
|
|
- (void)set##setProp:(CGFloat)value \
|
|
{ \
|
|
_cssNode->style.dimensions[CSS_##cssProp] = value; \
|
|
[self dirtyLayout]; \
|
|
} \
|
|
- (CGFloat)getProp \
|
|
{ \
|
|
return _cssNode->style.dimensions[CSS_##cssProp]; \
|
|
}
|
|
|
|
RCT_DIMENSIONS_PROPERTY(Width, width, WIDTH)
|
|
RCT_DIMENSIONS_PROPERTY(Height, height, HEIGHT)
|
|
|
|
// Position
|
|
|
|
#define RCT_POSITION_PROPERTY(setProp, getProp, cssProp) \
|
|
- (void)set##setProp:(CGFloat)value \
|
|
{ \
|
|
_cssNode->style.position[CSS_##cssProp] = value; \
|
|
[self dirtyLayout]; \
|
|
} \
|
|
- (CGFloat)getProp \
|
|
{ \
|
|
return _cssNode->style.position[CSS_##cssProp]; \
|
|
}
|
|
|
|
RCT_POSITION_PROPERTY(Top, top, TOP)
|
|
RCT_POSITION_PROPERTY(Right, right, RIGHT)
|
|
RCT_POSITION_PROPERTY(Bottom, bottom, BOTTOM)
|
|
RCT_POSITION_PROPERTY(Left, left, LEFT)
|
|
|
|
- (void)setFrame:(CGRect)frame
|
|
{
|
|
_cssNode->style.position[CSS_LEFT] = CGRectGetMinX(frame);
|
|
_cssNode->style.position[CSS_TOP] = CGRectGetMinY(frame);
|
|
_cssNode->style.dimensions[CSS_WIDTH] = CGRectGetWidth(frame);
|
|
_cssNode->style.dimensions[CSS_HEIGHT] = CGRectGetHeight(frame);
|
|
[self dirtyLayout];
|
|
}
|
|
|
|
// Flex
|
|
|
|
#define RCT_STYLE_PROPERTY(setProp, getProp, cssProp, type) \
|
|
- (void)set##setProp:(type)value \
|
|
{ \
|
|
_cssNode->style.cssProp = value; \
|
|
[self dirtyLayout]; \
|
|
} \
|
|
- (type)getProp \
|
|
{ \
|
|
return _cssNode->style.cssProp; \
|
|
}
|
|
|
|
RCT_STYLE_PROPERTY(Flex, flex, flex, CGFloat)
|
|
RCT_STYLE_PROPERTY(FlexDirection, flexDirection, flex_direction, css_flex_direction_t)
|
|
RCT_STYLE_PROPERTY(JustifyContent, justifyContent, justify_content, css_justify_t)
|
|
RCT_STYLE_PROPERTY(AlignSelf, alignSelf, align_self, css_align_t)
|
|
RCT_STYLE_PROPERTY(AlignItems, alignItems, align_items, css_align_t)
|
|
RCT_STYLE_PROPERTY(PositionType, positionType, position_type, css_position_type_t)
|
|
RCT_STYLE_PROPERTY(FlexWrap, flexWrap, flex_wrap, css_wrap_type_t)
|
|
|
|
- (void)setBackgroundColor:(UIColor *)color
|
|
{
|
|
_backgroundColor = color;
|
|
[self dirtyPropagation];
|
|
}
|
|
|
|
@end
|