Skip to content

Commit

Permalink
[Writing Tools] Hide blinking insertion cursor while text placeholder…
Browse files Browse the repository at this point in the history
…s are present

https://bugs.webkit.org/show_bug.cgi?id=278067
rdar://132432864

Reviewed by Aditya Keerthi.

Hide the caret when inserting the placeholder, and unhide it when removing the placeholder.

Also remove some staging declarations.

* Source/WebCore/PAL/pal/spi/mac/NSTextInputContextSPI.h:
* Source/WebCore/editing/Editor.cpp:
(WebCore::Editor::insertTextPlaceholder):
(WebCore::Editor::removeTextPlaceholder):
* Source/WebCore/editing/FrameSelection.cpp:
(WebCore::FrameSelection::FrameSelection):
(WebCore::FrameSelection::focusedOrActiveStateChanged):
(WebCore::FrameSelection::addCaretVisibilitySuppressionReason):
(WebCore::FrameSelection::removeCaretVisibilitySuppressionReason):
(WebCore::FrameSelection::updateCaretVisibility):
(WebCore::FrameSelection::setCaretVisibility): Deleted.
* Source/WebCore/editing/FrameSelection.h:
* Source/WebCore/testing/Internals.cpp:
(WebCore::Internals::isCaretVisible):
* Source/WebCore/testing/Internals.h:
* Source/WebCore/testing/Internals.idl:
* Source/WebKit/UIProcess/Cocoa/WKTextSelectionRect.h:
* Source/WebKit/UIProcess/Cocoa/WKTextSelectionRect.mm:
* Tools/TestWebKitAPI/Tests/TestWebKitAPI/mac/AppKitSPI.h:
* Tools/TestWebKitAPI/Tests/WebKitCocoa/TextPlaceholderTests.mm:
(createWebViewForNSTextPlaceholder):
(TEST(NSTextPlaceholder, InsertTextPlaceholder)):
(TEST(NSTextPlaceholder, InsertAndRemoveTextPlaceholderWithoutIncomingText)):
(TEST(NSTextPlaceholder, InsertAndRemoveTextPlaceholderWithIncomingText)):

Canonical link: https://commits.webkit.org/282500@main
  • Loading branch information
rr-codes committed Aug 20, 2024
1 parent 3df65b3 commit 77eec0a
Show file tree
Hide file tree
Showing 11 changed files with 101 additions and 60 deletions.
13 changes: 0 additions & 13 deletions Source/WebCore/PAL/pal/spi/mac/NSTextInputContextSPI.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,6 @@
#import <AppKit/NSTextInputContext_Private.h>
#import <AppKit/NSTextPlaceholder_Private.h>

#if HAVE(NSTEXTPLACEHOLDER_RECTS)
// Staging for rdar://126696059
@interface NSTextPlaceholder (staging_126696059)
@property (nonatomic, readonly) NSArray *rects;
@end

@protocol NSTextInputClient_Async_staging_126696059
@optional
- (void)insertTextPlaceholderWithSize:(CGSize)size completionHandler:(void (^)(NSTextPlaceholder *))completionHandler;
- (void)removeTextPlaceholder:(NSTextPlaceholder *)placeholder willInsertText:(BOOL)willInsertText completionHandler:(void (^)(void))completionHandler;
@end
#endif // HAVE(NSTEXTPLACEHOLDER_RECTS)

#else // !USE(APPLE_INTERNAL_SDK)

@interface NSTextSelectionRect : NSObject
Expand Down
4 changes: 4 additions & 0 deletions Source/WebCore/editing/Editor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3670,6 +3670,8 @@ RefPtr<TextPlaceholderElement> Editor::insertTextPlaceholder(const IntSize& size
if (!placeholder->parentNode())
return nullptr;

document->selection().addCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason::TextPlaceholderIsShowing);

document->selection().setSelection(VisibleSelection { positionInParentBeforeNode(placeholder.ptr()) }, FrameSelection::defaultSetSelectionOptions(UserTriggered::Yes));

return placeholder;
Expand All @@ -3691,6 +3693,8 @@ void Editor::removeTextPlaceholder(TextPlaceholderElement& placeholder)
// To match the Legacy WebKit implementation, set the text insertion point to be before where the placeholder used to be.
if (document->selection().isFocusedAndActive() && document->focusedElement() == savedRootEditableElement)
document->selection().setSelection(VisibleSelection { savedPositionBeforePlaceholder }, FrameSelection::defaultSetSelectionOptions(UserTriggered::Yes));

document->selection().removeCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason::TextPlaceholderIsShowing);
}

static inline void collapseCaretWidth(IntRect& rect)
Expand Down
43 changes: 33 additions & 10 deletions Source/WebCore/editing/FrameSelection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -204,14 +204,19 @@ FrameSelection::FrameSelection(Document* document)
m_selection.setIsDirectional(true);

bool activeAndFocused = isFocusedAndActive();
if (activeAndFocused)
setSelectionFromNone();

#if USE(UIKIT_EDITING)
// Caret blinking (blinks | does not blink)
setCaretVisible(activeAndFocused);
constexpr auto shouldUpdateAppearance = ShouldUpdateAppearance::Yes;
#else
setCaretVisibility(activeAndFocused ? CaretVisibility::Visible : CaretVisibility::Hidden, ShouldUpdateAppearance::No);
constexpr auto shouldUpdateAppearance = ShouldUpdateAppearance::No;
#endif

// Caret blinking (blinks | does not blink)
if (activeAndFocused) {
setSelectionFromNone();
removeCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason::IsNotFocusedOrActive, shouldUpdateAppearance);
} else
addCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason::IsNotFocusedOrActive, shouldUpdateAppearance);
}

FrameSelection::~FrameSelection() = default;
Expand Down Expand Up @@ -2274,9 +2279,11 @@ void FrameSelection::focusedOrActiveStateChanged()

#if USE(UIKIT_EDITING)
// Caret blinking (blinks | does not blink)
if (activeAndFocused)
if (activeAndFocused) {
setSelectionFromNone();
setCaretVisible(activeAndFocused);
removeCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason::IsNotFocusedOrActive);
} else
addCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason::IsNotFocusedOrActive);
#else
// Because RenderObject::selectionBackgroundColor() and
// RenderObject::selectionForegroundColor() check if the frame is active,
Expand All @@ -2285,9 +2292,11 @@ void FrameSelection::focusedOrActiveStateChanged()
view->selection().repaint();

// Caret appears in the active frame.
if (activeAndFocused)
if (activeAndFocused) {
setSelectionFromNone();
setCaretVisibility(activeAndFocused ? CaretVisibility::Visible : CaretVisibility::Hidden, ShouldUpdateAppearance::Yes);
removeCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason::IsNotFocusedOrActive);
} else
addCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason::IsNotFocusedOrActive);
#endif
}

Expand Down Expand Up @@ -2421,8 +2430,22 @@ void FrameSelection::updateAppearance()
}
}

void FrameSelection::setCaretVisibility(CaretVisibility visibility, ShouldUpdateAppearance doAppearanceUpdate)
void FrameSelection::addCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason reason, ShouldUpdateAppearance doAppearanceUpdate)
{
m_caretVisibilitySuppressionReasons.add(reason);
updateCaretVisibility(doAppearanceUpdate);
}

void FrameSelection::removeCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason reason, ShouldUpdateAppearance doAppearanceUpdate)
{
m_caretVisibilitySuppressionReasons.remove(reason);
updateCaretVisibility(doAppearanceUpdate);
}

void FrameSelection::updateCaretVisibility(ShouldUpdateAppearance doAppearanceUpdate)
{
auto visibility = m_caretVisibilitySuppressionReasons.isEmpty() ? CaretVisibility::Visible : CaretVisibility::Hidden;

if (caretVisibility() == visibility)
return;

Expand Down
18 changes: 14 additions & 4 deletions Source/WebCore/editing/FrameSelection.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,19 @@ class VisiblePosition;
enum class UserTriggered : bool { No, Yes };
enum class RevealExtentOption : bool { RevealExtent, DoNotRevealExtent };
enum class ForceCenterScroll : bool { No, Yes };
enum class CaretVisibility : bool { Visible, Hidden };

enum class CaretVisibilitySuppressionReason : uint8_t {
IsNotFocusedOrActive = 1 << 0,
TextPlaceholderIsShowing = 1 << 1,
};

class CaretBase {
WTF_MAKE_NONCOPYABLE(CaretBase);
WTF_MAKE_FAST_ALLOCATED;
public:
WEBCORE_EXPORT static Color computeCaretColor(const RenderStyle& elementStyle, const Node*);
protected:
enum class CaretVisibility : bool { Visible, Hidden };
explicit CaretBase(CaretVisibility = CaretVisibility::Hidden);

void invalidateCaretRect(Node*, bool caretRectChanged = false, CaretAnimator* = nullptr);
Expand Down Expand Up @@ -117,6 +122,7 @@ class FrameSelection final : private CaretBase, public CaretAnimationClient, pub
WTF_MAKE_FAST_ALLOCATED;
WTF_OVERRIDE_DELETE_FOR_CHECKED_PTR(FrameSelection);
public:
enum class ShouldUpdateAppearance : bool { No, Yes };
enum class Alteration : bool { Move, Extend };
enum class CursorAlignOnScroll : bool { IfNeeded, Always };
enum class SetSelectionOption : uint16_t {
Expand Down Expand Up @@ -199,7 +205,10 @@ class FrameSelection final : private CaretBase, public CaretAnimationClient, pub
void nodeWillBeRemoved(Node&);
void textWasReplaced(CharacterData&, unsigned offset, unsigned oldLength, unsigned newLength);

void setCaretVisible(bool caretIsVisible) { setCaretVisibility(caretIsVisible ? CaretVisibility::Visible : CaretVisibility::Hidden, ShouldUpdateAppearance::Yes); }
WEBCORE_EXPORT void addCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason, ShouldUpdateAppearance = ShouldUpdateAppearance::Yes);
WEBCORE_EXPORT void removeCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason, ShouldUpdateAppearance = ShouldUpdateAppearance::Yes);

bool isCaretVisible() const { return CaretBase::caretIsVisible(); }
void paintCaret(GraphicsContext&, const LayoutPoint&);

// Used to suspend caret blinking while the mouse is down.
Expand Down Expand Up @@ -322,8 +331,7 @@ class FrameSelection final : private CaretBase, public CaretAnimationClient, pub
void setFocusedElementIfNeeded(OptionSet<SetSelectionOption>);
void focusedOrActiveStateChanged();

enum class ShouldUpdateAppearance : bool { No, Yes };
WEBCORE_EXPORT void setCaretVisibility(CaretVisibility, ShouldUpdateAppearance);
void updateCaretVisibility(ShouldUpdateAppearance);

bool recomputeCaretRect();
void invalidateCaretRect();
Expand Down Expand Up @@ -362,6 +370,8 @@ class FrameSelection final : private CaretBase, public CaretAnimationClient, pub

UniqueRef<CaretAnimator> m_caretAnimator;

OptionSet<CaretVisibilitySuppressionReason> m_caretVisibilitySuppressionReasons;

bool m_caretInsidePositionFixed : 1;
bool m_absCaretBoundsDirty : 1;
bool m_focused : 1;
Expand Down
11 changes: 10 additions & 1 deletion Source/WebCore/testing/Internals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1921,7 +1921,16 @@ ExceptionOr<Ref<DOMRect>> Internals::absoluteCaretBounds()

return DOMRect::create(document->frame()->selection().absoluteCaretBounds());
}


ExceptionOr<bool> Internals::isCaretVisible()
{
RefPtr document = contextDocument();
if (!document || !document->frame())
return Exception { ExceptionCode::InvalidAccessError };

return document->frame()->selection().isCaretVisible();
}

ExceptionOr<bool> Internals::isCaretBlinkingSuspended()
{
auto* document = contextDocument();
Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/testing/Internals.h
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ class Internals final
ExceptionOr<Ref<DOMRect>> absoluteLineRectFromPoint(int x, int y);

ExceptionOr<Ref<DOMRect>> absoluteCaretBounds();
ExceptionOr<bool> isCaretVisible();
ExceptionOr<bool> isCaretBlinkingSuspended();
ExceptionOr<bool> isCaretBlinkingSuspended(Document&);

Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/testing/Internals.idl
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,7 @@ enum RenderingMode {
DOMRect absoluteLineRectFromPoint(long x, long y);

DOMRect absoluteCaretBounds();
boolean isCaretVisible();

// isCaretBlinkingSuspended() returns whether the frame selection of the context document
// is suspended, while the parameterized method returns the state for a particular document
Expand Down
11 changes: 2 additions & 9 deletions Source/WebKit/UIProcess/Cocoa/WKTextSelectionRect.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* THE POSSIBILITY OF SUCH DAMAGE.
*/

#if PLATFORM(COCOA)
#if PLATFORM(IOS_FAMILY) || HAVE(NSTEXTPLACEHOLDER_RECTS)

#if PLATFORM(IOS_FAMILY)
#import <UIKit/UIKit.h>
Expand All @@ -38,19 +38,12 @@ class SelectionGeometry;
#if PLATFORM(IOS_FAMILY)
@interface WKTextSelectionRect : UITextSelectionRect
#else
@interface WKTextSelectionRect : NSObject // FIXME: Change to NSTextSelectionRect after rdar://126379463 lands
@interface WKTextSelectionRect : NSTextSelectionRect
#endif

- (instancetype)initWithCGRect:(CGRect)rect;
- (instancetype)initWithSelectionGeometry:(const WebCore::SelectionGeometry&)selectionGeometry scaleFactor:(CGFloat)scaleFactor;

#if PLATFORM(MAC)
@property (nonatomic, readonly) NSRect rect;
@property (nonatomic, readonly) NSWritingDirection writingDirection;
@property (nonatomic, readonly) BOOL isVertical;
@property (nonatomic, readonly) NSAffineTransform *transform;
#endif

@end

#endif // PLATFORM(COCOA)
4 changes: 4 additions & 0 deletions Source/WebKit/UIProcess/Cocoa/WKTextSelectionRect.mm
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ - (CGPoint)topRight

#endif // HAVE(UI_TEXT_SELECTION_RECT_CUSTOM_HANDLE_INFO)

#if PLATFORM(IOS_FAMILY) || HAVE(NSTEXTPLACEHOLDER_RECTS)

@implementation WKTextSelectionRect {
WebCore::SelectionGeometry _selectionGeometry;
CGFloat _scaleFactor;
Expand Down Expand Up @@ -173,3 +175,5 @@ - (BOOL)isVertical
}

@end

#endif
4 changes: 4 additions & 0 deletions Tools/TestWebKitAPI/Tests/TestWebKitAPI/mac/AppKitSPI.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,16 @@

#else

@class NSTextPlaceholder;

@protocol NSTextInputClient_Async
- (void)selectedRangeWithCompletionHandler:(void(^)(NSRange selectedRange))completionHandler;
- (void)markedRangeWithCompletionHandler:(void(^)(NSRange markedRange))completionHandler;
- (void)hasMarkedTextWithCompletionHandler:(void(^)(BOOL hasMarkedText))completionHandler;
- (void)attributedSubstringForProposedRange:(NSRange)range completionHandler:(void(^)(NSAttributedString *, NSRange actualRange))completionHandler;
- (void)firstRectForCharacterRange:(NSRange)range completionHandler:(void(^)(NSRect firstRect, NSRange actualRange))completionHandler;
- (void)insertTextPlaceholderWithSize:(CGSize)size completionHandler:(void (^)(NSTextPlaceholder *))completionHandler;
- (void)removeTextPlaceholder:(NSTextPlaceholder *)placeholder willInsertText:(BOOL)willInsertText completionHandler:(void (^)(void))completionHandler;
@end

@protocol NSInspectorBarClient <NSObject>
Expand Down
51 changes: 28 additions & 23 deletions Tools/TestWebKitAPI/Tests/WebKitCocoa/TextPlaceholderTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -37,68 +37,73 @@

#if PLATFORM(MAC)
#import <pal/spi/mac/NSTextInputContextSPI.h>

// FIXME: Remove this after rdar://126379463 lands
@interface WKTextSelectionRect : NSObject

@property (nonatomic, readonly) NSRect rect;
@property (nonatomic, readonly) NSWritingDirection writingDirection;
@property (nonatomic, readonly) BOOL isVertical;
@property (nonatomic, readonly) NSAffineTransform *transform;

@end
#endif

RetainPtr<WKWebView> createWebViewForNSTextPlaceholder()
{
WKWebViewConfiguration *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 500) configuration:configuration]);
RetainPtr configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 500) configuration:configuration.get()]);
[webView synchronouslyLoadHTMLString:@"<body contenteditable>Test</body><script>document.body.focus()</script>"];
return webView;
}

TEST(NSTextPlaceholder, InsertTextPlaceholder)
{
auto webView = createWebViewForNSTextPlaceholder();
RetainPtr webView = createWebViewForNSTextPlaceholder();

__block bool isDone = false;
[(id<NSTextInputClient_Async_staging_126696059>)webView.get() insertTextPlaceholderWithSize:CGSizeMake(50, 100) completionHandler:^(NSTextPlaceholder * placeholder) {
[(id<NSTextInputClient_Async>)webView.get() insertTextPlaceholderWithSize:CGSizeMake(50, 100) completionHandler:^(NSTextPlaceholder *placeholder) {
EXPECT_WK_STREQ("<div style=\"display: inline-block; vertical-align: top; visibility: hidden !important; width: 50px; height: 100px;\"></div>Test<script>document.body.focus()</script>", [webView stringByEvaluatingJavaScript:@"document.body.innerHTML"]);
isDone = true;
EXPECT_TRUE(CGSizeEqualToSize([(WKTextSelectionRect *)[[placeholder rects] firstObject] rect].size, CGSizeMake(50, 100)));
EXPECT_TRUE(CGSizeEqualToSize(placeholder.rects.firstObject.rect.size, CGSizeMake(50, 100)));
}];

TestWebKitAPI::Util::run(&isDone);
}

TEST(NSTextPlaceholder, InsertAndRemoveTextPlaceholderWithoutIncomingText)
{
auto webView = createWebViewForNSTextPlaceholder();
RetainPtr webView = createWebViewForNSTextPlaceholder();

__block bool isDone = false;
[(id<NSTextInputClient_Async_staging_126696059>)webView.get() insertTextPlaceholderWithSize:CGSizeMake(50, 100) completionHandler:^(NSTextPlaceholder *placeholder) {
[(id<NSTextInputClient_Async>)webView.get() insertTextPlaceholderWithSize:CGSizeMake(50, 100) completionHandler:^(NSTextPlaceholder *placeholder) {
EXPECT_WK_STREQ("<div style=\"display: inline-block; vertical-align: top; visibility: hidden !important; width: 50px; height: 100px;\"></div>Test<script>document.body.focus()</script>", [webView stringByEvaluatingJavaScript:@"document.body.innerHTML"]);
EXPECT_TRUE(CGSizeEqualToSize([(WKTextSelectionRect *)[[placeholder rects] firstObject] rect].size, CGSizeMake(50, 100)));
[(id<NSTextInputClient_Async_staging_126696059>)webView.get() removeTextPlaceholder:placeholder willInsertText:NO completionHandler:^{
EXPECT_TRUE(CGSizeEqualToSize(placeholder.rects.firstObject.rect.size, CGSizeMake(50, 100)));

EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.isCaretVisible()"] boolValue]);

[(id<NSTextInputClient_Async>)webView.get() removeTextPlaceholder:placeholder willInsertText:NO completionHandler:^{
EXPECT_WK_STREQ("Test<script>document.body.focus()</script>", [webView stringByEvaluatingJavaScript:@"document.body.innerHTML"]);

EXPECT_TRUE([[webView objectByEvaluatingJavaScript:@"internals.isCaretVisible()"] boolValue]);

isDone = true;
}];
}];

TestWebKitAPI::Util::run(&isDone);
}

TEST(NSTextPlaceholder, InsertAndRemoveTextPlaceholderWithIncomingText)
{
auto webView = createWebViewForNSTextPlaceholder();
RetainPtr webView = createWebViewForNSTextPlaceholder();

__block bool isDone = false;
[(id<NSTextInputClient_Async_staging_126696059>)webView.get() insertTextPlaceholderWithSize:CGSizeMake(50, 100) completionHandler:^(NSTextPlaceholder *placeholder) {
[(id<NSTextInputClient_Async>)webView.get() insertTextPlaceholderWithSize:CGSizeMake(50, 100) completionHandler:^(NSTextPlaceholder *placeholder) {
EXPECT_WK_STREQ("<div style=\"display: inline-block; vertical-align: top; visibility: hidden !important; width: 50px; height: 100px;\"></div>Test<script>document.body.focus()</script>", [webView stringByEvaluatingJavaScript:@"document.body.innerHTML"]);
EXPECT_TRUE(CGSizeEqualToSize([(WKTextSelectionRect *)[[placeholder rects] firstObject] rect].size, CGSizeMake(50, 100)));
[(id<NSTextInputClient_Async_staging_126696059>)webView.get() removeTextPlaceholder:placeholder willInsertText:YES completionHandler:^{
EXPECT_TRUE(CGSizeEqualToSize(placeholder.rects.firstObject.rect.size, CGSizeMake(50, 100)));

EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.isCaretVisible()"] boolValue]);

[(id<NSTextInputClient_Async>)webView.get() removeTextPlaceholder:placeholder willInsertText:YES completionHandler:^{
EXPECT_WK_STREQ("Test<script>document.body.focus()</script>", [webView stringByEvaluatingJavaScript:@"document.body.innerHTML"]);

EXPECT_TRUE([[webView objectByEvaluatingJavaScript:@"internals.isCaretVisible()"] boolValue]);

isDone = true;
}];
}];

TestWebKitAPI::Util::run(&isDone);
}

Expand Down

0 comments on commit 77eec0a

Please sign in to comment.