feat: get currentColor from caller instead of parent (#2521)

# Summary

Fixes #2520
When an element uses `currentColor`, it should look for color in its
caller, not in its parent.
Example: 
```svg
<Svg width="100" height="100" viewBox="0 0 100 100" color="red">
  <Defs color="blue">
    <G color="green">
      <Rect id="a" x="0" y="0" width="50" height="50" fill="currentColor"/>
    </G>
  </Defs>
  <G color="pink">
    <Use href="#a"/>												<!-- #1 -->
  </G>
  <Use href="#a" transform="translate(25 25)"/>						<!-- #2 -->
  <G color="green">
    <Use href="#a" transform="translate(50 50)"/>					<!-- #3 -->
  </G>
</Svg>
```

* `#1` should be **pink**
* `#2` should be **red**
* `#3` should be **green**


![image](https://github.com/user-attachments/assets/b7ba2ec6-ea05-4bcb-9f40-0cf024e5c749)

## Test Plan

Example app -> test -> Test2520

## Compatibility

| OS      | Implemented |
| ------- | :---------: |
| iOS     |          |
| MacOS   |          |
| Android |          |
This commit is contained in:
Jakub Grzywacz
2024-10-31 16:00:36 +01:00
committed by GitHub
parent 405ff97eea
commit 2a58016ec1
14 changed files with 82 additions and 43 deletions

View File

@@ -96,6 +96,7 @@ public abstract class RenderableView extends VirtualView implements ReactHitSlop
private @Nullable ArrayList<Object> mOriginProperties; private @Nullable ArrayList<Object> mOriginProperties;
private @Nullable ArrayList<String> mPropList; private @Nullable ArrayList<String> mPropList;
private @Nullable ArrayList<String> mAttributeList; private @Nullable ArrayList<String> mAttributeList;
private @Nullable RenderableView mCaller;
@Nullable String mFilter; @Nullable String mFilter;
@@ -134,6 +135,9 @@ public abstract class RenderableView extends VirtualView implements ReactHitSlop
if (this.mCurrentColor != 0) { if (this.mCurrentColor != 0) {
return this.mCurrentColor; return this.mCurrentColor;
} }
if (this.mCaller != null) {
return this.mCaller.getCurrentColor();
}
ViewParent parent = this.getParent(); ViewParent parent = this.getParent();
if (parent instanceof VirtualView) { if (parent instanceof VirtualView) {
return ((RenderableView) parent).getCurrentColor(); return ((RenderableView) parent).getCurrentColor();
@@ -756,6 +760,7 @@ public abstract class RenderableView extends VirtualView implements ReactHitSlop
} }
void mergeProperties(RenderableView target) { void mergeProperties(RenderableView target) {
mCaller = target;
ArrayList<String> targetAttributeList = target.getAttributeList(); ArrayList<String> targetAttributeList = target.getAttributeList();
if (targetAttributeList == null || targetAttributeList.size() == 0) { if (targetAttributeList == null || targetAttributeList.size() == 0) {
@@ -798,6 +803,7 @@ public abstract class RenderableView extends VirtualView implements ReactHitSlop
mLastMergedList = null; mLastMergedList = null;
mOriginProperties = null; mOriginProperties = null;
mAttributeList = mPropList; mAttributeList = mPropList;
mCaller = null;
} }
} }

View File

@@ -11,6 +11,7 @@ package com.horcrux.svg;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Rect; import android.graphics.Rect;
@@ -189,7 +190,7 @@ public class SvgView extends ReactViewGroup implements ReactCompoundView, ReactC
final Matrix mInvViewBoxMatrix = new Matrix(); final Matrix mInvViewBoxMatrix = new Matrix();
private boolean mInvertible = true; private boolean mInvertible = true;
private boolean mRendered = false; private boolean mRendered = false;
int mCurrentColor = 0; int mCurrentColor = Color.BLACK;
boolean notRendered() { boolean notRendered() {
return !mRendered; return !mRendered;

View File

@@ -49,7 +49,7 @@
BOOL fillColor; BOOL fillColor;
if (brush.class == RNSVGBrush.class) { if (brush.class == RNSVGBrush.class) {
CGContextSetFillColorWithColor(context, [element.tintColor CGColor]); CGContextSetFillColorWithColor(context, [element getCurrentColor]);
fillColor = YES; fillColor = YES;
} else { } else {
fillColor = [brush applyFillColor:context opacity:opacity]; fillColor = [brush applyFillColor:context opacity:opacity];
@@ -70,7 +70,7 @@
BOOL strokeColor; BOOL strokeColor;
if (brush.class == RNSVGBrush.class) { if (brush.class == RNSVGBrush.class) {
CGContextSetStrokeColorWithColor(context, [element.tintColor CGColor]); CGContextSetStrokeColorWithColor(context, [element getCurrentColor]);
strokeColor = YES; strokeColor = YES;
} else { } else {
strokeColor = [brush applyStrokeColor:context opacity:opacity]; strokeColor = [brush applyStrokeColor:context opacity:opacity];

View File

@@ -19,6 +19,7 @@
@interface RNSVGSvgView : RNSVGView <RNSVGContainer> @interface RNSVGSvgView : RNSVGView <RNSVGContainer>
@property (nonatomic, strong) RNSVGColor *color;
@property (nonatomic, strong) RNSVGLength *bbWidth; @property (nonatomic, strong) RNSVGLength *bbWidth;
@property (nonatomic, strong) RNSVGLength *bbHeight; @property (nonatomic, strong) RNSVGLength *bbHeight;
@property (nonatomic, assign) CGFloat minX; @property (nonatomic, assign) CGFloat minX;

View File

@@ -47,8 +47,6 @@ using namespace facebook::react;
// This is necessary to ensure that [self setNeedsDisplay] actually triggers // This is necessary to ensure that [self setNeedsDisplay] actually triggers
// a redraw when our parent transitions between hidden and visible. // a redraw when our parent transitions between hidden and visible.
self.contentMode = UIViewContentModeRedraw; self.contentMode = UIViewContentModeRedraw;
// We don't want the dimming effect on tint as it's used as currentColor
self.tintAdjustmentMode = UIViewTintAdjustmentModeNormal;
#endif // TARGET_OS_OSX #endif // TARGET_OS_OSX
rendered = false; rendered = false;
#ifdef RCT_NEW_ARCH_ENABLED #ifdef RCT_NEW_ARCH_ENABLED
@@ -90,7 +88,7 @@ using namespace facebook::react;
self.align = RCTNSStringFromStringNilIfEmpty(newProps.align); self.align = RCTNSStringFromStringNilIfEmpty(newProps.align);
self.meetOrSlice = intToRNSVGVBMOS(newProps.meetOrSlice); self.meetOrSlice = intToRNSVGVBMOS(newProps.meetOrSlice);
if (RCTUIColorFromSharedColor(newProps.color)) { if (RCTUIColorFromSharedColor(newProps.color)) {
self.tintColor = RCTUIColorFromSharedColor(newProps.color); self.color = RCTUIColorFromSharedColor(newProps.color);
} }
[super updateProps:props oldProps:oldProps]; [super updateProps:props oldProps:oldProps];
} }
@@ -184,10 +182,13 @@ using namespace facebook::react;
[self setNeedsDisplay]; [self setNeedsDisplay];
} }
- (void)tintColorDidChange - (void)setColor:(RNSVGColor *)color
{ {
if (color == _color) {
return;
}
[self invalidate]; [self invalidate];
[self clearChildCache]; _color = color;
} }
- (void)setMinX:(CGFloat)minX - (void)setMinX:(CGFloat)minX

View File

@@ -19,6 +19,7 @@
@interface RNSVGRenderable : RNSVGNode @interface RNSVGRenderable : RNSVGNode
@property (class) RNSVGRenderable *contextElement; @property (class) RNSVGRenderable *contextElement;
@property (nonatomic, strong) RNSVGColor *color;
@property (nonatomic, strong) RNSVGBrush *fill; @property (nonatomic, strong) RNSVGBrush *fill;
@property (nonatomic, assign) CGFloat fillOpacity; @property (nonatomic, assign) CGFloat fillOpacity;
@property (nonatomic, assign) RNSVGCGFCRule fillRule; @property (nonatomic, assign) RNSVGCGFCRule fillRule;
@@ -45,4 +46,6 @@
- (void)resetProperties; - (void)resetProperties;
- (CGColor *)getCurrentColor;
@end @end

View File

@@ -25,6 +25,7 @@
NSArray<RNSVGLength *> *_sourceStrokeDashArray; NSArray<RNSVGLength *> *_sourceStrokeDashArray;
CGFloat *_strokeDashArrayData; CGFloat *_strokeDashArrayData;
CGPathRef _srcHitPath; CGPathRef _srcHitPath;
RNSVGRenderable *_caller;
} }
static RNSVGRenderable *_contextElement; static RNSVGRenderable *_contextElement;
@@ -61,11 +62,11 @@ static RNSVGRenderable *_contextElement;
- (void)setColor:(RNSVGColor *)color - (void)setColor:(RNSVGColor *)color
{ {
if (color == self.tintColor) { if (color == _color) {
return; return;
} }
[self invalidate]; [self invalidate];
self.tintColor = color; _color = color;
} }
- (void)setFill:(RNSVGBrush *)fill - (void)setFill:(RNSVGBrush *)fill
@@ -560,7 +561,7 @@ UInt32 saturate(CGFloat value)
if (self.fill) { if (self.fill) {
if (self.fill.class == RNSVGBrush.class) { if (self.fill.class == RNSVGBrush.class) {
CGContextSetFillColorWithColor(context, [self.tintColor CGColor]); CGContextSetFillColorWithColor(context, [self getCurrentColor]);
fillColor = YES; fillColor = YES;
} else { } else {
fillColor = [self.fill applyFillColor:context opacity:self.fillOpacity]; fillColor = [self.fill applyFillColor:context opacity:self.fillOpacity];
@@ -608,7 +609,7 @@ UInt32 saturate(CGFloat value)
BOOL strokeColor; BOOL strokeColor;
if (self.stroke.class == RNSVGBrush.class) { if (self.stroke.class == RNSVGBrush.class) {
CGContextSetStrokeColorWithColor(context, [self.tintColor CGColor]); CGContextSetStrokeColorWithColor(context, [self getCurrentColor]);
strokeColor = YES; strokeColor = YES;
} else { } else {
strokeColor = [self.stroke applyStrokeColor:context opacity:self.strokeOpacity]; strokeColor = [self.stroke applyStrokeColor:context opacity:self.strokeOpacity];
@@ -724,6 +725,7 @@ UInt32 saturate(CGFloat value)
- (void)mergeProperties:(__kindof RNSVGRenderable *)target - (void)mergeProperties:(__kindof RNSVGRenderable *)target
{ {
_caller = target;
NSArray<NSString *> *targetAttributeList = [target getAttributeList]; NSArray<NSString *> *targetAttributeList = [target getAttributeList];
if (targetAttributeList.count == 0) { if (targetAttributeList.count == 0) {
@@ -754,9 +756,28 @@ UInt32 saturate(CGFloat value)
[self setValue:[_originProperties valueForKey:key] forKey:key]; [self setValue:[_originProperties valueForKey:key] forKey:key];
} }
_caller = nil;
_lastMergedList = nil; _lastMergedList = nil;
_attributeList = _propList; _attributeList = _propList;
self.merging = false; self.merging = false;
} }
- (CGColor *)getCurrentColor
{
if (self.color != nil) {
return [self.color CGColor];
}
if (_caller != nil) {
return [_caller getCurrentColor];
}
RNSVGPlatformView *parentView = [self superview];
if ([parentView isKindOfClass:[RNSVGRenderable class]]) {
return [(RNSVGRenderable *)parentView getCurrentColor];
} else if ([parentView isKindOfClass:[RNSVGSvgView class]]) {
return [[(RNSVGSvgView *)parentView color] CGColor];
}
return nil;
}
@end @end

View File

@@ -1,7 +1,6 @@
#import "RNSVGUIKit.h" #import "RNSVGUIKit.h"
@implementation RNSVGView { @implementation RNSVGView {
NSColor *_tintColor;
} }
- (CGPoint)center - (CGPoint)center
@@ -20,29 +19,6 @@
self.frame = CGRectMake(xOrigin, yOrigin, frameRect.size.width, frameRect.size.height); self.frame = CGRectMake(xOrigin, yOrigin, frameRect.size.width, frameRect.size.height);
} }
- (NSColor *)tintColor
{
if (_tintColor != nil) {
return _tintColor;
}
// To mimic iOS's tintColor, we crawl up the view hierarchy until either:
// (a) we find a valid color
// (b) we reach a view that isn't an RNSVGView
NSView *parentView = [self superview];
if ([parentView isKindOfClass:[RNSVGView class]]) {
return [(RNSVGView *)parentView tintColor];
} else {
return [NSColor controlAccentColor];
}
}
- (void)setTintColor:(NSColor *)tintColor
{
_tintColor = tintColor;
[self setNeedsDisplay:YES];
}
@end @end
@implementation NSImage (RNSVGMacOSExtensions) @implementation NSImage (RNSVGMacOSExtensions)

View File

@@ -133,7 +133,7 @@ using namespace facebook::react;
if (self.inlineSize != nil && self.inlineSize.value != 0) { if (self.inlineSize != nil && self.inlineSize.value != 0) {
if (self.fill) { if (self.fill) {
if (self.fill.class == RNSVGBrush.class) { if (self.fill.class == RNSVGBrush.class) {
CGColorRef color = [self.tintColor CGColor]; CGColorRef color = [self getCurrentColor];
[self drawWrappedText:context gc:gc rect:rect color:color]; [self drawWrappedText:context gc:gc rect:rect color:color];
} else { } else {
CGColorRef color = [self.fill getColorWithOpacity:self.fillOpacity]; CGColorRef color = [self.fill getColorWithOpacity:self.fillOpacity];
@@ -143,7 +143,7 @@ using namespace facebook::react;
} }
if (self.stroke) { if (self.stroke) {
if (self.stroke.class == RNSVGBrush.class) { if (self.stroke.class == RNSVGBrush.class) {
CGColorRef color = [self.tintColor CGColor]; CGColorRef color = [self getCurrentColor];
[self drawWrappedText:context gc:gc rect:rect color:color]; [self drawWrappedText:context gc:gc rect:rect color:color];
} else { } else {
CGColorRef color = [self.stroke getColorWithOpacity:self.strokeOpacity]; CGColorRef color = [self.stroke getColorWithOpacity:self.strokeOpacity];

View File

@@ -24,7 +24,7 @@ RCT_EXPORT_MODULE()
return [RNSVGRenderable new]; return [RNSVGRenderable new];
} }
RCT_REMAP_VIEW_PROPERTY(color, tintColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(color, UIColor)
RCT_EXPORT_VIEW_PROPERTY(fill, RNSVGBrush) RCT_EXPORT_VIEW_PROPERTY(fill, RNSVGBrush)
RCT_EXPORT_VIEW_PROPERTY(fillOpacity, CGFloat) RCT_EXPORT_VIEW_PROPERTY(fillOpacity, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(fillRule, RNSVGCGFCRule) RCT_EXPORT_VIEW_PROPERTY(fillRule, RNSVGCGFCRule)

View File

@@ -26,7 +26,7 @@ RCT_EXPORT_VIEW_PROPERTY(vbWidth, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(vbHeight, CGFloat) RCT_EXPORT_VIEW_PROPERTY(vbHeight, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(align, NSString) RCT_EXPORT_VIEW_PROPERTY(align, NSString)
RCT_EXPORT_VIEW_PROPERTY(meetOrSlice, RNSVGVBMOS) RCT_EXPORT_VIEW_PROPERTY(meetOrSlice, RNSVGVBMOS)
RCT_REMAP_VIEW_PROPERTY(color, tintColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(color, UIColor)
RCT_CUSTOM_VIEW_PROPERTY(hitSlop, UIEdgeInsets, RNSVGSvgView) RCT_CUSTOM_VIEW_PROPERTY(hitSlop, UIEdgeInsets, RNSVGSvgView)
{ {
if ([view respondsToSelector:@selector(setHitTestEdgeInsets:)]) { if ([view respondsToSelector:@selector(setHitTestEdgeInsets:)]) {

View File

@@ -0,0 +1,32 @@
import React from 'react';
import {View} from 'react-native';
import {Defs, G, Rect, Svg, Use} from 'react-native-svg';
export default () => {
return (
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<Svg width="100" height="100" viewBox="0 0 100 100" color="red">
{/* @ts-ignore */}
<Defs color="blue">
<G color="green">
<Rect
id="a"
x="0"
y="0"
width="50"
height="50"
fill="currentColor"
/>
</G>
</Defs>
<G color="pink">
<Use href="#a" />
</G>
<Use href="#a" transform="translate(25 25)" />
<G color="green">
<Use href="#a" transform="translate(50 50)" />
</G>
</Svg>
</View>
);
};

View File

@@ -34,6 +34,7 @@ import Test2407 from './Test2407';
import Test2417 from './Test2417'; import Test2417 from './Test2417';
import Test2455 from './Test2455'; import Test2455 from './Test2455';
import Test2471 from './Test2471'; import Test2471 from './Test2471';
import Test2520 from './Test2520';
export default function App() { export default function App() {
return <ColorTest />; return <ColorTest />;

View File

@@ -107,7 +107,6 @@ export default class Svg extends Shape<SvgProps> {
...extracted, ...extracted,
}; };
let { let {
color,
width, width,
height, height,
focusable, focusable,
@@ -173,8 +172,6 @@ export default class Svg extends Shape<SvgProps> {
extractResponder(props, props, this as ResponderInstanceProps); extractResponder(props, props, this as ResponderInstanceProps);
props.tintColor = color;
if (onLayout != null) { if (onLayout != null) {
props.onLayout = onLayout; props.onLayout = onLayout;
} }