[macOS] Fix some font attribute conversion bugs in preparation for "Font > Styles...
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 4 Oct 2018 22:08:51 +0000 (22:08 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 4 Oct 2018 22:08:51 +0000 (22:08 +0000)
https://bugs.webkit.org/show_bug.cgi?id=190289
<rdar://problem/45020806>

Reviewed by Ryosuke Niwa.

Source/WebCore:

Makes some small adjustments to fix two bugs in font attribute conversion logic. See below for more detail.

Tests:  FontManagerTests.AddFontShadowUsingFontOptions
        FontManagerTests.AddAndRemoveColorsUsingFontOptions

* editing/FontAttributeChanges.cpp:
(WebCore::cssValueListForShadow):
* editing/cocoa/FontAttributesCocoa.mm:

Currently, we bail from adding a font shadow if the shadow's offset is empty. However, valid shadow offsets may
have negative dimensions, so a check for `isZero()` should be used instead.

(WebCore::FontAttributes::createDictionary const):
* platform/mac/WebCoreNSFontManagerExtras.mm:

Fall back to a transparent background color; this allows senders to remove the current background color by just
removing NSBackgroundColorAttributeName from the attribute dictionary, rather than explicitly setting it to the
transparent color (this scenario is exercised when using "Font > Styles…" to specify a font style without a
background color).

(WebCore::computedFontAttributeChanges):

Tools:

Add new API tests to exercise two corner cases when using NSFontOptions ("Font > Styles…") to change font
attributes at the current selection.

* TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
* TestWebKitAPI/Tests/TestWebKitAPI/mac/AppKitSPI.h:
* TestWebKitAPI/Tests/mac/FontManagerTests.mm:
(webViewForFontManagerTesting):
(TestWebKitAPI::TEST):
* TestWebKitAPI/mac/TestFontOptions.h: Copied from Source/WebCore/editing/cocoa/FontAttributesCocoa.mm.
* TestWebKitAPI/mac/TestFontOptions.mm: Added.

Introduce TestFontOptions, which wraps the shared NSFontOptions and swizzles `-sharedFontOptions` to return a
global instance of itself. TestFontOptions supports several testing helpers to add or remove font shadows,
foreground colors, and background colors.

(sharedFontOptionsForTesting):
(+[TestFontOptions sharedInstance]):
(-[TestFontOptions initWithFontOptions:]):
(-[TestFontOptions selectedAttributes]):
(-[TestFontOptions fontOptions]):
(-[TestFontOptions shadowWidth]):
(-[TestFontOptions setShadowWidth:]):
(-[TestFontOptions shadowHeight]):
(-[TestFontOptions setShadowHeight:]):
(-[TestFontOptions setShadowBlurRadius:]):
(-[TestFontOptions setHasShadow:]):
(-[TestFontOptions foregroundColor]):
(-[TestFontOptions setForegroundColor:]):
(-[TestFontOptions backgroundColor]):
(-[TestFontOptions setBackgroundColor:]):
(-[TestFontOptions _dispatchFontAttributeChanges]):
(-[TestFontOptions convertAttributes:]):
(-[TestFontOptions setSelectedAttributes:isMultiple:]):
(-[TestFontOptions forwardInvocation:]):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@236854 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/WebCore/ChangeLog
Source/WebCore/editing/FontAttributeChanges.cpp
Source/WebCore/editing/cocoa/FontAttributesCocoa.mm
Source/WebCore/platform/mac/WebCoreNSFontManagerExtras.mm
Tools/ChangeLog
Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
Tools/TestWebKitAPI/Tests/TestWebKitAPI/mac/AppKitSPI.h
Tools/TestWebKitAPI/Tests/mac/FontManagerTests.mm
Tools/TestWebKitAPI/mac/TestFontOptions.h [new file with mode: 0644]
Tools/TestWebKitAPI/mac/TestFontOptions.mm [new file with mode: 0644]

index abc78dd..663aace 100644 (file)
@@ -1,3 +1,33 @@
+2018-10-04  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [macOS] Fix some font attribute conversion bugs in preparation for "Font > Styles…" support in WebKit2
+        https://bugs.webkit.org/show_bug.cgi?id=190289
+        <rdar://problem/45020806>
+
+        Reviewed by Ryosuke Niwa.
+
+        Makes some small adjustments to fix two bugs in font attribute conversion logic. See below for more detail.
+
+        Tests:  FontManagerTests.AddFontShadowUsingFontOptions
+                FontManagerTests.AddAndRemoveColorsUsingFontOptions
+
+        * editing/FontAttributeChanges.cpp:
+        (WebCore::cssValueListForShadow):
+        * editing/cocoa/FontAttributesCocoa.mm:
+
+        Currently, we bail from adding a font shadow if the shadow's offset is empty. However, valid shadow offsets may
+        have negative dimensions, so a check for `isZero()` should be used instead.
+
+        (WebCore::FontAttributes::createDictionary const):
+        * platform/mac/WebCoreNSFontManagerExtras.mm:
+
+        Fall back to a transparent background color; this allows senders to remove the current background color by just
+        removing NSBackgroundColorAttributeName from the attribute dictionary, rather than explicitly setting it to the
+        transparent color (this scenario is exercised when using "Font > Styles…" to specify a font style without a
+        background color).
+
+        (WebCore::computedFontAttributeChanges):
+
 2018-10-03  Ryosuke Niwa  <rniwa@webkit.org>
 
         MutationRecord doesn't keep JS wrappers of target, addedNodes, and removedNodes alive
index 3a15376..f0b13ec 100644 (file)
@@ -81,7 +81,7 @@ Ref<MutableStyleProperties> FontChanges::createStyleProperties() const
 
 static RefPtr<CSSValueList> cssValueListForShadow(const FontShadow& shadow)
 {
-    if (shadow.offset.isEmpty() && !shadow.blurRadius)
+    if (shadow.offset.isZero() && !shadow.blurRadius)
         return nullptr;
 
     auto list = CSSValueList::createCommaSeparated();
index 470e292..aabeb20 100644 (file)
@@ -42,10 +42,8 @@ RetainPtr<NSDictionary> FontAttributes::createDictionary() const
     if (backgroundColor.isValid())
         attributes[NSBackgroundColorAttributeName] = platformColor(backgroundColor);
 
-    if (fontShadow.color.isValid() && (!fontShadow.offset.isEmpty() || fontShadow.blurRadius)) {
-        auto shadow = fontShadow.createShadow();
-        attributes[NSShadowAttributeName] = shadow.get();
-    }
+    if (fontShadow.color.isValid() && (!fontShadow.offset.isZero() || fontShadow.blurRadius))
+        attributes[NSShadowAttributeName] = fontShadow.createShadow().get();
 
     if (subscriptOrSuperscript == SubscriptOrSuperscript::Subscript)
         attributes[NSSuperscriptAttributeName] = @(-1);
index 9f7d7dd..9d0b68e 100644 (file)
@@ -126,7 +126,7 @@ FontAttributeChanges computedFontAttributeChanges(NSFontManager *fontManager, id
 
     NSColor *convertedBackgroundColorA = [convertedAttributesA objectForKey:NSBackgroundColorAttributeName];
     if (convertedBackgroundColorA == [convertedAttributesB objectForKey:NSBackgroundColorAttributeName])
-        changes.setBackgroundColor(colorFromNSColor(convertedBackgroundColorA));
+        changes.setBackgroundColor(colorFromNSColor(convertedBackgroundColorA ?: NSColor.clearColor));
 
     changes.setFontChanges(computedFontChanges(fontManager, originalFontA, [convertedAttributesA objectForKey:NSFontAttributeName], [convertedAttributesB objectForKey:NSFontAttributeName]));
 
index 48aa3a3..eb8e38a 100644 (file)
@@ -1,3 +1,46 @@
+2018-10-04  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [macOS] Fix some font attribute conversion bugs in preparation for "Font > Styles…" support in WebKit2
+        https://bugs.webkit.org/show_bug.cgi?id=190289
+        <rdar://problem/45020806>
+
+        Reviewed by Ryosuke Niwa.
+
+        Add new API tests to exercise two corner cases when using NSFontOptions ("Font > Styles…") to change font
+        attributes at the current selection.
+
+        * TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
+        * TestWebKitAPI/Tests/TestWebKitAPI/mac/AppKitSPI.h:
+        * TestWebKitAPI/Tests/mac/FontManagerTests.mm:
+        (webViewForFontManagerTesting):
+        (TestWebKitAPI::TEST):
+        * TestWebKitAPI/mac/TestFontOptions.h: Copied from Source/WebCore/editing/cocoa/FontAttributesCocoa.mm.
+        * TestWebKitAPI/mac/TestFontOptions.mm: Added.
+
+        Introduce TestFontOptions, which wraps the shared NSFontOptions and swizzles `-sharedFontOptions` to return a
+        global instance of itself. TestFontOptions supports several testing helpers to add or remove font shadows,
+        foreground colors, and background colors.
+
+        (sharedFontOptionsForTesting):
+        (+[TestFontOptions sharedInstance]):
+        (-[TestFontOptions initWithFontOptions:]):
+        (-[TestFontOptions selectedAttributes]):
+        (-[TestFontOptions fontOptions]):
+        (-[TestFontOptions shadowWidth]):
+        (-[TestFontOptions setShadowWidth:]):
+        (-[TestFontOptions shadowHeight]):
+        (-[TestFontOptions setShadowHeight:]):
+        (-[TestFontOptions setShadowBlurRadius:]):
+        (-[TestFontOptions setHasShadow:]):
+        (-[TestFontOptions foregroundColor]):
+        (-[TestFontOptions setForegroundColor:]):
+        (-[TestFontOptions backgroundColor]):
+        (-[TestFontOptions setBackgroundColor:]):
+        (-[TestFontOptions _dispatchFontAttributeChanges]):
+        (-[TestFontOptions convertAttributes:]):
+        (-[TestFontOptions setSelectedAttributes:isMultiple:]):
+        (-[TestFontOptions forwardInvocation:]):
+
 2018-10-04  Jiewen Tan  <jiewen_tan@apple.com>
 
         [WebAuthN] Move time out control from WebProcess to UIProcess
index c15c479..04b5da5 100644 (file)
                F4F137921D9B683E002BEC57 /* large-video-test-now-playing.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4F137911D9B6832002BEC57 /* large-video-test-now-playing.html */; };
                F4F405BC1D4C0D1C007A9707 /* full-size-autoplaying-video-with-audio.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4F405BA1D4C0CF8007A9707 /* full-size-autoplaying-video-with-audio.html */; };
                F4F405BD1D4C0D1C007A9707 /* skinny-autoplaying-video-with-audio.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4F405BB1D4C0CF8007A9707 /* skinny-autoplaying-video-with-audio.html */; };
+               F4F5BB5221667BAA002D06B9 /* TestFontOptions.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4F5BB5121667BAA002D06B9 /* TestFontOptions.mm */; };
                F4FA91811E61849B007B8C1D /* WKWebViewMacEditingTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4FA917F1E61849B007B8C1D /* WKWebViewMacEditingTests.mm */; };
                F4FA91831E61857B007B8C1D /* double-click-does-not-select-trailing-space.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4FA91821E618566007B8C1D /* double-click-does-not-select-trailing-space.html */; };
                F4FC077720F013B600CA043C /* significant-text-milestone.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4FC077620F0108100CA043C /* significant-text-milestone.html */; };
                F4F137911D9B6832002BEC57 /* large-video-test-now-playing.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "large-video-test-now-playing.html"; sourceTree = "<group>"; };
                F4F405BA1D4C0CF8007A9707 /* full-size-autoplaying-video-with-audio.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "full-size-autoplaying-video-with-audio.html"; sourceTree = "<group>"; };
                F4F405BB1D4C0CF8007A9707 /* skinny-autoplaying-video-with-audio.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "skinny-autoplaying-video-with-audio.html"; sourceTree = "<group>"; };
+               F4F5BB5021667BAA002D06B9 /* TestFontOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestFontOptions.h; sourceTree = "<group>"; };
+               F4F5BB5121667BAA002D06B9 /* TestFontOptions.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = TestFontOptions.mm; sourceTree = "<group>"; };
                F4FA917F1E61849B007B8C1D /* WKWebViewMacEditingTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WKWebViewMacEditingTests.mm; sourceTree = "<group>"; };
                F4FA91821E618566007B8C1D /* double-click-does-not-select-trailing-space.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = "double-click-does-not-select-trailing-space.html"; path = "Tests/WebKitCocoa/double-click-does-not-select-trailing-space.html"; sourceTree = SOURCE_ROOT; };
                F4FC077620F0108100CA043C /* significant-text-milestone.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "significant-text-milestone.html"; sourceTree = "<group>"; };
                                F46128CA211D475100D9FADB /* TestDraggingInfo.mm */,
                                F4E0A2B62122847400AF7C7F /* TestFilePromiseReceiver.h */,
                                F4E0A2B72122847400AF7C7F /* TestFilePromiseReceiver.mm */,
+                               F4F5BB5021667BAA002D06B9 /* TestFontOptions.h */,
+                               F4F5BB5121667BAA002D06B9 /* TestFontOptions.mm */,
                                F45D388F215A7B4B002A2979 /* TestInspectorBar.h */,
                                F45D3890215A7B4B002A2979 /* TestInspectorBar.mm */,
                                C08587BE13FE956C001EF4E5 /* WebKitAgnosticTest.h */,
                                7CCE7EA91A411A1D00447C4C /* TestBrowsingContextLoadDelegate.mm in Sources */,
                                F46128CB211D475100D9FADB /* TestDraggingInfo.mm in Sources */,
                                F4E0A2B82122847400AF7C7F /* TestFilePromiseReceiver.mm in Sources */,
+                               F4F5BB5221667BAA002D06B9 /* TestFontOptions.mm in Sources */,
                                F45E15762112CE6200307E82 /* TestInputDelegate.mm in Sources */,
                                F45D3891215A7B4B002A2979 /* TestInspectorBar.mm in Sources */,
                                2D1C04A71D76298B000A6816 /* TestNavigationDelegate.mm in Sources */,
index 2c5a7a8..4f1f9bb 100644 (file)
@@ -103,4 +103,8 @@ NSString * const NSInspectorBarTextAlignmentItemIdentifier = @"NSInspectorBarTex
 - (void)menuNeedsUpdate:(NSMenu *)menu;
 @end
 
+@interface NSFontOptions : NSObject
++ (instancetype)sharedFontOptions;
+@end
+
 #endif // PLATFORM(MAC)
index 7061673..6a44679 100644 (file)
@@ -31,6 +31,7 @@
 #import "AppKitSPI.h"
 #import "NSFontPanelTesting.h"
 #import "PlatformUtilities.h"
+#import "TestFontOptions.h"
 #import "TestInspectorBar.h"
 #import "TestWKWebView.h"
 #import <WebKit/WKWebViewPrivate.h>
 
 @end
 
-static RetainPtr<FontManagerTestWKWebView> webViewForFontManagerTesting(NSFontManager *fontManager)
+static RetainPtr<FontManagerTestWKWebView> webViewForFontManagerTesting(NSFontManager *fontManager, NSString *markup)
 {
     auto webView = adoptNS([[FontManagerTestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400)]);
-    [webView synchronouslyLoadHTMLString:@"<body contenteditable>"
-        "<span id='foo'>foo</span> <span id='bar'>bar</span> <span id='baz'>baz</span>"
-        "</body><script>document.body.addEventListener('input', event => lastInputEvent = event)</script>"];
+    [webView synchronouslyLoadHTMLString:markup];
     [webView stringByEvaluatingJavaScript:@"document.body.focus()"];
     [webView _setEditable:YES];
     fontManager.target = webView.get();
     return webView;
 }
 
+static RetainPtr<FontManagerTestWKWebView> webViewForFontManagerTesting(NSFontManager *fontManager)
+{
+    return webViewForFontManagerTesting(fontManager, @"<body contenteditable>"
+        "<span id='foo'>foo</span> <span id='bar'>bar</span> <span id='baz'>baz</span>"
+        "</body><script>document.body.addEventListener('input', event => lastInputEvent = event)</script>");
+}
+
 static RetainPtr<NSMenuItemCell> menuItemCellForFontAction(NSUInteger tag)
 {
     auto menuItem = adoptNS([[NSMenuItem alloc] init]);
@@ -474,6 +480,48 @@ TEST(FontManagerTests, TypingAttributesAfterSubscriptAndSuperscript)
     EXPECT_EQ(0, [[webView typingAttributes][NSSuperscriptAttributeName] integerValue]);
 }
 
+TEST(FontManagerTests, AddFontShadowUsingFontOptions)
+{
+    TestFontOptions *options = TestFontOptions.sharedInstance;
+    auto webView = webViewForFontManagerTesting(NSFontManager.sharedFontManager);
+
+    [webView selectWord:nil];
+    options.shadowWidth = 3;
+    options.shadowHeight = -3;
+    options.hasShadow = YES;
+
+    EXPECT_WK_STREQ("rgba(0, 0, 0, 0.333333) 3px -3px 0px", [webView stylePropertyAtSelectionStart:@"text-shadow"]);
+    [webView waitForNextPresentationUpdate];
+    NSShadow *shadow = [webView typingAttributes][NSShadowAttributeName];
+    EXPECT_EQ(shadow.shadowOffset.width, 3);
+    EXPECT_EQ(shadow.shadowOffset.height, -3);
+}
+
+TEST(FontManagerTests, AddAndRemoveColorsUsingFontOptions)
+{
+    TestFontOptions *options = TestFontOptions.sharedInstance;
+    auto webView = webViewForFontManagerTesting(NSFontManager.sharedFontManager, @"<body contenteditable>hello</body>");
+    [webView selectWord:nil];
+
+    options.backgroundColor = [NSColor colorWithRed:1 green:0 blue:0 alpha:0.2];
+    options.foregroundColor = [NSColor colorWithRed:0 green:0 blue:1 alpha:1];
+
+    EXPECT_WK_STREQ("rgb(0, 0, 255)", [webView stylePropertyAtSelectionStart:@"color"]);
+    EXPECT_WK_STREQ("rgba(255, 0, 0, 0.2)", [webView stylePropertyAtSelectionStart:@"background-color"]);
+    NSDictionary *attributes = [webView typingAttributes];
+    EXPECT_TRUE([[NSColor colorWithRed:0 green:0 blue:1 alpha:1] isEqual:attributes[NSForegroundColorAttributeName]]);
+    EXPECT_TRUE([[NSColor colorWithRed:1 green:0 blue:0 alpha:0.2] isEqual:attributes[NSBackgroundColorAttributeName]]);
+
+    options.backgroundColor = nil;
+    options.foregroundColor = nil;
+
+    EXPECT_WK_STREQ("rgb(0, 0, 0)", [webView stylePropertyAtSelectionStart:@"color"]);
+    EXPECT_WK_STREQ("rgba(0, 0, 0, 0)", [webView stylePropertyAtSelectionStart:@"background-color"]);
+    attributes = [webView typingAttributes];
+    EXPECT_NULL(attributes[NSForegroundColorAttributeName]);
+    EXPECT_NULL(attributes[NSBackgroundColorAttributeName]);
+}
+
 } // namespace TestWebKitAPI
 
 #endif // PLATFORM(MAC) && WK_API_ENABLED
diff --git a/Tools/TestWebKitAPI/mac/TestFontOptions.h b/Tools/TestWebKitAPI/mac/TestFontOptions.h
new file mode 100644 (file)
index 0000000..3f8ba07
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#if PLATFORM(MAC) && WK_API_ENABLED
+
+#import <AppKit/AppKit.h>
+
+@interface TestFontOptions : NSObject
+
+@property (class, readonly) TestFontOptions *sharedInstance;
+@property (nonatomic, readonly) NSDictionary *selectedAttributes;
+@property (nonatomic, readonly) BOOL hasMultipleFonts;
+
+// Font shadow manipulation.
+@property (nonatomic) BOOL hasShadow;
+@property (nonatomic) CGFloat shadowWidth;
+@property (nonatomic) CGFloat shadowHeight;
+@property (nonatomic) CGFloat shadowBlurRadius;
+
+// Font colors.
+@property (nonatomic, copy) NSColor *foregroundColor;
+@property (nonatomic, copy) NSColor *backgroundColor;
+
+@end
+
+#endif
diff --git a/Tools/TestWebKitAPI/mac/TestFontOptions.mm b/Tools/TestWebKitAPI/mac/TestFontOptions.mm
new file mode 100644 (file)
index 0000000..3066c6e
--- /dev/null
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "config.h"
+#import "TestFontOptions.h"
+
+#if PLATFORM(MAC) && WK_API_ENABLED
+
+#import "AppKitSPI.h"
+#import "ClassMethodSwizzler.h"
+#import <objc/runtime.h>
+#import <wtf/RetainPtr.h>
+#import <wtf/SetForScope.h>
+
+using namespace TestWebKitAPI;
+
+static TestFontOptions *sharedFontOptionsForTesting()
+{
+    return TestFontOptions.sharedInstance;
+}
+
+@interface TestFontOptions ()
+- (instancetype)initWithFontOptions:(NSFontOptions *)fontOptions;
+@end
+
+@implementation TestFontOptions {
+    RetainPtr<NSFontOptions> _fontOptions;
+    CGSize _shadowOffset;
+    CGFloat _shadowBlurRadius;
+    BOOL _hasShadow;
+    BOOL _hasPendingShadowChanges;
+
+    RetainPtr<NSColor> _foregroundColor;
+    RetainPtr<NSColor> _backgroundColor;
+    BOOL _hasPendingColorChanges;
+
+    std::unique_ptr<ClassMethodSwizzler> _replaceFontOptionsSwizzler;
+    RetainPtr<NSDictionary> _selectedAttributes;
+    BOOL _hasMultipleFonts;
+}
+
+@synthesize hasShadow=_hasShadow;
+@synthesize shadowBlurRadius=_shadowBlurRadius;
+@synthesize hasMultipleFonts=_hasMultipleFonts;
+
++ (instancetype)sharedInstance
+{
+    static TestFontOptions *sharedInstance;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        NSFontOptions *sharedFontOptions = [NSClassFromString(@"NSFontOptions") sharedFontOptions];
+        sharedInstance = [[TestFontOptions alloc] initWithFontOptions:sharedFontOptions];
+    });
+    return sharedInstance;
+}
+
+- (instancetype)initWithFontOptions:(NSFontOptions *)fontOptions
+{
+    if (!(self = [super init]))
+        return nil;
+
+    _fontOptions = fontOptions;
+    ASSERT(_fontOptions);
+
+    _shadowOffset = CGSizeZero;
+    _shadowBlurRadius = 0;
+    _replaceFontOptionsSwizzler = std::make_unique<ClassMethodSwizzler>(NSClassFromString(@"NSFontOptions"), @selector(sharedFontOptions), reinterpret_cast<IMP>(sharedFontOptionsForTesting));
+    _hasPendingShadowChanges = NO;
+    _hasMultipleFonts = NO;
+
+    return self;
+}
+
+- (NSDictionary *)selectedAttributes
+{
+    return _selectedAttributes.get();
+}
+
+- (NSFontOptions *)fontOptions
+{
+    return _fontOptions.get();
+}
+
+- (CGFloat)shadowWidth
+{
+    return _shadowOffset.width;
+}
+
+- (void)setShadowWidth:(CGFloat)shadowWidth
+{
+    if (_shadowOffset.width == shadowWidth)
+        return;
+
+    SetForScope<BOOL> hasPendingFontShadowChanges(_hasPendingShadowChanges, YES);
+    _shadowOffset.width = shadowWidth;
+    [self _dispatchFontAttributeChanges];
+}
+
+- (CGFloat)shadowHeight
+{
+    return _shadowOffset.height;
+}
+
+- (void)setShadowHeight:(CGFloat)shadowHeight
+{
+    if (_shadowOffset.height == shadowHeight)
+        return;
+
+    SetForScope<BOOL> hasPendingFontShadowChanges(_hasPendingShadowChanges, YES);
+    _shadowOffset.height = shadowHeight;
+    [self _dispatchFontAttributeChanges];
+}
+
+- (void)setShadowBlurRadius:(CGFloat)shadowBlurRadius
+{
+    if (_shadowBlurRadius == shadowBlurRadius)
+        return;
+
+    SetForScope<BOOL> hasPendingFontShadowChanges(_hasPendingShadowChanges, YES);
+    _shadowBlurRadius = shadowBlurRadius;
+    [self _dispatchFontAttributeChanges];
+}
+
+- (void)setHasShadow:(BOOL)hasShadow
+{
+    if (_hasShadow == hasShadow)
+        return;
+
+    SetForScope<BOOL> hasPendingFontShadowChanges(_hasPendingShadowChanges, YES);
+    _hasShadow = hasShadow;
+    [self _dispatchFontAttributeChanges];
+}
+
+- (NSColor *)foregroundColor
+{
+    return _foregroundColor.get();
+}
+
+- (void)setForegroundColor:(NSColor *)color
+{
+    SetForScope<BOOL> hasPendingColorChanges(_hasPendingColorChanges, YES);
+    _foregroundColor = adoptNS([color copy]);
+    [self _dispatchFontAttributeChanges];
+}
+
+- (NSColor *)backgroundColor
+{
+    return _backgroundColor.get();
+}
+
+- (void)setBackgroundColor:(NSColor *)color
+{
+    SetForScope<BOOL> hasPendingColorChanges(_hasPendingColorChanges, YES);
+    _backgroundColor = adoptNS([color copy]);
+    [self _dispatchFontAttributeChanges];
+}
+
+- (void)_dispatchFontAttributeChanges
+{
+    [NSFontManager.sharedFontManager.target performSelector:@selector(changeAttributes:) withObject:self];
+}
+
+- (NSDictionary *)convertAttributes:(NSDictionary *)attributes
+{
+    auto convertedAttributes = adoptNS([attributes mutableCopy]);
+
+    if (_hasPendingShadowChanges) {
+        if (_hasShadow) {
+            auto shadow = adoptNS([[NSShadow alloc] init]);
+            [shadow setShadowBlurRadius:_shadowBlurRadius];
+            [shadow setShadowOffset:_shadowOffset];
+            [convertedAttributes setObject:shadow.get() forKey:NSShadowAttributeName];
+        } else
+            [convertedAttributes removeObjectForKey:NSShadowAttributeName];
+    }
+
+    if (_hasPendingColorChanges) {
+        if (_foregroundColor)
+            [convertedAttributes setObject:_foregroundColor.get() forKey:NSForegroundColorAttributeName];
+        else
+            [convertedAttributes removeObjectForKey:NSForegroundColorAttributeName];
+
+        if (_backgroundColor)
+            [convertedAttributes setObject:_backgroundColor.get() forKey:NSBackgroundColorAttributeName];
+        else
+            [convertedAttributes removeObjectForKey:NSBackgroundColorAttributeName];
+    }
+
+    return convertedAttributes.autorelease();
+}
+
+- (void)setSelectedAttributes:(NSDictionary *)attributes isMultiple:(BOOL)multiple
+{
+    _hasMultipleFonts = multiple;
+    _selectedAttributes = attributes;
+}
+
+- (void)forwardInvocation:(NSInvocation *)invocation
+{
+    if ([_fontOptions respondsToSelector:invocation.selector]) {
+        [invocation invokeWithTarget:_fontOptions.get()];
+        return;
+    }
+
+    [super forwardInvocation:invocation];
+}
+
+@end
+
+#endif // PLATFORM(MAC) && WK_API_ENABLED