diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h index 124a85c65c17..6b4076ffcee4 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h @@ -14,6 +14,12 @@ #import #import +#include + +namespace facebook::react { +class ImageManager; +} // namespace facebook::react + NS_ASSUME_NONNULL_BEGIN /** @@ -60,6 +66,12 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets; +/** + * Used to load the `url()` entries of `backgroundImage`. + * Set by `RCTComponentViewRegistry` when it creates the view. + */ +- (void)setBackgroundImageManager:(std::shared_ptr)imageManager; + /** * Enforcing `call super` semantic for overridden methods from `RCTComponentViewProtocol`. * The methods update the instance variables. 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 8a074cd2b604..c62ebb181868 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -11,12 +11,14 @@ #import #import #import +#import #import #import #import #import #import +#import #import #import #import @@ -123,6 +125,8 @@ @implementation RCTViewComponentView { NSMutableSet *_accessibilityOrderNativeIDs; RCTSwiftUIContainerViewWrapper *_swiftUIWrapper; BOOL _focusable; + std::shared_ptr _imageManager; + RCTBackgroundImageURLLoader *_backgroundImageURLLoader; } #ifdef RCT_DYNAMIC_FRAMEWORKS @@ -842,6 +846,9 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask { [super finalizeUpdates:updateMask]; _useCustomContainerView = [self styleWouldClipOverflowInk]; + if ((updateMask & RNComponentViewUpdateMaskProps) != 0) { + [self updateBackgroundImageURLRequests]; + } if (!_needsInvalidateLayer) { return; } @@ -882,6 +889,7 @@ - (void)prepareForRecycle [_filterLayer removeFromSuperlayer]; _filterLayer = nil; [self clearExistingBackgroundImageLayers]; + _backgroundImageURLLoader = nil; [self _setLastSentSafeAreaInsets:nil]; _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = nil; @@ -1419,29 +1427,48 @@ - (void)invalidateLayer backgroundRepeat = _props->backgroundRepeat[imageIndex % _props->backgroundRepeat.size()]; } + CGSize itemIntrinsicSize = backgroundPositioningArea.size; + BOOL itemHasIntrinsicRatio = NO; + UIImage *urlImage = nil; + if (const auto *urlBackgroundImage = std::get_if(&backgroundImage)) { + urlImage = [_backgroundImageURLLoader imageForURI:urlBackgroundImage->uri]; + // Bundled assets send their natural size in layout units. A plain url + // has none, so use the size of the decoded image. + if (urlBackgroundImage->intrinsicWidth > 0 && urlBackgroundImage->intrinsicHeight > 0) { + itemIntrinsicSize = CGSizeMake(urlBackgroundImage->intrinsicWidth, urlBackgroundImage->intrinsicHeight); + itemHasIntrinsicRatio = YES; + } else if (urlImage != nil) { + itemIntrinsicSize = urlImage.size; + itemHasIntrinsicRatio = YES; + } + } + CGSize backgroundImageSize = [RCTBackgroundImageUtils calculateBackgroundImageSize:backgroundPositioningArea - itemIntrinsicSize:backgroundPositioningArea.size + itemIntrinsicSize:itemIntrinsicSize + itemHasIntrinsicRatio:itemHasIntrinsicRatio backgroundSize:backgroundSize backgroundRepeat:backgroundRepeat]; - CALayer *gradientLayer; + CALayer *itemLayer; if (std::holds_alternative(backgroundImage)) { const auto &linearGradient = std::get(backgroundImage); - gradientLayer = [RCTLinearGradient gradientLayerWithSize:backgroundImageSize gradient:linearGradient]; + itemLayer = [RCTLinearGradient gradientLayerWithSize:backgroundImageSize gradient:linearGradient]; } else if (std::holds_alternative(backgroundImage)) { const auto &radialGradient = std::get(backgroundImage); - gradientLayer = [RCTRadialGradient gradientLayerWithSize:backgroundImageSize gradient:radialGradient]; + itemLayer = [RCTRadialGradient gradientLayerWithSize:backgroundImageSize gradient:radialGradient]; + } else if (urlImage != nil) { + itemLayer = [RCTBackgroundImageUtils createURLImageLayerWithImage:urlImage size:backgroundImageSize]; } - if (gradientLayer != nil) { + if (itemLayer != nil) { CALayer *backgroundImageLayer = [RCTBackgroundImageUtils createBackgroundImageLayerWithSize:backgroundPositioningArea paintingArea:backgroundPaintingArea itemSize:backgroundImageSize backgroundPosition:backgroundPosition backgroundRepeat:backgroundRepeat - itemLayer:gradientLayer]; + itemLayer:itemLayer]; [self shapeLayerToMatchView:backgroundImageLayer borderMetrics:borderMetricsBI]; backgroundImageLayer.masksToBounds = YES; backgroundImageLayer.zPosition = BACKGROUND_COLOR_ZPOSITION; @@ -1543,6 +1570,35 @@ - (CAShapeLayer *)createMaskLayer:(CGRect)bounds cornerInsets:(RCTCornerInsets)c return maskLayer; } +- (void)setBackgroundImageManager:(std::shared_ptr)imageManager +{ + _imageManager = std::move(imageManager); +} + +- (void)updateBackgroundImageURLRequests +{ + bool hasURLBackgroundImage = std::ranges::any_of(_props->backgroundImage, [](const auto &backgroundImage) { + return std::holds_alternative(backgroundImage); + }); + if (!hasURLBackgroundImage || !_imageManager) { + // Releasing it cancels all loads + _backgroundImageURLLoader = nil; + return; + } + + if (_backgroundImageURLLoader == nil) { + __weak RCTViewComponentView *weakSelf = self; + _backgroundImageURLLoader = [[RCTBackgroundImageURLLoader alloc] initWithImageManager:_imageManager + onLoad:^{ + [weakSelf invalidateLayer]; + }]; + } + + const auto &eventTarget = _eventEmitter ? _eventEmitter->getEventTarget() : nullptr; + [_backgroundImageURLLoader loadBackgroundImages:_props->backgroundImage + surfaceId:eventTarget ? eventTarget->getSurfaceId() : -1]; +} + - (void)clearExistingBackgroundImageLayers { if (_backgroundImageLayers == nil) { diff --git a/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.h b/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.h index 584c4cb7ea97..dea33bb434ab 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.h +++ b/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.h @@ -12,6 +12,12 @@ #import #import +#include + +namespace facebook::react { +class ImageManager; +} // namespace facebook::react + NS_ASSUME_NONNULL_BEGIN /** @@ -50,6 +56,12 @@ NS_ASSUME_NONNULL_BEGIN */ - (nullable UIView *)findComponentViewWithTag:(facebook::react::Tag)tag; +/** + * Passed to every `RCTViewComponentView` the registry creates, to load + * `url()` background images. + */ +- (void)setImageManager:(std::shared_ptr)imageManager; + @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.mm b/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.mm index 0834bbfb7d14..8bf55b6a20be 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.mm +++ b/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.mm @@ -24,6 +24,7 @@ @implementation RCTComponentViewRegistry { std::unordered_map _registry; std::unordered_map> _recyclePool; + std::shared_ptr _imageManager; } - (instancetype)init @@ -93,7 +94,11 @@ - (RCTComponentViewDescriptor)_dequeueComponentViewWithComponentHandle:(Componen auto &recycledViews = _recyclePool[componentHandle]; if (recycledViews.empty()) { - return [self.componentViewFactory createComponentViewWithComponentHandle:componentHandle]; + auto componentViewDescriptor = [self.componentViewFactory createComponentViewWithComponentHandle:componentHandle]; + if (_imageManager && [componentViewDescriptor.view isKindOfClass:[RCTViewComponentView class]]) { + [(RCTViewComponentView *)componentViewDescriptor.view setBackgroundImageManager:_imageManager]; + } + return componentViewDescriptor; } auto componentViewDescriptor = recycledViews.back(); @@ -119,6 +124,11 @@ - (void)_enqueueComponentViewWithComponentHandle:(ComponentHandle)componentHandl recycledViews.push_back(componentViewDescriptor); } +- (void)setImageManager:(std::shared_ptr)imageManager +{ + _imageManager = std::move(imageManager); +} + - (void)handleApplicationDidReceiveMemoryWarningNotification { _recyclePool.clear(); diff --git a/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm index 68662f06d59b..bd0686bf73f1 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm +++ b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm @@ -15,6 +15,7 @@ #import #import #import +#import #import #import #import @@ -159,6 +160,12 @@ - (instancetype)init - (void)setContextContainer:(std::shared_ptr)contextContainer { _contextContainer = contextContainer; + + if (_contextContainer) { + if (auto imageManager = _contextContainer->find>(ImageManagerKey)) { + [_componentViewRegistry setImageManager:*imageManager]; + } + } } - (void)attachSurfaceToView:(UIView *)view surfaceId:(SurfaceId)surfaceId diff --git a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm index e5b19bd47658..69f72135ccb2 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm +++ b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm @@ -29,7 +29,9 @@ #import #import +#import #import +#import #import #import #import @@ -96,6 +98,13 @@ - (instancetype)initWithContextContainer:(std::shared_ptr` and `url()` background images, because `RCTImageLoader` must not get + // requests from more than one queue. Its constructor requires the image loader. + if (!contextContainer->find>(ImageManagerKey).has_value() && + contextContainer->find>("RCTImageLoader").has_value()) { + contextContainer->insert(ImageManagerKey, std::make_shared(contextContainer)); + } + _surfaceRegistry = [RCTSurfaceRegistry new]; _mountingManager = [RCTMountingManager new]; _mountingManager.contextContainer = contextContainer; diff --git a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.h b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.h new file mode 100644 index 000000000000..23a903c5895e --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.h @@ -0,0 +1,48 @@ +/* + * 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 +#import +#import + +#include +#include +#include + +namespace facebook::react { +class ImageManager; +} // namespace facebook::react + +NS_ASSUME_NONNULL_BEGIN + +/* + * Loads the `url()` entries of a view's `backgroundImage`. + * Releasing the loader cancels its requests. + */ +@interface RCTBackgroundImageURLLoader : NSObject + +/* + * `onLoad` is called on the main queue each time an image finishes loading. + */ +- (instancetype)initWithImageManager:(std::shared_ptr)imageManager + onLoad:(void (^)(void))onLoad; + +/* + * Requests every `url()` entry in `backgroundImages`. Does nothing if the + * urls did not change since the last call. + */ +- (void)loadBackgroundImages:(const std::vector &)backgroundImages + surfaceId:(facebook::react::SurfaceId)surfaceId; + +/* + * Returns nil until the image for `uri` has loaded. + */ +- (nullable UIImage *)imageForURI:(const std::string &)uri; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm new file mode 100644 index 000000000000..4e062e54e15b --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm @@ -0,0 +1,192 @@ +/* + * 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 "RCTBackgroundImageURLLoader.h" + +#import +#import +#import +#import +#import + +#include +#include + +using namespace facebook::react; + +/* + * The image for one uri. It is the delegate of its own observer proxy. A + * callback that is queued on the main queue retains it, so the callback + * always reaches the entry it was made for, even after a cancel. + */ +@interface RCTBackgroundImageURLEntry : NSObject + +@property (nonatomic, readonly) std::string uri; +@property (nonatomic, readonly, nullable) UIImage *image; + +- (instancetype)initWithURI:(std::string)uri onLoad:(void (^)(void))onLoad; +- (void)startWithImageManager:(const ImageManager &)imageManager surfaceId:(SurfaceId)surfaceId; +- (void)cancel; + +@end + +@implementation RCTBackgroundImageURLEntry { + void (^_onLoad)(void); + std::optional _imageRequest; + std::shared_ptr _observer; + BOOL _cancelled; +} + +- (instancetype)initWithURI:(std::string)uri onLoad:(void (^)(void))onLoad +{ + if (self = [super init]) { + _uri = std::move(uri); + _onLoad = onLoad; + } + return self; +} + +- (void)dealloc +{ + [self cancel]; +} + +- (void)startWithImageManager:(const ImageManager &)imageManager surfaceId:(SurfaceId)surfaceId +{ + ImageSource imageSource; + imageSource.type = ImageSource::Type::Remote; + imageSource.uri = _uri; + // CSS treats one image pixel as one point. Bundled assets send their + // natural size in props instead. + imageSource.scale = 1; + _imageRequest.emplace(imageManager.requestImage(imageSource, surfaceId)); + _observer = std::make_shared(self); + // Can deliver a cached response synchronously. + _imageRequest->getObserverCoordinator().addObserver(_observer); +} + +- (void)cancel +{ + _cancelled = YES; + if (_imageRequest) { + // Removing the last observer cancels the load if it is still running. + _imageRequest->getObserverCoordinator().removeObserver(_observer); + } +} + +#pragma mark - RCTImageResponseDelegate + +- (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(const void *)observer +{ + if (_cancelled) { + return; + } + _image = image; + _onLoad(); +} + +- (void)didReceiveProgress:(float)progress + loaded:(int64_t)loaded + total:(int64_t)total + fromObserver:(const void *)observer +{ +} + +- (void)didReceiveFailure:(NSError *)error fromObserver:(const void *)observer +{ + if (_cancelled) { + return; + } + RCTLogWarn(@"Failed to load background image: %s - %@", _uri.c_str(), error); +} + +@end + +@implementation RCTBackgroundImageURLLoader { + std::shared_ptr _imageManager; + void (^_onLoad)(void); + NSArray *_entries; +} + +- (instancetype)initWithImageManager:(std::shared_ptr)imageManager onLoad:(void (^)(void))onLoad +{ + if (self = [super init]) { + _imageManager = std::move(imageManager); + _onLoad = onLoad; + _entries = @[]; + } + return self; +} + +- (void)dealloc +{ + for (RCTBackgroundImageURLEntry *entry in _entries) { + [entry cancel]; + } +} + +- (void)loadBackgroundImages:(const std::vector &)backgroundImages surfaceId:(SurfaceId)surfaceId +{ + RCTAssertMainQueue(); + + std::vector uris; + for (const auto &backgroundImage : backgroundImages) { + if (const auto *urlBackgroundImage = std::get_if(&backgroundImage)) { + const auto &uri = urlBackgroundImage->uri; + if (!uri.empty() && std::ranges::find(uris, uri) == uris.end()) { + uris.push_back(uri); + } + } + } + + BOOL unchanged = uris.size() == _entries.count; + for (NSUInteger i = 0; unchanged && i < uris.size(); i++) { + unchanged = _entries[i].uri == uris[i]; + } + if (unchanged) { + return; + } + + NSMutableArray *entries = [NSMutableArray arrayWithCapacity:uris.size()]; + NSMutableArray *newEntries = [NSMutableArray new]; + for (const auto &uri : uris) { + RCTBackgroundImageURLEntry *entry = [self entryForURI:uri]; + if (entry == nil) { + entry = [[RCTBackgroundImageURLEntry alloc] initWithURI:uri onLoad:_onLoad]; + [newEntries addObject:entry]; + } + [entries addObject:entry]; + } + + for (RCTBackgroundImageURLEntry *entry in _entries) { + if (![entries containsObject:entry]) { + [entry cancel]; + } + } + + _entries = entries; + for (RCTBackgroundImageURLEntry *entry in newEntries) { + [entry startWithImageManager:*_imageManager surfaceId:surfaceId]; + } +} + +- (UIImage *)imageForURI:(const std::string &)uri +{ + return [self entryForURI:uri].image; +} + +- (nullable RCTBackgroundImageURLEntry *)entryForURI:(const std::string &)uri +{ + for (RCTBackgroundImageURLEntry *entry in _entries) { + if (entry.uri == uri) { + return entry; + } + } + return nil; +} + +@end diff --git a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.h b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.h index d06ca170f6a1..47c2a239628d 100644 --- a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.h +++ b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.h @@ -8,11 +8,13 @@ #import #import #import +#import @interface RCTBackgroundImageUtils : NSObject + (CGSize)calculateBackgroundImageSize:(const CGRect &)positioningArea itemIntrinsicSize:(CGSize)itemIntrinsicSize + itemHasIntrinsicRatio:(BOOL)itemHasIntrinsicRatio backgroundSize:(const facebook::react::BackgroundSize &)backgroundSize backgroundRepeat:(const facebook::react::BackgroundRepeat &)backgroundRepeat; @@ -23,4 +25,6 @@ backgroundRepeat:(const facebook::react::BackgroundRepeat &)backgroundRepeat itemLayer:(CALayer *)itemLayer; ++ (CALayer *)createURLImageLayerWithImage:(UIImage *)image size:(CGSize)size; + @end diff --git a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.mm b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.mm index 278d53a226fd..3cbc4ffbd40a 100644 --- a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.mm +++ b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.mm @@ -148,8 +148,60 @@ + (CALayer *)createBackgroundImageLayerWithSize:(const CGRect &)positioningArea return backgroundImageLayer; } +// Layers draw the `CGImage`, which ignores `imageOrientation`, so a transform orients it. ++ (CALayer *)createURLImageLayerWithImage:(UIImage *)image size:(CGSize)size +{ + CALayer *itemLayer = [CALayer layer]; + itemLayer.frame = CGRectMake(0, 0, size.width, size.height); + if (image.imageOrientation == UIImageOrientationUp) { + itemLayer.contents = (__bridge id)image.CGImage; + return itemLayer; + } + + CATransform3D transform = CATransform3DIdentity; + BOOL swapsAxes = NO; + switch (image.imageOrientation) { + case UIImageOrientationUp: + break; + case UIImageOrientationUpMirrored: + transform = CATransform3DMakeScale(-1, 1, 1); + break; + case UIImageOrientationDown: + transform = CATransform3DMakeRotation(M_PI, 0, 0, 1); + break; + case UIImageOrientationDownMirrored: + transform = CATransform3DMakeScale(1, -1, 1); + break; + case UIImageOrientationRight: + transform = CATransform3DMakeRotation(M_PI_2, 0, 0, 1); + swapsAxes = YES; + break; + case UIImageOrientationLeft: + transform = CATransform3DMakeRotation(-M_PI_2, 0, 0, 1); + swapsAxes = YES; + break; + case UIImageOrientationLeftMirrored: + transform = CATransform3DConcat(CATransform3DMakeRotation(M_PI_2, 0, 0, 1), CATransform3DMakeScale(-1, 1, 1)); + swapsAxes = YES; + break; + case UIImageOrientationRightMirrored: + transform = CATransform3DConcat(CATransform3DMakeRotation(-M_PI_2, 0, 0, 1), CATransform3DMakeScale(-1, 1, 1)); + swapsAxes = YES; + break; + } + + CALayer *imageLayer = [CALayer layer]; + imageLayer.contents = (__bridge id)image.CGImage; + imageLayer.bounds = swapsAxes ? CGRectMake(0, 0, size.height, size.width) : CGRectMake(0, 0, size.width, size.height); + imageLayer.position = CGPointMake(size.width / 2, size.height / 2); + imageLayer.transform = transform; + [itemLayer addSublayer:imageLayer]; + return itemLayer; +} + + (CGSize)calculateBackgroundImageSize:(const CGRect &)positioningArea itemIntrinsicSize:(CGSize)itemIntrinsicSize + itemHasIntrinsicRatio:(BOOL)itemHasIntrinsicRatio backgroundSize:(const facebook::react::BackgroundSize &)backgroundSize backgroundRepeat:(const facebook::react::BackgroundRepeat &)backgroundRepeat { @@ -159,9 +211,13 @@ + (CGSize)calculateBackgroundImageSize:(const CGRect &)positioningArea CGSize itemFinalSize = itemIntrinsicSize; CGSize positioningAreaSize = positioningArea.size; + bool xAuto = true; + bool yAuto = true; if (std::holds_alternative(backgroundSize)) { auto backgroundSizeLengthPercentage = std::get(backgroundSize); + xAuto = backgroundSizeLengthPercentage.isXAuto(); + yAuto = backgroundSizeLengthPercentage.isYAuto(); if (std::holds_alternative(backgroundSizeLengthPercentage.x)) { auto xValue = std::get(backgroundSizeLengthPercentage.x); if (xValue.unit == facebook::react::UnitType::Percent) { @@ -181,13 +237,21 @@ + (CGSize)calculateBackgroundImageSize:(const CGRect &)positioningArea } } + // An `auto` dimension keeps the image's aspect ratio. Without a ratio (gradients), it stays at its intrinsic size. + bool keepsRatio = itemHasIntrinsicRatio && itemIntrinsicSize.width > 0 && itemIntrinsicSize.height > 0; + CGFloat ratio = keepsRatio ? itemIntrinsicSize.width / itemIntrinsicSize.height : 0; + if (keepsRatio && !xAuto && yAuto) { + itemFinalSize.height = itemFinalSize.width / ratio; + } else if (keepsRatio && xAuto && !yAuto) { + itemFinalSize.width = itemFinalSize.height * ratio; + } + if (backgroundRepeat.x == facebook::react::BackgroundRepeatStyle::Round) { if (itemFinalSize.width > 0 && positioningAreaSize.width > 0) { if (fmod(positioningAreaSize.width, itemFinalSize.width) != 0) { - auto divisor = static_cast(round(positioningAreaSize.width / itemFinalSize.width)); - if (divisor > 0) { - itemFinalSize.width = positioningAreaSize.width / divisor; - } + // CSS rounds to the nearest whole number of tiles, but at least one. + CGFloat divisor = std::max(1, round(positioningAreaSize.width / itemFinalSize.width)); + itemFinalSize.width = positioningAreaSize.width / divisor; } } } @@ -195,14 +259,22 @@ + (CGSize)calculateBackgroundImageSize:(const CGRect &)positioningArea if (backgroundRepeat.y == facebook::react::BackgroundRepeatStyle::Round) { if (itemFinalSize.height > 0 && positioningAreaSize.height > 0) { if (fmod(positioningAreaSize.height, itemFinalSize.height) != 0) { - auto divisor = static_cast(round(positioningAreaSize.height / itemFinalSize.height)); - if (divisor > 0) { - itemFinalSize.height = positioningAreaSize.height / divisor; - } + // CSS rounds to the nearest whole number of tiles, but at least one. + CGFloat divisor = std::max(1, round(positioningAreaSize.height / itemFinalSize.height)); + itemFinalSize.height = positioningAreaSize.height / divisor; } } } + // With `round` on one axis only, an `auto` other axis is scaled again to restore the ratio. + bool roundX = backgroundRepeat.x == facebook::react::BackgroundRepeatStyle::Round; + bool roundY = backgroundRepeat.y == facebook::react::BackgroundRepeatStyle::Round; + if (keepsRatio && roundX && !roundY && yAuto) { + itemFinalSize.height = itemFinalSize.width / ratio; + } else if (keepsRatio && roundY && !roundX && xAuto) { + itemFinalSize.width = itemFinalSize.height * ratio; + } + return itemFinalSize; } diff --git a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj index 1d336b424136..356cdec6bc71 100644 --- a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj +++ b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ E7DB20E622B2BAA6005AC45F /* RCTImageLoaderHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20C922B2BAA5005AC45F /* RCTImageLoaderHelpers.m */; }; E7DB20E722B2BAA6005AC45F /* RCTConvert_NSURLTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20CA22B2BAA5005AC45F /* RCTConvert_NSURLTests.m */; }; E7DB20E822B2BAA6005AC45F /* RCTModuleMethodTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20CB22B2BAA5005AC45F /* RCTModuleMethodTests.mm */; }; + B7C4E2A15D3F48096A1B2C02 /* RCTBackgroundImageUtilsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = B7C4E2A15D3F48096A1B2C01 /* RCTBackgroundImageUtilsTests.mm */; }; E7DB20E922B2BAA6005AC45F /* RCTComponentPropsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20CC22B2BAA5005AC45F /* RCTComponentPropsTests.m */; }; E7DB20EA22B2BAA6005AC45F /* RCTImageUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20CD22B2BAA5005AC45F /* RCTImageUtilTests.m */; }; E7DB20EB22B2BAA6005AC45F /* RCTConvert_YGValueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20CE22B2BAA5005AC45F /* RCTConvert_YGValueTests.m */; }; @@ -129,6 +130,7 @@ E7DB20C922B2BAA5005AC45F /* RCTImageLoaderHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoaderHelpers.m; sourceTree = ""; }; E7DB20CA22B2BAA5005AC45F /* RCTConvert_NSURLTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert_NSURLTests.m; sourceTree = ""; }; E7DB20CB22B2BAA5005AC45F /* RCTModuleMethodTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTModuleMethodTests.mm; sourceTree = ""; }; + B7C4E2A15D3F48096A1B2C01 /* RCTBackgroundImageUtilsTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTBackgroundImageUtilsTests.mm; sourceTree = ""; }; E7DB20CC22B2BAA5005AC45F /* RCTComponentPropsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTComponentPropsTests.m; sourceTree = ""; }; E7DB20CD22B2BAA5005AC45F /* RCTImageUtilTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageUtilTests.m; sourceTree = ""; }; E7DB20CE22B2BAA5005AC45F /* RCTConvert_YGValueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert_YGValueTests.m; sourceTree = ""; }; @@ -322,6 +324,7 @@ E7DB20A322B2BA84005AC45F /* Info.plist */, E7DB20C622B2BAA5005AC45F /* RCTAllocationTests.m */, E7DB20B222B2BAA4005AC45F /* RCTAnimationUtilsTests.m */, + B7C4E2A15D3F48096A1B2C01 /* RCTBackgroundImageUtilsTests.mm */, E7DB20AB22B2BAA3005AC45F /* RCTBlobManagerTests.m */, E7DB20A922B2BAA3005AC45F /* RCTBundleURLProviderTests.m */, E7DB20CC22B2BAA5005AC45F /* RCTComponentPropsTests.m */, @@ -757,6 +760,7 @@ E7DB20EC22B2BAA6005AC45F /* RCTMultipartStreamReaderTests.m in Sources */, E7DB20E022B2BAA6005AC45F /* RCTMethodArgumentTests.m in Sources */, E7DB20E822B2BAA6005AC45F /* RCTModuleMethodTests.mm in Sources */, + B7C4E2A15D3F48096A1B2C02 /* RCTBackgroundImageUtilsTests.mm in Sources */, F1A0B1C23D4E5F6071829302 /* RCTTurboModuleArrayBufferTests.mm in Sources */, F1A0B1C23D4E5F6071829322 /* RCTTurboModuleStringConversionTests.mm in Sources */, E7DB20E222B2BAA6005AC45F /* RCTGzipTests.m in Sources */, diff --git a/packages/rn-tester/RNTesterUnitTests/RCTBackgroundImageUtilsTests.mm b/packages/rn-tester/RNTesterUnitTests/RCTBackgroundImageUtilsTests.mm new file mode 100644 index 000000000000..ce61ebd066fa --- /dev/null +++ b/packages/rn-tester/RNTesterUnitTests/RCTBackgroundImageUtilsTests.mm @@ -0,0 +1,139 @@ +/* + * 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 +#import + +#import + +#include +#include +#include +#include + +using namespace facebook::react; + +static BackgroundSize RCTBackgroundSizeInPoints(std::optional width, std::optional height) +{ + BackgroundSizeLengthPercentage size; + if (width) { + size.x = ValueUnit(*width, UnitType::Point); + } + if (height) { + size.y = ValueUnit(*height, UnitType::Point); + } + return size; +} + +// A 60x40 image with four colored quadrants, so every rotation and mirror gives different pixels. +static CGImageRef RCTCreateQuadrantImage() +{ + CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + CGContextRef context = + CGBitmapContextCreate(nullptr, 60, 40, 8, 0, colorSpace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast); + const CGFloat colors[4][3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {1, 1, 0}}; + for (int i = 0; i < 4; i++) { + CGContextSetRGBFillColor(context, colors[i][0], colors[i][1], colors[i][2], 1); + CGContextFillRect(context, CGRectMake((i % 2) * 30, (i / 2) * 20, 30, 20)); + } + CGImageRef image = CGBitmapContextCreateImage(context); + CGContextRelease(context); + CGColorSpaceRelease(colorSpace); + return image; +} + +static std::vector RCTPixels(UIImage *image) +{ + CGImageRef cgImage = image.CGImage; + size_t width = CGImageGetWidth(cgImage); + size_t height = CGImageGetHeight(cgImage); + std::vector pixels(width * height * 4); + CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + CGContextRef context = CGBitmapContextCreate( + pixels.data(), width, height, 8, width * 4, colorSpace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast); + CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); + CGContextRelease(context); + CGColorSpaceRelease(colorSpace); + return pixels; +} + +@interface RCTBackgroundImageUtilsTests : XCTestCase +@end + +@implementation RCTBackgroundImageUtilsTests + +- (void)testAutoHeightKeepsTheRatioOfAURLImage +{ + CGSize size = [RCTBackgroundImageUtils calculateBackgroundImageSize:CGRectMake(0, 0, 200, 200) + itemIntrinsicSize:CGSizeMake(300, 150) + itemHasIntrinsicRatio:YES + backgroundSize:RCTBackgroundSizeInPoints(100, std::nullopt) + backgroundRepeat:BackgroundRepeat{}]; + XCTAssertEqualWithAccuracy(size.width, 100, 0.001); + XCTAssertEqualWithAccuracy(size.height, 50, 0.001); +} + +- (void)testAutoHeightOfAGradientStaysAtItsIntrinsicSize +{ + CGSize size = [RCTBackgroundImageUtils calculateBackgroundImageSize:CGRectMake(0, 0, 200, 200) + itemIntrinsicSize:CGSizeMake(200, 200) + itemHasIntrinsicRatio:NO + backgroundSize:RCTBackgroundSizeInPoints(100, std::nullopt) + backgroundRepeat:BackgroundRepeat{}]; + XCTAssertEqualWithAccuracy(size.width, 100, 0.001); + XCTAssertEqualWithAccuracy(size.height, 200, 0.001); +} + +- (void)testRoundUsesAtLeastOneTile +{ + BackgroundRepeat repeat; + repeat.x = BackgroundRepeatStyle::Round; + repeat.y = BackgroundRepeatStyle::Round; + CGSize size = [RCTBackgroundImageUtils calculateBackgroundImageSize:CGRectMake(0, 0, 100, 100) + itemIntrinsicSize:CGSizeMake(100, 100) + itemHasIntrinsicRatio:NO + backgroundSize:RCTBackgroundSizeInPoints(250, 250) + backgroundRepeat:repeat]; + XCTAssertEqualWithAccuracy(size.width, 100, 0.001); + XCTAssertEqualWithAccuracy(size.height, 100, 0.001); +} + +- (void)testURLImageLayerDrawsEveryOrientationLikeUIImage +{ + CGImageRef cgImage = RCTCreateQuadrantImage(); + UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat preferredFormat]; + format.scale = 1; + + for (NSInteger orientation = UIImageOrientationUp; orientation <= UIImageOrientationRightMirrored; orientation++) { + UIImage *image = [UIImage imageWithCGImage:cgImage scale:1 orientation:(UIImageOrientation)orientation]; + CGSize size = image.size; + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size format:format]; + + UIImage *expected = [renderer imageWithActions:^(__unused UIGraphicsImageRendererContext *context) { + [image drawInRect:CGRectMake(0, 0, size.width, size.height)]; + }]; + CALayer *layer = [RCTBackgroundImageUtils createURLImageLayerWithImage:image size:size]; + UIImage *actual = [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) { + [layer renderInContext:context.CGContext]; + }]; + + std::vector expectedPixels = RCTPixels(expected); + std::vector actualPixels = RCTPixels(actual); + XCTAssertEqual(expectedPixels.size(), actualPixels.size(), @"orientation %ld", (long)orientation); + size_t differentBytes = 0; + for (size_t i = 0; i < std::min(expectedPixels.size(), actualPixels.size()); i++) { + if (std::abs(expectedPixels[i] - actualPixels[i]) > 2) { + differentBytes++; + } + } + XCTAssertEqual(differentBytes, 0u, @"orientation %ld", (long)orientation); + } + + CGImageRelease(cgImage); +} + +@end diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index e04f3b738889..c906f8bdf833 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -653,9 +653,16 @@ interface RCTBackedTextViewDelegateAdapter : public NSObject { public virtual void skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange* textRange); } +interface RCTBackgroundImageURLLoader : public NSObject { + public virtual _Nullable UIImage* imageForURI:(const std::string& uri); + public virtual instancetype initWithImageManager:onLoad:(std::shared_ptr imageManager, void(^)(void) onLoad); + public virtual void loadBackgroundImages:surfaceId:(const std::vector& backgroundImages, facebook::react::SurfaceId surfaceId); +} + interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); - public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); + public virtual static CALayer* createURLImageLayerWithImage:size:(UIImage* image, CGSize size); + public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:itemHasIntrinsicRatio:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, BOOL itemHasIntrinsicRatio, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); } interface RCTBlobManager : public NSObject { @@ -828,6 +835,7 @@ interface RCTComponentViewRegistry : public NSObject { public virtual const RCTComponentViewDescriptor& componentViewDescriptorWithTag:(facebook::react::Tag tag); public virtual const RCTComponentViewDescriptor& dequeueComponentViewWithComponentHandle:tag:(facebook::react::ComponentHandle componentHandle, facebook::react::Tag tag); public virtual void enqueueComponentViewWithComponentHandle:tag:componentViewDescriptor:(facebook::react::ComponentHandle componentHandle, facebook::react::Tag tag, RCTComponentViewDescriptor componentViewDescriptor); + public virtual void setImageManager:(std::shared_ptr imageManager); } interface RCTComposedViewRegistry : public NSMutableDictionary { @@ -2066,6 +2074,7 @@ interface RCTViewComponentView : public UIView imageManager); public virtual void updateEventEmitter:(const facebook::react::EventEmitter::Shared& eventEmitter); public virtual void updateLayoutMetrics:oldLayoutMetrics:(const facebook::react::LayoutMetrics& layoutMetrics, const facebook::react::LayoutMetrics& oldLayoutMetrics); public virtual void updateProps:oldProps:(const facebook::react::Props::Shared& props, const facebook::react::Props::Shared& oldProps); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api index cb8ca685d4f2..1c18657fa447 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -662,9 +662,16 @@ interface RCTBackedTextViewDelegateAdapter : public NSObject { public virtual void skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange* textRange); } +interface RCTBackgroundImageURLLoader : public NSObject { + public virtual _Nullable UIImage* imageForURI:(const std::string& uri); + public virtual instancetype initWithImageManager:onLoad:(std::shared_ptr imageManager, void(^)(void) onLoad); + public virtual void loadBackgroundImages:surfaceId:(const std::vector& backgroundImages, facebook::react::SurfaceId surfaceId); +} + interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); - public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); + public virtual static CALayer* createURLImageLayerWithImage:size:(UIImage* image, CGSize size); + public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:itemHasIntrinsicRatio:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, BOOL itemHasIntrinsicRatio, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); } interface RCTBlobManager : public NSObject { @@ -837,6 +844,7 @@ interface RCTComponentViewRegistry : public NSObject { public virtual const RCTComponentViewDescriptor& componentViewDescriptorWithTag:(facebook::react::Tag tag); public virtual const RCTComponentViewDescriptor& dequeueComponentViewWithComponentHandle:tag:(facebook::react::ComponentHandle componentHandle, facebook::react::Tag tag); public virtual void enqueueComponentViewWithComponentHandle:tag:componentViewDescriptor:(facebook::react::ComponentHandle componentHandle, facebook::react::Tag tag, RCTComponentViewDescriptor componentViewDescriptor); + public virtual void setImageManager:(std::shared_ptr imageManager); } interface RCTComposedViewRegistry : public NSMutableDictionary { @@ -2124,6 +2132,7 @@ interface RCTViewComponentView : public UIView imageManager); public virtual void updateEventEmitter:(const facebook::react::EventEmitter::Shared& eventEmitter); public virtual void updateLayoutMetrics:oldLayoutMetrics:(const facebook::react::LayoutMetrics& layoutMetrics, const facebook::react::LayoutMetrics& oldLayoutMetrics); public virtual void updateProps:oldProps:(const facebook::react::Props::Shared& props, const facebook::react::Props::Shared& oldProps); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 9010fcf23951..6b09da439ca1 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -653,9 +653,16 @@ interface RCTBackedTextViewDelegateAdapter : public NSObject { public virtual void skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange* textRange); } +interface RCTBackgroundImageURLLoader : public NSObject { + public virtual _Nullable UIImage* imageForURI:(const std::string& uri); + public virtual instancetype initWithImageManager:onLoad:(std::shared_ptr imageManager, void(^)(void) onLoad); + public virtual void loadBackgroundImages:surfaceId:(const std::vector& backgroundImages, facebook::react::SurfaceId surfaceId); +} + interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); - public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); + public virtual static CALayer* createURLImageLayerWithImage:size:(UIImage* image, CGSize size); + public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:itemHasIntrinsicRatio:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, BOOL itemHasIntrinsicRatio, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); } interface RCTBlobManager : public NSObject { @@ -828,6 +835,7 @@ interface RCTComponentViewRegistry : public NSObject { public virtual const RCTComponentViewDescriptor& componentViewDescriptorWithTag:(facebook::react::Tag tag); public virtual const RCTComponentViewDescriptor& dequeueComponentViewWithComponentHandle:tag:(facebook::react::ComponentHandle componentHandle, facebook::react::Tag tag); public virtual void enqueueComponentViewWithComponentHandle:tag:componentViewDescriptor:(facebook::react::ComponentHandle componentHandle, facebook::react::Tag tag, RCTComponentViewDescriptor componentViewDescriptor); + public virtual void setImageManager:(std::shared_ptr imageManager); } interface RCTComposedViewRegistry : public NSMutableDictionary { @@ -2059,6 +2067,7 @@ interface RCTViewComponentView : public UIView imageManager); public virtual void updateEventEmitter:(const facebook::react::EventEmitter::Shared& eventEmitter); public virtual void updateLayoutMetrics:oldLayoutMetrics:(const facebook::react::LayoutMetrics& layoutMetrics, const facebook::react::LayoutMetrics& oldLayoutMetrics); public virtual void updateProps:oldProps:(const facebook::react::Props::Shared& props, const facebook::react::Props::Shared& oldProps); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 6bb9397bd2e4..b5c8bf51a5c7 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -653,9 +653,16 @@ interface RCTBackedTextViewDelegateAdapter : public NSObject { public virtual void skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange* textRange); } +interface RCTBackgroundImageURLLoader : public NSObject { + public virtual _Nullable UIImage* imageForURI:(const std::string& uri); + public virtual instancetype initWithImageManager:onLoad:(std::shared_ptr imageManager, void(^)(void) onLoad); + public virtual void loadBackgroundImages:surfaceId:(const std::vector& backgroundImages, facebook::react::SurfaceId surfaceId); +} + interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); - public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); + public virtual static CALayer* createURLImageLayerWithImage:size:(UIImage* image, CGSize size); + public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:itemHasIntrinsicRatio:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, BOOL itemHasIntrinsicRatio, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); } interface RCTBlobManager : public NSObject { @@ -828,6 +835,7 @@ interface RCTComponentViewRegistry : public NSObject { public virtual const RCTComponentViewDescriptor& componentViewDescriptorWithTag:(facebook::react::Tag tag); public virtual const RCTComponentViewDescriptor& dequeueComponentViewWithComponentHandle:tag:(facebook::react::ComponentHandle componentHandle, facebook::react::Tag tag); public virtual void enqueueComponentViewWithComponentHandle:tag:componentViewDescriptor:(facebook::react::ComponentHandle componentHandle, facebook::react::Tag tag, RCTComponentViewDescriptor componentViewDescriptor); + public virtual void setImageManager:(std::shared_ptr imageManager); } interface RCTComposedViewRegistry : public NSMutableDictionary { @@ -2066,6 +2074,7 @@ interface RCTViewComponentView : public UIView imageManager); public virtual void updateEventEmitter:(const facebook::react::EventEmitter::Shared& eventEmitter); public virtual void updateLayoutMetrics:oldLayoutMetrics:(const facebook::react::LayoutMetrics& layoutMetrics, const facebook::react::LayoutMetrics& oldLayoutMetrics); public virtual void updateProps:oldProps:(const facebook::react::Props::Shared& props, const facebook::react::Props::Shared& oldProps);