From 0728b306dffe586dd3921c1bebe5cc6cdf2b0591 Mon Sep 17 00:00:00 2001 From: Marcin Szalski Date: Fri, 16 Oct 2026 09:47:27 +0200 Subject: [PATCH] feat: add backdrop-filter ios support --- .../View/RCTBackdropFilterLayer.h | 41 +++ .../View/RCTBackdropFilterLayer.mm | 259 ++++++++++++++++++ .../View/RCTBackdropFilterUtils.h | 30 ++ .../View/RCTBackdropFilterUtils.mm | 145 ++++++++++ .../View/RCTViewComponentView.mm | 28 ++ .../components/view/BaseViewProps.cpp | 7 + .../renderer/components/view/BaseViewProps.h | 3 + 7 files changed, 513 insertions(+) create mode 100644 packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackdropFilterLayer.h create mode 100644 packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackdropFilterLayer.mm create mode 100644 packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackdropFilterUtils.h create mode 100644 packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackdropFilterUtils.mm diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackdropFilterLayer.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackdropFilterLayer.h new file mode 100644 index 000000000000..6b86af1bf788 --- /dev/null +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackdropFilterLayer.h @@ -0,0 +1,41 @@ +/* + * 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. + */ + +#pragma once + +#include + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * A CALayer that renders CSS backdrop-filter on iOS using a Metal pipeline. + * + * On each display, CARenderer captures the composited content behind the host + * view into a MTLTexture (GPU-only), then a Metal-backed CIContext applies the + * CSS filter chain. The result is set as the layer's contents and inserted + * as the first sublayer of the host view's layer so it renders behind all + * of the view's own content. + * + * Call setNeedsDisplay whenever the backdrop or filter functions change. + */ +@interface RCTBackdropFilterLayer : CALayer + +@property (nonatomic) std::vector filterFunctions; + +/** + * The UIView whose superview provides the backdrop source. + * Must be set before the layer renders. + */ +@property (nonatomic, weak, nullable) UIView *hostView; +@property (nonatomic, weak, nullable) CALayer *hostBackgroundColorLayer; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackdropFilterLayer.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackdropFilterLayer.mm new file mode 100644 index 000000000000..2dab2caca967 --- /dev/null +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackdropFilterLayer.mm @@ -0,0 +1,259 @@ +/* + * 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 "RCTBackdropFilterLayer.h" + +#import +#import +#import + +// Layers whose host view was off-screen when -display fired. Stored as weak refs so +// deallocated layers vanish automatically. A single CADisplayLink polls their visibility +// every frame and calls -setNeedsDisplay once a layer scrolls into view, then stops +// itself when the set is empty (no CPU cost while idle). +static NSHashTable *sPendingLayers; +static CADisplayLink *sVisibilityLink; + +// Shared CIContext backed by Metal. CIContext creation (MTLCreateSystemDefaultDevice + +// contextWithMTLDevice:) can take 200–500 ms on first call; pre-warming in +initialize +// on a background queue ensures it is ready before the first -display fires. +static CIContext *sSharedCIContext; + +@implementation RCTBackdropFilterLayer + ++ (void)initialize +{ + if (self == [RCTBackdropFilterLayer class]) { + sPendingLayers = [NSHashTable weakObjectsHashTable]; + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + // CSS filter functions are defined to operate in sRGB. Forcing the working colour space + // to sRGB here matches Android (ColorMatrix in sRGB) and avoids vivid/saturated artefacts + // that occur when Core Image processes in its default linear/Display-P3 working space. + CGColorSpaceRef srgb = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + NSDictionary *options = @{ + kCIContextWorkingColorSpace : (__bridge id)srgb, + kCIContextOutputColorSpace : (__bridge id)srgb, + }; + CGColorSpaceRelease(srgb); + id device = MTLCreateSystemDefaultDevice(); + CIContext *ctx = + device ? [CIContext contextWithMTLDevice:device options:options] : [CIContext contextWithOptions:options]; + // Pointer-sized store is atomic on ARM64; benign race — worst case another thread + // creates a second context and stores it, both are valid. + sSharedCIContext = ctx; + }); + } +} + +static BOOL IsHostVisible(UIView *host) +{ + if (!host.window) { + return NO; + } + // Check visibility relative to the nearest scroll view ancestor so that + // navigation slide-in transitions (which transform the whole screen in + // window coordinates) don't incorrectly defer on-screen content. + for (UIView *v = host.superview; v != nil; v = v.superview) { + if ([v isKindOfClass:[UIWindow class]]) { + break; + } + if ([v isKindOfClass:[UIScrollView class]]) { + CGRect hostInSV = [host convertRect:host.bounds toView:v]; + return CGRectIntersectsRect(hostInSV, v.bounds); + } + } + return YES; +} + ++ (void)_visibilityLinkFired:(CADisplayLink *)__unused link +{ + for (RCTBackdropFilterLayer *layer in sPendingLayers.allObjects) { + UIView *host = layer.hostView; + if (host && IsHostVisible(host)) { + [sPendingLayers removeObject:layer]; + [layer setNeedsDisplay]; + } + } + if (sPendingLayers.count == 0) { + [sVisibilityLink invalidate]; + sVisibilityLink = nil; + } +} + +- (void)_deferUntilVisible +{ + self.contents = nil; + [sPendingLayers addObject:self]; + if (!sVisibilityLink) { + sVisibilityLink = [CADisplayLink displayLinkWithTarget:[self class] selector:@selector(_visibilityLinkFired:)]; + [sVisibilityLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } +} + +- (CIContext *)_ciContext +{ + if (!sSharedCIContext) { + // Background pre-warm hasn't finished yet; create synchronously and cache. + CGColorSpaceRef srgb = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + NSDictionary *options = @{ + kCIContextWorkingColorSpace : (__bridge id)srgb, + kCIContextOutputColorSpace : (__bridge id)srgb, + }; + CGColorSpaceRelease(srgb); + id device = MTLCreateSystemDefaultDevice(); + sSharedCIContext = + device ? [CIContext contextWithMTLDevice:device options:options] : [CIContext contextWithOptions:options]; + } + return sSharedCIContext; +} + +- (void)_flushPendingDisplayInSubtree:(CALayer *)layer + renderRootLayer:(CALayer *)rootLayer + captureRect:(CGRect)captureRect +{ + if (layer == self) { + return; + } + + NSString *delegateClass = + layer.delegate ? NSStringFromClass([layer.delegate class]) : NSStringFromClass([layer class]); + // Match both legacy RCTParagraphTextView and Fabric RCTParagraphComponentView. + BOOL isTextLayer = [delegateClass containsString:@"Paragraph"]; + + if (isTextLayer) { + // Force display regardless of dirty state: recycled/first-render text views may have + // needsDisplay=NO with nil or stale contents. Calling display directly triggers + // drawRect:, which renders fresh content or clears the stale backing store. + [layer display]; + } else if (layer.needsDisplay && ![layer isKindOfClass:[RCTBackdropFilterLayer class]]) { + [layer displayIfNeeded]; + } + for (CALayer *sub in layer.sublayers) { + [self _flushPendingDisplayInSubtree:sub renderRootLayer:rootLayer captureRect:captureRect]; + } +} + +- (void)display +{ + UIView *host = self.hostView; + + if (!host || !host.superview || !self.superlayer) { + self.contents = nil; + return; + } + + CGSize captureSize = self.bounds.size; + + if (captureSize.width <= 0 || captureSize.height <= 0) { + self.contents = nil; + return; + } + + // Lazy rendering: skip the expensive capture+filter work when the host is off-screen + // (e.g. scrolled below the fold). The display link re-triggers -setNeedsDisplay once + // the host scrolls into view, spreading cost over time instead of paying it all upfront. + if (!IsHostVisible(host)) { + [self _deferUntilVisible]; + return; + } + + UIView *renderRoot = host.superview; + for (UIView *candidate = host.superview.superview; candidate != nil; candidate = candidate.superview) { + // A clipping ancestor already constrains what can be seen behind the host — + // nothing outside renderRoot.bounds can bleed through, so there is no point + // walking further up the tree (and renderInContext: on a huge parent is expensive). + if (renderRoot.clipsToBounds) { + break; + } + if ([candidate isKindOfClass:[UIScrollView class]] || [candidate isKindOfClass:[UIWindow class]]) { + break; + } + if ([candidate.nextResponder isKindOfClass:[UIViewController class]]) { + break; + } + UIView *parentView = candidate.superview; + if (parentView) { + CGRect hostRectInParent = [host convertRect:host.bounds toView:parentView]; + for (UIView *sibling in parentView.subviews) { + if (sibling != candidate && CGRectIntersectsRect(sibling.frame, hostRectInParent)) { + goto doneWalking; + } + } + } + renderRoot = candidate; + } +doneWalking:; + + CGFloat scale = UIScreen.mainScreen.scale; + CGRect hostRectInRoot = [host convertRect:host.bounds toView:renderRoot]; + + [self _flushPendingDisplayInSubtree:renderRoot.layer renderRootLayer:renderRoot.layer captureRect:hostRectInRoot]; + + // Find the opaque background colour visible behind renderRoot so that transparent gaps + // in the captured image are filled with the actual screen background rather than (0,0,0,0). + // Without this, filters such as invert() incorrectly keep transparent areas transparent, + // which composites as white on the iOS screen — matching what Android (pre-filled white) does. + UIColor *backdropFill = nil; + for (UIView *ancestor = renderRoot.superview; ancestor != nil; ancestor = ancestor.superview) { + CGColorRef bg = ancestor.layer.backgroundColor; + if (bg != NULL && CGColorGetAlpha(bg) >= 1.0f - 1e-4f) { + backdropFill = [UIColor colorWithCGColor:bg]; + break; + } + if ([ancestor isKindOfClass:[UIWindow class]]) { + break; + } + } + if (!backdropFill) { + backdropFill = UIColor.systemBackgroundColor; + } + + host.layer.hidden = YES; + + UIGraphicsImageRendererFormat *captureFmt = [UIGraphicsImageRendererFormat preferredFormat]; + captureFmt.scale = scale; + UIGraphicsImageRenderer *captureRenderer = [[UIGraphicsImageRenderer alloc] initWithSize:captureSize + format:captureFmt]; + UIImage *backdropImage = [captureRenderer imageWithActions:^(UIGraphicsImageRendererContext *rendererCtx) { + [backdropFill setFill]; + UIRectFill(CGRectMake(0, 0, captureSize.width, captureSize.height)); + CGContextTranslateCTM(rendererCtx.CGContext, -hostRectInRoot.origin.x, -hostRectInRoot.origin.y); + [renderRoot.layer renderInContext:rendererCtx.CGContext]; + }]; + + host.layer.hidden = NO; + + CIContext *ciContext = [self _ciContext]; + CIImage *ciImage = [[CIImage alloc] initWithImage:backdropImage]; + CIImage *filtered = [RCTBackdropFilterUtils applyCIFilterFunctions:_filterFunctions toImage:ciImage scale:scale]; + + CGColorRef bgColor = host.layer.backgroundColor; + if (!bgColor) { + bgColor = self.hostBackgroundColorLayer.backgroundColor; + } + if (bgColor && CGColorGetAlpha(bgColor) > 0) { + CIColor *ciColor = [[CIColor alloc] initWithCGColor:bgColor]; + CIImage *tint = [[CIImage imageWithColor:ciColor] imageByCroppingToRect:filtered.extent]; + CIFilter *over = [CIFilter filterWithName:@"CISourceOverCompositing"]; + [over setValue:tint forKey:kCIInputImageKey]; + [over setValue:filtered forKey:kCIInputBackgroundImageKey]; + filtered = over.outputImage; + } + + CGImageRef cgImage = [ciContext createCGImage:filtered fromRect:filtered.extent]; + + if (!cgImage) { + self.contents = nil; + return; + } + + self.contents = (__bridge id)cgImage; + self.contentsScale = scale; + CGImageRelease(cgImage); +} + +@end diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackdropFilterUtils.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackdropFilterUtils.h new file mode 100644 index 000000000000..9c9da270d666 --- /dev/null +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackdropFilterUtils.h @@ -0,0 +1,30 @@ +/* + * 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. + */ + +#pragma once + +#include + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RCTBackdropFilterUtils : NSObject + +/** + * Applies filterFunctions as a chained CIFilter pipeline to image and returns + * the result. scale is the display scale factor used to convert CSS pixel + * values to device pixels (e.g. blur radius). Returns the original image if + * no applicable filters are present. + */ ++ (CIImage *)applyCIFilterFunctions:(const std::vector &)filterFunctions + toImage:(CIImage *)image + scale:(CGFloat)scale; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackdropFilterUtils.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackdropFilterUtils.mm new file mode 100644 index 000000000000..cacb9fe8a959 --- /dev/null +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackdropFilterUtils.mm @@ -0,0 +1,145 @@ +/* + * 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 "RCTBackdropFilterUtils.h" + +#import +#import + +using namespace facebook::react; + +@implementation RCTBackdropFilterUtils + ++ (CIImage *)applyCIFilterFunctions:(const std::vector &)filterFunctions + toImage:(CIImage *)image + scale:(CGFloat)scale +{ + CIImage *result = image; + for (const auto &primitive : filterFunctions) { + if (primitive.type == FilterType::DropShadow) { + continue; + } + CIFilter *filter = [self _ciFilterFromFilterFunction:primitive scale:scale]; + if (!filter) { + continue; + } + CGRect extent = result.extent; + CIImage *input = (primitive.type == FilterType::Blur) ? [result imageByClampingToExtent] : result; + [filter setValue:input forKey:kCIInputImageKey]; + CIImage *output = filter.outputImage; + if (output) { + result = (primitive.type == FilterType::Blur) ? [output imageByCroppingToRect:extent] : output; + } + } + return result; +} + +// MARK: - Private + ++ (nullable CIFilter *)_ciFilterFromFilterFunction:(const FilterFunction &)primitive scale:(CGFloat)scale +{ + if (!std::holds_alternative(primitive.parameters)) { + return nil; + } + + const Float value = std::get(primitive.parameters); + + switch (primitive.type) { + case FilterType::Blur: { + CIFilter *filter = [CIFilter filterWithName:@"CIGaussianBlur"]; + [filter setValue:@(value * scale) forKey:@"inputRadius"]; + return filter; + } + + case FilterType::Brightness: { + // CSS brightness(n) is multiplicative: 0=black, 1=normal, 2=double. + // CIColorMatrix scales each RGB channel independently. + CIFilter *filter = [CIFilter filterWithName:@"CIColorMatrix"]; + [filter setValue:[CIVector vectorWithX:value Y:0 Z:0 W:0] forKey:@"inputRVector"]; + [filter setValue:[CIVector vectorWithX:0 Y:value Z:0 W:0] forKey:@"inputGVector"]; + [filter setValue:[CIVector vectorWithX:0 Y:0 Z:value W:0] forKey:@"inputBVector"]; + [filter setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:1] forKey:@"inputAVector"]; + [filter setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:0] forKey:@"inputBiasVector"]; + return filter; + } + + case FilterType::Contrast: { + // CIColorControls.inputContrast: 0=gray, 1=normal, >1=more contrast. + // Matches CSS contrast() semantics directly. + CIFilter *filter = [CIFilter filterWithName:@"CIColorControls"]; + [filter setValue:@(value) forKey:@"inputContrast"]; + [filter setValue:@(0.0f) forKey:@"inputBrightness"]; + [filter setValue:@(1.0f) forKey:@"inputSaturation"]; + return filter; + } + + case FilterType::Grayscale: { + // CSS grayscale(0)=color, grayscale(1)=gray. + // Map to CIColorControls: saturation = 1 - grayscaleAmount. + CIFilter *filter = [CIFilter filterWithName:@"CIColorControls"]; + [filter setValue:@(1.0f - value) forKey:@"inputSaturation"]; + [filter setValue:@(0.0f) forKey:@"inputBrightness"]; + [filter setValue:@(1.0f) forKey:@"inputContrast"]; + return filter; + } + + case FilterType::Saturate: { + // CIColorControls.inputSaturation: 0=gray, 1=normal, >1=vibrant. + // Matches CSS saturate() semantics directly. + CIFilter *filter = [CIFilter filterWithName:@"CIColorControls"]; + [filter setValue:@(value) forKey:@"inputSaturation"]; + [filter setValue:@(0.0f) forKey:@"inputBrightness"]; + [filter setValue:@(1.0f) forKey:@"inputContrast"]; + return filter; + } + + case FilterType::HueRotate: { + // CIHueAdjust.inputAngle takes radians; CSS value arrives in degrees. + CIFilter *filter = [CIFilter filterWithName:@"CIHueAdjust"]; + [filter setValue:@(value * M_PI / 180.0) forKey:@"inputAngle"]; + return filter; + } + + case FilterType::Invert: { + // CSS invert(n) in straight-alpha: R_out = (1-2n)*R_in + n + // In premultiplied space (CIImage) the constant term n must be scaled by alpha + // so that transparent pixels stay transparent: R_out_premult = (1-2n)*R_premult + n*A + // This is expressed via the W (alpha) component of each RGB row — not a bias vector. + const Float s = 1.0f - 2.0f * value; + CIFilter *filter = [CIFilter filterWithName:@"CIColorMatrix"]; + [filter setValue:[CIVector vectorWithX:s Y:0 Z:0 W:value] forKey:@"inputRVector"]; + [filter setValue:[CIVector vectorWithX:0 Y:s Z:0 W:value] forKey:@"inputGVector"]; + [filter setValue:[CIVector vectorWithX:0 Y:0 Z:s W:value] forKey:@"inputBVector"]; + [filter setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:1] forKey:@"inputAVector"]; + [filter setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:0] forKey:@"inputBiasVector"]; + return filter; + } + + case FilterType::Opacity: { + // CSS opacity(n): A_out = n * A_in. + CIFilter *filter = [CIFilter filterWithName:@"CIColorMatrix"]; + [filter setValue:[CIVector vectorWithX:1 Y:0 Z:0 W:0] forKey:@"inputRVector"]; + [filter setValue:[CIVector vectorWithX:0 Y:1 Z:0 W:0] forKey:@"inputGVector"]; + [filter setValue:[CIVector vectorWithX:0 Y:0 Z:1 W:0] forKey:@"inputBVector"]; + [filter setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:value] forKey:@"inputAVector"]; + [filter setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:0] forKey:@"inputBiasVector"]; + return filter; + } + + case FilterType::Sepia: { + // CISepiaTone.inputIntensity: 0=no effect, 1=full sepia. Matches CSS sepia(). + CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone"]; + [filter setValue:@(value) forKey:@"inputIntensity"]; + return filter; + } + + case FilterType::DropShadow: + return nil; + } +} + +@end diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 582557336d61..28361a1b42ca 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -17,6 +17,7 @@ #import #import +#import #import #import #import @@ -112,6 +113,7 @@ @implementation RCTViewComponentView { CALayer *_outlineLayer; NSMutableArray *_boxShadowLayers; CALayer *_filterLayer; + RCTBackdropFilterLayer *_backdropFilterLayer; NSMutableArray *_backgroundImageLayers; BOOL _needsInvalidateLayer; BOOL _isJSResponder; @@ -599,6 +601,11 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & needsInvalidateLayer = YES; } + // `backdropFilter` + if (oldViewProps.backdropFilter != newViewProps.backdropFilter) { + needsInvalidateLayer = YES; + } + // `focusable` #if TARGET_OS_TV if (oldViewProps.focusable != newViewProps.focusable) { @@ -869,6 +876,8 @@ - (void)prepareForRecycle _outlineLayer = nil; [_filterLayer removeFromSuperlayer]; _filterLayer = nil; + [_backdropFilterLayer removeFromSuperlayer]; + _backdropFilterLayer = nil; [self clearExistingBackgroundImageLayers]; [self _setLastSentSafeAreaInsets:nil]; @@ -1379,6 +1388,25 @@ - (void)invalidateLayer } } + // backdrop filter + if (_props->backdropFilter.empty()) { + [_backdropFilterLayer removeFromSuperlayer]; + _backdropFilterLayer = nil; + } else if (_backdropFilterLayer != nil) { + _backdropFilterLayer.filterFunctions = _props->backdropFilter; + _backdropFilterLayer.frame = self.bounds; + [_backdropFilterLayer setNeedsDisplay]; + } else { + RCTBackdropFilterLayer *backdropLayer = [RCTBackdropFilterLayer layer]; + backdropLayer.filterFunctions = _props->backdropFilter; + backdropLayer.hostView = self; + backdropLayer.hostBackgroundColorLayer = _backgroundColorLayer; + backdropLayer.frame = self.bounds; + [self.layer insertSublayer:backdropLayer atIndex:0]; + _backdropFilterLayer = backdropLayer; + [backdropLayer setNeedsDisplay]; + } + // background image [self clearExistingBackgroundImageLayers]; if (!_props->backgroundImage.empty()) { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp index 713ab1470fd0..0dab5a8c9e6d 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp @@ -203,6 +203,12 @@ BaseViewProps::BaseViewProps( {})), filter( convertRawProp(context, rawProps, "filter", sourceProps.filter, {})), + backdropFilter(convertRawProp( + context, + rawProps, + "backdropFilter", + sourceProps.backdropFilter, + {})), backgroundImage(convertRawProp( context, rawProps, @@ -390,6 +396,7 @@ void BaseViewProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(outlineStyle); RAW_SET_PROP_SWITCH_CASE_BASIC(outlineWidth); RAW_SET_PROP_SWITCH_CASE_BASIC(filter); + RAW_SET_PROP_SWITCH_CASE_BASIC(backdropFilter); RAW_SET_PROP_SWITCH_CASE_BASIC(boxShadow); RAW_SET_PROP_SWITCH_CASE_BASIC(mixBlendMode); // events field diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h index b72c5f944f63..fbde3d8d2524 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h @@ -74,6 +74,9 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps { // Filter std::vector filter{}; + // Backdrop Filter + std::vector backdropFilter{}; + // Background Image std::vector backgroundImage{};