diff --git a/Source/WTF/wtf/PlatformEnableCocoa.h b/Source/WTF/wtf/PlatformEnableCocoa.h index 323a4babc2491..bc023f0405061 100644 --- a/Source/WTF/wtf/PlatformEnableCocoa.h +++ b/Source/WTF/wtf/PlatformEnableCocoa.h @@ -1010,10 +1010,17 @@ #define ENABLE_WIRELESS_PLAYBACK_TARGET_AVAILABILITY_API 1 #endif -#if !defined(ENABLE_WK_WEB_EXTENSIONS) && (PLATFORM(MAC) || PLATFORM(MACCATALYST) || PLATFORM(IOS) || PLATFORM(IOS_SIMULATOR) || PLATFORM(VISION)) +#if !defined(ENABLE_WK_WEB_EXTENSIONS) && (PLATFORM(MAC) || PLATFORM(MACCATALYST) || PLATFORM(IOS) || PLATFORM(VISION)) #define ENABLE_WK_WEB_EXTENSIONS 1 #endif +#if !defined(ENABLE_WK_WEB_EXTENSIONS_ICON_VARIANTS) \ + && ((PLATFORM(MAC) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 150500) \ + || ((PLATFORM(IOS) || PLATFORM(MACCATALYST)) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 180500) \ + || (PLATFORM(VISION) && __VISION_OS_VERSION_MAX_ALLOWED >= 20500)) +#define ENABLE_WK_WEB_EXTENSIONS_ICON_VARIANTS ENABLE_WK_WEB_EXTENSIONS +#endif + #if !defined(ENABLE_WK_WEB_EXTENSIONS_SIDEBAR) #define ENABLE_WK_WEB_EXTENSIONS_SIDEBAR 0 && ENABLE_WK_WEB_EXTENSIONS #endif diff --git a/Source/WebCore/en.lproj/Localizable.strings b/Source/WebCore/en.lproj/Localizable.strings index 6a1bf0a5698e5..c5f49801e64ad 100644 --- a/Source/WebCore/en.lproj/Localizable.strings +++ b/Source/WebCore/en.lproj/Localizable.strings @@ -532,6 +532,15 @@ /* WKWebExtensionErrorInvalidManifestEntry description for externally_connectable */ "Empty or invalid `externally_connectable` manifest entry." = "Empty or invalid `externally_connectable` manifest entry."; +/* WKWebExtensionErrorInvalidManifestEntry description for icon_variants in action only */ +"Empty or invalid `icon_variants` for the `action` manifest entry." = "Empty or invalid `icon_variants` for the `action` manifest entry."; + +/* WKWebExtensionErrorInvalidManifestEntry description for icon_variants in browser_action or page_action */ +"Empty or invalid `icon_variants` for the `browser_action` or `page_action` manifest entry." = "Empty or invalid `icon_variants` for the `browser_action` or `page_action` manifest entry."; + +/* WKWebExtensionErrorInvalidManifestEntry description for icon_variants */ +"Empty or invalid `icon_variants` manifest entry." = "Empty or invalid `icon_variants` manifest entry."; + /* WKWebExtensionErrorInvalidManifestEntry description for invalid new tab entry */ "Empty or invalid `newtab` manifest entry." = "Empty or invalid `newtab` manifest entry."; @@ -616,6 +625,12 @@ /* WKWebExtensionErrorInvalidActionIcon description for failing to load images for browser_action or page_action */ "Failed to load images in `default_icon` for the `browser_action` or `page_action` manifest entry." = "Failed to load images in `default_icon` for the `browser_action` or `page_action` manifest entry."; +/* WKWebExtensionErrorInvalidActionIcon description for failing to load image variants for action */ +"Failed to load images in `icon_variants` for the `action` manifest entry." = "Failed to load images in `icon_variants` for the `action` manifest entry."; + +/* WKWebExtensionErrorInvalidIcon description for failing to load image variants */ +"Failed to load images in `icon_variants` manifest entry." = "Failed to load images in `icon_variants` manifest entry."; + /* WKWebExtensionErrorInvalidIcon description for failing to load images */ "Failed to load images in `icons` manifest entry." = "Failed to load images in `icons` manifest entry."; diff --git a/Source/WebKit/Platform/cocoa/CocoaHelpers.h b/Source/WebKit/Platform/cocoa/CocoaHelpers.h index 4d30f4fcb7abf..b267d4fe6f2f6 100644 --- a/Source/WebKit/Platform/cocoa/CocoaHelpers.h +++ b/Source/WebKit/Platform/cocoa/CocoaHelpers.h @@ -136,6 +136,9 @@ NSString *escapeCharactersInString(NSString *, NSString *charactersToEscape); void callAfterRandomDelay(Function&&); +NSSet *availableScreenScales(); +CGFloat largestDisplayScale(); + NSDate *toAPI(const WallTime&); WallTime toImpl(NSDate *); diff --git a/Source/WebKit/Platform/cocoa/CocoaHelpers.mm b/Source/WebKit/Platform/cocoa/CocoaHelpers.mm index 428b3930ea9c2..16390fad2e827 100644 --- a/Source/WebKit/Platform/cocoa/CocoaHelpers.mm +++ b/Source/WebKit/Platform/cocoa/CocoaHelpers.mm @@ -38,6 +38,10 @@ #import #import +#if PLATFORM(IOS_FAMILY) +#import +#endif + namespace WebKit { static NSString * const privacyPreservingDescriptionKey = @"privacyPreservingDescription"; @@ -440,6 +444,40 @@ void callAfterRandomDelay(Function&& completionHandler) dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay.nanosecondsAs()), dispatch_get_main_queue(), makeBlockPtr(WTFMove(completionHandler)).get()); } +NSSet *availableScreenScales() +{ + NSMutableSet *screenScales = [NSMutableSet set]; + +#if USE(APPKIT) + for (NSScreen *screen in NSScreen.screens) + [screenScales addObject:@(screen.backingScaleFactor)]; +#else + ALLOW_DEPRECATED_DECLARATIONS_BEGIN + for (UIScreen *screen in UIScreen.screens) + [screenScales addObject:@(screen.scale)]; + ALLOW_DEPRECATED_DECLARATIONS_END +#endif + + if (screenScales.count) + return [screenScales copy]; + + // Assume 1x if we got no results. This can happen on headless devices (bots). + return [NSSet setWithObject:@1]; +} + +CGFloat largestDisplayScale() +{ + auto largestDisplayScale = 1.0; + + for (NSNumber *scale in availableScreenScales()) { + auto doubleValue = scale.doubleValue; + if (doubleValue > largestDisplayScale) + largestDisplayScale = doubleValue; + } + + return largestDisplayScale; +} + NSDate *toAPI(const WallTime& time) { if (time.isNaN()) diff --git a/Source/WebKit/Platform/spi/ios/UIKitSPI.h b/Source/WebKit/Platform/spi/ios/UIKitSPI.h index f685418d76e7f..03bca88ccd8ef 100644 --- a/Source/WebKit/Platform/spi/ios/UIKitSPI.h +++ b/Source/WebKit/Platform/spi/ios/UIKitSPI.h @@ -45,6 +45,7 @@ #import #import #import +#import #import #import #import @@ -338,6 +339,10 @@ typedef enum { @property (nonatomic, setter=_setShowsFileSizePicker:) BOOL _showsFileSizePicker; @end +@interface UIImageAsset () ++ (instancetype)_dynamicAssetNamed:(NSString *)name generator:(UIImage *(^)(UIImageAsset *, UIImageConfiguration *, UIImage *))block; +@end + typedef struct CGSVGDocument *CGSVGDocumentRef; @interface UIImage () @@ -345,6 +350,7 @@ typedef struct CGSVGDocument *CGSVGDocumentRef; - (UIImage *)_flatImageWithColor:(UIColor *)color; + (UIImage *)_systemImageNamed:(NSString *)name; + (UIImage *)_imageWithCGSVGDocument:(CGSVGDocumentRef)cgSVGDocument; ++ (UIImage *)_imageWithCGSVGDocument:(CGSVGDocumentRef)cgSVGDocument scale:(CGFloat)scale orientation:(UIImageOrientation)orientation; @end @protocol UIKeyboardImplGeometryDelegate @@ -1111,6 +1117,10 @@ typedef NS_ENUM(NSUInteger, _UIScrollDeviceCategory) { + (UIColor *)insertionPointColor; @end +@interface UIImage () +- (UIImage *)_rasterizedImage; +@end + @interface UIView (IPI) - (UIScrollView *)_scroller; - (CGPoint)accessibilityConvertPointFromSceneReferenceCoordinates:(CGPoint)point; diff --git a/Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionActionCocoa.mm b/Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionActionCocoa.mm index 81b9a166d26db..c481e509f5f44 100644 --- a/Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionActionCocoa.mm +++ b/Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionActionCocoa.mm @@ -646,11 +646,12 @@ - (void)_otherPopoverWillShow:(NSNotification *)notification return nil; if (m_customIcons) { - NSError *error; - if (CocoaImage *result = extensionContext()->extension().bestImageInIconsDictionary(m_customIcons.get(), idealSize.width > idealSize.height ? idealSize.width : idealSize.height, &error)) - return result; + auto *result = extensionContext()->extension().bestImageInIconsDictionary(m_customIcons.get(), idealSize, [&](auto *error) { + extensionContext()->recordError(error); + }); - extensionContext()->recordErrorIfNeeded(error); + if (result) + return result; // If custom icons fail, fallback to the default icons. } diff --git a/Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionCocoa.mm b/Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionCocoa.mm index 8ffb0471be128..540fdef53c17e 100644 --- a/Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionCocoa.mm +++ b/Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionCocoa.mm @@ -78,6 +78,14 @@ static NSString * const iconsManifestKey = @"icons"; +#if ENABLE(WK_WEB_EXTENSIONS_ICON_VARIANTS) +static NSString * const iconVariantsManifestKey = @"icon_variants"; +static NSString * const colorSchemesManifestKey = @"color_schemes"; +static NSString * const lightManifestKey = @"light"; +static NSString * const darkManifestKey = @"dark"; +static NSString * const anyManifestKey = @"any"; +#endif + static NSString * const actionManifestKey = @"action"; static NSString * const browserActionManifestKey = @"browser_action"; static NSString * const pageActionManifestKey = @"page_action"; @@ -700,6 +708,14 @@ static WKWebExtensionError toAPI(WebExtension::Error error) break; case Error::InvalidActionIcon: +#if ENABLE(WK_WEB_EXTENSIONS_ICON_VARIANTS) + if (m_actionDictionary.get()[iconVariantsManifestKey]) { + if (supportsManifestVersion(3)) + localizedDescription = WEB_UI_STRING("Empty or invalid `icon_variants` for the `action` manifest entry.", "WKWebExtensionErrorInvalidManifestEntry description for icon_variants in action only"); + else + localizedDescription = WEB_UI_STRING("Empty or invalid `icon_variants` for the `browser_action` or `page_action` manifest entry.", "WKWebExtensionErrorInvalidManifestEntry description for icon_variants in browser_action or page_action"); + } else +#endif if (supportsManifestVersion(3)) localizedDescription = WEB_UI_STRING("Empty or invalid `default_icon` for the `action` manifest entry.", "WKWebExtensionErrorInvalidManifestEntry description for default_icon in action only"); else @@ -740,7 +756,12 @@ static WKWebExtensionError toAPI(WebExtension::Error error) break; case Error::InvalidIcon: - localizedDescription = WEB_UI_STRING("Missing or empty `icons` manifest entry.", "WKWebExtensionErrorInvalidManifestEntry description for icons"); +#if ENABLE(WK_WEB_EXTENSIONS_ICON_VARIANTS) + if ([manifest() objectForKey:iconVariantsManifestKey]) + localizedDescription = WEB_UI_STRING("Empty or invalid `icon_variants` manifest entry.", "WKWebExtensionErrorInvalidManifestEntry description for icon_variants"); + else +#endif + localizedDescription = WEB_UI_STRING("Missing or empty `icons` manifest entry.", "WKWebExtensionErrorInvalidManifestEntry description for icons"); break; case Error::InvalidName: @@ -1011,8 +1032,15 @@ static WKWebExtensionError toAPI(WebExtension::Error error) if (!manifestParsedSuccessfully()) return nil; +#if ENABLE(WK_WEB_EXTENSIONS_ICON_VARIANTS) + if (m_manifest.get()[iconVariantsManifestKey]) { + NSString *localizedErrorDescription = WEB_UI_STRING("Failed to load images in `icon_variants` manifest entry.", "WKWebExtensionErrorInvalidIcon description for failing to load image variants"); + return bestImageForIconVariantsManifestKey(m_manifest.get(), iconVariantsManifestKey, size, m_iconsCache, Error::InvalidIcon, localizedErrorDescription); + } +#endif + NSString *localizedErrorDescription = WEB_UI_STRING("Failed to load images in `icons` manifest entry.", "WKWebExtensionErrorInvalidIcon description for failing to load images"); - return bestImageForIconsDictionaryManifestKey(m_manifest.get(), iconsManifestKey, size, m_icon, Error::InvalidIcon, localizedErrorDescription); + return bestImageForIconsDictionaryManifestKey(m_manifest.get(), iconsManifestKey, size, m_iconsCache, Error::InvalidIcon, localizedErrorDescription); } CocoaImage *WebExtension::actionIcon(CGSize size) @@ -1022,13 +1050,25 @@ static WKWebExtensionError toAPI(WebExtension::Error error) populateActionPropertiesIfNeeded(); + if (m_defaultActionIcon) + return m_defaultActionIcon.get(); + +#if ENABLE(WK_WEB_EXTENSIONS_ICON_VARIANTS) + if (m_actionDictionary.get()[iconVariantsManifestKey]) { + NSString *localizedErrorDescription = WEB_UI_STRING("Failed to load images in `icon_variants` for the `action` manifest entry.", "WKWebExtensionErrorInvalidActionIcon description for failing to load image variants for action"); + if (auto *result = bestImageForIconVariantsManifestKey(m_actionDictionary.get(), iconVariantsManifestKey, size, m_actionIconsCache, Error::InvalidActionIcon, localizedErrorDescription)) + return result; + return icon(size); + } +#endif + NSString *localizedErrorDescription; if (supportsManifestVersion(3)) localizedErrorDescription = WEB_UI_STRING("Failed to load images in `default_icon` for the `action` manifest entry.", "WKWebExtensionErrorInvalidActionIcon description for failing to load images for action only"); else localizedErrorDescription = WEB_UI_STRING("Failed to load images in `default_icon` for the `browser_action` or `page_action` manifest entry.", "WKWebExtensionErrorInvalidActionIcon description for failing to load images for browser_action or page_action"); - if (auto *result = bestImageForIconsDictionaryManifestKey(m_actionDictionary.get(), defaultIconManifestKey, size, m_actionIcon, Error::InvalidActionIcon, localizedErrorDescription)) + if (auto *result = bestImageForIconsDictionaryManifestKey(m_actionDictionary.get(), defaultIconManifestKey, size, m_actionIconsCache, Error::InvalidActionIcon, localizedErrorDescription)) return result; return icon(size); } @@ -1086,12 +1126,11 @@ static WKWebExtensionError toAPI(WebExtension::Error error) return; // Look for the "default_icon" as a string, which is useful for SVG icons. Only supported by Firefox currently. - NSString *defaultIconPath = objectForKey(m_actionDictionary, defaultIconManifestKey); - if (defaultIconPath.length) { + if (auto *defaultIconPath = objectForKey(m_actionDictionary, defaultIconManifestKey)) { NSError *resourceError; - m_actionIcon = imageForPath(defaultIconPath, &resourceError); + m_defaultActionIcon = imageForPath(defaultIconPath, &resourceError); - if (!m_actionIcon) { + if (!m_defaultActionIcon) { recordError(resourceError); NSString *localizedErrorDescription; @@ -1153,28 +1192,30 @@ static WKWebExtensionError toAPI(WebExtension::Error error) void WebExtension::populateSidebarActionProperties(RetainPtr sidebarActionDictionary) { // FIXME: implement sidebar icon parsing - m_sidebarIcon = nil; + m_sidebarIconsCache = nil; m_sidebarTitle = objectForKey(sidebarActionDictionary, sidebarActionTitleManifestKey); m_sidebarDocumentPath = objectForKey(sidebarActionDictionary, sidebarActionPathManifestKey); } void WebExtension::populateSidePanelProperties(RetainPtr sidePanelDictionary) { - // Since sidePanel cannot set a default title or icon from the manifest, setting these nil here is intentional - m_sidebarIcon = nil; + // Since sidePanel cannot set a default title or icon from the manifest, setting these nil here is intentional. + m_sidebarIconsCache = nil; m_sidebarTitle = nil; m_sidebarDocumentPath = objectForKey(sidePanelDictionary, sidePanelPathManifestKey); } #endif -CocoaImage *WebExtension::imageForPath(NSString *imagePath, NSError **outError) +CocoaImage *WebExtension::imageForPath(NSString *imagePath, NSError **outError, CGSize sizeForResizing) { ASSERT(imagePath); - NSData *imageData = resourceDataForPath(imagePath, outError, CacheResult::Yes); + NSData *imageData = resourceDataForPath(imagePath, outError); if (!imageData) return nil; + CocoaImage *result; + #if !USE(NSIMAGE_FOR_SVG_SUPPORT) UTType *imageType = resourceTypeForPath(imagePath); if ([imageType.identifier isEqualToString:UTTypeSVG.identifier]) { @@ -1186,59 +1227,75 @@ static WKWebExtensionError toAPI(WebExtension::Error error) if (!imageRep) return nil; - NSImage *result = [[NSImage alloc] init]; + result = [[NSImage alloc] init]; [result addRepresentation:imageRep]; result.size = imageRep.size; - - return result; #else CGSVGDocumentRef document = CGSVGDocumentCreateFromData(bridge_cast(imageData), nullptr); if (!document) return nil; - UIImage *result = [UIImage _imageWithCGSVGDocument:document]; + // Since we need to rasterize, scale the image for the densest display, so it will have enough pixels to be sharp. + result = [UIImage _imageWithCGSVGDocument:document scale:largestDisplayScale() orientation:UIImageOrientationUp]; CGSVGDocumentRelease(document); - - return result; #endif // not USE(APPKIT) } #endif // !USE(NSIMAGE_FOR_SVG_SUPPORT) - return [[CocoaImage alloc] initWithData:imageData]; + if (!result) + result = [[CocoaImage alloc] initWithData:imageData]; + +#if USE(APPKIT) + if (!CGSizeEqualToSize(sizeForResizing, CGSizeZero)) { + // Proportionally scale the size. + auto originalSize = result.size; + auto aspectWidth = sizeForResizing.width / originalSize.width; + auto aspectHeight = sizeForResizing.height / originalSize.height; + auto aspectRatio = std::min(aspectWidth, aspectHeight); + + result.size = CGSizeMake(originalSize.width * aspectRatio, originalSize.height * aspectRatio); + } + + return result; +#else + // Rasterization is needed because UIImageAsset will not register the image unless it is a CGImage. + // If the image is already a CGImage bitmap, this operation is a no-op. + result = result._rasterizedImage; + + if (!CGSizeEqualToSize(sizeForResizing, CGSizeZero)) + result = [result imageByPreparingThumbnailOfSize:sizeForResizing]; + + return result; +#endif // not USE(APPKIT) } -NSString *WebExtension::pathForBestImageInIconsDictionary(NSDictionary *iconsDictionary, size_t idealPixelSize) +size_t WebExtension::bestSizeInIconsDictionary(NSDictionary *iconsDictionary, size_t idealPixelSize) { if (!iconsDictionary.count) - return nil; + return 0; + +#if ENABLE(WK_WEB_EXTENSIONS_ICON_VARIANTS) + // Check if the "any" size exists (typically a vector image), and prefer it. + if (iconsDictionary[anyManifestKey]) { + // Return max to ensure it takes precedence over all other sizes. + return std::numeric_limits::max(); + } +#endif // Check if the ideal size exists, if so return it. NSString *idealSizeString = @(idealPixelSize).stringValue; - if (NSString *resultPath = iconsDictionary[idealSizeString]) - return resultPath; - - // Check if the ideal retina size exists, if so return it. This will cause downsampling to the ideal size, - // but it is an even multiplier so it is better than a fractional scaling. - idealSizeString = @(idealPixelSize * 2).stringValue; - if (NSString *resultPath = iconsDictionary[idealSizeString]) - return resultPath; - -#if PLATFORM(IOS) || PLATFORM(VISION) - // Check if the ideal 3x retina size exists. This will usually be called on 2x iOS devices when a 3x image might exist. - // Since the ideal size is likly already 2x, multiply by 1.5x to get the 3x pixel size. - idealSizeString = @(idealPixelSize * 1.5).stringValue; - if (NSString *resultPath = iconsDictionary[idealSizeString]) - return resultPath; -#endif + if (iconsDictionary[idealSizeString]) + return idealPixelSize; - // Since the ideal size does not exist, sort the keys and return the next largest size. This could cause - // fractional scaling when the final image is displayed. Better than nothing. + // Sort the remaining keys and find the next largest size. NSArray *sizeKeys = filterObjects(iconsDictionary.allKeys, ^bool(id, id value) { - return [value isKindOfClass:NSString.class]; + // Filter the values to only include numeric strings representing sizes. This will exclude non-numeric string + // values such as "any", "color_schemes", and any other strings that cannot be converted to a positive integer. + return dynamic_objc_cast(value).integerValue > 0; }); if (!sizeKeys.count) - return nil; + return 0; NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"self" ascending:YES selector:@selector(localizedStandardCompare:)]; NSArray *sortedKeys = [sizeKeys sortedArrayUsingDescriptors:@[ sortDescriptor ]]; @@ -1250,62 +1307,116 @@ static WKWebExtensionError toAPI(WebExtension::Error error) break; } - ASSERT(bestSize); + return bestSize; +} + +NSString *WebExtension::pathForBestImageInIconsDictionary(NSDictionary *iconsDictionary, size_t idealPixelSize) +{ + size_t bestSize = bestSizeInIconsDictionary(iconsDictionary, idealPixelSize); + if (!bestSize) + return nil; + +#if ENABLE(WK_WEB_EXTENSIONS_ICON_VARIANTS) + if (bestSize == std::numeric_limits::max()) + return iconsDictionary[anyManifestKey]; +#endif return iconsDictionary[@(bestSize).stringValue]; } -CocoaImage *WebExtension::bestImageInIconsDictionary(NSDictionary *iconsDictionary, size_t idealPointSize, NSError **outError) +CocoaImage *WebExtension::bestImageInIconsDictionary(NSDictionary *iconsDictionary, CGSize idealSize, const Function& reportError) { if (!iconsDictionary.count) return nil; - auto standardPixelSize = idealPointSize; -#if PLATFORM(IOS) || PLATFORM(VISION) - standardPixelSize *= UIScreen.mainScreen.scale; + auto idealPointSize = idealSize.width > idealSize.height ? idealSize.width : idealSize.height; + auto *screenScales = availableScreenScales(); + auto *uniquePaths = [NSMutableSet set]; +#if PLATFORM(IOS_FAMILY) + auto *scalePaths = [NSMutableDictionary dictionary]; #endif - auto *standardIconPath = pathForBestImageInIconsDictionary(iconsDictionary, standardPixelSize); - if (!standardIconPath.length) - return nil; + for (NSNumber *scale in screenScales) { + auto pixelSize = idealPointSize * scale.doubleValue; + auto *iconPath = pathForBestImageInIconsDictionary(iconsDictionary, pixelSize); + if (!iconPath) + continue; - auto *resultImage = imageForPath(standardIconPath, outError); - if (!resultImage) - return nil; + [uniquePaths addObject:iconPath]; -#if PLATFORM(MAC) - static Class svgImageRep = NSClassFromString(@"_NSSVGImageRep"); - RELEASE_ASSERT(svgImageRep); +#if PLATFORM(IOS_FAMILY) + scalePaths[scale] = iconPath; +#endif + } - BOOL isVectorImage = [resultImage.representations indexOfObjectPassingTest:^BOOL(NSImageRep *imageRep, NSUInteger, BOOL *) { - return [imageRep isKindOfClass:svgImageRep]; - }] != NSNotFound; + if (!uniquePaths.count) + return nil; - if (isVectorImage) - return resultImage; +#if USE(APPKIT) + // Return a combined image so the system can select the most appropriate representation based on the current screen scale. + NSImage *resultImage; - auto retinaPixelSize = standardPixelSize * 2; - auto *retinaIconPath = pathForBestImageInIconsDictionary(iconsDictionary, retinaPixelSize); - if (retinaIconPath.length && ![retinaIconPath isEqualToString:standardIconPath]) { - if (auto *retinaImage = imageForPath(retinaIconPath, nullptr)) - [resultImage addRepresentations:retinaImage.representations]; + for (NSString *iconPath in uniquePaths) { + NSError *resourceError; + if (auto *image = imageForPath(iconPath, &resourceError, idealSize)) { + if (!resultImage) + resultImage = image; + else + [resultImage addRepresentations:image.representations]; + } else if (reportError && resourceError) + reportError(resourceError); } -#endif // PLATFORM(MAC) return resultImage; +#else + if (uniquePaths.count == 1) { + [scalePaths removeAllObjects]; + + // Add a single value back that has 0 for the scale, which is the + // unspecified (universal) trait value for display scale. + scalePaths[@0] = uniquePaths.anyObject; + } + + auto *images = mapObjects(scalePaths, ^id(NSNumber *scale, NSString *path) { + NSError *resourceError; + if (auto *image = imageForPath(path, &resourceError, idealSize)) + return image; + + if (reportError && resourceError) + reportError(resourceError); + + return nil; + }); + + // Make a dynamic image asset that returns an image based on the trait collection. + auto *imageAsset = [UIImageAsset _dynamicAssetNamed:NSUUID.UUID.UUIDString generator:^(UIImageAsset *, UIImageConfiguration *configuration, UIImage *) { + return images[@(configuration.traitCollection.displayScale)] ?: images[@0]; + }]; + + // The returned image retains its link to the image asset and adapts to trait changes, + // automatically displaying the correct variant based on the current traits. + return [imageAsset imageWithTraitCollection:UITraitCollection.currentTraitCollection]; +#endif // not USE(APPKIT) } -CocoaImage *WebExtension::bestImageForIconsDictionaryManifestKey(NSDictionary *dictionary, NSString *manifestKey, CGSize idealSize, RetainPtr& cacheLocation, Error error, NSString *customLocalizedDescription) +CocoaImage *WebExtension::bestImageForIconsDictionaryManifestKey(NSDictionary *dictionary, NSString *manifestKey, CGSize idealSize, RetainPtr& cacheLocation, Error error, NSString *customLocalizedDescription) { - if (cacheLocation) - return cacheLocation.get(); + // Clear the cache if the display scales change (connecting display, etc.) + auto *currentScales = availableScreenScales(); + auto *cachedScales = objectForKey(cacheLocation, @"scales"); + if (!cacheLocation || ![currentScales isEqualToSet:cachedScales]) + cacheLocation = [NSMutableDictionary dictionaryWithObject:currentScales forKey:@"scales"]; + + auto *cacheKey = @(idealSize); + if (id cachedResult = cacheLocation.get()[cacheKey]) + return dynamic_objc_cast(cachedResult); - CGFloat idealPointSize = idealSize.width > idealSize.height ? idealSize.width : idealSize.height; - NSDictionary *iconDictionary = objectForKey(dictionary, manifestKey); + auto *iconDictionary = objectForKey(dictionary, manifestKey); + auto *result = bestImageInIconsDictionary(iconDictionary, idealSize, [&](auto *error) { + recordError(error); + }); - NSError *resourceError; - auto *result = bestImageInIconsDictionary(iconDictionary, idealPointSize, &resourceError); - recordErrorIfNeeded(resourceError); + cacheLocation.get()[cacheKey] = result ?: NSNull.null; if (!result) { if (iconDictionary.count) { @@ -1319,12 +1430,162 @@ static WKWebExtensionError toAPI(WebExtension::Error error) return nil; } - // Cache the icon if there is only one size, usually this is a vector icon or a large bitmap. - if (iconDictionary.count == 1) - cacheLocation = result; + return result; +} + +#if ENABLE(WK_WEB_EXTENSIONS_ICON_VARIANTS) +static OptionSet toColorSchemes(id value) +{ + using ColorScheme = WebExtension::ColorScheme; + + if (!value) { + // A nil value counts as all color schemes. + return { ColorScheme::Light, ColorScheme::Dark }; + } + + OptionSet result; + + auto *array = dynamic_objc_cast(value); + if ([array containsObject:lightManifestKey]) + result.add(ColorScheme::Light); + + if ([array containsObject:darkManifestKey]) + result.add(ColorScheme::Dark); + + return result; +} + +NSDictionary *WebExtension::iconsDictionaryForBestIconVariant(NSArray *variants, size_t idealPixelSize, ColorScheme idealColorScheme) +{ + if (!variants.count) + return nil; + + if (variants.count == 1) + return variants.firstObject; + + NSDictionary *bestVariant; + NSDictionary *fallbackVariant; + bool foundIdealFallbackVariant = false; + + size_t bestSize = 0; + size_t fallbackSize = 0; + + // Pick the first variant matching color scheme and/or size. + for (NSDictionary *variant in variants) { + auto colorSchemes = toColorSchemes(variant[colorSchemesManifestKey]); + auto currentBestSize = bestSizeInIconsDictionary(variant, idealPixelSize); + + if (colorSchemes.contains(idealColorScheme)) { + if (currentBestSize >= idealPixelSize) { + // Found the best variant, return it. + return variant; + } + + if (currentBestSize > bestSize) { + // Found a larger ideal variant. + bestSize = currentBestSize; + bestVariant = variant; + } + } else if (!foundIdealFallbackVariant && currentBestSize >= idealPixelSize) { + // Found an ideal fallback variant, based only on size. + fallbackSize = currentBestSize; + fallbackVariant = variant; + foundIdealFallbackVariant = true; + } else if (!foundIdealFallbackVariant && currentBestSize > fallbackSize) { + // Found a smaller fallback variant. + fallbackSize = currentBestSize; + fallbackVariant = variant; + } + } + + return bestVariant ?: fallbackVariant; +} + +CocoaImage *WebExtension::bestImageForIconVariants(NSArray *variants, CGSize idealSize, const Function& reportError) +{ + auto idealPointSize = idealSize.width > idealSize.height ? idealSize.width : idealSize.height; + auto *lightIconsDictionary = iconsDictionaryForBestIconVariant(variants, idealPointSize, ColorScheme::Light); + auto *darkIconsDictionary = iconsDictionaryForBestIconVariant(variants, idealPointSize, ColorScheme::Dark); + + // If the light and dark icons dictionaries are the same, or if either is nil, return the available image directly. + if (!lightIconsDictionary || !darkIconsDictionary || [lightIconsDictionary isEqualToDictionary:darkIconsDictionary]) + return bestImageInIconsDictionary(lightIconsDictionary ?: darkIconsDictionary, idealSize, reportError); + + auto *lightImage = bestImageInIconsDictionary(lightIconsDictionary, idealSize, reportError); + auto *darkImage = bestImageInIconsDictionary(darkIconsDictionary, idealSize, reportError); + + // If either the light or dark icon is nil, return the available image directly. + if (!lightImage || !darkImage) + return lightImage ?: darkImage; + +#if USE(APPKIT) + // The images need to be the same size to draw correctly in the block. + auto imageSize = lightImage.size.width >= darkImage.size.width ? lightImage.size : darkImage.size; + lightImage.size = imageSize; + darkImage.size = imageSize; + + // Make a dynamic image that draws the light or dark image based on the current appearance. + return [NSImage imageWithSize:imageSize flipped:NO drawingHandler:^BOOL(NSRect rect) { + static auto *darkAppearanceNames = @[ + NSAppearanceNameDarkAqua, + NSAppearanceNameVibrantDark, + NSAppearanceNameAccessibilityHighContrastDarkAqua, + NSAppearanceNameAccessibilityHighContrastVibrantDark, + ]; + + if ([NSAppearance.currentDrawingAppearance bestMatchFromAppearancesWithNames:darkAppearanceNames]) + [darkImage drawInRect:rect]; + else + [lightImage drawInRect:rect]; + + return YES; + }]; +#else + // Make a dynamic image asset that returns the light or dark image based on the trait collection. + auto *imageAsset = [UIImageAsset _dynamicAssetNamed:NSUUID.UUID.UUIDString generator:^(UIImageAsset *, UIImageConfiguration *configuration, UIImage *) { + return configuration.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark ? darkImage : lightImage; + }]; + + // The returned image retains its link to the image asset and adapts to trait changes, + // automatically displaying the correct variant based on the current traits. + return [imageAsset imageWithTraitCollection:UITraitCollection.currentTraitCollection]; +#endif // not USE(APPKIT) +} + +CocoaImage *WebExtension::bestImageForIconVariantsManifestKey(NSDictionary *dictionary, NSString *manifestKey, CGSize idealSize, RetainPtr& cacheLocation, Error error, NSString *customLocalizedDescription) +{ + // Clear the cache if the display scales change (connecting display, etc.) + auto *currentScales = availableScreenScales(); + auto *cachedScales = objectForKey(cacheLocation, @"scales"); + if (!cacheLocation || ![currentScales isEqualToSet:cachedScales]) + cacheLocation = [NSMutableDictionary dictionaryWithObject:currentScales forKey:@"scales"]; + + auto *cacheKey = @(idealSize); + if (id cachedResult = cacheLocation.get()[cacheKey]) + return dynamic_objc_cast(cachedResult); + + auto *variants = objectForKey(dictionary, manifestKey, false, NSDictionary.class); + auto *result = bestImageForIconVariants(variants, idealSize, [&](auto *error) { + recordError(error); + }); + + cacheLocation.get()[cacheKey] = result ?: NSNull.null; + + if (!result) { + if (variants.count) { + // Record an error if the array had values, meaning the likely failure is the images were missing on disk or bad format. + recordError(createError(error, customLocalizedDescription)); + } else if ((variants && !variants.count) || dictionary[manifestKey]) { + // Record an error if the key had an array that was empty, or the key had a value of the wrong type. + recordError(createError(error)); + } + + return nil; + } return result; } +#endif // ENABLE(WK_WEB_EXTENSIONS_ICON_VARIANTS) bool WebExtension::hasBackgroundContent() { diff --git a/Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionMenuItemCocoa.mm b/Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionMenuItemCocoa.mm index a47afc81565dc..2cd1aad33a191 100644 --- a/Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionMenuItemCocoa.mm +++ b/Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionMenuItemCocoa.mm @@ -331,11 +331,9 @@ - (IBAction)_performAction:(id)sender { ASSERT(extensionContext()); - NSError *error; - auto *result = extensionContext()->extension().bestImageInIconsDictionary(m_icons.get(), idealSize.width > idealSize.height ? idealSize.width : idealSize.height, &error); - extensionContext()->recordErrorIfNeeded(error); - - return result; + return extensionContext()->extension().bestImageInIconsDictionary(m_icons.get(), idealSize, [&](auto *error) { + extensionContext()->recordError(error); + }); } } // namespace WebKit diff --git a/Source/WebKit/UIProcess/Extensions/WebExtension.h b/Source/WebKit/UIProcess/Extensions/WebExtension.h index 418cd0277a67e..1461779f4f1ad 100644 --- a/Source/WebKit/UIProcess/Extensions/WebExtension.h +++ b/Source/WebKit/UIProcess/Extensions/WebExtension.h @@ -119,6 +119,11 @@ class WebExtension : public API::ObjectImpl, pu ServiceWorker, }; + enum class ColorScheme : uint8_t { + Light = 1 << 0, + Dark = 1 << 1 + }; + using PermissionsSet = HashSet; using MatchPatternSet = HashSet>; @@ -240,11 +245,19 @@ class WebExtension : public API::ObjectImpl, pu NSString *sidebarTitle(); #endif - CocoaImage *imageForPath(NSString *, NSError **); + CocoaImage *imageForPath(NSString *, NSError **, CGSize sizeForResizing = CGSizeZero); + size_t bestSizeInIconsDictionary(NSDictionary *, size_t idealPixelSize); NSString *pathForBestImageInIconsDictionary(NSDictionary *, size_t idealPixelSize); - CocoaImage *bestImageInIconsDictionary(NSDictionary *, size_t idealPointSize, NSError **); - CocoaImage *bestImageForIconsDictionaryManifestKey(NSDictionary *, NSString *manifestKey, CGSize idealSize, RetainPtr& cacheLocation, Error, NSString *customLocalizedDescription); + + CocoaImage *bestImageInIconsDictionary(NSDictionary *, CGSize idealSize, const Function&); + CocoaImage *bestImageForIconsDictionaryManifestKey(NSDictionary *, NSString *manifestKey, CGSize idealSize, RetainPtr& cacheLocation, Error, NSString *customLocalizedDescription); + +#if ENABLE(WK_WEB_EXTENSIONS_ICON_VARIANTS) + NSDictionary *iconsDictionaryForBestIconVariant(NSArray *, size_t idealPixelSize, ColorScheme); + CocoaImage *bestImageForIconVariants(NSArray *, CGSize idealSize, const Function&); + CocoaImage *bestImageForIconVariantsManifestKey(NSDictionary *, NSString *manifestKey, CGSize idealSize, RetainPtr& cacheLocation, Error, NSString *customLocalizedDescription); +#endif bool hasBackgroundContent(); bool backgroundContentIsPersistent(); @@ -358,18 +371,19 @@ class WebExtension : public API::ObjectImpl, pu RetainPtr m_displayDescription; RetainPtr m_version; - RetainPtr m_icon; + RetainPtr m_iconsCache; RetainPtr m_actionDictionary; - RetainPtr m_actionIcon; + RetainPtr m_actionIconsCache; + RetainPtr m_defaultActionIcon; RetainPtr m_displayActionLabel; RetainPtr m_actionPopupPath; #if ENABLE(WK_WEB_EXTENSIONS_SIDEBAR) - RetainPtr m_sidebarIcon; + RetainPtr m_sidebarIconsCache; RetainPtr m_sidebarDocumentPath; RetainPtr m_sidebarTitle; -#endif // ENABLE(WK_WEB_EXTENSIONS_SIDEBAR) +#endif RetainPtr m_contentSecurityPolicy; diff --git a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/xcshareddata/xcschemes/TestWebKitAPI.xcscheme b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/xcshareddata/xcschemes/TestWebKitAPI.xcscheme index b9419725b7251..b19232e3d3509 100644 --- a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/xcshareddata/xcschemes/TestWebKitAPI.xcscheme +++ b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/xcshareddata/xcschemes/TestWebKitAPI.xcscheme @@ -64,12 +64,6 @@ ReferencedContainer = "container:TestWebKitAPI.xcodeproj"> - - - - "; + + auto *resources = @{ + @"icon-any.svg": iconAny, + }; + + auto testExtension = [[WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary resources:resources]; + EXPECT_NS_EQUAL(testExtension.errors, @[ ]); + + auto *icon = [testExtension iconForSize:CGSizeMake(64, 64)]; + EXPECT_NOT_NULL(icon); + EXPECT_TRUE(CGSizeEqualToSize(icon.size, CGSizeMake(64, 64))); +} + +TEST(WKWebExtension, NoIconVariants) +{ + auto *testManifestDictionary = @{ + @"manifest_version": @3, + + @"name": @"Test No Variants", + @"version": @"1.0", + @"description": @"Test with no icon variants" + }; + + auto testExtension = [[WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary resources:@{ }]; + EXPECT_NS_EQUAL(testExtension.errors, @[ ]); + + auto *icon = [testExtension iconForSize:CGSizeMake(32, 32)]; + EXPECT_NULL(icon); +} + +TEST(WKWebExtension, IconsAndIconVariantsSpecified) +{ + auto *testManifestDictionary = @{ + @"manifest_version": @3, + + @"name": @"Test Icons and Variants", + @"version": @"1.0", + @"description": @"Test with both icons and icon variants specified", + + @"icons": @{ + @"32": @"icon-legacy.png" + }, + + @"icon_variants": @[ + @{ @"32": @"icon-variant.png" } + ] + }; + + auto *iconLegacy = Util::makePNGData(CGSizeMake(32, 32), @selector(blackColor)); + auto *iconVariant = Util::makePNGData(CGSizeMake(32, 32), @selector(whiteColor)); + + auto *resources = @{ + @"icon-legacy.png": iconLegacy, + @"icon-variant.png": iconVariant, + }; + + auto testExtension = [[WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary resources:resources]; + EXPECT_NS_EQUAL(testExtension.errors, @[ ]); + + auto *icon = [testExtension iconForSize:CGSizeMake(32, 32)]; + EXPECT_NOT_NULL(icon); + EXPECT_TRUE(CGSizeEqualToSize(icon.size, CGSizeMake(32, 32))); + EXPECT_TRUE(Util::compareColors(Util::pixelColor(icon), [CocoaColor whiteColor])); +} + +TEST(WKWebExtension, ActionIconVariantsMultiple) +{ + auto *testManifestDictionary = @{ + @"manifest_version": @3, + + @"name": @"Test Action Multiple Variants", + @"version": @"1.0", + @"description": @"Test action with multiple icon variants", + + @"action": @{ + @"icon_variants": @[ + @{ @"32": @"action-dark-32.png", @"64": @"action-dark-64.png", @"color_schemes": @[ @"dark" ] }, + @{ @"32": @"action-light-32.png", @"64": @"action-light-64.png", @"color_schemes": @[ @"light" ] } + ] + } + }; + + auto *dark32Icon = Util::makePNGData(CGSizeMake(32, 32), @selector(whiteColor)); + auto *dark64Icon = Util::makePNGData(CGSizeMake(64, 64), @selector(whiteColor)); + auto *light32Icon = Util::makePNGData(CGSizeMake(32, 32), @selector(blackColor)); + auto *light64Icon = Util::makePNGData(CGSizeMake(64, 64), @selector(blackColor)); + + auto *resources = @{ + @"action-dark-32.png": dark32Icon, + @"action-dark-64.png": dark64Icon, + @"action-light-32.png": light32Icon, + @"action-light-64.png": light64Icon, + }; + + auto testExtension = [[WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary resources:resources]; + EXPECT_NS_EQUAL(testExtension.errors, @[ ]); + + Util::performWithAppearance(Util::Appearance::Dark, ^{ + auto *iconDark32 = [testExtension actionIconForSize:CGSizeMake(32, 32)]; + EXPECT_NOT_NULL(iconDark32); + EXPECT_TRUE(CGSizeEqualToSize(iconDark32.size, CGSizeMake(32, 32))); + EXPECT_TRUE(Util::compareColors(Util::pixelColor(iconDark32), [CocoaColor whiteColor])); + + auto *iconDark64 = [testExtension actionIconForSize:CGSizeMake(64, 64)]; + EXPECT_NOT_NULL(iconDark64); + EXPECT_TRUE(CGSizeEqualToSize(iconDark64.size, CGSizeMake(64, 64))); + EXPECT_TRUE(Util::compareColors(Util::pixelColor(iconDark64), [CocoaColor whiteColor])); + }); + + Util::performWithAppearance(Util::Appearance::Light, ^{ + auto *iconLight32 = [testExtension actionIconForSize:CGSizeMake(32, 32)]; + EXPECT_NOT_NULL(iconLight32); + EXPECT_TRUE(CGSizeEqualToSize(iconLight32.size, CGSizeMake(32, 32))); + EXPECT_TRUE(Util::compareColors(Util::pixelColor(iconLight32), [CocoaColor blackColor])); + + auto *iconLight64 = [testExtension actionIconForSize:CGSizeMake(64, 64)]; + EXPECT_NOT_NULL(iconLight64); + EXPECT_TRUE(CGSizeEqualToSize(iconLight64.size, CGSizeMake(64, 64))); + EXPECT_TRUE(Util::compareColors(Util::pixelColor(iconLight64), [CocoaColor blackColor])); + }); +} + +TEST(WKWebExtension, ActionIconSingleVariant) +{ + auto *testManifestDictionary = @{ + @"manifest_version": @3, + + @"name": @"Test Action Single Variant", + @"version": @"1.0", + @"description": @"Test action with a single icon variant", + + @"action": @{ + @"icon_variants": @[ + @{ @"32": @"action-icon-32.png" } + ] + } + }; + + auto *icon32 = Util::makePNGData(CGSizeMake(32, 32), @selector(whiteColor)); + + auto *resources = @{ + @"action-icon-32.png": icon32, + }; + + auto testExtension = [[WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary resources:resources]; + EXPECT_NS_EQUAL(testExtension.errors, @[ ]); + + auto *icon = [testExtension actionIconForSize:CGSizeMake(32, 32)]; + EXPECT_NOT_NULL(icon); + EXPECT_TRUE(CGSizeEqualToSize(icon.size, CGSizeMake(32, 32))); + EXPECT_TRUE(Util::compareColors(Util::pixelColor(icon), [CocoaColor whiteColor])); +} + +TEST(WKWebExtension, ActionIconAnySizeVariant) +{ + auto *testManifestDictionary = @{ + @"manifest_version": @3, + + @"name": @"Test Action Any Size", + @"version": @"1.0", + @"description": @"Test action with any size icon", + + @"action": @{ + @"icon_variants": @[ + @{ @"any": @"action-icon-any.svg" } + ] + } + }; + + auto *iconAny = @""; + + auto *resources = @{ + @"action-icon-any.svg": iconAny, + }; + + auto testExtension = [[WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary resources:resources]; + EXPECT_NS_EQUAL(testExtension.errors, @[ ]); + + auto *icon = [testExtension actionIconForSize:CGSizeMake(64, 64)]; + EXPECT_NOT_NULL(icon); + EXPECT_TRUE(CGSizeEqualToSize(icon.size, CGSizeMake(64, 64))); +} + +TEST(WKWebExtension, ActionNoIconVariants) +{ + auto *testManifestDictionary = @{ + @"manifest_version": @3, + + @"name": @"Test Action No Variants", + @"version": @"1.0", + @"description": @"Test action with no icon variants" + }; + + auto testExtension = [[WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary resources:@{ }]; + EXPECT_NS_EQUAL(testExtension.errors, @[ ]); + + auto *icon = [testExtension actionIconForSize:CGSizeMake(32, 32)]; + EXPECT_NULL(icon); +} + +TEST(WKWebExtension, ActionIconsAndIconVariantsSpecified) +{ + auto *testManifestDictionary = @{ + @"manifest_version": @3, + + @"name": @"Test Action Icons and Variants", + @"version": @"1.0", + @"description": @"Test action with both icons and icon variants specified", + + @"action": @{ + @"default_icon": @{ + @"32": @"action-icon-legacy.png" + }, + @"icon_variants": @[ + @{ @"32": @"action-icon-variant.png" } + ] + } + }; + + auto *iconLegacy = Util::makePNGData(CGSizeMake(32, 32), @selector(blackColor)); + auto *iconVariant = Util::makePNGData(CGSizeMake(32, 32), @selector(whiteColor)); + + auto *resources = @{ + @"action-icon-legacy.png": iconLegacy, + @"action-icon-variant.png": iconVariant, + }; + + auto testExtension = [[WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary resources:resources]; + EXPECT_NS_EQUAL(testExtension.errors, @[ ]); + + auto *icon = [testExtension actionIconForSize:CGSizeMake(32, 32)]; + EXPECT_NOT_NULL(icon); + EXPECT_TRUE(CGSizeEqualToSize(icon.size, CGSizeMake(32, 32))); + EXPECT_TRUE(Util::compareColors(Util::pixelColor(icon), [CocoaColor whiteColor])); +} +#endif // ENABLE(WK_WEB_EXTENSIONS_ICON_VARIANTS) + TEST(WKWebExtension, ActionParsing) { NSDictionary *testManifestDictionary = @{ @"manifest_version": @2, @"name": @"Test", @"description": @"Test", @"version": @"1.0" }; diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebExtensionAPIAction.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebExtensionAPIAction.mm index 9960cb1388a5b..25556b8c23d30 100644 --- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebExtensionAPIAction.mm +++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebExtensionAPIAction.mm @@ -158,11 +158,7 @@ auto *smallIcon = [action iconForSize:CGSizeMake(16, 16)]; EXPECT_NOT_NULL(smallIcon); -#if USE(APPKIT) EXPECT_TRUE(CGSizeEqualToSize(smallIcon.size, CGSizeMake(16, 16))); -#else - EXPECT_TRUE(CGSizeEqualToSize(smallIcon.size, CGSizeMake(32, 32))); -#endif auto *largeIcon = [action iconForSize:CGSizeMake(32, 32)]; EXPECT_NOT_NULL(largeIcon); @@ -680,18 +676,10 @@ auto *icon128 = [action iconForSize:CGSizeMake(128, 128)]; EXPECT_NOT_NULL(icon48); -#if USE(APPKIT) EXPECT_TRUE(CGSizeEqualToSize(icon48.size, CGSizeMake(48, 48))); -#else - EXPECT_TRUE(CGSizeEqualToSize(icon48.size, CGSizeMake(128, 128))); -#endif EXPECT_NOT_NULL(icon96); -#if USE(APPKIT) EXPECT_TRUE(CGSizeEqualToSize(icon96.size, CGSizeMake(96, 96))); -#else - EXPECT_TRUE(CGSizeEqualToSize(icon96.size, CGSizeMake(128, 128))); -#endif EXPECT_NOT_NULL(icon128); EXPECT_TRUE(CGSizeEqualToSize(icon128.size, CGSizeMake(128, 128))); @@ -755,18 +743,10 @@ auto *icon128 = [action iconForSize:CGSizeMake(128, 128)]; EXPECT_NOT_NULL(icon48); -#if USE(APPKIT) EXPECT_TRUE(CGSizeEqualToSize(icon48.size, CGSizeMake(48, 48))); -#else - EXPECT_TRUE(CGSizeEqualToSize(icon48.size, CGSizeMake(128, 128))); -#endif EXPECT_NOT_NULL(icon96); -#if USE(APPKIT) EXPECT_TRUE(CGSizeEqualToSize(icon96.size, CGSizeMake(96, 96))); -#else - EXPECT_TRUE(CGSizeEqualToSize(icon96.size, CGSizeMake(128, 128))); -#endif EXPECT_NOT_NULL(icon128); EXPECT_TRUE(CGSizeEqualToSize(icon128.size, CGSizeMake(128, 128))); @@ -844,18 +824,10 @@ auto *icon128 = [action iconForSize:CGSizeMake(128, 128)]; EXPECT_NOT_NULL(icon48); -#if USE(APPKIT) EXPECT_TRUE(CGSizeEqualToSize(icon48.size, CGSizeMake(48, 48))); -#else - EXPECT_TRUE(CGSizeEqualToSize(icon48.size, CGSizeMake(128, 128))); -#endif EXPECT_NOT_NULL(icon96); -#if USE(APPKIT) EXPECT_TRUE(CGSizeEqualToSize(icon96.size, CGSizeMake(96, 96))); -#else - EXPECT_TRUE(CGSizeEqualToSize(icon96.size, CGSizeMake(128, 128))); -#endif EXPECT_NOT_NULL(icon128); EXPECT_TRUE(CGSizeEqualToSize(icon128.size, CGSizeMake(128, 128))); @@ -942,11 +914,7 @@ manager.get().internalDelegate.presentPopupForAction = ^(WKWebExtensionAction *action) { auto *icon48 = [action iconForSize:CGSizeMake(48, 48)]; EXPECT_NOT_NULL(icon48); -#if USE(APPKIT) EXPECT_TRUE(CGSizeEqualToSize(icon48.size, CGSizeMake(48, 48))); -#else - EXPECT_TRUE(CGSizeEqualToSize(icon48.size, CGSizeMake(96, 96))); -#endif auto *icon96 = [action iconForSize:CGSizeMake(96, 96)]; EXPECT_NOT_NULL(icon96); diff --git a/Tools/TestWebKitAPI/cocoa/WebExtensionUtilities.h b/Tools/TestWebKitAPI/cocoa/WebExtensionUtilities.h index 9e5fe2f5d91fb..84256e04801bf 100644 --- a/Tools/TestWebKitAPI/cocoa/WebExtensionUtilities.h +++ b/Tools/TestWebKitAPI/cocoa/WebExtensionUtilities.h @@ -32,6 +32,16 @@ #include "Utilities.h" #include "WTFTestUtilities.h" +#if USE(APPKIT) +OBJC_CLASS NSImage; +using CocoaImage = NSImage; +using CocoaColor = NSColor; +#else +OBJC_CLASS UIImage; +using CocoaImage = UIImage; +using CocoaColor = UIColor; +#endif + #ifdef __OBJC__ @class TestWebExtensionTab; @@ -147,6 +157,14 @@ NSData *makePNGData(CGSize, SEL colorSelector); void runScriptWithUserGesture(const String&, WKWebView *); +enum class Appearance { Light, Dark }; + +void performWithAppearance(Appearance, void (^block)(void)); + +CocoaColor *pixelColor(CocoaImage *, CGPoint = CGPointZero); +CocoaColor *toSRGBColor(CocoaColor *); +bool compareColors(CocoaColor *, CocoaColor *); + #endif RetainPtr loadAndRunExtension(WKWebExtension *, WKWebExtensionControllerConfiguration * = nil); diff --git a/Tools/TestWebKitAPI/cocoa/WebExtensionUtilities.mm b/Tools/TestWebKitAPI/cocoa/WebExtensionUtilities.mm index c6f44a90d46cf..7df0104a7387c 100644 --- a/Tools/TestWebKitAPI/cocoa/WebExtensionUtilities.mm +++ b/Tools/TestWebKitAPI/cocoa/WebExtensionUtilities.mm @@ -929,6 +929,73 @@ void runScriptWithUserGesture(const String& script, WKWebView *webView) TestWebKitAPI::Util::run(&callbackComplete); } +void performWithAppearance(Appearance appearance, void (^block)(void)) +{ +#if USE(APPKIT) + auto *appearanceName = appearance == Appearance::Dark ? NSAppearanceNameDarkAqua : NSAppearanceNameAqua; + [[NSAppearance appearanceNamed:appearanceName] performAsCurrentDrawingAppearance:block]; +#else + auto *traitCollection = appearance == Appearance::Dark ? [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark] + : [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight]; + [traitCollection performAsCurrentTraitCollection:block]; +#endif +} + +CocoaColor *pixelColor(CocoaImage *image, CGPoint point) +{ +#if USE(APPKIT) + auto imageRef = [image CGImageForProposedRect:nullptr context:nil hints:nil]; + auto *bitmap = [[NSBitmapImageRep alloc] initWithCGImage:imageRef]; + auto *color = [bitmap colorAtX:point.x y:point.y]; + return color; +#else + UIGraphicsBeginImageContext(image.size); + + [image drawAtPoint:CGPointZero]; + + CGContextRef context = UIGraphicsGetCurrentContext(); + unsigned char *data = (unsigned char *)CGBitmapContextGetData(context); + if (!data) + return nil; + + unsigned offset = ((image.size.width * point.y) + point.x) * 4; + UIColor *color = [UIColor colorWithRed:data[offset] / 255.0 green:data[offset + 1] / 255.0 blue:data[offset + 2] / 255.0 alpha:data[offset + 3] / 255.0]; + + UIGraphicsEndImageContext(); + + return color; +#endif +} + +CocoaColor *toSRGBColor(CocoaColor *color) +{ +#if USE(APPKIT) + return [color colorUsingColorSpace:NSColorSpace.sRGBColorSpace]; +#else + return color; +#endif +} + +bool compareColors(CocoaColor *color1, CocoaColor *color2) +{ + if (color1 == color2 || [color1 isEqual:color2]) + return true; + + if (!color1 || !color2) + return false; + + color1 = toSRGBColor(color1); + color2 = toSRGBColor(color2); + + CGFloat red1, green1, blue1, alpha1; + [color1 getRed:&red1 green:&green1 blue:&blue1 alpha:&alpha1]; + + CGFloat red2, green2, blue2, alpha2; + [color2 getRed:&red2 green:&green2 blue:&blue2 alpha:&alpha2]; + + return fabs(red1 - red2) < 0.01 && fabs(green1 - green2) < 0.01 && fabs(blue1 - blue2) < 0.01 && fabs(alpha1 - alpha2) < 0.01; +} + } // namespace Util } // namespace TestWebKitAPI