/* * Copyright (C) 2012-2019 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 "WebPage.h" #if PLATFORM(IOS_FAMILY) #import "AccessibilityIOS.h" #import "DataReference.h" #import "DocumentEditingContext.h" #import "DrawingArea.h" #import "EditingRange.h" #import "EditorState.h" #import "InteractionInformationAtPosition.h" #import "Logging.h" #import "NativeWebKeyboardEvent.h" #import "PluginView.h" #import "PrintInfo.h" #import "RemoteLayerTreeDrawingArea.h" #import "SandboxUtilities.h" #import "SharedMemory.h" #import "SyntheticEditingCommandType.h" #import "TextCheckingControllerProxy.h" #import "UIKitSPI.h" #import "UserData.h" #import "ViewGestureGeometryCollector.h" #import "VisibleContentRectUpdateInfo.h" #import "WKAccessibilityWebPageObjectIOS.h" #import "WebAutocorrectionContext.h" #import "WebAutocorrectionData.h" #import "WebChromeClient.h" #import "WebCoreArgumentCoders.h" #import "WebFrame.h" #import "WebImage.h" #import "WebPageMessages.h" #import "WebPageProxyMessages.h" #import "WebPreviewLoaderClient.h" #import "WebProcess.h" #import <CoreText/CTFont.h> #import <WebCore/Autofill.h> #import <WebCore/AutofillElements.h> #import <WebCore/Chrome.h> #import <WebCore/ContentChangeObserver.h> #import <WebCore/DOMTimerHoldingTank.h> #import <WebCore/DataDetection.h> #import <WebCore/DiagnosticLoggingClient.h> #import <WebCore/DiagnosticLoggingKeys.h> #import <WebCore/DocumentLoader.h> #import <WebCore/DragController.h> #import <WebCore/Editing.h> #import <WebCore/Editor.h> #import <WebCore/Element.h> #import <WebCore/ElementAncestorIterator.h> #import <WebCore/EventHandler.h> #import <WebCore/File.h> #import <WebCore/FloatQuad.h> #import <WebCore/FocusController.h> #import <WebCore/Frame.h> #import <WebCore/FrameLoaderClient.h> #import <WebCore/FrameView.h> #import <WebCore/GeometryUtilities.h> #import <WebCore/HTMLAreaElement.h> #import <WebCore/HTMLAttachmentElement.h> #import <WebCore/HTMLBodyElement.h> #import <WebCore/HTMLElement.h> #import <WebCore/HTMLElementTypeHelpers.h> #import <WebCore/HTMLFormElement.h> #import <WebCore/HTMLIFrameElement.h> #import <WebCore/HTMLImageElement.h> #import <WebCore/HTMLInputElement.h> #import <WebCore/HTMLLabelElement.h> #import <WebCore/HTMLOptGroupElement.h> #import <WebCore/HTMLOptionElement.h> #import <WebCore/HTMLParserIdioms.h> #import <WebCore/HTMLSelectElement.h> #import <WebCore/HTMLSummaryElement.h> #import <WebCore/HTMLTextAreaElement.h> #import <WebCore/HTMLTextFormControlElement.h> #import <WebCore/HistoryItem.h> #import <WebCore/HitTestResult.h> #import <WebCore/InputMode.h> #import <WebCore/KeyboardEvent.h> #import <WebCore/LibWebRTCProvider.h> #import <WebCore/MediaSessionManagerIOS.h> #import <WebCore/Node.h> #import <WebCore/NodeList.h> #import <WebCore/NotImplemented.h> #import <WebCore/Page.h> #import <WebCore/Pasteboard.h> #import <WebCore/PlatformKeyboardEvent.h> #import <WebCore/PlatformMouseEvent.h> #import <WebCore/PointerCaptureController.h> #import <WebCore/Quirks.h> #import <WebCore/RenderBlock.h> #import <WebCore/RenderImage.h> #import <WebCore/RenderThemeIOS.h> #import <WebCore/RenderView.h> #import <WebCore/RuntimeApplicationChecks.h> #import <WebCore/Settings.h> #import <WebCore/ShadowRoot.h> #import <WebCore/SharedBuffer.h> #import <WebCore/StyleProperties.h> #import <WebCore/TextIndicator.h> #import <WebCore/TextIterator.h> #import <WebCore/UserAgent.h> #import <WebCore/VisibleUnits.h> #import <WebCore/WebEvent.h> #import <wtf/MathExtras.h> #import <wtf/MemoryPressureHandler.h> #import <wtf/SetForScope.h> #import <wtf/SoftLinking.h> #import <wtf/cocoa/Entitlements.h> #import <wtf/text/TextStream.h> #define RELEASE_LOG_IF_ALLOWED(channel, fmt, ...) RELEASE_LOG_IF(isAlwaysOnLoggingAllowed(), channel, "%p - WebPage::" fmt, this, ##__VA_ARGS__) #define RELEASE_LOG_ERROR_IF_ALLOWED(channel, fmt, ...) RELEASE_LOG_ERROR_IF(isAlwaysOnLoggingAllowed(), channel, "%p - WebPage::" fmt, this, ##__VA_ARGS__) namespace WebKit { using namespace WebCore; void WebPage::platformInitialize() { platformInitializeAccessibility(); } void WebPage::platformDetach() { [m_mockAccessibilityElement setWebPage:nullptr]; } void WebPage::platformInitializeAccessibility() { m_mockAccessibilityElement = adoptNS([[WKAccessibilityWebPageObject alloc] init]); [m_mockAccessibilityElement setWebPage:this]; accessibilityTransferRemoteToken(accessibilityRemoteTokenData()); } void WebPage::platformReinitialize() { accessibilityTransferRemoteToken(accessibilityRemoteTokenData()); } RetainPtr<NSData> WebPage::accessibilityRemoteTokenData() const { return newAccessibilityRemoteToken([NSUUID UUID]); } static void computeEditableRootHasContentAndPlainText(const VisibleSelection& selection, EditorState::PostLayoutData& data) { data.hasContent = false; data.hasPlainText = false; if (!selection.isContentEditable()) return; if (data.selectedTextLength || data.characterAfterSelection || data.characterBeforeSelection || data.twoCharacterBeforeSelection) { // If any of these variables have been previously set, the editable root must have plain text content, so we can bail from the remainder of the check. data.hasContent = true; data.hasPlainText = true; return; } auto* root = selection.rootEditableElement(); if (!root) return; auto startInEditableRoot = firstPositionInNode(root); data.hasContent = root->hasChildNodes() && !isEndOfEditableOrNonEditableContent(startInEditableRoot); data.hasPlainText = data.hasContent && hasAnyPlainText(Range::create(root->document(), VisiblePosition { startInEditableRoot }, VisiblePosition { lastPositionInNode(root) })); } bool WebPage::isTransparentOrFullyClipped(const Element& element) const { auto* renderer = element.renderer(); if (!renderer) return false; auto* enclosingLayer = renderer->enclosingLayer(); if (enclosingLayer && enclosingLayer->isTransparentRespectingParentFrames()) return true; return renderer->hasNonEmptyVisibleRectRespectingParentFrames(); } void WebPage::platformEditorState(Frame& frame, EditorState& result, IncludePostLayoutDataHint shouldIncludePostLayoutData) const { FrameView* view = frame.view(); if (!view) { result.isMissingPostLayoutData = true; return; } if (frame.editor().hasComposition()) { RefPtr<Range> compositionRange = frame.editor().compositionRange(); Vector<WebCore::SelectionRect> compositionRects; if (compositionRange) { compositionRange->collectSelectionRects(compositionRects); if (compositionRects.size()) result.firstMarkedRect = view->contentsToRootView(compositionRects[0].rect()); if (compositionRects.size() > 1) result.lastMarkedRect = view->contentsToRootView(compositionRects.last().rect()); else result.lastMarkedRect = result.firstMarkedRect; result.markedText = plainTextReplacingNoBreakSpace(compositionRange.get()); } } // We only set the remaining EditorState entries if layout is done as a performance optimization // to avoid the need to force a synchronous layout here to compute these entries. If we // have a composition or are using a hardware keyboard then we send the full editor state // immediately so that the UIProcess can update UI, including the position of the caret. bool needsLayout = view->needsLayout(); bool requiresPostLayoutData = frame.editor().hasComposition(); #if !PLATFORM(MACCATALYST) requiresPostLayoutData |= m_keyboardIsAttached; #endif if ((shouldIncludePostLayoutData == IncludePostLayoutDataHint::No || needsLayout) && !requiresPostLayoutData) { result.isMissingPostLayoutData = true; return; } auto& postLayoutData = result.postLayoutData(); const VisibleSelection& selection = frame.selection().selection(); postLayoutData.isStableStateUpdate = m_isInStableState; bool startNodeIsInsideFixedPosition = false; bool endNodeIsInsideFixedPosition = false; if (selection.isCaret()) { postLayoutData.caretRectAtStart = view->contentsToRootView(frame.selection().absoluteCaretBounds(&startNodeIsInsideFixedPosition)); endNodeIsInsideFixedPosition = startNodeIsInsideFixedPosition; postLayoutData.caretRectAtEnd = postLayoutData.caretRectAtStart; // FIXME: The following check should take into account writing direction. postLayoutData.isReplaceAllowed = result.isContentEditable && atBoundaryOfGranularity(selection.start(), WordGranularity, DirectionForward); postLayoutData.wordAtSelection = plainTextReplacingNoBreakSpace(wordRangeFromPosition(selection.start()).get()); if (selection.isContentEditable()) charactersAroundPosition(selection.start(), postLayoutData.characterAfterSelection, postLayoutData.characterBeforeSelection, postLayoutData.twoCharacterBeforeSelection); } else if (selection.isRange()) { postLayoutData.caretRectAtStart = view->contentsToRootView(VisiblePosition(selection.start()).absoluteCaretBounds(&startNodeIsInsideFixedPosition)); postLayoutData.caretRectAtEnd = view->contentsToRootView(VisiblePosition(selection.end()).absoluteCaretBounds(&endNodeIsInsideFixedPosition)); RefPtr<Range> selectedRange = selection.toNormalizedRange(); String selectedText; if (selectedRange) { selectedRange->collectSelectionRects(postLayoutData.selectionRects); convertSelectionRectsToRootView(view, postLayoutData.selectionRects); selectedText = plainTextReplacingNoBreakSpace(selectedRange.get(), TextIteratorDefaultBehavior, true); postLayoutData.selectedTextLength = selectedText.length(); const int maxSelectedTextLength = 200; postLayoutData.wordAtSelection = selectedText.left(maxSelectedTextLength); } // FIXME: We should disallow replace when the string contains only CJ characters. postLayoutData.isReplaceAllowed = result.isContentEditable && !result.isInPasswordField && !selectedText.isAllSpecialCharacters<isHTMLSpace>(); } postLayoutData.atStartOfSentence = frame.selection().selectionAtSentenceStart(); postLayoutData.insideFixedPosition = startNodeIsInsideFixedPosition || endNodeIsInsideFixedPosition; if (!selection.isNone()) { if (m_focusedElement && m_focusedElement->renderer()) { auto& renderer = *m_focusedElement->renderer(); postLayoutData.focusedElementRect = rootViewInteractionBoundsForElement(*m_focusedElement); postLayoutData.caretColor = renderer.style().caretColor(); } if (result.isContentEditable) { if (auto editableRootOrFormControl = makeRefPtr(selection.rootEditableElement())) { if (is<HTMLTextFormControlElement>(editableRootOrFormControl->shadowHost())) editableRootOrFormControl = editableRootOrFormControl->shadowHost(); postLayoutData.editableRootIsTransparentOrFullyClipped = isTransparentOrFullyClipped(*editableRootOrFormControl); } } computeEditableRootHasContentAndPlainText(selection, postLayoutData); postLayoutData.selectionStartIsAtParagraphBoundary = atBoundaryOfGranularity(selection.visibleStart(), TextGranularity::ParagraphGranularity, SelectionDirection::DirectionBackward); postLayoutData.selectionEndIsAtParagraphBoundary = atBoundaryOfGranularity(selection.visibleEnd(), TextGranularity::ParagraphGranularity, SelectionDirection::DirectionForward); } } void WebPage::platformWillPerformEditingCommand() { auto& frame = m_page->focusController().focusedOrMainFrame(); if (auto* document = frame.document()) { if (auto* holdingTank = document->domTimerHoldingTankIfExists()) holdingTank->removeAll(); } } FloatSize WebPage::screenSize() const { return m_screenSize; } FloatSize WebPage::availableScreenSize() const { return m_availableScreenSize; } FloatSize WebPage::overrideScreenSize() const { return m_overrideScreenSize; } void WebPage::didReceiveMobileDocType(bool isMobileDoctype) { resetViewportDefaultConfiguration(m_mainFrame.get(), isMobileDoctype); } void WebPage::savePageState(HistoryItem& historyItem) { historyItem.setScaleIsInitial(!m_userHasChangedPageScaleFactor); historyItem.setMinimumLayoutSizeInScrollViewCoordinates(m_viewportConfiguration.minimumLayoutSize()); historyItem.setContentSize(m_viewportConfiguration.contentsSize()); } static double scaleAfterViewportWidthChange(double currentScale, bool userHasChangedPageScaleFactor, const ViewportConfiguration& viewportConfiguration, float unobscuredWidthInScrollViewCoordinates, const IntSize& newContentSize, const IntSize& oldContentSize, float visibleHorizontalFraction) { double scale; if (!userHasChangedPageScaleFactor) scale = viewportConfiguration.initialScale(); else scale = std::max(std::min(currentScale, viewportConfiguration.maximumScale()), viewportConfiguration.minimumScale()); LOG(VisibleRects, "scaleAfterViewportWidthChange getting scale %.2f", scale); if (userHasChangedPageScaleFactor) { // When the content size changes, we keep the same relative horizontal content width in view, otherwise we would // end up zoomed too far in landscape->portrait, and too close in portrait->landscape. double widthToKeepInView = visibleHorizontalFraction * newContentSize.width(); double newScale = unobscuredWidthInScrollViewCoordinates / widthToKeepInView; scale = std::max(std::min(newScale, viewportConfiguration.maximumScale()), viewportConfiguration.minimumScale()); } return scale; } static FloatPoint relativeCenterAfterContentSizeChange(const FloatRect& originalContentRect, IntSize oldContentSize, IntSize newContentSize) { // If the content size has changed, keep the same relative position. FloatPoint oldContentCenter = originalContentRect.center(); float relativeHorizontalPosition = oldContentCenter.x() / oldContentSize.width(); float relativeVerticalPosition = oldContentCenter.y() / oldContentSize.height(); return FloatPoint(relativeHorizontalPosition * newContentSize.width(), relativeVerticalPosition * newContentSize.height()); } static inline FloatRect adjustExposedRectForNewScale(const FloatRect& exposedRect, double exposedRectScale, double newScale) { if (exposedRectScale == newScale) return exposedRect; float horizontalChange = exposedRect.width() * exposedRectScale / newScale - exposedRect.width(); float verticalChange = exposedRect.height() * exposedRectScale / newScale - exposedRect.height(); auto adjustedRect = exposedRect; adjustedRect.inflate({ horizontalChange / 2, verticalChange / 2 }); return adjustedRect; } void WebPage::restorePageState(const HistoryItem& historyItem) { // When a HistoryItem is cleared, its scale factor and scroll point are set to zero. We should not try to restore the other // parameters in those conditions. if (!historyItem.pageScaleFactor()) { send(Messages::WebPageProxy::CouldNotRestorePageState()); return; } // We can restore the exposed rect and scale, but we cannot touch the scroll position since the obscured insets // may be changing in the UIProcess. The UIProcess can update the position from the information we send and will then // scroll to the correct position through a regular VisibleContentRectUpdate. m_userHasChangedPageScaleFactor = !historyItem.scaleIsInitial(); FrameView& frameView = *m_page->mainFrame().view(); FloatSize currentMinimumLayoutSizeInScrollViewCoordinates = m_viewportConfiguration.minimumLayoutSize(); if (historyItem.minimumLayoutSizeInScrollViewCoordinates() == currentMinimumLayoutSizeInScrollViewCoordinates) { float boundedScale = historyItem.scaleIsInitial() ? m_viewportConfiguration.initialScale() : historyItem.pageScaleFactor(); boundedScale = std::min<float>(m_viewportConfiguration.maximumScale(), std::max<float>(m_viewportConfiguration.minimumScale(), boundedScale)); scalePage(boundedScale, IntPoint()); Optional<FloatPoint> scrollPosition; if (historyItem.shouldRestoreScrollPosition()) { m_drawingArea->setExposedContentRect(historyItem.exposedContentRect()); m_hasRestoredExposedContentRectAfterDidCommitLoad = true; scrollPosition = FloatPoint(historyItem.scrollPosition()); } send(Messages::WebPageProxy::RestorePageState(scrollPosition, frameView.scrollOrigin(), historyItem.obscuredInsets(), boundedScale)); } else { IntSize oldContentSize = historyItem.contentSize(); IntSize newContentSize = frameView.contentsSize(); double visibleHorizontalFraction = static_cast<float>(historyItem.unobscuredContentRect().width()) / oldContentSize.width(); double newScale = scaleAfterViewportWidthChange(historyItem.pageScaleFactor(), !historyItem.scaleIsInitial(), m_viewportConfiguration, currentMinimumLayoutSizeInScrollViewCoordinates.width(), newContentSize, oldContentSize, visibleHorizontalFraction); Optional<FloatPoint> newCenter; if (historyItem.shouldRestoreScrollPosition()) { if (!oldContentSize.isEmpty() && !newContentSize.isEmpty() && newContentSize != oldContentSize) newCenter = relativeCenterAfterContentSizeChange(historyItem.unobscuredContentRect(), oldContentSize, newContentSize); else newCenter = FloatRect(historyItem.unobscuredContentRect()).center(); } scalePage(newScale, IntPoint()); send(Messages::WebPageProxy::RestorePageCenterAndScale(newCenter, newScale)); } } double WebPage::minimumPageScaleFactor() const { if (!m_viewportConfiguration.allowsUserScaling()) return m_page->pageScaleFactor(); return m_viewportConfiguration.minimumScale(); } double WebPage::maximumPageScaleFactor() const { if (!m_viewportConfiguration.allowsUserScaling()) return m_page->pageScaleFactor(); return m_viewportConfiguration.maximumScale(); } double WebPage::maximumPageScaleFactorIgnoringAlwaysScalable() const { if (!m_viewportConfiguration.allowsUserScalingIgnoringAlwaysScalable()) return m_page->pageScaleFactor(); return m_viewportConfiguration.maximumScaleIgnoringAlwaysScalable(); } bool WebPage::allowsUserScaling() const { return m_viewportConfiguration.allowsUserScaling(); } bool WebPage::handleEditingKeyboardEvent(KeyboardEvent& event) { auto* platformEvent = event.underlyingPlatformEvent(); if (!platformEvent) return false; // Don't send synthetic events to the UIProcess. They are only // used for interacting with JavaScript. if (platformEvent->isSyntheticEvent()) return false; // FIXME: Interpret the event immediately upon receiving it in UI process, without sending to WebProcess first. bool eventWasHandled = false; bool sendResult = WebProcess::singleton().parentProcessConnection()->sendSync(Messages::WebPageProxy::InterpretKeyEvent(editorState(), platformEvent->type() == PlatformKeyboardEvent::Char), Messages::WebPageProxy::InterpretKeyEvent::Reply(eventWasHandled), m_identifier); return sendResult && eventWasHandled; } bool WebPage::parentProcessHasServiceWorkerEntitlement() const { static bool hasEntitlement = WTF::hasEntitlement(WebProcess::singleton().parentProcessConnection()->xpcConnection(), "com.apple.developer.WebKit.ServiceWorkers"); return hasEntitlement; } void WebPage::sendComplexTextInputToPlugin(uint64_t, const String&) { notImplemented(); } bool WebPage::performNonEditingBehaviorForSelector(const String&, WebCore::KeyboardEvent*) { notImplemented(); return false; } bool WebPage::performDefaultBehaviorForKeyEvent(const WebKeyboardEvent&) { notImplemented(); return false; } void WebPage::getSelectionContext(CallbackID callbackID) { Frame& frame = m_page->focusController().focusedOrMainFrame(); if (!frame.selection().isRange()) { send(Messages::WebPageProxy::SelectionContextCallback(String(), String(), String(), callbackID)); return; } const int selectionExtendedContextLength = 350; String selectedText = plainTextReplacingNoBreakSpace(frame.selection().selection().toNormalizedRange().get()); String textBefore = plainTextReplacingNoBreakSpace(rangeExpandedByCharactersInDirectionAtWordBoundary(frame.selection().selection().start(), selectionExtendedContextLength, DirectionBackward).get(), TextIteratorDefaultBehavior, true); String textAfter = plainTextReplacingNoBreakSpace(rangeExpandedByCharactersInDirectionAtWordBoundary(frame.selection().selection().end(), selectionExtendedContextLength, DirectionForward).get(), TextIteratorDefaultBehavior, true); send(Messages::WebPageProxy::SelectionContextCallback(selectedText, textBefore, textAfter, callbackID)); } NSObject *WebPage::accessibilityObjectForMainFramePlugin() { if (!m_page) return nil; if (auto* pluginView = pluginViewForFrame(&m_page->mainFrame())) return pluginView->accessibilityObject(); return nil; } void WebPage::registerUIProcessAccessibilityTokens(const IPC::DataReference& elementToken, const IPC::DataReference&) { NSData *elementTokenData = [NSData dataWithBytes:elementToken.data() length:elementToken.size()]; [m_mockAccessibilityElement setRemoteTokenData:elementTokenData]; } void WebPage::readSelectionFromPasteboard(const String&, CompletionHandler<void(bool&&)>&& completionHandler) { notImplemented(); completionHandler(false); } void WebPage::getStringSelectionForPasteboard(CompletionHandler<void(String&&)>&& completionHandler) { notImplemented(); completionHandler({ }); } void WebPage::getDataSelectionForPasteboard(const String, CompletionHandler<void(SharedMemory::Handle&&, uint64_t)>&& completionHandler) { notImplemented(); completionHandler({ }, 0); } WKAccessibilityWebPageObject* WebPage::accessibilityRemoteObject() { notImplemented(); return 0; } bool WebPage::platformCanHandleRequest(const WebCore::ResourceRequest&) { notImplemented(); return false; } void WebPage::shouldDelayWindowOrderingEvent(const WebKit::WebMouseEvent&, CompletionHandler<void(bool)>&& completionHandler) { notImplemented(); completionHandler(false); } void WebPage::acceptsFirstMouse(int, const WebKit::WebMouseEvent&, CompletionHandler<void(bool)>&& completionHandler) { notImplemented(); completionHandler(false); } void WebPage::computePagesForPrintingPDFDocument(WebCore::FrameIdentifier, const PrintInfo&, Vector<IntRect>&) { notImplemented(); } void WebPage::drawPagesToPDFFromPDFDocument(CGContextRef, PDFDocument *, const PrintInfo&, uint32_t, uint32_t) { notImplemented(); } void WebPage::advanceToNextMisspelling(bool) { notImplemented(); } IntRect WebPage::rectForElementAtInteractionLocation() const { HitTestResult result = m_page->mainFrame().eventHandler().hitTestResultAtPoint(m_lastInteractionLocation, HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::AllowChildFrameContent); Node* hitNode = result.innerNode(); if (!hitNode || !hitNode->renderer()) return IntRect(); return result.innerNodeFrame()->view()->contentsToRootView(hitNode->renderer()->absoluteBoundingBoxRect(true)); } void WebPage::updateSelectionAppearance() { Frame& frame = m_page->focusController().focusedOrMainFrame(); if (!frame.editor().ignoreSelectionChanges() && (frame.editor().hasComposition() || !frame.selection().selection().isNone())) didChangeSelection(); } static void dispatchSyntheticMouseMove(Frame& mainFrame, const WebCore::FloatPoint& location, OptionSet<WebEvent::Modifier> modifiers, WebCore::PointerID pointerId = WebCore::mousePointerID) { IntPoint roundedAdjustedPoint = roundedIntPoint(location); auto shiftKey = modifiers.contains(WebEvent::Modifier::ShiftKey); auto ctrlKey = modifiers.contains(WebEvent::Modifier::ControlKey); auto altKey = modifiers.contains(WebEvent::Modifier::AltKey); auto metaKey = modifiers.contains(WebEvent::Modifier::MetaKey); auto mouseEvent = PlatformMouseEvent(roundedAdjustedPoint, roundedAdjustedPoint, NoButton, PlatformEvent::MouseMoved, 0, shiftKey, ctrlKey, altKey, metaKey, WallTime::now(), WebCore::ForceAtClick, WebCore::OneFingerTap, pointerId); // FIXME: Pass caps lock state. mainFrame.eventHandler().dispatchSyntheticMouseMove(mouseEvent); } void WebPage::generateSyntheticEditingCommand(SyntheticEditingCommandType command) { PlatformKeyboardEvent keyEvent; auto& frame = m_page->focusController().focusedOrMainFrame(); OptionSet<PlatformEvent::Modifier> modifiers; modifiers.add(PlatformEvent::Modifier::MetaKey); switch (command) { case SyntheticEditingCommandType::Undo: keyEvent = PlatformKeyboardEvent(PlatformEvent::KeyDown, "z", "z", "z", "KeyZ"_s, @"U+005A", 90, false, false, false, modifiers, WallTime::now()); break; case SyntheticEditingCommandType::Redo: keyEvent = PlatformKeyboardEvent(PlatformEvent::KeyDown, "y", "y", "y", "KeyY"_s, @"U+0059", 89, false, false, false, modifiers, WallTime::now()); break; case SyntheticEditingCommandType::ToggleBoldface: keyEvent = PlatformKeyboardEvent(PlatformEvent::KeyDown, "b", "b", "b", "KeyB"_s, @"U+0042", 66, false, false, false, modifiers, WallTime::now()); break; case SyntheticEditingCommandType::ToggleItalic: keyEvent = PlatformKeyboardEvent(PlatformEvent::KeyDown, "i", "i", "i", "KeyI"_s, @"U+0049", 73, false, false, false, modifiers, WallTime::now()); break; case SyntheticEditingCommandType::ToggleUnderline: keyEvent = PlatformKeyboardEvent(PlatformEvent::KeyDown, "u", "u", "u", "KeyU"_s, @"U+0055", 85, false, false, false, modifiers, WallTime::now()); break; default: break; } keyEvent.setIsSyntheticEvent(); PlatformKeyboardEvent::setCurrentModifierState(modifiers); frame.eventHandler().keyEvent(keyEvent); } void WebPage::handleSyntheticClick(Node& nodeRespondingToClick, const WebCore::FloatPoint& location, OptionSet<WebEvent::Modifier> modifiers, WebCore::PointerID pointerId) { if (!nodeRespondingToClick.document().settings().contentChangeObserverEnabled()) { completeSyntheticClick(nodeRespondingToClick, location, modifiers, WebCore::OneFingerTap, pointerId); return; } auto& respondingDocument = nodeRespondingToClick.document(); auto& contentChangeObserver = respondingDocument.contentChangeObserver(); auto targetNodeWentFromHiddenToVisible = contentChangeObserver.hiddenTouchTarget() == &nodeRespondingToClick && ContentChangeObserver::isConsideredVisible(nodeRespondingToClick); { LOG_WITH_STREAM(ContentObservation, stream << "handleSyntheticClick: node(" << &nodeRespondingToClick << ") " << location); ContentChangeObserver::MouseMovedScope observingScope(respondingDocument); auto& mainFrame = m_page->mainFrame(); dispatchSyntheticMouseMove(mainFrame, location, modifiers, pointerId); mainFrame.document()->updateStyleIfNeeded(); if (m_isClosed) return; } if (targetNodeWentFromHiddenToVisible) { LOG(ContentObservation, "handleSyntheticClick: target node was hidden and now is visible -> hover."); return; } auto nodeTriggersFastPath = [&](auto& targetNode) { if (!is<Element>(targetNode)) return false; if (is<HTMLFormControlElement>(targetNode)) return true; if (targetNode.document().quirks().shouldIgnoreAriaForFastPathContentObservationCheck()) return false; auto ariaRole = AccessibilityObject::ariaRoleToWebCoreRole(downcast<Element>(targetNode).getAttribute(HTMLNames::roleAttr)); return AccessibilityObject::isARIAControl(ariaRole) || AccessibilityObject::isARIAInput(ariaRole); }; auto targetNodeTriggersFastPath = nodeTriggersFastPath(nodeRespondingToClick); auto observedContentChange = contentChangeObserver.observedContentChange(); auto continueContentObservation = !(observedContentChange == WKContentVisibilityChange || targetNodeTriggersFastPath); if (continueContentObservation) { // Wait for callback to completePendingSyntheticClickForContentChangeObserver() to decide whether to send the click event. const Seconds observationDuration = 32_ms; contentChangeObserver.startContentObservationForDuration(observationDuration); LOG(ContentObservation, "handleSyntheticClick: Can't decide it yet -> wait."); m_pendingSyntheticClickNode = &nodeRespondingToClick; m_pendingSyntheticClickLocation = location; m_pendingSyntheticClickModifiers = modifiers; m_pendingSyntheticClickPointerId = pointerId; return; } contentChangeObserver.stopContentObservation(); callOnMainThread([protectedThis = makeRefPtr(this), targetNode = Ref<Node>(nodeRespondingToClick), location, modifiers, observedContentChange, pointerId] { if (protectedThis->m_isClosed || !protectedThis->corePage()) return; auto shouldStayAtHoverState = observedContentChange == WKContentVisibilityChange; if (shouldStayAtHoverState) { // The move event caused new contents to appear. Don't send synthetic click event, but just ensure that the mouse is on the most recent content. dispatchSyntheticMouseMove(protectedThis->corePage()->mainFrame(), location, modifiers, pointerId); LOG(ContentObservation, "handleSyntheticClick: Observed meaningful visible change -> hover."); return; } LOG(ContentObservation, "handleSyntheticClick: calling completeSyntheticClick -> click."); protectedThis->completeSyntheticClick(targetNode, location, modifiers, WebCore::OneFingerTap, pointerId); }); } void WebPage::didFinishContentChangeObserving(WKContentChange observedContentChange) { LOG_WITH_STREAM(ContentObservation, stream << "didFinishContentChangeObserving: pending target node(" << m_pendingSyntheticClickNode << ")"); if (!m_pendingSyntheticClickNode) return; callOnMainThread([protectedThis = makeRefPtr(this), targetNode = Ref<Node>(*m_pendingSyntheticClickNode), originalDocument = makeWeakPtr(m_pendingSyntheticClickNode->document()), observedContentChange, location = m_pendingSyntheticClickLocation, modifiers = m_pendingSyntheticClickModifiers, pointerId = m_pendingSyntheticClickPointerId] { if (protectedThis->m_isClosed || !protectedThis->corePage()) return; if (!originalDocument || &targetNode->document() != originalDocument) return; // Only dispatch the click if the document didn't get changed by any timers started by the move event. if (observedContentChange == WKContentNoChange) { LOG(ContentObservation, "No chage was observed -> click."); protectedThis->completeSyntheticClick(targetNode, location, modifiers, WebCore::OneFingerTap, pointerId); return; } // Ensure that the mouse is on the most recent content. LOG(ContentObservation, "Observed meaningful visible change -> hover."); dispatchSyntheticMouseMove(protectedThis->corePage()->mainFrame(), location, modifiers, pointerId); }); m_pendingSyntheticClickNode = nullptr; m_pendingSyntheticClickLocation = { }; m_pendingSyntheticClickModifiers = { }; m_pendingSyntheticClickPointerId = 0; } void WebPage::completeSyntheticClick(Node& nodeRespondingToClick, const WebCore::FloatPoint& location, OptionSet<WebEvent::Modifier> modifiers, SyntheticClickType syntheticClickType, WebCore::PointerID pointerId) { IntPoint roundedAdjustedPoint = roundedIntPoint(location); Frame& mainframe = m_page->mainFrame(); RefPtr<Frame> oldFocusedFrame = m_page->focusController().focusedFrame(); RefPtr<Element> oldFocusedElement = oldFocusedFrame ? oldFocusedFrame->document()->focusedElement() : nullptr; SetForScope<bool> userIsInteractingChange { m_userIsInteracting, true }; bool tapWasHandled = false; m_lastInteractionLocation = roundedAdjustedPoint; // FIXME: Pass caps lock state. bool shiftKey = modifiers.contains(WebEvent::Modifier::ShiftKey); bool ctrlKey = modifiers.contains(WebEvent::Modifier::ControlKey); bool altKey = modifiers.contains(WebEvent::Modifier::AltKey); bool metaKey = modifiers.contains(WebEvent::Modifier::MetaKey); tapWasHandled |= mainframe.eventHandler().handleMousePressEvent(PlatformMouseEvent(roundedAdjustedPoint, roundedAdjustedPoint, LeftButton, PlatformEvent::MousePressed, 1, shiftKey, ctrlKey, altKey, metaKey, WallTime::now(), WebCore::ForceAtClick, syntheticClickType, pointerId)); if (m_isClosed) return; tapWasHandled |= mainframe.eventHandler().handleMouseReleaseEvent(PlatformMouseEvent(roundedAdjustedPoint, roundedAdjustedPoint, LeftButton, PlatformEvent::MouseReleased, 1, shiftKey, ctrlKey, altKey, metaKey, WallTime::now(), WebCore::ForceAtClick, syntheticClickType, pointerId)); if (m_isClosed) return; RefPtr<Frame> newFocusedFrame = m_page->focusController().focusedFrame(); RefPtr<Element> newFocusedElement = newFocusedFrame ? newFocusedFrame->document()->focusedElement() : nullptr; // If the focus has not changed, we need to notify the client anyway, since it might be // necessary to start assisting the node. // If the node has been focused by JavaScript without user interaction, the // keyboard is not on screen. if (newFocusedElement && newFocusedElement == oldFocusedElement) elementDidRefocus(*newFocusedElement); if (nodeRespondingToClick.document().settings().contentChangeObserverEnabled()) { auto& document = nodeRespondingToClick.document(); // Dispatch mouseOut to dismiss tooltip content when tapping on the control bar buttons (cc, settings). if (document.quirks().needsYouTubeMouseOutQuirk()) { if (auto* frame = document.frame()) frame->eventHandler().dispatchSyntheticMouseOut(PlatformMouseEvent(roundedAdjustedPoint, roundedAdjustedPoint, LeftButton, PlatformEvent::NoType, 0, shiftKey, ctrlKey, altKey, metaKey, WallTime::now(), 0, WebCore::NoTap, pointerId)); } } if (m_isClosed) return; if (!tapWasHandled || !nodeRespondingToClick.isElementNode()) send(Messages::WebPageProxy::DidNotHandleTapAsClick(roundedIntPoint(location))); send(Messages::WebPageProxy::DidCompleteSyntheticClick()); } void WebPage::handleTap(const IntPoint& point, OptionSet<WebEvent::Modifier> modifiers, TransactionID lastLayerTreeTransactionId) { FloatPoint adjustedPoint; Node* nodeRespondingToClick = m_page->mainFrame().nodeRespondingToClickEvents(point, adjustedPoint); Frame* frameRespondingToClick = nodeRespondingToClick ? nodeRespondingToClick->document().frame() : nullptr; IntPoint adjustedIntPoint = roundedIntPoint(adjustedPoint); if (!frameRespondingToClick || lastLayerTreeTransactionId < WebFrame::fromCoreFrame(*frameRespondingToClick)->firstLayerTreeTransactionIDAfterDidCommitLoad()) send(Messages::WebPageProxy::DidNotHandleTapAsClick(adjustedIntPoint)); #if ENABLE(DATA_DETECTION) else if (is<Element>(*nodeRespondingToClick) && DataDetection::shouldCancelDefaultAction(downcast<Element>(*nodeRespondingToClick))) { InteractionInformationRequest request(adjustedIntPoint); requestPositionInformation(request); send(Messages::WebPageProxy::DidNotHandleTapAsClick(adjustedIntPoint)); } #endif else handleSyntheticClick(*nodeRespondingToClick, adjustedPoint, modifiers); } void WebPage::handleDoubleTapForDoubleClickAtPoint(const IntPoint& point, OptionSet<WebEvent::Modifier> modifiers, TransactionID lastLayerTreeTransactionId) { FloatPoint adjustedPoint; auto* nodeRespondingToDoubleClick = m_page->mainFrame().nodeRespondingToDoubleClickEvent(point, adjustedPoint); if (!nodeRespondingToDoubleClick) return; auto* frameRespondingToDoubleClick = nodeRespondingToDoubleClick->document().frame(); if (!frameRespondingToDoubleClick || lastLayerTreeTransactionId < WebFrame::fromCoreFrame(*frameRespondingToDoubleClick)->firstLayerTreeTransactionIDAfterDidCommitLoad()) return; bool shiftKey = modifiers.contains(WebEvent::Modifier::ShiftKey); bool ctrlKey = modifiers.contains(WebEvent::Modifier::ControlKey); bool altKey = modifiers.contains(WebEvent::Modifier::AltKey); bool metaKey = modifiers.contains(WebEvent::Modifier::MetaKey); auto roundedAdjustedPoint = roundedIntPoint(adjustedPoint); nodeRespondingToDoubleClick->document().frame()->eventHandler().handleMousePressEvent(PlatformMouseEvent(roundedAdjustedPoint, roundedAdjustedPoint, LeftButton, PlatformEvent::MousePressed, 2, shiftKey, ctrlKey, altKey, metaKey, WallTime::now(), 0, WebCore::OneFingerTap)); if (m_isClosed) return; nodeRespondingToDoubleClick->document().frame()->eventHandler().handleMouseReleaseEvent(PlatformMouseEvent(roundedAdjustedPoint, roundedAdjustedPoint, LeftButton, PlatformEvent::MouseReleased, 2, shiftKey, ctrlKey, altKey, metaKey, WallTime::now(), 0, WebCore::OneFingerTap)); } void WebPage::requestFocusedElementInformation(WebKit::CallbackID callbackID) { FocusedElementInformation info; if (m_focusedElement) getFocusedElementInformation(info); send(Messages::WebPageProxy::FocusedElementInformationCallback(info, callbackID)); } #if ENABLE(DATA_INTERACTION) void WebPage::requestDragStart(const IntPoint& clientPosition, const IntPoint& globalPosition, uint64_t allowedActions) { SetForScope<WebCore::DragSourceAction> allowedActionsForScope(m_allowedDragSourceActions, static_cast<WebCore::DragSourceAction>(allowedActions)); bool didStart = m_page->mainFrame().eventHandler().tryToBeginDragAtPoint(clientPosition, globalPosition); send(Messages::WebPageProxy::DidHandleDragStartRequest(didStart)); } void WebPage::requestAdditionalItemsForDragSession(const IntPoint& clientPosition, const IntPoint& globalPosition, uint64_t allowedActions) { SetForScope<WebCore::DragSourceAction> allowedActionsForScope(m_allowedDragSourceActions, static_cast<WebCore::DragSourceAction>(allowedActions)); // To augment the platform drag session with additional items, end the current drag session and begin a new drag session with the new drag item. // This process is opaque to the UI process, which still maintains the old drag item in its drag session. Similarly, this persistent drag session // is opaque to the web process, which only sees that the current drag has ended, and that a new one is beginning. PlatformMouseEvent event(clientPosition, globalPosition, LeftButton, PlatformEvent::MouseMoved, 0, false, false, false, false, WallTime::now(), 0, NoTap); m_page->dragController().dragEnded(); m_page->mainFrame().eventHandler().dragSourceEndedAt(event, DragOperationNone, MayExtendDragSession::Yes); bool didHandleDrag = m_page->mainFrame().eventHandler().tryToBeginDragAtPoint(clientPosition, globalPosition); send(Messages::WebPageProxy::DidHandleAdditionalDragItemsRequest(didHandleDrag)); } void WebPage::insertDroppedImagePlaceholders(const Vector<IntSize>& imageSizes, CompletionHandler<void(const Vector<IntRect>&, Optional<WebCore::TextIndicatorData>)>&& reply) { m_page->dragController().insertDroppedImagePlaceholdersAtCaret(imageSizes); auto placeholderRects = m_page->dragController().droppedImagePlaceholders().map([&] (auto& element) { return rootViewBoundsForElement(element); }); auto imagePlaceholderRange = m_page->dragController().droppedImagePlaceholderRange(); if (placeholderRects.size() != imageSizes.size()) { RELEASE_LOG(DragAndDrop, "Failed to insert dropped image placeholders: placeholder rect count (%tu) does not match image size count (%tu).", placeholderRects.size(), imageSizes.size()); reply({ }, WTF::nullopt); return; } if (!imagePlaceholderRange) { RELEASE_LOG(DragAndDrop, "Failed to insert dropped image placeholders: no image placeholder range."); reply({ }, WTF::nullopt); return; } Optional<TextIndicatorData> textIndicatorData; OptionSet<TextIndicatorOption> textIndicatorOptions = { TextIndicatorOptionIncludeSnapshotOfAllVisibleContentWithoutSelection, TextIndicatorOptionExpandClipBeyondVisibleRect, TextIndicatorOptionPaintAllContent, TextIndicatorOptionUseSelectionRectForSizing }; if (auto textIndicator = TextIndicator::createWithRange(*imagePlaceholderRange, textIndicatorOptions.toRaw(), TextIndicatorPresentationTransition::None, { })) textIndicatorData = textIndicator->data(); reply(WTFMove(placeholderRects), WTFMove(textIndicatorData)); } void WebPage::didConcludeDrop() { m_rangeForDropSnapshot = nullptr; m_pendingImageElementsForDropSnapshot.clear(); } void WebPage::didConcludeEditDrag() { send(Messages::WebPageProxy::WillReceiveEditDragSnapshot()); layoutIfNeeded(); m_pendingImageElementsForDropSnapshot.clear(); auto frame = makeRef(m_page->focusController().focusedOrMainFrame()); if (auto selectionRange = frame->selection().selection().toNormalizedRange()) { m_pendingImageElementsForDropSnapshot = visibleImageElementsInRangeWithNonLoadedImages(*selectionRange); auto collapsedRange = Range::create(selectionRange->ownerDocument(), selectionRange->endPosition(), selectionRange->endPosition()); frame->selection().setSelectedRange(collapsedRange.ptr(), DOWNSTREAM, FrameSelection::ShouldCloseTyping::Yes, UserTriggered); m_rangeForDropSnapshot = WTFMove(selectionRange); } if (m_pendingImageElementsForDropSnapshot.isEmpty()) computeAndSendEditDragSnapshot(); } void WebPage::didFinishLoadingImageForElement(WebCore::HTMLImageElement& element) { if (element.isDroppedImagePlaceholder()) m_page->dragController().finalizeDroppedImagePlaceholder(element); if (m_pendingImageElementsForDropSnapshot.isEmpty()) return; m_pendingImageElementsForDropSnapshot.remove(&element); if (m_pendingImageElementsForDropSnapshot.isEmpty()) computeAndSendEditDragSnapshot(); } void WebPage::computeAndSendEditDragSnapshot() { Optional<TextIndicatorData> textIndicatorData; static auto defaultTextIndicatorOptionsForEditDrag = TextIndicatorOptionIncludeSnapshotOfAllVisibleContentWithoutSelection | TextIndicatorOptionExpandClipBeyondVisibleRect | TextIndicatorOptionPaintAllContent | TextIndicatorOptionIncludeMarginIfRangeMatchesSelection | TextIndicatorOptionPaintBackgrounds | TextIndicatorOptionComputeEstimatedBackgroundColor | TextIndicatorOptionUseSelectionRectForSizing | TextIndicatorOptionIncludeSnapshotWithSelectionHighlight; if (auto range = std::exchange(m_rangeForDropSnapshot, nullptr)) { if (auto textIndicator = TextIndicator::createWithRange(*range, defaultTextIndicatorOptionsForEditDrag, TextIndicatorPresentationTransition::None, { })) textIndicatorData = textIndicator->data(); } send(Messages::WebPageProxy::DidReceiveEditDragSnapshot(WTFMove(textIndicatorData))); } #endif void WebPage::sendTapHighlightForNodeIfNecessary(uint64_t requestID, Node* node) { #if ENABLE(TOUCH_EVENTS) if (!node) return; if (m_page->isEditable() && node == m_page->mainFrame().document()->body()) return; if (is<Element>(*node)) { ASSERT(m_page); m_page->mainFrame().loader().client().prefetchDNS(downcast<Element>(*node).absoluteLinkURL().host().toString()); } if (is<HTMLAreaElement>(node)) { node = downcast<HTMLAreaElement>(node)->imageElement(); if (!node) return; } Vector<FloatQuad> quads; if (RenderObject *renderer = node->renderer()) { renderer->absoluteQuads(quads); Color highlightColor = renderer->style().tapHighlightColor(); if (!node->document().frame()->isMainFrame()) { FrameView* view = node->document().frame()->view(); for (size_t i = 0; i < quads.size(); ++i) { FloatQuad& currentQuad = quads[i]; currentQuad.setP1(view->contentsToRootView(roundedIntPoint(currentQuad.p1()))); currentQuad.setP2(view->contentsToRootView(roundedIntPoint(currentQuad.p2()))); currentQuad.setP3(view->contentsToRootView(roundedIntPoint(currentQuad.p3()))); currentQuad.setP4(view->contentsToRootView(roundedIntPoint(currentQuad.p4()))); } } RoundedRect::Radii borderRadii; if (is<RenderBox>(*renderer)) borderRadii = downcast<RenderBox>(*renderer).borderRadii(); bool nodeHasBuiltInClickHandling = is<HTMLFormControlElement>(*node) || is<HTMLAnchorElement>(*node) || is<HTMLLabelElement>(*node) || is<HTMLSummaryElement>(*node) || node->isLink(); send(Messages::WebPageProxy::DidGetTapHighlightGeometries(requestID, highlightColor, quads, roundedIntSize(borderRadii.topLeft()), roundedIntSize(borderRadii.topRight()), roundedIntSize(borderRadii.bottomLeft()), roundedIntSize(borderRadii.bottomRight()), nodeHasBuiltInClickHandling)); } #else UNUSED_PARAM(requestID); UNUSED_PARAM(node); #endif } void WebPage::handleTwoFingerTapAtPoint(const WebCore::IntPoint& point, OptionSet<WebKit::WebEvent::Modifier> modifiers, uint64_t requestID) { FloatPoint adjustedPoint; Node* nodeRespondingToClick = m_page->mainFrame().nodeRespondingToClickEvents(point, adjustedPoint); if (!nodeRespondingToClick || !nodeRespondingToClick->renderer()) { send(Messages::WebPageProxy::DidNotHandleTapAsClick(roundedIntPoint(adjustedPoint))); return; } sendTapHighlightForNodeIfNecessary(requestID, nodeRespondingToClick); #if ENABLE(DATA_DETECTION) if (is<Element>(*nodeRespondingToClick) && DataDetection::shouldCancelDefaultAction(downcast<Element>(*nodeRespondingToClick))) { InteractionInformationRequest request(roundedIntPoint(adjustedPoint)); requestPositionInformation(request); send(Messages::WebPageProxy::DidNotHandleTapAsClick(roundedIntPoint(adjustedPoint))); } else #endif completeSyntheticClick(*nodeRespondingToClick, adjustedPoint, modifiers, WebCore::TwoFingerTap); } void WebPage::handleStylusSingleTapAtPoint(const WebCore::IntPoint& point, uint64_t requestID) { SetForScope<bool> userIsInteractingChange { m_userIsInteracting, true }; auto& frame = m_page->focusController().focusedOrMainFrame(); auto pointInDocument = frame.view()->rootViewToContents(point); HitTestResult hitTest = frame.eventHandler().hitTestResultAtPoint(pointInDocument, HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::AllowChildFrameContent); Node* node = hitTest.innerNonSharedNode(); if (!node) return; auto renderer = node->renderer(); if (!renderer) return; if (renderer->isReplaced()) return; VisiblePosition position = renderer->positionForPoint(hitTest.localPoint(), nullptr); if (position.isNull()) position = firstPositionInOrBeforeNode(node); if (position.isNull()) return; auto range = Range::create(*frame.document(), position, position); frame.selection().setSelectedRange(range.ptr(), position.affinity(), WebCore::FrameSelection::ShouldCloseTyping::Yes, UserTriggered); auto image = frame.editor().insertEditableImage(); frame.document()->setFocusedElement(image.get()); } void WebPage::potentialTapAtPosition(uint64_t requestID, const WebCore::FloatPoint& position, bool shouldRequestMagnificationInformation) { m_potentialTapNode = m_page->mainFrame().nodeRespondingToClickEvents(position, m_potentialTapLocation, m_potentialTapSecurityOrigin.get()); if (shouldRequestMagnificationInformation && m_potentialTapNode && m_viewGestureGeometryCollector) { // FIXME: Could this be combined into tap highlight? FloatPoint origin = position; FloatRect renderRect; bool fitEntireRect; double viewportMinimumScale; double viewportMaximumScale; m_viewGestureGeometryCollector->computeZoomInformationForNode(*m_potentialTapNode, origin, renderRect, fitEntireRect, viewportMinimumScale, viewportMaximumScale); bool nodeIsRootLevel = is<WebCore::Document>(*m_potentialTapNode) || is<WebCore::HTMLBodyElement>(*m_potentialTapNode); send(Messages::WebPageProxy::HandleSmartMagnificationInformationForPotentialTap(requestID, renderRect, fitEntireRect, viewportMinimumScale, viewportMaximumScale, nodeIsRootLevel)); } sendTapHighlightForNodeIfNecessary(requestID, m_potentialTapNode.get()); #if ENABLE(TOUCH_EVENTS) if (m_potentialTapNode && !m_potentialTapNode->allowsDoubleTapGesture()) send(Messages::WebPageProxy::DisableDoubleTapGesturesDuringTapIfNecessary(requestID)); #endif } void WebPage::commitPotentialTap(OptionSet<WebEvent::Modifier> modifiers, TransactionID lastLayerTreeTransactionId, WebCore::PointerID pointerId) { auto invalidTargetForSingleClick = !m_potentialTapNode; if (!invalidTargetForSingleClick) { bool targetRenders = m_potentialTapNode->renderer(); if (!targetRenders && is<Element>(m_potentialTapNode.get())) targetRenders = downcast<Element>(*m_potentialTapNode).renderOrDisplayContentsStyle(); invalidTargetForSingleClick = !targetRenders && !is<HTMLAreaElement>(m_potentialTapNode.get()); } if (invalidTargetForSingleClick) { commitPotentialTapFailed(); return; } FloatPoint adjustedPoint; Node* nodeRespondingToClick = m_page->mainFrame().nodeRespondingToClickEvents(m_potentialTapLocation, adjustedPoint, m_potentialTapSecurityOrigin.get()); Frame* frameRespondingToClick = nodeRespondingToClick ? nodeRespondingToClick->document().frame() : nullptr; if (!frameRespondingToClick || lastLayerTreeTransactionId < WebFrame::fromCoreFrame(*frameRespondingToClick)->firstLayerTreeTransactionIDAfterDidCommitLoad()) { commitPotentialTapFailed(); return; } if (m_potentialTapNode == nodeRespondingToClick) { #if ENABLE(DATA_DETECTION) if (is<Element>(*nodeRespondingToClick) && DataDetection::shouldCancelDefaultAction(downcast<Element>(*nodeRespondingToClick))) { InteractionInformationRequest request(roundedIntPoint(m_potentialTapLocation)); requestPositionInformation(request); commitPotentialTapFailed(); } else #endif handleSyntheticClick(*nodeRespondingToClick, adjustedPoint, modifiers, pointerId); } else commitPotentialTapFailed(); m_potentialTapNode = nullptr; m_potentialTapLocation = FloatPoint(); m_potentialTapSecurityOrigin = nullptr; } void WebPage::commitPotentialTapFailed() { ContentChangeObserver::didCancelPotentialTap(m_page->mainFrame()); if (!m_page->focusController().focusedOrMainFrame().selection().selection().isContentEditable()) clearSelection(); send(Messages::WebPageProxy::CommitPotentialTapFailed()); send(Messages::WebPageProxy::DidNotHandleTapAsClick(roundedIntPoint(m_potentialTapLocation))); } void WebPage::cancelPotentialTap() { ContentChangeObserver::didCancelPotentialTap(m_page->mainFrame()); cancelPotentialTapInFrame(*m_mainFrame); } void WebPage::cancelPotentialTapInFrame(WebFrame& frame) { if (m_potentialTapNode) { auto* potentialTapFrame = m_potentialTapNode->document().frame(); if (potentialTapFrame && !potentialTapFrame->tree().isDescendantOf(frame.coreFrame())) return; } m_potentialTapNode = nullptr; m_potentialTapLocation = FloatPoint(); m_potentialTapSecurityOrigin = nullptr; } void WebPage::didRecognizeLongPress() { ContentChangeObserver::didRecognizeLongPress(m_page->mainFrame()); } void WebPage::tapHighlightAtPosition(uint64_t requestID, const FloatPoint& position) { Frame& mainframe = m_page->mainFrame(); FloatPoint adjustedPoint; sendTapHighlightForNodeIfNecessary(requestID, mainframe.nodeRespondingToClickEvents(position, adjustedPoint)); } void WebPage::inspectorNodeSearchMovedToPosition(const FloatPoint& position) { IntPoint adjustedPoint = roundedIntPoint(position); Frame& mainframe = m_page->mainFrame(); mainframe.eventHandler().mouseMoved(PlatformMouseEvent(adjustedPoint, adjustedPoint, NoButton, PlatformEvent::MouseMoved, 0, false, false, false, false, { }, 0, WebCore::NoTap)); mainframe.document()->updateStyleIfNeeded(); } void WebPage::inspectorNodeSearchEndedAtPosition(const FloatPoint& position) { if (Node* node = m_page->mainFrame().deepestNodeAtLocation(position)) node->inspect(); } void WebPage::updateInputContextAfterBlurringAndRefocusingElementIfNeeded(Element& element) { if (m_recentlyBlurredElement != &element || !m_isShowingInputViewForFocusedElement) return; m_hasPendingInputContextUpdateAfterBlurringAndRefocusingElement = true; callOnMainThread([this, protectedThis = makeRefPtr(this)] { if (m_hasPendingInputContextUpdateAfterBlurringAndRefocusingElement) send(Messages::WebPageProxy::UpdateInputContextAfterBlurringAndRefocusingElement()); m_hasPendingInputContextUpdateAfterBlurringAndRefocusingElement = false; }); } void WebPage::blurFocusedElement() { if (!m_focusedElement) return; m_focusedElement->blur(); } void WebPage::setIsShowingInputViewForFocusedElement(bool showingInputView) { m_isShowingInputViewForFocusedElement = showingInputView; } void WebPage::setFocusedElementValue(const String& value) { // FIXME: should also handle the case of HTMLSelectElement. if (is<HTMLInputElement>(m_focusedElement.get())) downcast<HTMLInputElement>(*m_focusedElement).setValue(value, DispatchInputAndChangeEvent); } void WebPage::setFocusedElementValueAsNumber(double value) { if (is<HTMLInputElement>(m_focusedElement.get())) downcast<HTMLInputElement>(*m_focusedElement).setValueAsNumber(value, DispatchInputAndChangeEvent); } void WebPage::setFocusedElementSelectedIndex(uint32_t index, bool allowMultipleSelection) { if (is<HTMLSelectElement>(m_focusedElement.get())) downcast<HTMLSelectElement>(*m_focusedElement).optionSelectedByUser(index, true, allowMultipleSelection); } void WebPage::showInspectorHighlight(const WebCore::Highlight& highlight) { send(Messages::WebPageProxy::ShowInspectorHighlight(highlight)); } void WebPage::hideInspectorHighlight() { send(Messages::WebPageProxy::HideInspectorHighlight()); } void WebPage::showInspectorIndication() { send(Messages::WebPageProxy::ShowInspectorIndication()); } void WebPage::hideInspectorIndication() { send(Messages::WebPageProxy::HideInspectorIndication()); } void WebPage::enableInspectorNodeSearch() { send(Messages::WebPageProxy::EnableInspectorNodeSearch()); } void WebPage::disableInspectorNodeSearch() { send(Messages::WebPageProxy::DisableInspectorNodeSearch()); } void WebPage::setForceAlwaysUserScalable(bool userScalable) { m_forceAlwaysUserScalable = userScalable; m_viewportConfiguration.setForceAlwaysUserScalable(userScalable); } static IntRect elementBoundsInFrame(const Frame& frame, const Element& focusedElement) { frame.document()->updateLayoutIgnorePendingStylesheets(); if (focusedElement.hasTagName(HTMLNames::textareaTag) || focusedElement.hasTagName(HTMLNames::inputTag) || focusedElement.hasTagName(HTMLNames::selectTag)) return WebPage::absoluteInteractionBoundsForElement(focusedElement); if (auto* rootEditableElement = focusedElement.rootEditableElement()) return WebPage::absoluteInteractionBoundsForElement(*rootEditableElement); return { }; } static IntPoint constrainPoint(const IntPoint& point, const Frame& frame, const Element& focusedElement) { ASSERT(&focusedElement.document() == frame.document()); const int DEFAULT_CONSTRAIN_INSET = 2; IntRect innerFrame = elementBoundsInFrame(frame, focusedElement); IntPoint constrainedPoint = point; int minX = innerFrame.x() + DEFAULT_CONSTRAIN_INSET; int maxX = innerFrame.maxX() - DEFAULT_CONSTRAIN_INSET; int minY = innerFrame.y() + DEFAULT_CONSTRAIN_INSET; int maxY = innerFrame.maxY() - DEFAULT_CONSTRAIN_INSET; if (point.x() < minX) constrainedPoint.setX(minX); else if (point.x() > maxX) constrainedPoint.setX(maxX); if (point.y() < minY) constrainedPoint.setY(minY); else if (point.y() >= maxY) constrainedPoint.setY(maxY); return constrainedPoint; } void WebPage::selectWithGesture(const IntPoint& point, uint32_t granularity, uint32_t gestureType, uint32_t gestureState, bool isInteractingWithFocusedElement, CallbackID callbackID) { if (static_cast<GestureRecognizerState>(gestureState) == GestureRecognizerState::Began) setFocusedFrameBeforeSelectingTextAtLocation(point); auto& frame = m_page->focusController().focusedOrMainFrame(); VisiblePosition position = visiblePositionInFocusedNodeForPoint(frame, point, isInteractingWithFocusedElement); if (position.isNull()) { send(Messages::WebPageProxy::GestureCallback(point, gestureType, gestureState, 0, callbackID)); return; } RefPtr<Range> range; SelectionFlags flags = None; GestureRecognizerState wkGestureState = static_cast<GestureRecognizerState>(gestureState); switch (static_cast<GestureType>(gestureType)) { case GestureType::PhraseBoundary: { if (!frame.editor().hasComposition()) break; RefPtr<Range> markedRange = frame.editor().compositionRange(); if (position < markedRange->startPosition()) position = markedRange->startPosition(); if (position > markedRange->endPosition()) position = markedRange->endPosition(); if (wkGestureState != GestureRecognizerState::Began) flags = distanceBetweenPositions(markedRange->startPosition(), frame.selection().selection().start()) != distanceBetweenPositions(markedRange->startPosition(), position) ? PhraseBoundaryChanged : None; else flags = PhraseBoundaryChanged; range = Range::create(*frame.document(), position, position); } break; case GestureType::OneFingerTap: { VisiblePosition result; // move the position at the end of the word if (atBoundaryOfGranularity(position, LineGranularity, DirectionForward)) { // Don't cross line boundaries. result = position; } else if (withinTextUnitOfGranularity(position, WordGranularity, DirectionForward)) { // The position lies within a word. RefPtr<Range> wordRange = enclosingTextUnitOfGranularity(position, WordGranularity, DirectionForward); if (wordRange) { result = wordRange->startPosition(); if (distanceBetweenPositions(position, result) > 1) result = wordRange->endPosition(); } flags = WordIsNearTap; } else if (atBoundaryOfGranularity(position, WordGranularity, DirectionBackward)) { // The position is at the end of a word. result = position; } else { // The position is not within a word. // Go to the next boundary. result = positionOfNextBoundaryOfGranularity(position, WordGranularity, DirectionForward); // If there is no such boundary we go to the end of the element. if (result.isNull()) result = endOfEditableContent(position); } if (result.isNotNull()) range = Range::create(*frame.document(), result, result); } break; case GestureType::Loupe: if (position.rootEditableElement()) range = Range::create(*frame.document(), position, position); else #if !PLATFORM(MACCATALYST) range = wordRangeFromPosition(position); #else switch (wkGestureState) { case GestureRecognizerState::Began: m_startingGestureRange = Range::create(*frame.document(), position, position); break; case GestureRecognizerState::Changed: if (m_startingGestureRange) { if (m_startingGestureRange->startPosition() < position) range = Range::create(*frame.document(), m_startingGestureRange->startPosition(), position); else range = Range::create(*frame.document(), position, m_startingGestureRange->startPosition()); } break; case GestureRecognizerState::Ended: case GestureRecognizerState::Cancelled: m_startingGestureRange = nullptr; break; case GestureRecognizerState::Failed: case GestureRecognizerState::Possible: ASSERT_NOT_REACHED(); break; } #endif break; case GestureType::TapAndAHalf: switch (wkGestureState) { case GestureRecognizerState::Began: range = wordRangeFromPosition(position); m_currentWordRange = range ? RefPtr<Range>(Range::create(*frame.document(), range->startPosition(), range->endPosition())) : nullptr; break; case GestureRecognizerState::Changed: if (!m_currentWordRange) break; range = Range::create(*frame.document(), m_currentWordRange->startPosition(), m_currentWordRange->endPosition()); if (position < range->startPosition()) range->setStart(position.deepEquivalent()); if (position > range->endPosition()) range->setEnd(position.deepEquivalent()); break; case GestureRecognizerState::Ended: case GestureRecognizerState::Cancelled: m_currentWordRange = nullptr; break; case GestureRecognizerState::Failed: case GestureRecognizerState::Possible: ASSERT_NOT_REACHED(); } break; case GestureType::OneFingerDoubleTap: if (atBoundaryOfGranularity(position, LineGranularity, DirectionForward)) { // Double-tap at end of line only places insertion point there. // This helps to get the callout for pasting at ends of lines, // paragraphs, and documents. range = Range::create(*frame.document(), position, position); } else range = wordRangeFromPosition(position); break; case GestureType::TwoFingerSingleTap: // Single tap with two fingers selects the entire paragraph. range = enclosingTextUnitOfGranularity(position, ParagraphGranularity, DirectionForward); break; case GestureType::OneFingerTripleTap: if (atBoundaryOfGranularity(position, LineGranularity, DirectionForward)) { // Triple-tap at end of line only places insertion point there. // This helps to get the callout for pasting at ends of lines, // paragraphs, and documents. range = Range::create(*frame.document(), position, position); } else range = enclosingTextUnitOfGranularity(position, ParagraphGranularity, DirectionForward); break; default: break; } if (range) frame.selection().setSelectedRange(range.get(), position.affinity(), WebCore::FrameSelection::ShouldCloseTyping::Yes, UserTriggered); send(Messages::WebPageProxy::GestureCallback(point, gestureType, gestureState, static_cast<uint32_t>(flags), callbackID)); } static RefPtr<Range> rangeForPointInRootViewCoordinates(Frame& frame, const IntPoint& pointInRootViewCoordinates, bool baseIsStart) { VisibleSelection existingSelection = frame.selection().selection(); VisiblePosition selectionStart = existingSelection.visibleStart(); VisiblePosition selectionEnd = existingSelection.visibleEnd(); auto pointInDocument = frame.view()->rootViewToContents(pointInRootViewCoordinates); if (baseIsStart) { int startY = selectionStart.absoluteCaretBounds().center().y(); if (pointInDocument.y() < startY) pointInDocument.setY(startY); } else { int endY = selectionEnd.absoluteCaretBounds().center().y(); if (pointInDocument.y() > endY) pointInDocument.setY(endY); } VisiblePosition result; RefPtr<Range> range; HitTestResult hitTest = frame.eventHandler().hitTestResultAtPoint(pointInDocument, HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::AllowChildFrameContent); if (hitTest.targetNode()) result = frame.eventHandler().selectionExtentRespectingEditingBoundary(frame.selection().selection(), hitTest.localPoint(), hitTest.targetNode()).deepEquivalent(); else result = frame.visiblePositionForPoint(pointInDocument).deepEquivalent(); if (baseIsStart) { if (comparePositions(result, selectionStart) <= 0) result = selectionStart.next(); else if (&selectionStart.deepEquivalent().anchorNode()->treeScope() != &hitTest.targetNode()->treeScope()) result = VisibleSelection::adjustPositionForEnd(result.deepEquivalent(), selectionStart.deepEquivalent().containerNode()); if (result.isNotNull()) range = Range::create(*frame.document(), selectionStart, result); } else { if (comparePositions(selectionEnd, result) <= 0) result = selectionEnd.previous(); else if (&hitTest.targetNode()->treeScope() != &selectionEnd.deepEquivalent().anchorNode()->treeScope()) result = VisibleSelection::adjustPositionForStart(result.deepEquivalent(), selectionEnd.deepEquivalent().containerNode()); if (result.isNotNull()) range = Range::create(*frame.document(), result.deepEquivalent(), selectionEnd); } return range; } static RefPtr<Range> rangeAtWordBoundaryForPosition(Frame* frame, const VisiblePosition& position, bool baseIsStart, SelectionDirection direction) { SelectionDirection sameDirection = baseIsStart ? DirectionForward : DirectionBackward; SelectionDirection oppositeDirection = baseIsStart ? DirectionBackward : DirectionForward; VisiblePosition base = baseIsStart ? frame->selection().selection().visibleStart() : frame->selection().selection().visibleEnd(); VisiblePosition extent = baseIsStart ? frame->selection().selection().visibleEnd() : frame->selection().selection().visibleStart(); VisiblePosition initialExtent = position; if (atBoundaryOfGranularity(extent, WordGranularity, sameDirection)) { // This is a word boundary. Leave selection where it is. return nullptr; } if (atBoundaryOfGranularity(extent, WordGranularity, oppositeDirection)) { // This is a word boundary in the wrong direction. Nudge the selection to a character before proceeding. extent = baseIsStart ? extent.previous() : extent.next(); } // Extend to the boundary of the word. VisiblePosition wordBoundary = positionOfNextBoundaryOfGranularity(extent, WordGranularity, sameDirection); if (wordBoundary.isNotNull() && atBoundaryOfGranularity(wordBoundary, WordGranularity, sameDirection) && initialExtent != wordBoundary) { extent = wordBoundary; return (base < extent) ? Range::create(*frame->document(), base, extent) : Range::create(*frame->document(), extent, base); } // Conversely, if the initial extent equals the current word boundary, then // run the rest of this function to see if the selection should extend // the other direction to the other word. // If this is where the extent was initially, then iterate in the other direction in the document until we hit the next word. while (extent.isNotNull() && !atBoundaryOfGranularity(extent, WordGranularity, sameDirection) && extent != base && !atBoundaryOfGranularity(extent, LineGranularity, sameDirection) && !atBoundaryOfGranularity(extent, LineGranularity, oppositeDirection)) { extent = baseIsStart ? extent.next() : extent.previous(); } // Don't let the smart extension make the extent equal the base. // Expand out to word boundary. if (extent.isNull() || extent == base) extent = wordBoundary; if (extent.isNull()) return nullptr; return (base < extent) ? Range::create(*frame->document(), base, extent) : Range::create(*frame->document(), extent, base); } IntRect WebPage::rootViewBoundsForElement(const Element& element) { auto* frame = element.document().frame(); if (!frame) return { }; auto* view = frame->view(); if (!view) return { }; auto* renderer = element.renderer(); if (!renderer) return { }; return view->contentsToRootView(renderer->absoluteBoundingBoxRect()); } IntRect WebPage::absoluteInteractionBoundsForElement(const Element& element) { auto* frame = element.document().frame(); if (!frame) return { }; auto* view = frame->view(); if (!view) return { }; auto* renderer = element.renderer(); if (!renderer) return { }; if (is<RenderBox>(*renderer)) { auto& box = downcast<RenderBox>(*renderer); FloatRect rect; // FIXME: want borders or not? if (box.style().isOverflowVisible()) rect = box.layoutOverflowRect(); else rect = box.clientBoxRect(); return box.localToAbsoluteQuad(rect).enclosingBoundingBox(); } auto& style = renderer->style(); FloatRect boundingBox = renderer->absoluteBoundingBoxRect(true /* use transforms*/); // This is wrong. It's subtracting borders after converting to absolute coords on something that probably doesn't represent a rectangular element. boundingBox.move(style.borderLeftWidth(), style.borderTopWidth()); boundingBox.setWidth(boundingBox.width() - style.borderLeftWidth() - style.borderRightWidth()); boundingBox.setHeight(boundingBox.height() - style.borderBottomWidth() - style.borderTopWidth()); return enclosingIntRect(boundingBox); } IntRect WebPage::rootViewInteractionBoundsForElement(const Element& element) { auto* frame = element.document().frame(); if (!frame) return { }; auto* view = frame->view(); if (!view) return { }; return view->contentsToRootView(absoluteInteractionBoundsForElement(element)); } void WebPage::clearSelection() { m_startingGestureRange = nullptr; m_page->focusController().focusedOrMainFrame().selection().clear(); } void WebPage::dispatchSyntheticMouseEventsForSelectionGesture(SelectionTouch touch, const IntPoint& point) { auto frame = makeRef(m_page->focusController().focusedOrMainFrame()); if (!frame->selection().selection().isContentEditable()) return; IntRect focusedElementRect; if (m_focusedElement) focusedElementRect = rootViewInteractionBoundsForElement(*m_focusedElement); if (focusedElementRect.isEmpty()) return; auto adjustedPoint = point.constrainedBetween(focusedElementRect.minXMinYCorner(), focusedElementRect.maxXMaxYCorner()); auto& eventHandler = m_page->mainFrame().eventHandler(); switch (touch) { case SelectionTouch::Started: eventHandler.handleMousePressEvent({ adjustedPoint, adjustedPoint, LeftButton, PlatformEvent::MousePressed, 1, false, false, false, false, WallTime::now(), WebCore::ForceAtClick, NoTap }); break; case SelectionTouch::Moved: eventHandler.dispatchSyntheticMouseMove({ adjustedPoint, adjustedPoint, LeftButton, PlatformEvent::MouseMoved, 0, false, false, false, false, WallTime::now(), WebCore::ForceAtClick, NoTap }); break; case SelectionTouch::Ended: case SelectionTouch::EndedMovingForward: case SelectionTouch::EndedMovingBackward: case SelectionTouch::EndedNotMoving: eventHandler.handleMouseReleaseEvent({ adjustedPoint, adjustedPoint, LeftButton, PlatformEvent::MouseReleased, 1, false, false, false, false, WallTime::now(), WebCore::ForceAtClick, NoTap }); break; } } void WebPage::updateSelectionWithTouches(const IntPoint& point, uint32_t touches, bool baseIsStart, CallbackID callbackID) { Frame& frame = m_page->focusController().focusedOrMainFrame(); IntPoint pointInDocument = frame.view()->rootViewToContents(point); VisiblePosition position = frame.visiblePositionForPoint(pointInDocument); if (position.isNull()) { send(Messages::WebPageProxy::TouchesCallback(point, touches, 0, callbackID)); return; } RefPtr<Range> range; VisiblePosition result; SelectionFlags flags = None; auto selectionTouch = static_cast<SelectionTouch>(touches); if (shouldDispatchSyntheticMouseEventsWhenModifyingSelection()) dispatchSyntheticMouseEventsForSelectionGesture(selectionTouch, point); switch (selectionTouch) { case SelectionTouch::Started: case SelectionTouch::EndedNotMoving: break; case SelectionTouch::Ended: if (frame.selection().selection().isContentEditable()) { result = closestWordBoundaryForPosition(position); if (result.isNotNull()) range = Range::create(*frame.document(), result, result); } else range = rangeForPointInRootViewCoordinates(frame, point, baseIsStart); break; case SelectionTouch::EndedMovingForward: range = rangeAtWordBoundaryForPosition(&frame, position, baseIsStart, DirectionForward); break; case SelectionTouch::EndedMovingBackward: range = rangeAtWordBoundaryForPosition(&frame, position, baseIsStart, DirectionBackward); break; case SelectionTouch::Moved: range = rangeForPointInRootViewCoordinates(frame, point, baseIsStart); break; } if (range) frame.selection().setSelectedRange(range.get(), position.affinity(), WebCore::FrameSelection::ShouldCloseTyping::Yes, UserTriggered); send(Messages::WebPageProxy::TouchesCallback(point, touches, flags, callbackID)); } void WebPage::selectWithTwoTouches(const WebCore::IntPoint& from, const WebCore::IntPoint& to, uint32_t gestureType, uint32_t gestureState, CallbackID callbackID) { Frame& frame = m_page->focusController().focusedOrMainFrame(); VisiblePosition fromPosition = frame.visiblePositionForPoint(frame.view()->rootViewToContents(from)); VisiblePosition toPosition = frame.visiblePositionForPoint(frame.view()->rootViewToContents(to)); RefPtr<Range> range; if (fromPosition.isNotNull() && toPosition.isNotNull()) { if (fromPosition < toPosition) range = Range::create(*frame.document(), fromPosition, toPosition); else range = Range::create(*frame.document(), toPosition, fromPosition); frame.selection().setSelectedRange(range.get(), fromPosition.affinity(), WebCore::FrameSelection::ShouldCloseTyping::Yes, UserTriggered); } // We can use the same callback for the gestures with one point. send(Messages::WebPageProxy::GestureCallback(from, gestureType, gestureState, 0, callbackID)); } void WebPage::extendSelection(uint32_t granularity) { Frame& frame = m_page->focusController().focusedOrMainFrame(); // For the moment we handle only WordGranularity. if (granularity != WordGranularity || !frame.selection().isCaret()) return; VisiblePosition position = frame.selection().selection().start(); auto wordRange = wordRangeFromPosition(position); if (!wordRange) return; IntPoint endLocationForSyntheticMouseEvents; bool shouldDispatchMouseEvents = shouldDispatchSyntheticMouseEventsWhenModifyingSelection(); if (shouldDispatchMouseEvents) { auto startLocationForSyntheticMouseEvents = frame.view()->contentsToRootView(VisiblePosition(wordRange->startPosition()).absoluteCaretBounds()).center(); endLocationForSyntheticMouseEvents = frame.view()->contentsToRootView(VisiblePosition(wordRange->endPosition()).absoluteCaretBounds()).center(); dispatchSyntheticMouseEventsForSelectionGesture(SelectionTouch::Started, startLocationForSyntheticMouseEvents); dispatchSyntheticMouseEventsForSelectionGesture(SelectionTouch::Moved, endLocationForSyntheticMouseEvents); } frame.selection().setSelectedRange(wordRange.get(), position.affinity(), WebCore::FrameSelection::ShouldCloseTyping::Yes, UserTriggered); if (shouldDispatchMouseEvents) dispatchSyntheticMouseEventsForSelectionGesture(SelectionTouch::Ended, endLocationForSyntheticMouseEvents); } void WebPage::platformDidSelectAll() { if (!shouldDispatchSyntheticMouseEventsWhenModifyingSelection()) return; auto frame = makeRef(m_page->focusController().focusedOrMainFrame()); auto startCaretRect = frame->view()->contentsToRootView(VisiblePosition(frame->selection().selection().start()).absoluteCaretBounds()); auto endCaretRect = frame->view()->contentsToRootView(VisiblePosition(frame->selection().selection().end()).absoluteCaretBounds()); dispatchSyntheticMouseEventsForSelectionGesture(SelectionTouch::Started, startCaretRect.center()); dispatchSyntheticMouseEventsForSelectionGesture(SelectionTouch::Moved, endCaretRect.center()); dispatchSyntheticMouseEventsForSelectionGesture(SelectionTouch::Ended, endCaretRect.center()); } void WebPage::selectWordBackward() { Frame& frame = m_page->focusController().focusedOrMainFrame(); if (!frame.selection().isCaret()) return; VisiblePosition position = frame.selection().selection().start(); VisiblePosition startPosition = positionOfNextBoundaryOfGranularity(position, WordGranularity, DirectionBackward); if (startPosition.isNotNull() && startPosition != position) frame.selection().setSelectedRange(Range::create(*frame.document(), startPosition, position).ptr(), position.affinity(), WebCore::FrameSelection::ShouldCloseTyping::Yes, UserTriggered); } void WebPage::moveSelectionByOffset(int32_t offset, CallbackID callbackID) { Frame& frame = m_page->focusController().focusedOrMainFrame(); VisiblePosition startPosition = frame.selection().selection().end(); if (startPosition.isNull()) return; SelectionDirection direction = offset < 0 ? DirectionBackward : DirectionForward; VisiblePosition position = startPosition; for (int i = 0; i < abs(offset); ++i) { position = positionOfNextBoundaryOfGranularity(position, CharacterGranularity, direction); if (position.isNull()) break; } if (position.isNotNull() && startPosition != position) frame.selection().setSelectedRange(Range::create(*frame.document(), position, position).ptr(), position.affinity(), WebCore::FrameSelection::ShouldCloseTyping::Yes, UserTriggered); send(Messages::WebPageProxy::VoidCallback(callbackID)); } void WebPage::startAutoscrollAtPosition(const WebCore::FloatPoint& positionInWindow) { if (m_focusedElement && m_focusedElement->renderer()) { m_page->mainFrame().eventHandler().startSelectionAutoscroll(m_focusedElement->renderer(), positionInWindow); return; } Frame& frame = m_page->focusController().focusedOrMainFrame(); VisibleSelection selection = frame.selection().selection(); if (!selection.isRange()) return; RefPtr<Range> range = frame.selection().toNormalizedRange(); if (!range) return; auto* renderer = range->startContainer().renderer(); if (!renderer) return; m_page->mainFrame().eventHandler().startSelectionAutoscroll(renderer, positionInWindow); } void WebPage::cancelAutoscroll() { m_page->mainFrame().eventHandler().cancelSelectionAutoscroll(); } void WebPage::requestEvasionRectsAboveSelection(CompletionHandler<void(const Vector<FloatRect>&)>&& reply) { auto& frame = m_page->focusController().focusedOrMainFrame(); auto frameView = makeRefPtr(frame.view()); if (!frameView) { reply({ }); return; } auto& selection = frame.selection().selection(); if (selection.isNone()) { reply({ }); return; } auto selectedRange = selection.toNormalizedRange(); if (!selectedRange) { reply({ }); return; } if (!m_focusedElement || !m_focusedElement->renderer() || isTransparentOrFullyClipped(*m_focusedElement)) { reply({ }); return; } float scaleFactor = pageScaleFactor(); const double factorOfContentArea = 0.5; auto unobscuredContentArea = m_page->mainFrame().view()->unobscuredContentRect().area(); if (unobscuredContentArea.hasOverflowed()) { reply({ }); return; } double contextMenuAreaLimit = factorOfContentArea * scaleFactor * unobscuredContentArea.unsafeGet(); FloatRect selectionBoundsInRootViewCoordinates; if (selection.isRange()) selectionBoundsInRootViewCoordinates = frameView->contentsToRootView(selectedRange->absoluteBoundingBox()); else selectionBoundsInRootViewCoordinates = frameView->contentsToRootView(frame.selection().absoluteCaretBounds()); auto centerOfTargetBounds = selectionBoundsInRootViewCoordinates.center(); FloatPoint centerTopInRootViewCoordinates { centerOfTargetBounds.x(), selectionBoundsInRootViewCoordinates.y() }; auto clickableNonEditableNode = [&] (const FloatPoint& locationInRootViewCoordinates) -> Node* { FloatPoint adjustedPoint; auto* hitNode = m_page->mainFrame().nodeRespondingToClickEvents(locationInRootViewCoordinates, adjustedPoint); if (!hitNode || is<HTMLBodyElement>(hitNode) || is<Document>(hitNode) || hitNode->hasEditableStyle()) return nullptr; return hitNode; }; // This heuristic attempts to find a list of rects to avoid when showing the callout menu on iOS. // First, hit-test several points above the bounds of the selection rect in search of clickable nodes that are not editable. // Secondly, hit-test several points around the edges of the selection rect and exclude any nodes found in the first round of // hit-testing if these nodes are also reachable by moving outwards from the left, right, or bottom edges of the selection. // Additionally, exclude any hit-tested nodes that are either very large relative to the size of the root view, or completely // encompass the selection bounds. The resulting rects are the bounds of these hit-tested nodes in root view coordinates. HashSet<Ref<Node>> hitTestedNodes; Vector<FloatRect> rectsToAvoidInRootViewCoordinates; const Vector<FloatPoint, 5> offsetsForHitTesting {{ -30, -50 }, { 30, -50 }, { -60, -35 }, { 60, -35 }, { 0, -20 }}; for (auto offset : offsetsForHitTesting) { offset.scale(1 / scaleFactor); if (auto* hitNode = clickableNonEditableNode(centerTopInRootViewCoordinates + offset)) hitTestedNodes.add(*hitNode); } const float marginForHitTestingSurroundingNodes = 80 / scaleFactor; Vector<FloatPoint, 3> exclusionHitTestLocations { { selectionBoundsInRootViewCoordinates.x() - marginForHitTestingSurroundingNodes, centerOfTargetBounds.y() }, { centerOfTargetBounds.x(), selectionBoundsInRootViewCoordinates.maxY() + marginForHitTestingSurroundingNodes }, { selectionBoundsInRootViewCoordinates.maxX() + marginForHitTestingSurroundingNodes, centerOfTargetBounds.y() } }; for (auto& location : exclusionHitTestLocations) { if (auto* nodeToExclude = clickableNonEditableNode(location)) hitTestedNodes.remove(*nodeToExclude); } for (auto& node : hitTestedNodes) { auto frameView = makeRefPtr(node->document().view()); auto* renderer = node->renderer(); if (!renderer || !frameView) continue; auto bounds = frameView->contentsToRootView(renderer->absoluteBoundingBoxRect()); auto area = bounds.area(); if (area.hasOverflowed() || area.unsafeGet() > contextMenuAreaLimit) continue; if (bounds.contains(enclosingIntRect(selectionBoundsInRootViewCoordinates))) continue; rectsToAvoidInRootViewCoordinates.append(WTFMove(bounds)); } reply(WTFMove(rectsToAvoidInRootViewCoordinates)); } void WebPage::getRectsForGranularityWithSelectionOffset(uint32_t granularity, int32_t offset, CallbackID callbackID) { Frame& frame = m_page->focusController().focusedOrMainFrame(); VisibleSelection selection = m_storedSelectionForAccessibility.isNone() ? frame.selection().selection() : m_storedSelectionForAccessibility; VisiblePosition selectionStart = selection.visibleStart(); if (selectionStart.isNull()) { send(Messages::WebPageProxy::SelectionRectsCallback({ }, callbackID)); return; } auto position = visiblePositionForPositionWithOffset(selectionStart, offset); SelectionDirection direction = offset < 0 ? DirectionBackward : DirectionForward; auto range = enclosingTextUnitOfGranularity(position, static_cast<WebCore::TextGranularity>(granularity), direction); if (!range || range->collapsed()) { send(Messages::WebPageProxy::SelectionRectsCallback({ }, callbackID)); return; } Vector<WebCore::SelectionRect> selectionRects; range->collectSelectionRectsWithoutUnionInteriorLines(selectionRects); convertSelectionRectsToRootView(frame.view(), selectionRects); send(Messages::WebPageProxy::SelectionRectsCallback(selectionRects, callbackID)); } void WebPage::storeSelectionForAccessibility(bool shouldStore) { if (!shouldStore) m_storedSelectionForAccessibility = VisibleSelection(); else { Frame& frame = m_page->focusController().focusedOrMainFrame(); m_storedSelectionForAccessibility = frame.selection().selection(); } } static RefPtr<Range> rangeNearPositionMatchesText(const VisiblePosition& position, RefPtr<Range> originalRange, const String& matchText, RefPtr<Range> selectionRange) { auto range = Range::create(selectionRange->ownerDocument(), selectionRange->startPosition(), position.deepEquivalent().parentAnchoredEquivalent()); unsigned targetOffset = TextIterator::rangeLength(range.ptr(), true); return findClosestPlainText(*selectionRange.get(), matchText, { }, targetOffset); } void WebPage::getRectsAtSelectionOffsetWithText(int32_t offset, const String& text, CallbackID callbackID) { Frame& frame = m_page->focusController().focusedOrMainFrame(); uint32_t length = text.length(); VisibleSelection selection = m_storedSelectionForAccessibility.isNone() ? frame.selection().selection() : m_storedSelectionForAccessibility; VisiblePosition selectionStart = selection.visibleStart(); VisiblePosition selectionEnd = selection.visibleEnd(); if (selectionStart.isNull() || selectionEnd.isNull()) { send(Messages::WebPageProxy::SelectionRectsCallback({ }, callbackID)); return; } auto startPosition = visiblePositionForPositionWithOffset(selectionStart, offset); auto endPosition = visiblePositionForPositionWithOffset(startPosition, length); auto range = Range::create(*frame.document(), startPosition, endPosition); if (range->collapsed()) { send(Messages::WebPageProxy::SelectionRectsCallback({ }, callbackID)); return; } String rangeText = plainTextReplacingNoBreakSpace(range.ptr(), TextIteratorDefaultBehavior, true); if (rangeText != text) { auto selectionRange = selection.toNormalizedRange(); // Try to search for a range which is the closest to the position within the selection range that matches the passed in text. if (auto wordRange = rangeNearPositionMatchesText(startPosition, range.ptr(), text, selectionRange)) { if (!wordRange->collapsed()) range = *wordRange; } } Vector<WebCore::SelectionRect> selectionRects; range->collectSelectionRectsWithoutUnionInteriorLines(selectionRects); convertSelectionRectsToRootView(frame.view(), selectionRects); send(Messages::WebPageProxy::SelectionRectsCallback(selectionRects, callbackID)); } VisiblePosition WebPage::visiblePositionInFocusedNodeForPoint(const Frame& frame, const IntPoint& point, bool isInteractingWithFocusedElement) { IntPoint adjustedPoint(frame.view()->rootViewToContents(point)); IntPoint constrainedPoint = m_focusedElement && isInteractingWithFocusedElement ? constrainPoint(adjustedPoint, frame, *m_focusedElement) : adjustedPoint; return frame.visiblePositionForPoint(constrainedPoint); } void WebPage::selectPositionAtPoint(const WebCore::IntPoint& point, bool isInteractingWithFocusedElement, CallbackID callbackID) { auto& frame = m_page->focusController().focusedOrMainFrame(); VisiblePosition position = visiblePositionInFocusedNodeForPoint(frame, point, isInteractingWithFocusedElement); if (position.isNotNull()) frame.selection().setSelectedRange(Range::create(*frame.document(), position, position).ptr(), position.affinity(), WebCore::FrameSelection::ShouldCloseTyping::Yes, UserTriggered); send(Messages::WebPageProxy::VoidCallback(callbackID)); } void WebPage::selectPositionAtBoundaryWithDirection(const WebCore::IntPoint& point, uint32_t granularity, uint32_t direction, bool isInteractingWithFocusedElement, CallbackID callbackID) { auto& frame = m_page->focusController().focusedOrMainFrame(); VisiblePosition position = visiblePositionInFocusedNodeForPoint(frame, point, isInteractingWithFocusedElement); if (position.isNotNull()) { position = positionOfNextBoundaryOfGranularity(position, static_cast<WebCore::TextGranularity>(granularity), static_cast<SelectionDirection>(direction)); if (position.isNotNull()) frame.selection().setSelectedRange(Range::create(*frame.document(), position, position).ptr(), UPSTREAM, WebCore::FrameSelection::ShouldCloseTyping::Yes, UserTriggered); } send(Messages::WebPageProxy::VoidCallback(callbackID)); } void WebPage::moveSelectionAtBoundaryWithDirection(uint32_t granularity, uint32_t direction, CallbackID callbackID) { Frame& frame = m_page->focusController().focusedOrMainFrame(); if (!frame.selection().selection().isNone()) { bool isForward = (direction == DirectionForward || direction == DirectionRight); VisiblePosition position = (isForward) ? frame.selection().selection().visibleEnd() : frame.selection().selection().visibleStart(); position = positionOfNextBoundaryOfGranularity(position, static_cast<WebCore::TextGranularity>(granularity), static_cast<SelectionDirection>(direction)); if (position.isNotNull()) frame.selection().setSelectedRange(Range::create(*frame.document(), position, position).ptr(), isForward? UPSTREAM : DOWNSTREAM, WebCore::FrameSelection::ShouldCloseTyping::Yes, UserTriggered); } send(Messages::WebPageProxy::VoidCallback(callbackID)); } RefPtr<Range> WebPage::rangeForGranularityAtPoint(Frame& frame, const WebCore::IntPoint& point, uint32_t granularity, bool isInteractingWithFocusedElement) { VisiblePosition position = visiblePositionInFocusedNodeForPoint(frame, point, isInteractingWithFocusedElement); RefPtr<Range> range; switch (static_cast<WebCore::TextGranularity>(granularity)) { case CharacterGranularity: range = makeRange(position, position); break; case WordGranularity: range = wordRangeFromPosition(position); break; case SentenceGranularity: range = enclosingTextUnitOfGranularity(position, SentenceGranularity, DirectionForward); break; case ParagraphGranularity: range = enclosingTextUnitOfGranularity(position, ParagraphGranularity, DirectionForward); break; case DocumentGranularity: // FIXME: It's not clear why this mutates the current selection and returns null. frame.selection().selectAll(); break; default: break; } return range; } static inline bool rectIsTooBigForSelection(const IntRect& blockRect, const Frame& frame) { const float factor = 0.97; return blockRect.height() > frame.view()->unobscuredContentRect().height() * factor; } void WebPage::setFocusedFrameBeforeSelectingTextAtLocation(const IntPoint& point) { auto result = m_page->mainFrame().eventHandler().hitTestResultAtPoint(point, HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::DisallowUserAgentShadowContent | HitTestRequest::AllowChildFrameContent); auto* hitNode = result.innerNode(); if (hitNode && hitNode->renderer()) m_page->focusController().setFocusedFrame(result.innerNodeFrame()); } void WebPage::selectTextWithGranularityAtPoint(const WebCore::IntPoint& point, uint32_t granularity, bool isInteractingWithFocusedElement, CallbackID callbackID) { setFocusedFrameBeforeSelectingTextAtLocation(point); auto& frame = m_page->focusController().focusedOrMainFrame(); auto range = rangeForGranularityAtPoint(frame, point, granularity, isInteractingWithFocusedElement); if (range) frame.selection().setSelectedRange(range.get(), UPSTREAM, WebCore::FrameSelection::ShouldCloseTyping::Yes, UserTriggered); m_initialSelection = range; send(Messages::WebPageProxy::VoidCallback(callbackID)); } void WebPage::beginSelectionInDirection(uint32_t direction, CallbackID callbackID) { m_selectionAnchor = (static_cast<SelectionDirection>(direction) == DirectionLeft) ? Start : End; send(Messages::WebPageProxy::UnsignedCallback(m_selectionAnchor == Start, callbackID)); } void WebPage::updateSelectionWithExtentPointAndBoundary(const WebCore::IntPoint& point, uint32_t granularity, bool isInteractingWithFocusedElement, CallbackID callbackID) { auto& frame = m_page->focusController().focusedOrMainFrame(); VisiblePosition position = visiblePositionInFocusedNodeForPoint(frame, point, isInteractingWithFocusedElement); RefPtr<Range> newRange = rangeForGranularityAtPoint(frame, point, granularity, isInteractingWithFocusedElement); if (position.isNull() || !m_initialSelection || !newRange) { send(Messages::WebPageProxy::UnsignedCallback(false, callbackID)); return; } RefPtr<Range> range; VisiblePosition selectionStart = m_initialSelection->startPosition(); VisiblePosition selectionEnd = m_initialSelection->endPosition(); if (position > m_initialSelection->endPosition()) selectionEnd = newRange->endPosition(); else if (position < m_initialSelection->startPosition()) selectionStart = newRange->startPosition(); if (selectionStart.isNotNull() && selectionEnd.isNotNull()) range = Range::create(*frame.document(), selectionStart, selectionEnd); if (range) frame.selection().setSelectedRange(range.get(), UPSTREAM, WebCore::FrameSelection::ShouldCloseTyping::Yes, UserTriggered); send(Messages::WebPageProxy::UnsignedCallback(selectionStart == m_initialSelection->startPosition(), callbackID)); } void WebPage::updateSelectionWithExtentPoint(const WebCore::IntPoint& point, bool isInteractingWithFocusedElement, RespectSelectionAnchor respectSelectionAnchor, CallbackID callbackID) { auto& frame = m_page->focusController().focusedOrMainFrame(); VisiblePosition position = visiblePositionInFocusedNodeForPoint(frame, point, isInteractingWithFocusedElement); if (position.isNull()) { send(Messages::WebPageProxy::UnsignedCallback(false, callbackID)); return; } RefPtr<Range> range; VisiblePosition selectionStart; VisiblePosition selectionEnd; if (respectSelectionAnchor == RespectSelectionAnchor::Yes) { if (m_selectionAnchor == Start) { selectionStart = frame.selection().selection().visibleStart(); selectionEnd = position; if (position <= selectionStart) { selectionStart = selectionStart.previous(); selectionEnd = frame.selection().selection().visibleEnd(); m_selectionAnchor = End; } } else { selectionStart = position; selectionEnd = frame.selection().selection().visibleEnd(); if (position >= selectionEnd) { selectionStart = frame.selection().selection().visibleStart(); selectionEnd = selectionEnd.next(); m_selectionAnchor = Start; } } } else { auto currentStart = frame.selection().selection().visibleStart(); auto currentEnd = frame.selection().selection().visibleEnd(); if (position <= currentStart) { selectionStart = position; selectionEnd = currentEnd; } else if (position >= currentEnd) { selectionStart = currentStart; selectionEnd = position; } } if (selectionStart.isNotNull() && selectionEnd.isNotNull()) range = Range::create(*frame.document(), selectionStart, selectionEnd); if (range) frame.selection().setSelectedRange(range.get(), UPSTREAM, WebCore::FrameSelection::ShouldCloseTyping::Yes, UserTriggered); send(Messages::WebPageProxy::UnsignedCallback(m_selectionAnchor == Start, callbackID)); } void WebPage::convertSelectionRectsToRootView(FrameView* view, Vector<SelectionRect>& selectionRects) { for (size_t i = 0; i < selectionRects.size(); ++i) { SelectionRect& currentRect = selectionRects[i]; currentRect.setRect(view->contentsToRootView(currentRect.rect())); } } void WebPage::requestDictationContext(CallbackID callbackID) { Frame& frame = m_page->focusController().focusedOrMainFrame(); VisiblePosition startPosition = frame.selection().selection().start(); VisiblePosition endPosition = frame.selection().selection().end(); const unsigned dictationContextWordCount = 5; String selectedText; if (frame.selection().isRange()) selectedText = plainTextReplacingNoBreakSpace(frame.selection().selection().toNormalizedRange().get()); String contextBefore; if (startPosition != startOfEditableContent(startPosition)) { VisiblePosition currentPosition = startPosition; VisiblePosition lastPosition = startPosition; for (unsigned i = 0; i < dictationContextWordCount; ++i) { currentPosition = startOfWord(positionOfNextBoundaryOfGranularity(lastPosition, WordGranularity, DirectionBackward)); if (currentPosition.isNull()) break; lastPosition = currentPosition; } if (lastPosition.isNotNull() && lastPosition != startPosition) contextBefore = plainTextReplacingNoBreakSpace(Range::create(*frame.document(), lastPosition, startPosition).ptr()); } String contextAfter; if (endPosition != endOfEditableContent(endPosition)) { VisiblePosition currentPosition = endPosition; VisiblePosition lastPosition = endPosition; for (unsigned i = 0; i < dictationContextWordCount; ++i) { currentPosition = endOfWord(positionOfNextBoundaryOfGranularity(lastPosition, WordGranularity, DirectionForward)); if (currentPosition.isNull()) break; lastPosition = currentPosition; } if (lastPosition.isNotNull() && lastPosition != endPosition) contextAfter = plainTextReplacingNoBreakSpace(Range::create(*frame.document(), endPosition, lastPosition).ptr()); } send(Messages::WebPageProxy::SelectionContextCallback(selectedText, contextBefore, contextAfter, callbackID)); } void WebPage::replaceSelectedText(const String& oldText, const String& newText) { Frame& frame = m_page->focusController().focusedOrMainFrame(); RefPtr<Range> wordRange = frame.selection().isCaret() ? wordRangeFromPosition(frame.selection().selection().start()) : frame.selection().toNormalizedRange(); if (plainTextReplacingNoBreakSpace(wordRange.get()) != oldText) return; frame.editor().setIgnoreSelectionChanges(true); frame.selection().setSelectedRange(wordRange.get(), UPSTREAM, WebCore::FrameSelection::ShouldCloseTyping::Yes); frame.editor().insertText(newText, 0); frame.editor().setIgnoreSelectionChanges(false); } void WebPage::replaceDictatedText(const String& oldText, const String& newText) { Frame& frame = m_page->focusController().focusedOrMainFrame(); if (frame.selection().isNone()) return; if (frame.selection().isRange()) { frame.editor().deleteSelectionWithSmartDelete(false); return; } VisiblePosition position = frame.selection().selection().start(); for (size_t i = 0; i < oldText.length(); ++i) position = position.previous(); if (position.isNull()) position = startOfDocument(static_cast<Node*>(frame.document()->documentElement())); auto range = Range::create(*frame.document(), position, frame.selection().selection().start()); if (plainTextReplacingNoBreakSpace(range.ptr()) != oldText) return; // We don't want to notify the client that the selection has changed until we are done inserting the new text. frame.editor().setIgnoreSelectionChanges(true); frame.selection().setSelectedRange(range.ptr(), UPSTREAM, WebCore::FrameSelection::ShouldCloseTyping::Yes); frame.editor().insertText(newText, 0); frame.editor().setIgnoreSelectionChanges(false); } void WebPage::requestAutocorrectionData(const String& textForAutocorrection, CompletionHandler<void(WebAutocorrectionData)>&& reply) { auto& frame = m_page->focusController().focusedOrMainFrame(); if (!frame.selection().isCaret()) { reply({ }); return; } VisiblePosition position = frame.selection().selection().start(); auto range = wordRangeFromPosition(position); if (!range) { reply({ }); return; } auto textForRange = plainTextReplacingNoBreakSpace(range.get()); const unsigned maxSearchAttempts = 5; for (size_t i = 0; i < maxSearchAttempts && textForRange != textForAutocorrection; ++i) { position = range->startPosition().previous(); if (position.isNull() || position == range->startPosition()) break; range = Range::create(*frame.document(), wordRangeFromPosition(position)->startPosition(), range->endPosition()); textForRange = plainTextReplacingNoBreakSpace(range.get()); } Vector<SelectionRect> selectionRects; if (textForRange == textForAutocorrection) range->collectSelectionRects(selectionRects); Vector<FloatRect> rectsForText; rectsForText.grow(selectionRects.size()); convertSelectionRectsToRootView(frame.view(), selectionRects); for (size_t i = 0; i < selectionRects.size(); i++) rectsForText[i] = selectionRects[i].rect(); bool multipleFonts = false; CTFontRef font = nil; if (auto* coreFont = frame.editor().fontForSelection(multipleFonts)) font = coreFont->getCTFont(); reply({ WTFMove(rectsForText), (__bridge UIFont *)font }); } void WebPage::applyAutocorrection(const String& correction, const String& originalText, CallbackID callbackID) { send(Messages::WebPageProxy::StringCallback(applyAutocorrectionInternal(correction, originalText) ? correction : String(), callbackID)); } Seconds WebPage::eventThrottlingDelay() const { auto behaviorOverride = m_page->eventThrottlingBehaviorOverride(); if (behaviorOverride) { switch (behaviorOverride.value()) { case EventThrottlingBehavior::Responsive: return 0_s; case EventThrottlingBehavior::Unresponsive: return 1_s; } } if (m_isInStableState || m_estimatedLatency <= Seconds(1.0 / 60)) return 0_s; return std::min(m_estimatedLatency * 2, 1_s); } void WebPage::syncApplyAutocorrection(const String& correction, const String& originalText, CompletionHandler<void(bool)>&& reply) { reply(applyAutocorrectionInternal(correction, originalText)); } bool WebPage::applyAutocorrectionInternal(const String& correction, const String& originalText) { auto& frame = m_page->focusController().focusedOrMainFrame(); if (!frame.selection().isCaretOrRange()) return false; RefPtr<Range> range; String textForRange; auto originalTextWithFoldedQuoteMarks = foldQuoteMarks(originalText); if (frame.selection().isCaret()) { VisiblePosition position = frame.selection().selection().start(); range = wordRangeFromPosition(position); textForRange = plainTextReplacingNoBreakSpace(range.get()); // If 'originalText' is not the same as 'textForRange' we need to move 'range' // forward such that it matches the original selection as much as possible. if (foldQuoteMarks(textForRange) != originalTextWithFoldedQuoteMarks) { // Search for the original text before the selection caret. for (size_t i = 0; i < originalText.length(); ++i) position = position.previous(); if (position.isNull()) position = startOfDocument(static_cast<Node*>(frame.document()->documentElement())); range = Range::create(*frame.document(), position, frame.selection().selection().start()); textForRange = plainTextReplacingNoBreakSpace(range.get()); unsigned loopCount = 0; const unsigned maxPositionsAttempts = 10; while (textForRange.length() && textForRange.length() > originalText.length() && loopCount < maxPositionsAttempts) { position = position.next(); if (position.isNotNull() && position >= frame.selection().selection().start()) range = nullptr; else range = Range::create(*frame.document(), position, frame.selection().selection().start()); textForRange = plainTextReplacingNoBreakSpace(range.get()); loopCount++; } } else if (textForRange.isEmpty() && range && !range->collapsed()) { // If 'range' does not include any text but it is not collapsed, we need to set // 'range' to match the selection. Otherwise non-text nodes will be removed. range = Range::create(*frame.document(), position, position); if (!range) return false; } } else { // Range selection. range = frame.selection().toNormalizedRange(); if (!range) return false; textForRange = plainTextReplacingNoBreakSpace(range.get()); } if (foldQuoteMarks(textForRange) != originalTextWithFoldedQuoteMarks) return false; // Correctly determine affinity, using logic currently only present in VisiblePosition EAffinity affinity = DOWNSTREAM; if (range && range->collapsed()) affinity = VisiblePosition(range->startPosition(), UPSTREAM).affinity(); frame.selection().setSelectedRange(range.get(), affinity, WebCore::FrameSelection::ShouldCloseTyping::Yes); if (correction.length()) frame.editor().insertText(correction, 0, originalText.isEmpty() ? TextEventInputKeyboard : TextEventInputAutocompletion); else if (originalText.length()) frame.editor().deleteWithDirection(DirectionBackward, CharacterGranularity, false, true); return true; } WebAutocorrectionContext WebPage::autocorrectionContext() { String contextBefore; String markedText; String selectedText; String contextAfter; EditingRange markedTextRange; auto& frame = m_page->focusController().focusedOrMainFrame(); RefPtr<Range> range; VisiblePosition startPosition = frame.selection().selection().start(); VisiblePosition endPosition = frame.selection().selection().end(); const unsigned minContextWordCount = 3; const unsigned minContextLenght = 12; const unsigned maxContextLength = 30; if (frame.selection().isRange()) selectedText = plainTextReplacingNoBreakSpace(frame.selection().selection().toNormalizedRange().get()); if (auto compositionRange = frame.editor().compositionRange()) { range = Range::create(*frame.document(), compositionRange->startPosition(), startPosition); String markedTextBefore; if (range) markedTextBefore = plainTextReplacingNoBreakSpace(range.get()); range = Range::create(*frame.document(), endPosition, compositionRange->endPosition()); String markedTextAfter; if (range) markedTextAfter = plainTextReplacingNoBreakSpace(range.get()); markedText = markedTextBefore + selectedText + markedTextAfter; if (!markedText.isEmpty()) { markedTextRange.location = markedTextBefore.length(); markedTextRange.length = selectedText.length(); } } else { if (startPosition != startOfEditableContent(startPosition)) { VisiblePosition currentPosition = startPosition; VisiblePosition previousPosition; unsigned totalContextLength = 0; for (unsigned i = 0; i < minContextWordCount; ++i) { if (contextBefore.length() >= minContextLenght) break; previousPosition = startOfWord(positionOfNextBoundaryOfGranularity(currentPosition, WordGranularity, DirectionBackward)); if (previousPosition.isNull()) break; String currentWord = plainTextReplacingNoBreakSpace(Range::create(*frame.document(), previousPosition, currentPosition).ptr()); totalContextLength += currentWord.length(); if (totalContextLength >= maxContextLength) break; currentPosition = previousPosition; } if (currentPosition.isNotNull() && currentPosition != startPosition) { contextBefore = plainTextReplacingNoBreakSpace(Range::create(*frame.document(), currentPosition, startPosition).ptr()); if (atBoundaryOfGranularity(currentPosition, ParagraphGranularity, DirectionBackward)) contextBefore = makeString("\n "_s, contextBefore); } } if (endPosition != endOfEditableContent(endPosition)) { VisiblePosition nextPosition; if (!atBoundaryOfGranularity(endPosition, WordGranularity, DirectionForward) && withinTextUnitOfGranularity(endPosition, WordGranularity, DirectionForward)) nextPosition = positionOfNextBoundaryOfGranularity(endPosition, WordGranularity, DirectionForward); if (nextPosition.isNotNull()) contextAfter = plainTextReplacingNoBreakSpace(Range::create(*frame.document(), endPosition, nextPosition).ptr()); } } WebAutocorrectionContext correction; correction.contextBefore = WTFMove(contextBefore); correction.markedText = WTFMove(markedText); correction.selectedText = WTFMove(selectedText); correction.contextAfter = WTFMove(contextAfter); correction.markedTextRange = WTFMove(markedTextRange); return correction; } void WebPage::requestAutocorrectionContext() { send(Messages::WebPageProxy::HandleAutocorrectionContext(autocorrectionContext())); } static HTMLAnchorElement* containingLinkElement(Element* element) { for (auto& currentElement : elementLineage(element)) { if (currentElement.isLink() && is<HTMLAnchorElement>(currentElement)) return downcast<HTMLAnchorElement>(¤tElement); } return nullptr; } static inline bool isAssistableElement(Element& element) { if (is<HTMLSelectElement>(element)) return true; if (is<HTMLTextAreaElement>(element)) return true; if (is<HTMLImageElement>(element) && downcast<HTMLImageElement>(element).hasEditableImageAttribute()) return true; if (is<HTMLInputElement>(element)) { HTMLInputElement& inputElement = downcast<HTMLInputElement>(element); // FIXME: This laundry list of types is not a good way to factor this. Need a suitable function on HTMLInputElement itself. #if ENABLE(INPUT_TYPE_COLOR) if (inputElement.isColorControl()) return true; #endif return inputElement.isTextField() || inputElement.isDateField() || inputElement.isDateTimeLocalField() || inputElement.isMonthField() || inputElement.isTimeField(); } if (is<HTMLIFrameElement>(element)) return false; return element.isContentEditable(); } void WebPage::getPositionInformation(const InteractionInformationRequest& request, CompletionHandler<void(InteractionInformationAtPosition&&)>&& reply) { // Avoid UIProcess hangs when the WebContent process is stuck on a sync IPC. if (IPC::UnboundedSynchronousIPCScope::hasOngoingUnboundedSyncIPC()) { RELEASE_LOG_ERROR_IF_ALLOWED(Process, "getPositionInformation - Not processing because the process is stuck on unbounded sync IPC"); return reply({ }); } m_pendingSynchronousPositionInformationReply = WTFMove(reply); auto information = positionInformation(request); if (auto reply = WTFMove(m_pendingSynchronousPositionInformationReply)) reply(WTFMove(information)); } static void focusedElementPositionInformation(WebPage& page, Element& focusedElement, const InteractionInformationRequest& request, InteractionInformationAtPosition& info) { const Frame& frame = page.corePage()->focusController().focusedOrMainFrame(); if (!frame.editor().hasComposition()) return; const uint32_t kHitAreaWidth = 66; const uint32_t kHitAreaHeight = 66; FrameView& view = *frame.view(); IntPoint adjustedPoint(view.rootViewToContents(request.point)); IntPoint constrainedPoint = constrainPoint(adjustedPoint, frame, focusedElement); VisiblePosition position = frame.visiblePositionForPoint(constrainedPoint); RefPtr<Range> compositionRange = frame.editor().compositionRange(); if (!compositionRange) return; if (position < compositionRange->startPosition()) position = compositionRange->startPosition(); else if (position > compositionRange->endPosition()) position = compositionRange->endPosition(); IntRect caretRect = view.contentsToRootView(position.absoluteCaretBounds()); float deltaX = abs(caretRect.x() + (caretRect.width() / 2) - request.point.x()); float deltaYFromTheTop = abs(caretRect.y() - request.point.y()); float deltaYFromTheBottom = abs(caretRect.y() + caretRect.height() - request.point.y()); info.isNearMarkedText = !(deltaX > kHitAreaWidth || deltaYFromTheTop > kHitAreaHeight || deltaYFromTheBottom > kHitAreaHeight); } static void linkIndicatorPositionInformation(WebPage& page, Element& linkElement, const InteractionInformationRequest& request, InteractionInformationAtPosition& info) { if (!request.includeLinkIndicator) return; auto linkRange = rangeOfContents(linkElement); float deviceScaleFactor = page.corePage()->deviceScaleFactor(); const float marginInPoints = request.linkIndicatorShouldHaveLegacyMargins ? 4 : 0; auto textIndicator = TextIndicator::createWithRange(linkRange.get(), TextIndicatorOptionTightlyFitContent | TextIndicatorOptionRespectTextColor | TextIndicatorOptionPaintBackgrounds | TextIndicatorOptionUseBoundingRectAndPaintAllContentForComplexRanges | TextIndicatorOptionIncludeMarginIfRangeMatchesSelection | TextIndicatorOptionComputeEstimatedBackgroundColor, TextIndicatorPresentationTransition::None, FloatSize(marginInPoints * deviceScaleFactor, marginInPoints * deviceScaleFactor)); if (textIndicator) info.linkIndicator = textIndicator->data(); } #if ENABLE(DATA_DETECTION) static void dataDetectorLinkPositionInformation(Element& element, InteractionInformationAtPosition& info) { if (!DataDetection::isDataDetectorLink(element)) return; info.isDataDetectorLink = true; const int dataDetectionExtendedContextLength = 350; info.dataDetectorIdentifier = DataDetection::dataDetectorIdentifier(element); info.dataDetectorResults = element.document().frame()->dataDetectionResults(); if (!DataDetection::requiresExtendedContext(element)) return; auto linkRange = Range::create(element.document()); linkRange->selectNodeContents(element); info.textBefore = plainTextReplacingNoBreakSpace(rangeExpandedByCharactersInDirectionAtWordBoundary(linkRange->startPosition(), dataDetectionExtendedContextLength, DirectionBackward).get(), TextIteratorDefaultBehavior, true); info.textAfter = plainTextReplacingNoBreakSpace(rangeExpandedByCharactersInDirectionAtWordBoundary(linkRange->endPosition(), dataDetectionExtendedContextLength, DirectionForward).get(), TextIteratorDefaultBehavior, true); } #endif static void imagePositionInformation(WebPage& page, Element& element, const InteractionInformationRequest& request, InteractionInformationAtPosition& info) { auto& renderImage = downcast<RenderImage>(*(element.renderer())); if (!renderImage.cachedImage() || renderImage.cachedImage()->errorOccurred()) return; auto* image = renderImage.cachedImage()->imageForRenderer(&renderImage); if (!image || image->width() <= 1 || image->height() <= 1) return; info.isImage = true; info.imageURL = element.document().completeURL(renderImage.cachedImage()->url()); info.isAnimatedImage = image->isAnimated(); if (!request.includeSnapshot) return; FloatSize screenSizeInPixels = screenSize(); FloatSize imageSize = renderImage.cachedImage()->imageSizeForRenderer(&renderImage); screenSizeInPixels.scale(page.corePage()->deviceScaleFactor()); FloatSize scaledSize = largestRectWithAspectRatioInsideRect(imageSize.width() / imageSize.height(), FloatRect(0, 0, screenSizeInPixels.width(), screenSizeInPixels.height())).size(); FloatSize bitmapSize = scaledSize.width() < imageSize.width() ? scaledSize : imageSize; // FIXME: Only select ExtendedColor on images known to need wide gamut ShareableBitmap::Configuration bitmapConfiguration; bitmapConfiguration.colorSpace.cgColorSpace = screenColorSpace(page.corePage()->mainFrame().view()); auto sharedBitmap = ShareableBitmap::createShareable(IntSize(bitmapSize), bitmapConfiguration); if (!sharedBitmap) return; auto graphicsContext = sharedBitmap->createGraphicsContext(); if (!graphicsContext) return; graphicsContext->drawImage(*image, FloatRect(0, 0, bitmapSize.width(), bitmapSize.height()), { renderImage.imageOrientation() }); info.image = sharedBitmap; } static void boundsPositionInformation(RenderElement& renderer, InteractionInformationAtPosition& info) { if (renderer.isRenderImage()) info.bounds = downcast<RenderImage>(renderer).absoluteContentQuad().enclosingBoundingBox(); else info.bounds = renderer.absoluteBoundingBoxRect(); if (!renderer.document().frame()->isMainFrame()) { FrameView *view = renderer.document().frame()->view(); info.bounds = view->contentsToRootView(info.bounds); } } static void elementPositionInformation(WebPage& page, Element& element, const InteractionInformationRequest& request, InteractionInformationAtPosition& info) { Element* linkElement = nullptr; if (element.renderer() && element.renderer()->isRenderImage()) linkElement = containingLinkElement(&element); else if (element.isLink()) linkElement = &element; info.isElement = true; info.idAttribute = element.getIdAttribute(); info.title = element.attributeWithoutSynchronization(HTMLNames::titleAttr).string(); if (linkElement && info.title.isEmpty()) info.title = element.innerText(); if (element.renderer()) info.touchCalloutEnabled = element.renderer()->style().touchCalloutEnabled(); if (linkElement) { info.isLink = true; info.url = linkElement->document().completeURL(stripLeadingAndTrailingHTMLSpaces(linkElement->getAttribute(HTMLNames::hrefAttr))); linkIndicatorPositionInformation(page, *linkElement, request, info); #if ENABLE(DATA_DETECTION) dataDetectorLinkPositionInformation(element, info); #endif } auto* elementForScrollTesting = linkElement ? linkElement : &element; if (auto* renderer = elementForScrollTesting->renderer()) { #if ENABLE(ASYNC_SCROLLING) if (auto* scrollingCoordinator = page.scrollingCoordinator()) info.containerScrollingNodeID = scrollingCoordinator->scrollableContainerNodeID(*renderer); #endif } if (auto* renderer = element.renderer()) { if (renderer->isRenderImage()) imagePositionInformation(page, element, request, info); boundsPositionInformation(*renderer, info); } info.elementContext = page.contextForElement(element); } static void selectionPositionInformation(WebPage& page, const InteractionInformationRequest& request, InteractionInformationAtPosition& info) { HitTestResult result = page.corePage()->mainFrame().eventHandler().hitTestResultAtPoint(request.point, HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::DisallowUserAgentShadowContent | HitTestRequest::AllowChildFrameContent); Node* hitNode = result.innerNode(); // Hit test could return HTMLHtmlElement that has no renderer, if the body is smaller than the document. if (!hitNode || !hitNode->renderer()) return; RenderObject* renderer = hitNode->renderer(); info.bounds = renderer->absoluteBoundingBoxRect(true); if (is<HTMLAttachmentElement>(*hitNode)) { info.isAttachment = true; HTMLAttachmentElement& attachment = downcast<HTMLAttachmentElement>(*hitNode); info.title = attachment.attachmentTitle(); linkIndicatorPositionInformation(page, attachment, request, info); if (attachment.file()) info.url = URL::fileURLWithFileSystemPath(downcast<HTMLAttachmentElement>(*hitNode).file()->path()); } else { info.isSelectable = renderer->style().userSelect() != UserSelect::None; // We don't want to select blocks that are larger than 97% of the visible area of the document. // FIXME: Is this heuristic still needed, now that block selection has been removed? if (info.isSelectable && !hitNode->isTextNode()) info.isSelectable = !isAssistableElement(*downcast<Element>(hitNode)) && !rectIsTooBigForSelection(info.bounds, *result.innerNodeFrame()); } for (auto currentNode = makeRefPtr(hitNode); currentNode; currentNode = currentNode->parentOrShadowHostNode()) { auto* renderer = currentNode->renderer(); if (!renderer) continue; auto& style = renderer->style(); if (style.userSelect() == UserSelect::None && style.userDrag() == UserDrag::Element) { info.prefersDraggingOverTextSelection = true; break; } } #if PLATFORM(MACCATALYST) bool isInsideFixedPosition; VisiblePosition caretPosition(renderer->positionForPoint(request.point, nullptr)); info.caretRect = caretPosition.absoluteCaretBounds(&isInsideFixedPosition); #endif } #if ENABLE(DATALIST_ELEMENT) static void textInteractionPositionInformation(WebPage& page, const HTMLInputElement& input, const InteractionInformationRequest& request, InteractionInformationAtPosition& info) { if (!input.list()) return; HitTestResult result = page.corePage()->mainFrame().eventHandler().hitTestResultAtPoint(request.point, HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::AllowChildFrameContent); if (result.innerNode() == input.dataListButtonElement()) info.preventTextInteraction = true; } #endif RefPtr<ShareableBitmap> WebPage::shareableBitmapSnapshotForNode(Element& element) { // Ensure that the image contains at most 600K pixels, so that it is not too big. if (RefPtr<WebImage> snapshot = snapshotNode(element, SnapshotOptionsShareable, 600 * 1024)) return &snapshot->bitmap(); return nullptr; } static bool canForceCaretForPosition(const VisiblePosition& position) { auto* node = position.deepEquivalent().anchorNode(); if (!node) return false; auto* renderer = node->renderer(); auto* style = renderer ? &renderer->style() : nullptr; auto cursorType = style ? style->cursor() : CursorType::Auto; if (cursorType == CursorType::Text) return true; if (cursorType != CursorType::Auto) return false; if (node->hasEditableStyle()) return true; if (!renderer) return false; return renderer->isText() && node->canStartSelection(); } static void populateCaretContext(const HitTestResult& hitTestResult, const InteractionInformationRequest& request, InteractionInformationAtPosition& info) { auto frame = makeRefPtr(hitTestResult.innerNodeFrame()); if (!frame) return; auto view = makeRefPtr(frame->view()); if (!view) return; auto node = hitTestResult.innerNode(); if (!node) return; auto* renderer = node->renderer(); if (!renderer) return; while (renderer && !is<RenderBlockFlow>(*renderer)) renderer = renderer->parent(); if (!renderer) return; // FIXME: We should be able to retrieve this geometry information without // forcing the text to fall out of Simple Line Layout. auto& blockFlow = downcast<RenderBlockFlow>(*renderer); auto position = frame->visiblePositionForPoint(view->rootViewToContents(request.point)); auto lineRect = position.absoluteSelectionBoundsForLine(); bool isEditable = node->hasEditableStyle(); if (isEditable) lineRect.setWidth(blockFlow.contentWidth()); info.lineCaretExtent = view->contentsToRootView(lineRect); info.caretHeight = info.lineCaretExtent.height(); bool lineContainsRequestPoint = info.lineCaretExtent.contains(request.point); // Force an I-beam cursor if the page didn't request a hand, and we're inside the bounds of the line. if (lineContainsRequestPoint && info.cursor->type() != Cursor::Hand && canForceCaretForPosition(position)) info.cursor = Cursor::fromType(Cursor::IBeam); if (!lineContainsRequestPoint && info.cursor->type() == Cursor::IBeam) { auto approximateLineRectInContentCoordinates = renderer->absoluteBoundingBoxRect(); approximateLineRectInContentCoordinates.setHeight(renderer->style().computedLineHeight()); info.lineCaretExtent = view->contentsToRootView(approximateLineRectInContentCoordinates); if (!info.lineCaretExtent.contains(request.point) || !isEditable) info.lineCaretExtent.setY(request.point.y() - info.lineCaretExtent.height() / 2); info.caretHeight = info.lineCaretExtent.height(); } auto nodeShouldNotUseIBeam = ^(Node* node) { if (!node) return false; RenderObject *renderer = node->renderer(); if (!renderer) return false; return is<RenderReplaced>(*renderer); }; const auto& deepPosition = position.deepEquivalent(); info.shouldNotUseIBeamInEditableContent = nodeShouldNotUseIBeam(node) || nodeShouldNotUseIBeam(deepPosition.computeNodeBeforePosition()) || nodeShouldNotUseIBeam(deepPosition.computeNodeAfterPosition()); } InteractionInformationAtPosition WebPage::positionInformation(const InteractionInformationRequest& request) { InteractionInformationAtPosition info; info.request = request; FloatPoint adjustedPoint; auto* nodeRespondingToClickEvents = m_page->mainFrame().nodeRespondingToClickEvents(request.point, adjustedPoint); info.adjustedPointForNodeRespondingToClickEvents = adjustedPoint; info.nodeAtPositionHasDoubleClickHandler = m_page->mainFrame().nodeRespondingToDoubleClickEvent(request.point, adjustedPoint); auto& eventHandler = m_page->mainFrame().eventHandler(); HitTestResult hitTestResultForCursor = eventHandler.hitTestResultAtPoint(request.point, HitTestRequest::ReadOnly | HitTestRequest::AllowFrameScrollbars | HitTestRequest::AllowChildFrameContent); if (auto* hitFrame = hitTestResultForCursor.innerNodeFrame()) { info.cursor = hitFrame->eventHandler().selectCursor(hitTestResultForCursor, false); if (request.includeCaretContext) populateCaretContext(hitTestResultForCursor, request, info); } if (m_focusedElement) focusedElementPositionInformation(*this, *m_focusedElement, request, info); if (is<Element>(nodeRespondingToClickEvents)) { auto& element = downcast<Element>(*nodeRespondingToClickEvents); elementPositionInformation(*this, element, request, info); if (info.isLink && !info.isImage && request.includeSnapshot) info.image = shareableBitmapSnapshotForNode(element); } if (!(info.isLink || info.isImage)) selectionPositionInformation(*this, request, info); // Prevent the callout bar from showing when tapping on the datalist button. #if ENABLE(DATALIST_ELEMENT) if (is<HTMLInputElement>(nodeRespondingToClickEvents)) textInteractionPositionInformation(*this, downcast<HTMLInputElement>(*nodeRespondingToClickEvents), request, info); #endif return info; } void WebPage::requestPositionInformation(const InteractionInformationRequest& request) { send(Messages::WebPageProxy::DidReceivePositionInformation(positionInformation(request))); } void WebPage::startInteractionWithElementContextOrPosition(Optional<WebCore::ElementContext>&& elementContext, WebCore::IntPoint&& point) { if (elementContext) { m_interactionNode = elementForContext(*elementContext); if (m_interactionNode) return; } FloatPoint adjustedPoint; m_interactionNode = m_page->mainFrame().nodeRespondingToInteraction(point, adjustedPoint); } void WebPage::stopInteraction() { m_interactionNode = nullptr; } void WebPage::performActionOnElement(uint32_t action) { if (!is<HTMLElement>(m_interactionNode.get())) return; HTMLElement& element = downcast<HTMLElement>(*m_interactionNode); if (!element.renderer()) return; if (static_cast<SheetAction>(action) == SheetAction::Copy) { if (is<RenderImage>(*element.renderer())) { URL url; String title; if (auto* linkElement = containingLinkElement(&element)) { url = linkElement->href(); title = linkElement->attributeWithoutSynchronization(HTMLNames::titleAttr); if (!title.length()) title = linkElement->textContent(); title = stripLeadingAndTrailingHTMLSpaces(title); } m_interactionNode->document().frame()->editor().writeImageToPasteboard(*Pasteboard::createForCopyAndPaste(), element, url, title); } else if (element.isLink()) { m_interactionNode->document().frame()->editor().copyURL(element.document().completeURL(stripLeadingAndTrailingHTMLSpaces(element.attributeWithoutSynchronization(HTMLNames::hrefAttr))), element.textContent()); } } else if (static_cast<SheetAction>(action) == SheetAction::SaveImage) { if (!is<RenderImage>(*element.renderer())) return; CachedImage* cachedImage = downcast<RenderImage>(*element.renderer()).cachedImage(); if (!cachedImage) return; RefPtr<SharedBuffer> buffer = cachedImage->resourceBuffer(); if (!buffer) return; uint64_t bufferSize = buffer->size(); RefPtr<SharedMemory> sharedMemoryBuffer = SharedMemory::allocate(bufferSize); memcpy(sharedMemoryBuffer->data(), buffer->data(), bufferSize); SharedMemory::Handle handle; sharedMemoryBuffer->createHandle(handle, SharedMemory::Protection::ReadOnly); send(Messages::WebPageProxy::SaveImageToLibrary(handle, bufferSize)); } } static inline Element* nextAssistableElement(Node* startNode, Page& page, bool isForward) { if (!is<Element>(startNode)) return nullptr; Element* nextElement = downcast<Element>(startNode); do { nextElement = isForward ? page.focusController().nextFocusableElement(*nextElement) : page.focusController().previousFocusableElement(*nextElement); } while (nextElement && !isAssistableElement(*nextElement)); return nextElement; } void WebPage::focusNextFocusedElement(bool isForward, CallbackID callbackID) { Element* nextElement = nextAssistableElement(m_focusedElement.get(), *m_page, isForward); m_userIsInteracting = true; if (nextElement) nextElement->focus(); m_userIsInteracting = false; send(Messages::WebPageProxy::VoidCallback(callbackID)); } void WebPage::getFocusedElementInformation(FocusedElementInformation& information) { auto focusedElement = m_focusedElement.copyRef(); layoutIfNeeded(); if (focusedElement != m_focusedElement) return; information.lastInteractionLocation = m_lastInteractionLocation; if (auto elementContext = contextForElement(*focusedElement)) information.elementContext = WTFMove(*elementContext); if (auto* renderer = focusedElement->renderer()) { information.interactionRect = rootViewInteractionBoundsForElement(*focusedElement); information.nodeFontSize = renderer->style().fontDescription().computedSize(); bool inFixed = false; renderer->localToContainerPoint(FloatPoint(), nullptr, UseTransforms, &inFixed); information.insideFixedPosition = inFixed; information.isRTL = renderer->style().direction() == TextDirection::RTL; } else information.interactionRect = { }; if (is<HTMLElement>(focusedElement)) information.isSpellCheckingEnabled = downcast<HTMLElement>(*focusedElement).spellcheck(); information.minimumScaleFactor = minimumPageScaleFactor(); information.maximumScaleFactor = maximumPageScaleFactor(); information.maximumScaleFactorIgnoringAlwaysScalable = maximumPageScaleFactorIgnoringAlwaysScalable(); information.allowsUserScaling = m_viewportConfiguration.allowsUserScaling(); information.allowsUserScalingIgnoringAlwaysScalable = m_viewportConfiguration.allowsUserScalingIgnoringAlwaysScalable(); if (auto* nextElement = nextAssistableElement(focusedElement.get(), *m_page, true)) { information.nextNodeRect = rootViewBoundsForElement(*nextElement); information.hasNextNode = true; } if (auto* previousElement = nextAssistableElement(focusedElement.get(), *m_page, false)) { information.previousNodeRect = rootViewBoundsForElement(*previousElement); information.hasPreviousNode = true; } information.focusedElementIdentifier = m_currentFocusedElementIdentifier; if (is<LabelableElement>(*focusedElement)) { auto labels = downcast<LabelableElement>(*focusedElement).labels(); Vector<Ref<Element>> associatedLabels; for (unsigned index = 0; index < labels->length(); ++index) { if (is<Element>(labels->item(index)) && labels->item(index)->renderer()) associatedLabels.append(downcast<Element>(*labels->item(index))); } for (auto& labelElement : associatedLabels) { auto text = labelElement->innerText(); if (!text.isEmpty()) { information.label = WTFMove(text); break; } } } information.title = focusedElement->title(); information.ariaLabel = focusedElement->attributeWithoutSynchronization(HTMLNames::aria_labelAttr); if (is<HTMLSelectElement>(*focusedElement)) { HTMLSelectElement& element = downcast<HTMLSelectElement>(*focusedElement); information.elementType = InputType::Select; const Vector<HTMLElement*>& items = element.listItems(); size_t count = items.size(); int parentGroupID = 0; // The parent group ID indicates the group the option belongs to and is 0 for group elements. // If there are option elements in between groups, they are given it's own group identifier. // If a select does not have groups, all the option elements have group ID 0. for (size_t i = 0; i < count; ++i) { HTMLElement* item = items[i]; if (is<HTMLOptionElement>(*item)) { HTMLOptionElement& option = downcast<HTMLOptionElement>(*item); information.selectOptions.append(OptionItem(option.text(), false, parentGroupID, option.selected(), option.hasAttributeWithoutSynchronization(WebCore::HTMLNames::disabledAttr))); } else if (is<HTMLOptGroupElement>(*item)) { HTMLOptGroupElement& group = downcast<HTMLOptGroupElement>(*item); parentGroupID++; information.selectOptions.append(OptionItem(group.groupLabelText(), true, 0, false, group.hasAttributeWithoutSynchronization(WebCore::HTMLNames::disabledAttr))); } } information.selectedIndex = element.selectedIndex(); information.isMultiSelect = element.multiple(); } else if (is<HTMLTextAreaElement>(*focusedElement)) { HTMLTextAreaElement& element = downcast<HTMLTextAreaElement>(*focusedElement); information.autocapitalizeType = element.autocapitalizeType(); information.isAutocorrect = element.shouldAutocorrect(); information.elementType = InputType::TextArea; information.isReadOnly = element.isReadOnly(); information.value = element.value(); information.autofillFieldName = WebCore::toAutofillFieldName(element.autofillData().fieldName); information.placeholder = element.attributeWithoutSynchronization(HTMLNames::placeholderAttr); information.inputMode = element.canonicalInputMode(); information.enterKeyHint = element.canonicalEnterKeyHint(); } else if (is<HTMLInputElement>(*focusedElement)) { HTMLInputElement& element = downcast<HTMLInputElement>(*focusedElement); HTMLFormElement* form = element.form(); if (form) information.formAction = form->getURLAttribute(WebCore::HTMLNames::actionAttr); if (auto autofillElements = WebCore::AutofillElements::computeAutofillElements(element)) { information.acceptsAutofilledLoginCredentials = true; information.isAutofillableUsernameField = autofillElements->username() == focusedElement; } information.representingPageURL = element.document().urlForBindings(); information.autocapitalizeType = element.autocapitalizeType(); information.isAutocorrect = element.shouldAutocorrect(); information.placeholder = element.attributeWithoutSynchronization(HTMLNames::placeholderAttr); if (element.isPasswordField()) information.elementType = InputType::Password; else if (element.isSearchField()) information.elementType = InputType::Search; else if (element.isEmailField()) information.elementType = InputType::Email; else if (element.isTelephoneField()) information.elementType = InputType::Phone; else if (element.isNumberField()) information.elementType = element.getAttribute("pattern") == "\\d*" || element.getAttribute("pattern") == "[0-9]*" ? InputType::NumberPad : InputType::Number; else if (element.isDateTimeLocalField()) information.elementType = InputType::DateTimeLocal; else if (element.isDateField()) information.elementType = InputType::Date; else if (element.isDateTimeField()) information.elementType = InputType::DateTime; else if (element.isTimeField()) information.elementType = InputType::Time; else if (element.isWeekField()) information.elementType = InputType::Week; else if (element.isMonthField()) information.elementType = InputType::Month; else if (element.isURLField()) information.elementType = InputType::URL; else if (element.isText()) { const AtomString& pattern = element.attributeWithoutSynchronization(HTMLNames::patternAttr); if (pattern == "\\d*" || pattern == "[0-9]*") information.elementType = InputType::NumberPad; else { information.elementType = InputType::Text; if (!information.formAction.isEmpty() && (element.getNameAttribute().contains("search") || element.getIdAttribute().contains("search") || element.attributeWithoutSynchronization(HTMLNames::titleAttr).contains("search"))) information.elementType = InputType::Search; } } #if ENABLE(INPUT_TYPE_COLOR) else if (element.isColorControl()) { information.elementType = InputType::Color; #if ENABLE(DATALIST_ELEMENT) information.suggestedColors = element.suggestedColors(); #endif } #endif #if ENABLE(DATALIST_ELEMENT) information.hasSuggestions = !!element.list(); #endif information.inputMode = element.canonicalInputMode(); information.enterKeyHint = element.canonicalEnterKeyHint(); information.isReadOnly = element.isReadOnly(); information.value = element.value(); information.valueAsNumber = element.valueAsNumber(); information.autofillFieldName = WebCore::toAutofillFieldName(element.autofillData().fieldName); } else if (is<HTMLImageElement>(*focusedElement) && downcast<HTMLImageElement>(*focusedElement).hasEditableImageAttribute()) { information.elementType = InputType::Drawing; information.embeddedViewID = downcast<HTMLImageElement>(*focusedElement).editableImageViewID(); } else if (focusedElement->hasEditableStyle()) { information.elementType = InputType::ContentEditable; if (is<HTMLElement>(*focusedElement)) { auto& focusedHTMLElement = downcast<HTMLElement>(*focusedElement); information.isAutocorrect = focusedHTMLElement.shouldAutocorrect(); information.autocapitalizeType = focusedHTMLElement.autocapitalizeType(); information.inputMode = focusedHTMLElement.canonicalInputMode(); information.enterKeyHint = focusedHTMLElement.canonicalEnterKeyHint(); information.shouldSynthesizeKeyEventsForEditing = focusedHTMLElement.document().settings().syntheticEditingCommandsEnabled(); } else { information.isAutocorrect = true; information.autocapitalizeType = AutocapitalizeTypeDefault; } information.isReadOnly = false; } if (focusedElement->document().quirks().shouldSuppressAutocorrectionAndAutocaptializationInHiddenEditableAreas() && isTransparentOrFullyClipped(*focusedElement)) { information.autocapitalizeType = AutocapitalizeTypeNone; information.isAutocorrect = false; } auto& quirks = focusedElement->document().quirks(); information.shouldAvoidResizingWhenInputViewBoundsChange = quirks.shouldAvoidResizingWhenInputViewBoundsChange(); information.shouldAvoidScrollingWhenFocusedContentIsVisible = quirks.shouldAvoidScrollingWhenFocusedContentIsVisible(); information.shouldUseLegacySelectPopoverDismissalBehaviorInDataActivation = quirks.shouldUseLegacySelectPopoverDismissalBehaviorInDataActivation(); } void WebPage::autofillLoginCredentials(const String& username, const String& password) { if (is<HTMLInputElement>(m_focusedElement.get())) { if (auto autofillElements = AutofillElements::computeAutofillElements(downcast<HTMLInputElement>(*m_focusedElement))) autofillElements->autofill(username, password); } } // WebCore stores the page scale factor as float instead of double. When we get a scale from WebCore, // we need to ignore differences that are within a small rounding error on floats. static inline bool areEssentiallyEqualAsFloat(float a, float b) { return WTF::areEssentiallyEqual(a, b); } void WebPage::setViewportConfigurationViewLayoutSize(const FloatSize& size, double scaleFactor, double minimumEffectiveDeviceWidth) { LOG_WITH_STREAM(VisibleRects, stream << "WebPage " << m_identifier << " setViewportConfigurationViewLayoutSize " << size << " scaleFactor " << scaleFactor << " minimumEffectiveDeviceWidth " << minimumEffectiveDeviceWidth); auto previousLayoutSizeScaleFactor = m_viewportConfiguration.layoutSizeScaleFactor(); auto clampedMinimumEffectiveDevice = m_viewportConfiguration.isKnownToLayOutWiderThanViewport() ? WTF::nullopt : Optional<double>(minimumEffectiveDeviceWidth); if (!m_viewportConfiguration.setViewLayoutSize(size, scaleFactor, WTFMove(clampedMinimumEffectiveDevice))) return; auto zoomToInitialScale = ZoomToInitialScale::No; auto newInitialScale = m_viewportConfiguration.initialScale(); auto currentPageScaleFactor = pageScaleFactor(); if (scaleFactor > previousLayoutSizeScaleFactor && newInitialScale > currentPageScaleFactor) zoomToInitialScale = ZoomToInitialScale::Yes; else if (scaleFactor < previousLayoutSizeScaleFactor && newInitialScale < currentPageScaleFactor) zoomToInitialScale = ZoomToInitialScale::Yes; viewportConfigurationChanged(zoomToInitialScale); } void WebPage::setMaximumUnobscuredSize(const FloatSize& maximumUnobscuredSize) { m_maximumUnobscuredSize = maximumUnobscuredSize; updateViewportSizeForCSSViewportUnits(); } void WebPage::setDeviceOrientation(int32_t deviceOrientation) { if (deviceOrientation == m_deviceOrientation) return; m_deviceOrientation = deviceOrientation; m_page->mainFrame().orientationChanged(); } void WebPage::setOverrideViewportArguments(const Optional<WebCore::ViewportArguments>& arguments) { m_page->setOverrideViewportArguments(arguments); } void WebPage::dynamicViewportSizeUpdate(const FloatSize& viewLayoutSize, const WebCore::FloatSize& maximumUnobscuredSize, const FloatRect& targetExposedContentRect, const FloatRect& targetUnobscuredRect, const WebCore::FloatRect& targetUnobscuredRectInScrollViewCoordinates, const WebCore::FloatBoxExtent& targetUnobscuredSafeAreaInsets, double targetScale, int32_t deviceOrientation, double minimumEffectiveDeviceWidth, DynamicViewportSizeUpdateID dynamicViewportSizeUpdateID) { SetForScope<bool> dynamicSizeUpdateGuard(m_inDynamicSizeUpdate, true); // FIXME: this does not handle the cases where the content would change the content size or scroll position from JavaScript. // To handle those cases, we would need to redo this computation on every change until the next visible content rect update. LOG_WITH_STREAM(VisibleRects, stream << "\nWebPage::dynamicViewportSizeUpdate - viewLayoutSize " << viewLayoutSize << " targetUnobscuredRect " << targetUnobscuredRect << " targetExposedContentRect " << targetExposedContentRect << " targetScale " << targetScale); FrameView& frameView = *m_page->mainFrame().view(); IntSize oldContentSize = frameView.contentsSize(); float oldPageScaleFactor = m_page->pageScaleFactor(); m_dynamicSizeUpdateHistory.add(std::make_pair(oldContentSize, oldPageScaleFactor), frameView.scrollPosition()); RefPtr<Node> oldNodeAtCenter; double visibleHorizontalFraction = 1; float relativeHorizontalPositionInNodeAtCenter = 0; float relativeVerticalPositionInNodeAtCenter = 0; { visibleHorizontalFraction = frameView.unobscuredContentSize().width() / oldContentSize.width(); IntPoint unobscuredContentRectCenter = frameView.unobscuredContentRect().center(); HitTestResult hitTestResult = HitTestResult(unobscuredContentRectCenter); if (auto* document = frameView.frame().document()) document->hitTest(HitTestRequest(), hitTestResult); if (Node* node = hitTestResult.innerNode()) { if (RenderObject* renderer = node->renderer()) { FrameView& containingView = *node->document().frame()->view(); FloatRect boundingBox = containingView.contentsToRootView(renderer->absoluteBoundingBoxRect(true)); relativeHorizontalPositionInNodeAtCenter = (unobscuredContentRectCenter.x() - boundingBox.x()) / boundingBox.width(); relativeVerticalPositionInNodeAtCenter = (unobscuredContentRectCenter.y() - boundingBox.y()) / boundingBox.height(); oldNodeAtCenter = node; } } } LOG_WITH_STREAM(VisibleRects, stream << "WebPage::dynamicViewportSizeUpdate setting view layout size to " << viewLayoutSize); bool viewportChanged = m_viewportConfiguration.setIsKnownToLayOutWiderThanViewport(false); viewportChanged |= m_viewportConfiguration.setViewLayoutSize(viewLayoutSize, WTF::nullopt, minimumEffectiveDeviceWidth); if (viewportChanged) viewportConfigurationChanged(); IntSize newLayoutSize = m_viewportConfiguration.layoutSize(); #if ENABLE(TEXT_AUTOSIZING) if (setFixedLayoutSize(newLayoutSize)) resetTextAutosizing(); #endif setMaximumUnobscuredSize(maximumUnobscuredSize); m_page->setUnobscuredSafeAreaInsets(targetUnobscuredSafeAreaInsets); frameView.updateLayoutAndStyleIfNeededRecursive(); IntSize newContentSize = frameView.contentsSize(); double scale = scaleAfterViewportWidthChange(targetScale, m_userHasChangedPageScaleFactor, m_viewportConfiguration, targetUnobscuredRectInScrollViewCoordinates.width(), newContentSize, oldContentSize, visibleHorizontalFraction); FloatRect newUnobscuredContentRect = targetUnobscuredRect; FloatRect newExposedContentRect = targetExposedContentRect; bool scaleChanged = !areEssentiallyEqualAsFloat(scale, targetScale); if (scaleChanged) { // The target scale the UI is using cannot be reached by the content. We need to compute new targets based // on the viewport constraint and report everything back to the UIProcess. // 1) Compute a new unobscured rect centered around the original one. double scaleDifference = targetScale / scale; double newUnobscuredRectWidth = targetUnobscuredRect.width() * scaleDifference; double newUnobscuredRectHeight = targetUnobscuredRect.height() * scaleDifference; double newUnobscuredRectX = targetUnobscuredRect.x() - (newUnobscuredRectWidth - targetUnobscuredRect.width()) / 2; double newUnobscuredRectY = targetUnobscuredRect.y() - (newUnobscuredRectHeight - targetUnobscuredRect.height()) / 2; newUnobscuredContentRect = FloatRect(newUnobscuredRectX, newUnobscuredRectY, newUnobscuredRectWidth, newUnobscuredRectHeight); // 2) Extend our new unobscuredRect by the obscured margins to get a new exposed rect. double obscuredTopMargin = (targetUnobscuredRect.y() - targetExposedContentRect.y()) * scaleDifference; double obscuredLeftMargin = (targetUnobscuredRect.x() - targetExposedContentRect.x()) * scaleDifference; double obscuredBottomMargin = (targetExposedContentRect.maxY() - targetUnobscuredRect.maxY()) * scaleDifference; double obscuredRightMargin = (targetExposedContentRect.maxX() - targetUnobscuredRect.maxX()) * scaleDifference; newExposedContentRect = FloatRect(newUnobscuredRectX - obscuredLeftMargin, newUnobscuredRectY - obscuredTopMargin, newUnobscuredRectWidth + obscuredLeftMargin + obscuredRightMargin, newUnobscuredRectHeight + obscuredTopMargin + obscuredBottomMargin); } if (oldContentSize != newContentSize || scaleChanged) { // Snap the new unobscured rect back into the content rect. newUnobscuredContentRect.setWidth(std::min(static_cast<float>(newContentSize.width()), newUnobscuredContentRect.width())); newUnobscuredContentRect.setHeight(std::min(static_cast<float>(newContentSize.height()), newUnobscuredContentRect.height())); bool positionWasRestoredFromSizeUpdateHistory = false; const auto& previousPosition = m_dynamicSizeUpdateHistory.find(std::pair<IntSize, float>(newContentSize, scale)); if (previousPosition != m_dynamicSizeUpdateHistory.end()) { IntPoint restoredPosition = previousPosition->value; FloatPoint deltaPosition(restoredPosition.x() - newUnobscuredContentRect.x(), restoredPosition.y() - newUnobscuredContentRect.y()); newUnobscuredContentRect.moveBy(deltaPosition); newExposedContentRect.moveBy(deltaPosition); positionWasRestoredFromSizeUpdateHistory = true; } else if (oldContentSize != newContentSize) { FloatPoint newRelativeContentCenter; if (RenderObject* renderer = oldNodeAtCenter ? oldNodeAtCenter->renderer() : nullptr) { FrameView& containingView = *oldNodeAtCenter->document().frame()->view(); FloatRect newBoundingBox = containingView.contentsToRootView(renderer->absoluteBoundingBoxRect(true)); newRelativeContentCenter = FloatPoint(newBoundingBox.x() + relativeHorizontalPositionInNodeAtCenter * newBoundingBox.width(), newBoundingBox.y() + relativeVerticalPositionInNodeAtCenter * newBoundingBox.height()); } else newRelativeContentCenter = relativeCenterAfterContentSizeChange(targetUnobscuredRect, oldContentSize, newContentSize); FloatPoint newUnobscuredContentRectCenter = newUnobscuredContentRect.center(); FloatPoint positionDelta(newRelativeContentCenter.x() - newUnobscuredContentRectCenter.x(), newRelativeContentCenter.y() - newUnobscuredContentRectCenter.y()); newUnobscuredContentRect.moveBy(positionDelta); newExposedContentRect.moveBy(positionDelta); } // Make the top/bottom edges "sticky" within 1 pixel. if (!positionWasRestoredFromSizeUpdateHistory) { if (targetUnobscuredRect.maxY() > oldContentSize.height() - 1) { float bottomVerticalPosition = newContentSize.height() - newUnobscuredContentRect.height(); newUnobscuredContentRect.setY(bottomVerticalPosition); newExposedContentRect.setY(bottomVerticalPosition); } if (targetUnobscuredRect.y() < 1) { newUnobscuredContentRect.setY(0); newExposedContentRect.setY(0); } bool likelyResponsiveDesignViewport = newLayoutSize.width() == viewLayoutSize.width() && areEssentiallyEqualAsFloat(scale, 1); bool contentBleedsOutsideLayoutWidth = newContentSize.width() > newLayoutSize.width(); bool originalScrollPositionWasOnTheLeftEdge = targetUnobscuredRect.x() <= 0; if (likelyResponsiveDesignViewport && contentBleedsOutsideLayoutWidth && originalScrollPositionWasOnTheLeftEdge) { // This is a special heuristics for "responsive" design with odd layout. It is quite common for responsive design // to have content "bleeding" outside of the minimal layout width, usually from an image or table larger than expected. // In those cases, the design usually does not adapt to the new width and remain at the newLayoutSize except for the // large boxes. // It is worth revisiting this special case as web developers get better with responsive design. newExposedContentRect.setX(0); newUnobscuredContentRect.setX(0); } } float horizontalAdjustment = 0; if (newUnobscuredContentRect.maxX() > newContentSize.width()) horizontalAdjustment -= newUnobscuredContentRect.maxX() - newContentSize.width(); float verticalAdjustment = 0; if (newUnobscuredContentRect.maxY() > newContentSize.height()) verticalAdjustment -= newUnobscuredContentRect.maxY() - newContentSize.height(); if (newUnobscuredContentRect.x() < 0) horizontalAdjustment += - newUnobscuredContentRect.x(); if (newUnobscuredContentRect.y() < 0) verticalAdjustment += - newUnobscuredContentRect.y(); FloatPoint adjustmentDelta(horizontalAdjustment, verticalAdjustment); newUnobscuredContentRect.moveBy(adjustmentDelta); newExposedContentRect.moveBy(adjustmentDelta); } frameView.setScrollVelocity({ 0, 0, 0, MonotonicTime::now() }); IntPoint roundedUnobscuredContentRectPosition = roundedIntPoint(newUnobscuredContentRect.location()); frameView.setUnobscuredContentSize(newUnobscuredContentRect.size()); m_drawingArea->setExposedContentRect(newExposedContentRect); scalePage(scale, roundedUnobscuredContentRectPosition); frameView.updateLayoutAndStyleIfNeededRecursive(); auto& settings = frameView.frame().settings(); LayoutRect documentRect = IntRect(frameView.scrollOrigin(), frameView.contentsSize()); auto layoutViewportSize = FrameView::expandedLayoutViewportSize(frameView.baseLayoutViewportSize(), LayoutSize(documentRect.size()), settings.layoutViewportHeightExpansionFactor()); LayoutRect layoutViewportRect = FrameView::computeUpdatedLayoutViewportRect(frameView.layoutViewportRect(), documentRect, LayoutSize(newUnobscuredContentRect.size()), LayoutRect(newUnobscuredContentRect), layoutViewportSize, frameView.minStableLayoutViewportOrigin(), frameView.maxStableLayoutViewportOrigin(), FrameView::LayoutViewportConstraint::ConstrainedToDocumentRect); frameView.setLayoutViewportOverrideRect(layoutViewportRect); frameView.layoutOrVisualViewportChanged(); frameView.setCustomSizeForResizeEvent(expandedIntSize(targetUnobscuredRectInScrollViewCoordinates.size())); setDeviceOrientation(deviceOrientation); frameView.setScrollOffset(roundedUnobscuredContentRectPosition); m_page->updateRendering(); #if ENABLE(VIEWPORT_RESIZING) if (immediatelyShrinkToFitContent()) viewportConfigurationChanged(); #endif m_drawingArea->scheduleCompositingLayerFlush(); m_pendingDynamicViewportSizeUpdateID = dynamicViewportSizeUpdateID; } void WebPage::resetViewportDefaultConfiguration(WebFrame* frame, bool hasMobileDocType) { LOG_WITH_STREAM(VisibleRects, stream << "WebPage " << m_identifier << " resetViewportDefaultConfiguration"); if (m_useTestingViewportConfiguration) { m_viewportConfiguration.setDefaultConfiguration(ViewportConfiguration::testingParameters()); return; } auto parametersForStandardFrame = [&] { if (shouldIgnoreMetaViewport()) return m_viewportConfiguration.nativeWebpageParameters(); return ViewportConfiguration::webpageParameters(); }; if (!frame) { m_viewportConfiguration.setDefaultConfiguration(parametersForStandardFrame()); return; } if (hasMobileDocType) { m_viewportConfiguration.setDefaultConfiguration(ViewportConfiguration::xhtmlMobileParameters()); return; } auto* document = frame->coreFrame()->document(); if (document->isImageDocument()) m_viewportConfiguration.setDefaultConfiguration(ViewportConfiguration::imageDocumentParameters()); else if (document->isTextDocument()) m_viewportConfiguration.setDefaultConfiguration(ViewportConfiguration::textDocumentParameters()); else m_viewportConfiguration.setDefaultConfiguration(parametersForStandardFrame()); } #if ENABLE(TEXT_AUTOSIZING) void WebPage::resetIdempotentTextAutosizingIfNeeded(double previousInitialScale) { if (!m_page->settings().textAutosizingEnabled() || !m_page->settings().textAutosizingUsesIdempotentMode()) return; const float minimumScaleChangeBeforeRecomputingTextAutosizing = 0.01; if (std::abs(previousInitialScale - m_page->initialScale()) < minimumScaleChangeBeforeRecomputingTextAutosizing) return; if (m_page->initialScale() >= 1 && previousInitialScale >= 1) return; if (!m_page->mainFrame().view()) return; auto textAutoSizingDelay = [&] { auto& frameView = *m_page->mainFrame().view(); auto isVisaullyNonEmpty = frameView.isVisuallyNonEmpty(); auto willBeVisuallyNonEmptySoon = !isVisaullyNonEmpty && frameView.qualifiesAsVisuallyNonEmpty(); if (willBeVisuallyNonEmptySoon) { // Be a bit more agressive on the first display. const Seconds shortTextAutoSizingDelayOnViewportChange = 20_ms; return shortTextAutoSizingDelayOnViewportChange; } if (!isVisaullyNonEmpty) { // We don't anticipate any paining after the next upcoming layout. const Seconds longTextAutoSizingDelayOnViewportChange = 100_ms; return longTextAutoSizingDelayOnViewportChange; } const Seconds defaultTextAutoSizingDelayOnViewportChange = 80_ms; return defaultTextAutoSizingDelayOnViewportChange; }; // We don't need to update text sizing eagerly. There might be multiple incoming dynamic viewport changes. m_textAutoSizingAdjustmentTimer.startOneShot(textAutoSizingDelay()); } void WebPage::resetTextAutosizing() { for (Frame* frame = &m_page->mainFrame(); frame; frame = frame->tree().traverseNext()) { Document* document = frame->document(); if (!document || !document->renderView()) continue; document->renderView()->resetTextAutosizing(); } } #endif #if ENABLE(VIEWPORT_RESIZING) void WebPage::scheduleShrinkToFitContent() { if (m_isClosed) return; m_shrinkToFitContentTimer.restart(); } void WebPage::shrinkToFitContentTimerFired() { if (immediatelyShrinkToFitContent()) viewportConfigurationChanged(ZoomToInitialScale::Yes); } bool WebPage::immediatelyShrinkToFitContent() { if (m_isClosed) return false; if (!m_page->settings().allowViewportShrinkToFitContent()) return false; if (m_useTestingViewportConfiguration) return false; if (!shouldIgnoreMetaViewport()) return false; if (!m_viewportConfiguration.viewportArguments().shrinkToFit) return false; if (m_viewportConfiguration.canIgnoreScalingConstraints()) return false; auto mainFrame = makeRefPtr(m_mainFrame->coreFrame()); if (!mainFrame) return false; auto view = makeRefPtr(mainFrame->view()); auto mainDocument = makeRefPtr(mainFrame->document()); if (!view || !mainDocument) return false; mainDocument->updateLayout(); static const int toleratedHorizontalScrollingDistance = 20; static const int maximumExpandedLayoutWidth = 1280; static const int maximumContentWidthBeforeAvoidingShrinkToFit = 1920; auto scaledViewWidth = [&] () -> int { return std::round(m_viewportConfiguration.viewLayoutSize().width() / m_viewportConfiguration.initialScale()); }; int originalContentWidth = view->contentsWidth(); int originalViewWidth = scaledViewWidth(); int originalLayoutWidth = m_viewportConfiguration.layoutWidth(); int originalHorizontalOverflowAmount = originalContentWidth - originalViewWidth; if (originalHorizontalOverflowAmount <= toleratedHorizontalScrollingDistance || originalLayoutWidth >= maximumExpandedLayoutWidth || originalContentWidth <= originalViewWidth || originalContentWidth > maximumContentWidthBeforeAvoidingShrinkToFit) return false; auto changeMinimumEffectiveDeviceWidth = [this, mainDocument] (int targetLayoutWidth) -> bool { if (m_viewportConfiguration.setMinimumEffectiveDeviceWidth(targetLayoutWidth)) { viewportConfigurationChanged(); mainDocument->updateLayout(); return true; } return false; }; m_viewportConfiguration.setIsKnownToLayOutWiderThanViewport(true); double originalMinimumDeviceWidth = m_viewportConfiguration.minimumEffectiveDeviceWidth(); if (changeMinimumEffectiveDeviceWidth(std::min(maximumExpandedLayoutWidth, originalContentWidth)) && view->contentsWidth() - scaledViewWidth() > originalHorizontalOverflowAmount) { changeMinimumEffectiveDeviceWidth(originalMinimumDeviceWidth); m_viewportConfiguration.setIsKnownToLayOutWiderThanViewport(false); } // FIXME (197429): Consider additionally logging an error message to the console if a responsive meta viewport tag was used. RELEASE_LOG(ViewportSizing, "Shrink-to-fit: content width %d => %d; layout width %d => %d", originalContentWidth, view->contentsWidth(), originalLayoutWidth, m_viewportConfiguration.layoutWidth()); return true; } #endif // ENABLE(VIEWPORT_RESIZING) bool WebPage::shouldIgnoreMetaViewport() const { if (auto* mainDocument = m_page->mainFrame().document()) { auto* loader = mainDocument->loader(); if (loader && loader->metaViewportPolicy() == WebCore::MetaViewportPolicy::Ignore) return true; } return m_page->settings().shouldIgnoreMetaViewport(); } void WebPage::viewportConfigurationChanged(ZoomToInitialScale zoomToInitialScale) { double initialScale = m_viewportConfiguration.initialScale(); #if ENABLE(TEXT_AUTOSIZING) double previousInitialScale = m_page->initialScale(); m_page->setInitialScale(initialScale); resetIdempotentTextAutosizingIfNeeded(previousInitialScale); if (setFixedLayoutSize(m_viewportConfiguration.layoutSize())) resetTextAutosizing(); #endif double scale; if (m_userHasChangedPageScaleFactor && zoomToInitialScale == ZoomToInitialScale::No) scale = std::max(std::min(pageScaleFactor(), m_viewportConfiguration.maximumScale()), m_viewportConfiguration.minimumScale()); else scale = initialScale; LOG_WITH_STREAM(VisibleRects, stream << "WebPage " << m_identifier << " viewportConfigurationChanged - setting zoomedOutPageScaleFactor to " << m_viewportConfiguration.minimumScale() << " and scale to " << scale); m_page->setZoomedOutPageScaleFactor(m_viewportConfiguration.minimumScale()); updateViewportSizeForCSSViewportUnits(); FrameView& frameView = *mainFrameView(); IntPoint scrollPosition = frameView.scrollPosition(); if (!m_hasReceivedVisibleContentRectsAfterDidCommitLoad) { FloatSize minimumLayoutSizeInScrollViewCoordinates = m_viewportConfiguration.viewLayoutSize(); minimumLayoutSizeInScrollViewCoordinates.scale(1 / scale); IntSize minimumLayoutSizeInDocumentCoordinates = roundedIntSize(minimumLayoutSizeInScrollViewCoordinates); frameView.setUnobscuredContentSize(minimumLayoutSizeInDocumentCoordinates); frameView.setScrollVelocity({ 0, 0, 0, MonotonicTime::now() }); // FIXME: We could send down the obscured margins to find a better exposed rect and unobscured rect. // It is not a big deal at the moment because the tile coverage will always extend past the obscured bottom inset. if (!m_hasRestoredExposedContentRectAfterDidCommitLoad) m_drawingArea->setExposedContentRect(FloatRect(scrollPosition, minimumLayoutSizeInDocumentCoordinates)); } scalePage(scale, scrollPosition); if (!m_hasReceivedVisibleContentRectsAfterDidCommitLoad) { // This takes scale into account, so do after the scale change. frameView.setCustomFixedPositionLayoutRect(enclosingIntRect(frameView.viewportConstrainedObjectsRect())); frameView.setCustomSizeForResizeEvent(expandedIntSize(m_viewportConfiguration.minimumLayoutSize())); } } void WebPage::updateViewportSizeForCSSViewportUnits() { FloatSize largestUnobscuredSize = m_maximumUnobscuredSize; if (largestUnobscuredSize.isEmpty()) largestUnobscuredSize = m_viewportConfiguration.viewLayoutSize(); FrameView& frameView = *mainFrameView(); largestUnobscuredSize.scale(1 / m_viewportConfiguration.initialScaleIgnoringContentSize()); frameView.setViewportSizeForCSSViewportUnits(roundedIntSize(largestUnobscuredSize)); } void WebPage::applicationWillResignActive() { [[NSNotificationCenter defaultCenter] postNotificationName:WebUIApplicationWillResignActiveNotification object:nil]; if (m_page) m_page->applicationWillResignActive(); } void WebPage::applicationDidEnterBackground(bool isSuspendedUnderLock) { [[NSNotificationCenter defaultCenter] postNotificationName:WebUIApplicationDidEnterBackgroundNotification object:nil userInfo:@{@"isSuspendedUnderLock": [NSNumber numberWithBool:isSuspendedUnderLock]}]; m_isSuspendedUnderLock = isSuspendedUnderLock; freezeLayerTree(LayerTreeFreezeReason::BackgroundApplication); if (m_page) m_page->applicationDidEnterBackground(); } void WebPage::applicationDidFinishSnapshottingAfterEnteringBackground() { markLayersVolatile(); } void WebPage::applicationWillEnterForeground(bool isSuspendedUnderLock) { m_isSuspendedUnderLock = false; cancelMarkLayersVolatile(); unfreezeLayerTree(LayerTreeFreezeReason::BackgroundApplication); [[NSNotificationCenter defaultCenter] postNotificationName:WebUIApplicationWillEnterForegroundNotification object:nil userInfo:@{@"isSuspendedUnderLock": @(isSuspendedUnderLock)}]; if (m_page) m_page->applicationWillEnterForeground(); } void WebPage::applicationDidBecomeActive() { [[NSNotificationCenter defaultCenter] postNotificationName:WebUIApplicationDidBecomeActiveNotification object:nil]; if (m_page) m_page->applicationDidBecomeActive(); } static inline void adjustVelocityDataForBoundedScale(VelocityData& velocityData, double exposedRectScale, double minimumScale, double maximumScale) { if (velocityData.scaleChangeRate) { velocityData.horizontalVelocity = 0; velocityData.verticalVelocity = 0; } if (exposedRectScale >= maximumScale || exposedRectScale <= minimumScale) velocityData.scaleChangeRate = 0; } Optional<float> WebPage::scaleFromUIProcess(const VisibleContentRectUpdateInfo& visibleContentRectUpdateInfo) const { auto transactionIDForLastScaleFromUIProcess = visibleContentRectUpdateInfo.lastLayerTreeTransactionID(); if (m_lastTransactionIDWithScaleChange > transactionIDForLastScaleFromUIProcess) return WTF::nullopt; float scaleFromUIProcess = visibleContentRectUpdateInfo.scale(); float currentScale = m_page->pageScaleFactor(); double scaleNoiseThreshold = 0.005; if (!m_isInStableState && fabs(scaleFromUIProcess - currentScale) < scaleNoiseThreshold) { // Tiny changes of scale during interactive zoom cause content to jump by one pixel, creating // visual noise. We filter those useless updates. scaleFromUIProcess = currentScale; } scaleFromUIProcess = std::min<float>(m_viewportConfiguration.maximumScale(), std::max<float>(m_viewportConfiguration.minimumScale(), scaleFromUIProcess)); if (areEssentiallyEqualAsFloat(currentScale, scaleFromUIProcess)) return WTF::nullopt; return scaleFromUIProcess; } static bool selectionIsInsideFixedPositionContainer(Frame& frame) { auto& selection = frame.selection().selection(); if (selection.isNone()) return false; bool isInsideFixedPosition = false; if (selection.isCaret()) { frame.selection().absoluteCaretBounds(&isInsideFixedPosition); return isInsideFixedPosition; } selection.visibleStart().absoluteCaretBounds(&isInsideFixedPosition); if (isInsideFixedPosition) return true; selection.visibleEnd().absoluteCaretBounds(&isInsideFixedPosition); return isInsideFixedPosition; } void WebPage::updateVisibleContentRects(const VisibleContentRectUpdateInfo& visibleContentRectUpdateInfo, MonotonicTime oldestTimestamp) { LOG_WITH_STREAM(VisibleRects, stream << "\nWebPage " << m_identifier << " updateVisibleContentRects " << visibleContentRectUpdateInfo); // Skip any VisibleContentRectUpdate that have been queued before DidCommitLoad suppresses the updates in the UIProcess. if (visibleContentRectUpdateInfo.lastLayerTreeTransactionID() < m_mainFrame->firstLayerTreeTransactionIDAfterDidCommitLoad() && !visibleContentRectUpdateInfo.isFirstUpdateForNewViewSize()) return; m_hasReceivedVisibleContentRectsAfterDidCommitLoad = true; m_isInStableState = visibleContentRectUpdateInfo.inStableState(); auto scaleFromUIProcess = this->scaleFromUIProcess(visibleContentRectUpdateInfo); // Skip progressively redrawing tiles if pinch-zooming while the system is under memory pressure. if (scaleFromUIProcess && !m_isInStableState && MemoryPressureHandler::singleton().isUnderMemoryPressure()) return; if (m_isInStableState) m_hasStablePageScaleFactor = true; else { if (!m_oldestNonStableUpdateVisibleContentRectsTimestamp) m_oldestNonStableUpdateVisibleContentRectsTimestamp = oldestTimestamp; } float scaleToUse = scaleFromUIProcess.valueOr(m_page->pageScaleFactor()); FloatRect exposedContentRect = visibleContentRectUpdateInfo.exposedContentRect(); FloatRect adjustedExposedContentRect = adjustExposedRectForNewScale(exposedContentRect, visibleContentRectUpdateInfo.scale(), scaleToUse); m_drawingArea->setExposedContentRect(adjustedExposedContentRect); IntPoint scrollPosition = roundedIntPoint(visibleContentRectUpdateInfo.unobscuredContentRect().location()); bool hasSetPageScale = false; if (scaleFromUIProcess) { m_scaleWasSetByUIProcess = true; m_hasStablePageScaleFactor = m_isInStableState; m_dynamicSizeUpdateHistory.clear(); m_page->setPageScaleFactor(scaleFromUIProcess.value(), scrollPosition, m_isInStableState); hasSetPageScale = true; send(Messages::WebPageProxy::PageScaleFactorDidChange(scaleFromUIProcess.value())); } if (!hasSetPageScale && m_isInStableState) { m_page->setPageScaleFactor(scaleToUse, scrollPosition, true); hasSetPageScale = true; } auto& frame = m_page->mainFrame(); FrameView& frameView = *frame.view(); if (scrollPosition != frameView.scrollPosition()) m_dynamicSizeUpdateHistory.clear(); if (m_viewportConfiguration.setCanIgnoreScalingConstraints(m_ignoreViewportScalingConstraints || visibleContentRectUpdateInfo.allowShrinkToFit())) viewportConfigurationChanged(); frameView.setUnobscuredContentSize(visibleContentRectUpdateInfo.unobscuredContentRect().size()); m_page->setContentInsets(visibleContentRectUpdateInfo.contentInsets()); m_page->setObscuredInsets(visibleContentRectUpdateInfo.obscuredInsets()); m_page->setUnobscuredSafeAreaInsets(visibleContentRectUpdateInfo.unobscuredSafeAreaInsets()); m_page->setEnclosedInScrollableAncestorView(visibleContentRectUpdateInfo.enclosedInScrollableAncestorView()); VelocityData scrollVelocity = visibleContentRectUpdateInfo.scrollVelocity(); adjustVelocityDataForBoundedScale(scrollVelocity, visibleContentRectUpdateInfo.scale(), m_viewportConfiguration.minimumScale(), m_viewportConfiguration.maximumScale()); frameView.setScrollVelocity(scrollVelocity); if (m_isInStableState) { if (visibleContentRectUpdateInfo.unobscuredContentRect() != visibleContentRectUpdateInfo.unobscuredContentRectRespectingInputViewBounds()) frameView.setVisualViewportOverrideRect(LayoutRect(visibleContentRectUpdateInfo.unobscuredContentRectRespectingInputViewBounds())); else frameView.setVisualViewportOverrideRect(WTF::nullopt); LOG_WITH_STREAM(VisibleRects, stream << "WebPage::updateVisibleContentRects - setLayoutViewportOverrideRect " << visibleContentRectUpdateInfo.layoutViewportRect()); frameView.setLayoutViewportOverrideRect(LayoutRect(visibleContentRectUpdateInfo.layoutViewportRect())); if (selectionIsInsideFixedPositionContainer(frame)) { // Ensure that the next layer tree commit contains up-to-date caret/selection rects. frameView.frame().selection().setCaretRectNeedsUpdate(); scheduleFullEditorStateUpdate(); } frameView.layoutOrVisualViewportChanged(); } if (!visibleContentRectUpdateInfo.isChangingObscuredInsetsInteractively()) frameView.setCustomSizeForResizeEvent(expandedIntSize(visibleContentRectUpdateInfo.unobscuredRectInScrollViewCoordinates().size())); if (ScrollingCoordinator* scrollingCoordinator = this->scrollingCoordinator()) { ViewportRectStability viewportStability = ViewportRectStability::Stable; ScrollingLayerPositionAction layerAction = ScrollingLayerPositionAction::Sync; if (visibleContentRectUpdateInfo.isChangingObscuredInsetsInteractively()) { viewportStability = ViewportRectStability::ChangingObscuredInsetsInteractively; layerAction = ScrollingLayerPositionAction::SetApproximate; } else if (!m_isInStableState) { viewportStability = ViewportRectStability::Unstable; layerAction = ScrollingLayerPositionAction::SetApproximate; } scrollingCoordinator->reconcileScrollingState(frameView, scrollPosition, visibleContentRectUpdateInfo.layoutViewportRect(), ScrollType::User, viewportStability, layerAction); } } void WebPage::willStartUserTriggeredZooming() { m_page->diagnosticLoggingClient().logDiagnosticMessage(DiagnosticLoggingKeys::webViewKey(), DiagnosticLoggingKeys::userZoomActionKey(), ShouldSample::No); m_userHasChangedPageScaleFactor = true; } #if ENABLE(IOS_TOUCH_EVENTS) void WebPage::dispatchAsynchronousTouchEvents(const Vector<std::pair<WebTouchEvent, Optional<CallbackID>>, 1>& queue) { for (auto& eventAndCallbackID : queue) { bool handled; dispatchTouchEvent(eventAndCallbackID.first, handled); if (eventAndCallbackID.second) send(Messages::WebPageProxy::BoolCallback(handled, *eventAndCallbackID.second)); } } #endif void WebPage::computePagesForPrintingAndDrawToPDF(WebCore::FrameIdentifier frameID, const PrintInfo& printInfo, CallbackID callbackID, Messages::WebPage::ComputePagesForPrintingAndDrawToPDF::DelayedReply&& reply) { if (printInfo.snapshotFirstPage) { reply(1); IntSize snapshotSize { FloatSize { printInfo.availablePaperWidth, printInfo.availablePaperHeight } }; IntRect snapshotRect { {0, 0}, snapshotSize }; auto& frameView = *m_page->mainFrame().view(); auto originalLayoutViewportOverrideRect = frameView.layoutViewportOverrideRect(); frameView.setLayoutViewportOverrideRect(LayoutRect(snapshotRect)); auto pdfData = pdfSnapshotAtSize(snapshotRect, snapshotSize, 0); frameView.setLayoutViewportOverrideRect(originalLayoutViewportOverrideRect); send(Messages::WebPageProxy::DrawToPDFCallback(IPC::DataReference(CFDataGetBytePtr(pdfData.get()), CFDataGetLength(pdfData.get())), callbackID)); return; } Vector<WebCore::IntRect> pageRects; double totalScaleFactor; auto margin = printInfo.margin; computePagesForPrintingImpl(frameID, printInfo, pageRects, totalScaleFactor, margin); ASSERT(pageRects.size() >= 1); std::size_t pageCount = pageRects.size(); ASSERT(pageCount <= std::numeric_limits<uint32_t>::max()); reply(pageCount); RetainPtr<CFMutableDataRef> pdfPageData; drawPagesToPDFImpl(frameID, printInfo, 0, pageCount, pdfPageData); send(Messages::WebPageProxy::DrawToPDFCallback(IPC::DataReference(CFDataGetBytePtr(pdfPageData.get()), CFDataGetLength(pdfPageData.get())), callbackID)); endPrinting(); } void WebPage::contentSizeCategoryDidChange(const String& contentSizeCategory) { RenderThemeIOS::setContentSizeCategory(contentSizeCategory); Page::updateStyleForAllPagesAfterGlobalChangeInEnvironment(); } String WebPage::platformUserAgent(const URL&) const { if (!m_page->settings().needsSiteSpecificQuirks()) return String(); auto document = m_mainFrame->coreFrame()->document(); if (!document) return String(); if (document->quirks().shouldAvoidUsingIOS13ForGmail() && osNameForUserAgent() == "iPhone OS") return standardUserAgentWithApplicationName({ }, "12_1_3"); return String(); } void WebPage::hardwareKeyboardAvailabilityChanged(bool keyboardIsAttached) { m_keyboardIsAttached = keyboardIsAttached; if (auto* focusedFrame = m_page->focusController().focusedFrame()) focusedFrame->eventHandler().capsLockStateMayHaveChanged(); } void WebPage::updateStringForFind(const String& findString) { send(Messages::WebPageProxy::UpdateStringForFind(findString)); } bool WebPage::platformPrefersTextLegibilityBasedZoomScaling() const { #if PLATFORM(WATCHOS) return true; #else return false; #endif } void WebPage::updateSelectionWithDelta(int64_t locationDelta, int64_t lengthDelta, CompletionHandler<void()>&& completionHandler) { Ref<Frame> frame = corePage()->focusController().focusedOrMainFrame(); VisibleSelection selection = frame->selection().selection(); if (selection.isNone()) { completionHandler(); return; } auto root = frame->selection().rootEditableElementOrDocumentElement(); auto range = selection.toNormalizedRange(); if (!root || !range) { completionHandler(); return; } size_t selectionLocation; size_t selectionLength; TextIterator::getLocationAndLengthFromRange(root, range.get(), selectionLocation, selectionLength); CheckedInt64 newSelectionLocation { selectionLocation }; CheckedInt64 newSelectionLength { selectionLength }; newSelectionLocation += locationDelta; newSelectionLength += lengthDelta; if (newSelectionLocation.hasOverflowed() || newSelectionLength.hasOverflowed()) { completionHandler(); return; } if (auto range = TextIterator::rangeFromLocationAndLength(root, newSelectionLocation.unsafeGet(), newSelectionLength.unsafeGet())) frame->selection().setSelectedRange(range.get(), DOWNSTREAM, WebCore::FrameSelection::ShouldCloseTyping::Yes, UserTriggered); completionHandler(); } static VisiblePosition moveByGranularityRespectingWordBoundary(const VisiblePosition& position, TextGranularity granularity, uint64_t granularityCount, SelectionDirection direction) { ASSERT(granularityCount); ASSERT(position.isNotNull()); bool backwards = direction == DirectionBackward; auto farthestPositionInDirection = backwards ? startOfEditableContent(position) : endOfEditableContent(position); if (position == farthestPositionInDirection) return backwards ? startOfWord(position) : endOfWord(position); VisiblePosition currentPosition = position; VisiblePosition nextPosition; do { nextPosition = positionOfNextBoundaryOfGranularity(currentPosition, granularity, direction); if (nextPosition.isNull()) break; currentPosition = nextPosition; if (atBoundaryOfGranularity(currentPosition, granularity, direction)) --granularityCount; } while (granularityCount); if (granularity == SentenceGranularity) { ASSERT(atBoundaryOfGranularity(currentPosition, SentenceGranularity, direction)); return currentPosition; } // Note that this rounds to the nearest word, which may cross a line boundary when using line granularity. // For example, suppose the text is laid out as follows and the insertion point is at |: // |This is the first sen // tence in a paragraph. // Then moving 1 line of granularity forward will return the postion after the 'e' in sentence. return backwards ? startOfWord(currentPosition) : endOfWord(currentPosition); } static VisiblePosition visiblePositionForPointInRootViewCoordinates(Frame& frame, FloatPoint pointInRootViewCoordinates) { auto pointInDocument = frame.view()->rootViewToContents(roundedIntPoint(pointInRootViewCoordinates)); return frame.visiblePositionForPoint(pointInDocument); } void WebPage::requestDocumentEditingContext(DocumentEditingContextRequest request, CompletionHandler<void(DocumentEditingContext)>&& completionHandler) { if (!request.options.contains(DocumentEditingContextRequest::Options::Text) && !request.options.contains(DocumentEditingContextRequest::Options::AttributedText)) { completionHandler({ }); return; } m_page->focusController().focusedOrMainFrame().document()->updateLayoutIgnorePendingStylesheets(); Ref<Frame> frame = m_page->focusController().focusedOrMainFrame(); VisibleSelection selection = frame->selection().selection(); VisiblePosition rangeOfInterestStart; VisiblePosition rangeOfInterestEnd; VisiblePosition selectionStart = selection.visibleStart(); VisiblePosition selectionEnd = selection.visibleEnd(); bool isSpatialRequest = request.options.contains(DocumentEditingContextRequest::Options::Spatial); bool wantsRects = request.options.contains(DocumentEditingContextRequest::Options::Rects); bool wantsMarkedTextRects = request.options.contains(DocumentEditingContextRequest::Options::MarkedTextRects); if (auto textInputContext = request.textInputContext) { RefPtr<Element> element = elementForContext(*textInputContext); if (!element) { completionHandler({ }); return; } if (is<HTMLTextFormControlElement>(element)) { auto& textFormControlElement = downcast<HTMLTextFormControlElement>(*element); rangeOfInterestStart = textFormControlElement.visiblePositionForIndex(0); rangeOfInterestEnd = textFormControlElement.visiblePositionForIndex(textFormControlElement.value().length()); } else { rangeOfInterestStart = firstPositionInOrBeforeNode(element.get()); rangeOfInterestEnd = lastPositionInOrAfterNode(element.get()); } } else if (isSpatialRequest) { // FIXME: We might need to be a bit more careful that we get something useful (test the other corners?). rangeOfInterestStart = visiblePositionForPointInRootViewCoordinates(frame.get(), request.rect.minXMinYCorner()); rangeOfInterestEnd = visiblePositionForPointInRootViewCoordinates(frame.get(), request.rect.maxXMaxYCorner()); if (rangeOfInterestEnd < rangeOfInterestStart) std::exchange(rangeOfInterestStart, rangeOfInterestEnd); } else if (!selection.isNone()) { rangeOfInterestStart = selectionStart; rangeOfInterestEnd = selectionEnd; } if (rangeOfInterestStart.isNull() || rangeOfInterestStart.isOrphan() || rangeOfInterestEnd.isNull() || rangeOfInterestEnd.isOrphan()) { completionHandler({ }); return; } DocumentEditingContext context; // The subset of the selection that is inside the range of interest. VisiblePosition startOfRangeOfInterestInSelection; VisiblePosition endOfRangeOfInterestInSelection; auto selectionRange = selection.toNormalizedRange(); auto rangeOfInterest = makeRange(rangeOfInterestStart, rangeOfInterestEnd); if (selectionRange && rangesOverlap(rangeOfInterest.get(), selectionRange.get())) { startOfRangeOfInterestInSelection = rangeOfInterestStart > selectionStart ? rangeOfInterestStart : selectionStart; endOfRangeOfInterestInSelection = rangeOfInterestEnd < selectionEnd ? rangeOfInterestEnd : selectionEnd; } else { size_t rangeOfInterestLocation; size_t rangeOfInterestLength; RefPtr<Node> rootNode = rangeOfInterest->commonAncestorContainer(); if (!rootNode) { completionHandler({ }); return; } RefPtr<ContainerNode> rootContainerNode = rootNode->isContainerNode() ? downcast<ContainerNode>(rootNode.get()) : rootNode->parentNode(); TextIterator::getLocationAndLengthFromRange(rootContainerNode.get(), rangeOfInterest.get(), rangeOfInterestLocation, rangeOfInterestLength); CheckedSize midpointLocation { rangeOfInterestLocation }; midpointLocation += rangeOfInterestLength / 2; if (midpointLocation.hasOverflowed()) { completionHandler({ }); return; } auto midpointRange = TextIterator::rangeFromLocationAndLength(rootContainerNode.get(), midpointLocation.unsafeGet(), 0); auto midpoint = midpointRange->startPosition(); startOfRangeOfInterestInSelection = startOfWord(midpoint); if (startOfRangeOfInterestInSelection < rangeOfInterestStart) { startOfRangeOfInterestInSelection = endOfWord(midpoint); if (startOfRangeOfInterestInSelection > rangeOfInterestEnd) startOfRangeOfInterestInSelection = midpoint; } endOfRangeOfInterestInSelection = startOfRangeOfInterestInSelection; } VisiblePosition contextBeforeStart; VisiblePosition contextAfterEnd; auto compositionRange = frame->editor().compositionRange(); if (request.granularityCount) { contextBeforeStart = moveByGranularityRespectingWordBoundary(rangeOfInterestStart, request.surroundingGranularity, request.granularityCount, DirectionBackward); contextAfterEnd = moveByGranularityRespectingWordBoundary(rangeOfInterestEnd, request.surroundingGranularity, request.granularityCount, DirectionForward); } else { contextBeforeStart = rangeOfInterestStart; contextAfterEnd = rangeOfInterestEnd; if (wantsMarkedTextRects && compositionRange) { // In the case where the client has requested marked text rects, additionally make sure that the // context range encompasses the entire marked text range. auto compositionStart = compositionRange->startPosition(); auto compositionEnd = compositionRange->endPosition(); contextBeforeStart = contextBeforeStart > compositionStart ? compositionStart : contextBeforeStart; contextAfterEnd = contextAfterEnd < compositionEnd ? compositionEnd : contextAfterEnd; } } auto makeString = [&](VisiblePosition& start, VisiblePosition& end) -> NSAttributedString * { if (start.isNull() || end.isNull() || start == end) return nil; // FIXME: This should return editing-offset-compatible attributed strings if that option is requested. return adoptNS([[NSAttributedString alloc] initWithString:plainTextReplacingNoBreakSpace(start.deepEquivalent(), end.deepEquivalent())]).autorelease(); }; context.contextBefore = makeString(contextBeforeStart, startOfRangeOfInterestInSelection); context.selectedText = makeString(startOfRangeOfInterestInSelection, endOfRangeOfInterestInSelection); context.contextAfter = makeString(endOfRangeOfInterestInSelection, contextAfterEnd); if (compositionRange && rangesOverlap(rangeOfInterest.get(), compositionRange.get())) { VisiblePosition compositionStart(compositionRange->startPosition()); VisiblePosition compositionEnd(compositionRange->endPosition()); context.markedText = makeString(compositionStart, compositionEnd); context.selectedRangeInMarkedText.location = distanceBetweenPositions(startOfRangeOfInterestInSelection, compositionStart); context.selectedRangeInMarkedText.length = [context.selectedText.string length]; } auto characterRectsForRange = [&] (Range& range, uint64_t locationOffset) { Vector<DocumentEditingContext::TextRectAndRange> rects; CharacterIterator contextIterator(range); unsigned currentLocation = locationOffset; while (!contextIterator.atEnd()) { unsigned length = contextIterator.text().length(); if (!length) { contextIterator.advance(1); continue; } rects.append({ contextIterator.range()->absoluteBoundingBox(), { currentLocation, 1 } }); currentLocation++; contextIterator.advance(1); } return rects; }; if (wantsRects) { if (auto contextRange = makeRange(contextBeforeStart, contextAfterEnd)) context.textRects = characterRectsForRange(*contextRange, 0); } else if (wantsMarkedTextRects && compositionRange) { auto compositionStartOffset = plainTextReplacingNoBreakSpace(contextBeforeStart.deepEquivalent(), compositionRange->startPosition()).length(); context.textRects = characterRectsForRange(*compositionRange, compositionStartOffset); } #if ENABLE(PLATFORM_DRIVEN_TEXT_CHECKING) if (request.options.contains(DocumentEditingContextRequest::Options::Annotation)) context.annotatedText = m_textCheckingControllerProxy->annotatedSubstringBetweenPositions(contextBeforeStart, contextAfterEnd); #endif completionHandler(context); } #if USE(QUICK_LOOK) void WebPage::didStartLoadForQuickLookDocumentInMainFrame(const String& fileName, const String& uti) { send(Messages::WebPageProxy::DidStartLoadForQuickLookDocumentInMainFrame(fileName, uti)); } void WebPage::didFinishLoadForQuickLookDocumentInMainFrame(const SharedBuffer& buffer) { ASSERT(!buffer.isEmpty()); // FIXME: In some cases, buffer conains a single segment that wraps an existing ShareableResource. // If we could create a handle from that existing resource then we could avoid this extra // allocation and copy. auto sharedMemory = SharedMemory::copyBuffer(buffer); if (!sharedMemory) return; ShareableResource::Handle handle; auto shareableResource = ShareableResource::create(sharedMemory.releaseNonNull(), 0, buffer.size()); if (!shareableResource->createHandle(handle)) return; send(Messages::WebPageProxy::DidFinishLoadForQuickLookDocumentInMainFrame(handle)); } void WebPage::requestPasswordForQuickLookDocumentInMainFrame(const String& fileName, CompletionHandler<void(const String&)>&& completionHandler) { sendWithAsyncReply(Messages::WebPageProxy::RequestPasswordForQuickLookDocumentInMainFrame(fileName), WTFMove(completionHandler)); } #endif } // namespace WebKit #undef RELEASE_LOG_IF_ALLOWED #undef RELEASE_LOG_ERROR_IF_ALLOWED #endif // PLATFORM(IOS_FAMILY)