| /* |
| * Copyright (C) 2012-2016 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) |
| |
| #import "AccessibilityIOS.h" |
| #import "AssistedNodeInformation.h" |
| #import "DataReference.h" |
| #import "DrawingArea.h" |
| #import "EditingRange.h" |
| #import "EditorState.h" |
| #import "GestureTypes.h" |
| #import "InteractionInformationAtPosition.h" |
| #import "Logging.h" |
| #import "PluginView.h" |
| #import "PrintInfo.h" |
| #import "RemoteLayerTreeDrawingArea.h" |
| #import "UserData.h" |
| #import "VisibleContentRectUpdateInfo.h" |
| #import "WKAccessibilityWebPageObjectIOS.h" |
| #import "WebChromeClient.h" |
| #import "WebCoreArgumentCoders.h" |
| #import "WebFrame.h" |
| #import "WebImage.h" |
| #import "WebKitSystemInterface.h" |
| #import "WebPageProxyMessages.h" |
| #import "WebPreviewLoaderClient.h" |
| #import "WebProcess.h" |
| #import <CoreText/CTFont.h> |
| #import <WebCore/Autofill.h> |
| #import <WebCore/Chrome.h> |
| #import <WebCore/DataDetection.h> |
| #import <WebCore/DiagnosticLoggingClient.h> |
| #import <WebCore/DiagnosticLoggingKeys.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/HTMLImageElement.h> |
| #import <WebCore/HTMLInputElement.h> |
| #import <WebCore/HTMLOptGroupElement.h> |
| #import <WebCore/HTMLOptionElement.h> |
| #import <WebCore/HTMLParserIdioms.h> |
| #import <WebCore/HTMLSelectElement.h> |
| #import <WebCore/HTMLTextAreaElement.h> |
| #import <WebCore/HistoryItem.h> |
| #import <WebCore/HitTestResult.h> |
| #import <WebCore/KeyboardEvent.h> |
| #import <WebCore/MainFrame.h> |
| #import <WebCore/MediaSessionManagerIOS.h> |
| #import <WebCore/Node.h> |
| #import <WebCore/NotImplemented.h> |
| #import <WebCore/Page.h> |
| #import <WebCore/Pasteboard.h> |
| #import <WebCore/PlatformKeyboardEvent.h> |
| #import <WebCore/PlatformMouseEvent.h> |
| #import <WebCore/RenderBlock.h> |
| #import <WebCore/RenderImage.h> |
| #import <WebCore/RenderThemeIOS.h> |
| #import <WebCore/RenderView.h> |
| #import <WebCore/Settings.h> |
| #import <WebCore/SharedBuffer.h> |
| #import <WebCore/StyleProperties.h> |
| #import <WebCore/TextIndicator.h> |
| #import <WebCore/TextIterator.h> |
| #import <WebCore/VisibleUnits.h> |
| #import <WebCore/WKContentObservation.h> |
| #import <WebCore/WebEvent.h> |
| #import <wtf/MathExtras.h> |
| #import <wtf/MemoryPressureHandler.h> |
| #import <wtf/SetForScope.h> |
| #import <wtf/text/TextStream.h> |
| |
| using namespace WebCore; |
| |
| namespace WebKit { |
| |
| const int blockSelectionStartWidth = 100; |
| const int blockSelectionStartHeight = 100; |
| |
| void WebPage::platformInitialize() |
| { |
| platformInitializeAccessibility(); |
| } |
| |
| void WebPage::platformDetach() |
| { |
| [m_mockAccessibilityElement setWebPage:nullptr]; |
| } |
| |
| void WebPage::platformInitializeAccessibility() |
| { |
| m_mockAccessibilityElement = adoptNS([[WKAccessibilityWebPageObject alloc] init]); |
| [m_mockAccessibilityElement setWebPage:this]; |
| |
| NSData *remoteToken = newAccessibilityRemoteToken([NSUUID UUID]); |
| IPC::DataReference dataToken = IPC::DataReference(reinterpret_cast<const uint8_t*>([remoteToken bytes]), [remoteToken length]); |
| send(Messages::WebPageProxy::RegisterWebProcessAccessibilityToken(dataToken)); |
| } |
| |
| void WebPage::platformPreferencesDidChange(const WebPreferencesStore&) |
| { |
| notImplemented(); |
| } |
| |
| void WebPage::platformEditorState(Frame& frame, EditorState& result, IncludePostLayoutDataHint shouldIncludePostLayoutData) const |
| { |
| if (frame.editor().hasComposition()) { |
| RefPtr<Range> compositionRange = frame.editor().compositionRange(); |
| Vector<WebCore::SelectionRect> compositionRects; |
| if (compositionRange) { |
| compositionRange->collectSelectionRects(compositionRects); |
| if (compositionRects.size()) |
| result.firstMarkedRect = compositionRects[0].rect(); |
| if (compositionRects.size() > 1) |
| result.lastMarkedRect = compositionRects.last().rect(); |
| else |
| result.lastMarkedRect = result.firstMarkedRect; |
| result.markedText = plainTextReplacingNoBreakSpace(compositionRange.get()); |
| } |
| } |
| |
| // We only set the remaining EditorState entries if the layout is done. To compute these |
| // entries, we need the layout to be done and we don't want to trigger a synchronous |
| // layout as this would be bad for performance. If we have a composition, we send everything |
| // right away as the UIProcess needs the caretRects ASAP for marked text. |
| bool frameViewHasFinishedLayout = frame.view() && !frame.view()->needsLayout(); |
| if (shouldIncludePostLayoutData == IncludePostLayoutDataHint::No && !frameViewHasFinishedLayout && !frame.editor().hasComposition()) { |
| result.isMissingPostLayoutData = true; |
| return; |
| } |
| |
| auto& postLayoutData = result.postLayoutData(); |
| FrameView* view = frame.view(); |
| 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); |
| Node* root = selection.rootEditableElement(); |
| postLayoutData.hasContent = root && root->hasChildNodes() && !isEndOfEditableOrNonEditableContent(firstPositionInNode(root)); |
| } |
| } 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; |
| if (selectedText.length() <= maxSelectedTextLength) |
| postLayoutData.wordAtSelection = selectedText; |
| } |
| // FIXME: We should disallow replace when the string contains only CJ characters. |
| postLayoutData.isReplaceAllowed = result.isContentEditable && !result.isInPasswordField && !selectedText.containsOnlyWhitespace(); |
| } |
| postLayoutData.insideFixedPosition = startNodeIsInsideFixedPosition || endNodeIsInsideFixedPosition; |
| if (!selection.isNone()) { |
| if (m_assistedNode && m_assistedNode->renderer()) |
| postLayoutData.selectionClipRect = view->contentsToRootView(m_assistedNode->renderer()->absoluteBoundingBoxRect()); |
| } |
| } |
| |
| FloatSize WebPage::screenSize() const |
| { |
| return m_screenSize; |
| } |
| |
| FloatSize WebPage::availableScreenSize() const |
| { |
| return m_availableScreenSize; |
| } |
| |
| void WebPage::didReceiveMobileDocType(bool isMobileDoctype) |
| { |
| if (isMobileDoctype) |
| m_viewportConfiguration.setDefaultConfiguration(ViewportConfiguration::xhtmlMobileParameters()); |
| else |
| resetViewportDefaultConfiguration(m_mainFrame.get()); |
| } |
| |
| 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()); |
| |
| 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) |
| { |
| double overscaledWidth = exposedRect.width(); |
| double missingHorizonalMargin = exposedRect.width() * exposedRectScale / newScale - overscaledWidth; |
| |
| double overscaledHeight = exposedRect.height(); |
| double missingVerticalMargin = exposedRect.height() * exposedRectScale / newScale - overscaledHeight; |
| |
| return FloatRect(exposedRect.x() - missingHorizonalMargin / 2, exposedRect.y() - missingVerticalMargin / 2, exposedRect.width() + missingHorizonalMargin, exposedRect.height() + missingVerticalMargin); |
| } |
| |
| 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 = std::min<float>(m_viewportConfiguration.maximumScale(), std::max<float>(m_viewportConfiguration.minimumScale(), historyItem.pageScaleFactor())); |
| scalePage(boundedScale, IntPoint()); |
| |
| std::optional<FloatPoint> scrollPosition; |
| if (historyItem.shouldRestoreScrollPosition()) { |
| m_drawingArea->setExposedContentRect(historyItem.exposedContentRect()); |
| 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); |
| |
| std::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) |
| { |
| const PlatformKeyboardEvent* platformEvent = event->keyEvent(); |
| if (!platformEvent) |
| 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_pageID); |
| if (!sendResult) |
| return false; |
| |
| return eventWasHandled; |
| } |
| |
| void WebPage::sendComplexTextInputToPlugin(uint64_t, const String&) |
| { |
| notImplemented(); |
| } |
| |
| void WebPage::performDictionaryLookupAtLocation(const FloatPoint&) |
| { |
| notImplemented(); |
| } |
| |
| void WebPage::performDictionaryLookupForSelection(Frame*, const VisibleSelection&, TextIndicatorPresentationTransition) |
| { |
| notImplemented(); |
| } |
| |
| void WebPage::performDictionaryLookupForRange(Frame*, Range&, NSDictionary *, TextIndicatorPresentationTransition) |
| { |
| 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 0; |
| |
| if (PluginView* pluginView = pluginViewForFrame(&m_page->mainFrame())) |
| return pluginView->accessibilityObject(); |
| |
| return 0; |
| } |
| |
| 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&, bool&) |
| { |
| notImplemented(); |
| } |
| |
| void WebPage::getStringSelectionForPasteboard(String&) |
| { |
| notImplemented(); |
| } |
| |
| void WebPage::getDataSelectionForPasteboard(const String, SharedMemory::Handle&, uint64_t&) |
| { |
| notImplemented(); |
| } |
| |
| WKAccessibilityWebPageObject* WebPage::accessibilityRemoteObject() |
| { |
| notImplemented(); |
| return 0; |
| } |
| |
| bool WebPage::platformHasLocalDataForURL(const WebCore::URL&) |
| { |
| notImplemented(); |
| return false; |
| } |
| |
| String WebPage::cachedSuggestedFilenameForURL(const URL&) |
| { |
| notImplemented(); |
| return String(); |
| } |
| |
| String WebPage::cachedResponseMIMETypeForURL(const URL&) |
| { |
| notImplemented(); |
| return String(); |
| } |
| |
| RefPtr<SharedBuffer> WebPage::cachedResponseDataForURL(const URL&) |
| { |
| notImplemented(); |
| return nullptr; |
| } |
| |
| bool WebPage::platformCanHandleRequest(const WebCore::ResourceRequest&) |
| { |
| notImplemented(); |
| return false; |
| } |
| |
| void WebPage::shouldDelayWindowOrderingEvent(const WebKit::WebMouseEvent&, bool&) |
| { |
| notImplemented(); |
| } |
| |
| void WebPage::acceptsFirstMouse(int, const WebKit::WebMouseEvent&, bool&) |
| { |
| notImplemented(); |
| } |
| |
| void WebPage::computePagesForPrintingPDFDocument(uint64_t, 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() |
| { |
| 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(); |
| } |
| |
| void WebPage::handleSyntheticClick(Node* nodeRespondingToClick, const WebCore::FloatPoint& location) |
| { |
| IntPoint roundedAdjustedPoint = roundedIntPoint(location); |
| Frame& mainframe = m_page->mainFrame(); |
| |
| WKBeginObservingContentChanges(true); |
| |
| mainframe.eventHandler().mouseMoved(PlatformMouseEvent(roundedAdjustedPoint, roundedAdjustedPoint, NoButton, PlatformEvent::MouseMoved, 0, false, false, false, false, currentTime(), WebCore::ForceAtClick, WebCore::NoTap)); |
| mainframe.document()->updateStyleIfNeeded(); |
| |
| WKStopObservingContentChanges(); |
| |
| m_pendingSyntheticClickNode = nullptr; |
| m_pendingSyntheticClickLocation = FloatPoint(); |
| |
| if (m_isClosed) |
| return; |
| |
| switch (WKObservedContentChange()) { |
| case WKContentVisibilityChange: |
| // The move event caused new contents to appear. Don't send the click event. |
| return; |
| case WKContentIndeterminateChange: |
| // Wait for callback to completePendingSyntheticClickForContentChangeObserver() to decide whether to send the click event. |
| m_pendingSyntheticClickNode = nodeRespondingToClick; |
| m_pendingSyntheticClickLocation = location; |
| return; |
| case WKContentNoChange: |
| completeSyntheticClick(nodeRespondingToClick, location, WebCore::OneFingerTap); |
| return; |
| } |
| ASSERT_NOT_REACHED(); |
| } |
| |
| void WebPage::completePendingSyntheticClickForContentChangeObserver() |
| { |
| if (!m_pendingSyntheticClickNode) |
| return; |
| // Only dispatch the click if the document didn't get changed by any timers started by the move event. |
| if (WKObservedContentChange() == WKContentNoChange) |
| completeSyntheticClick(m_pendingSyntheticClickNode.get(), m_pendingSyntheticClickLocation, WebCore::OneFingerTap); |
| |
| m_pendingSyntheticClickNode = nullptr; |
| m_pendingSyntheticClickLocation = FloatPoint(); |
| } |
| |
| void WebPage::completeSyntheticClick(Node* nodeRespondingToClick, const WebCore::FloatPoint& location, SyntheticClickType syntheticClickType) |
| { |
| 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; |
| |
| tapWasHandled |= mainframe.eventHandler().handleMousePressEvent(PlatformMouseEvent(roundedAdjustedPoint, roundedAdjustedPoint, LeftButton, PlatformEvent::MousePressed, 1, false, false, false, false, currentTime(), WebCore::ForceAtClick, syntheticClickType)); |
| if (m_isClosed) |
| return; |
| |
| tapWasHandled |= mainframe.eventHandler().handleMouseReleaseEvent(PlatformMouseEvent(roundedAdjustedPoint, roundedAdjustedPoint, LeftButton, PlatformEvent::MouseReleased, 1, false, false, false, false, currentTime(), WebCore::ForceAtClick, syntheticClickType)); |
| 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) |
| elementDidFocus(newFocusedElement.get()); |
| |
| if (!tapWasHandled || !nodeRespondingToClick || !nodeRespondingToClick->isElementNode()) |
| send(Messages::WebPageProxy::DidNotHandleTapAsClick(roundedIntPoint(location))); |
| |
| send(Messages::WebPageProxy::DidCompleteSyntheticClick()); |
| } |
| |
| void WebPage::handleTap(const IntPoint& point, uint64_t 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); |
| } |
| |
| #if ENABLE(DATA_INTERACTION) |
| void WebPage::requestStartDataInteraction(const IntPoint& clientPosition, const IntPoint& globalPosition) |
| { |
| bool didStart = m_page->mainFrame().eventHandler().tryToBeginDataInteractionAtPoint(clientPosition, globalPosition); |
| send(Messages::WebPageProxy::DidHandleStartDataInteractionRequest(didStart)); |
| } |
| |
| void WebPage::didConcludeEditDataInteraction() |
| { |
| std::optional<TextIndicatorData> textIndicatorData; |
| |
| static auto defaultEditDataInteractionTextIndicatorOptions = TextIndicatorOptionIncludeSnapshotOfAllVisibleContentWithoutSelection | TextIndicatorOptionExpandClipBeyondVisibleRect | TextIndicatorOptionPaintAllContent | TextIndicatorOptionIncludeMarginIfRangeMatchesSelection | TextIndicatorOptionPaintBackgrounds | TextIndicatorOptionComputeEstimatedBackgroundColor| TextIndicatorOptionUseSelectionRectForSizing | TextIndicatorOptionIncludeSnapshotWithSelectionHighlight; |
| auto& frame = m_page->focusController().focusedOrMainFrame(); |
| if (auto range = frame.selection().selection().toNormalizedRange()) { |
| if (auto textIndicator = TextIndicator::createWithRange(*range, defaultEditDataInteractionTextIndicatorOptions, TextIndicatorPresentationTransition::None, FloatSize())) |
| textIndicatorData = textIndicator->data(); |
| } |
| |
| send(Messages::WebPageProxy::DidConcludeEditDataInteraction(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()); |
| } |
| |
| 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(); |
| |
| send(Messages::WebPageProxy::DidGetTapHighlightGeometries(requestID, highlightColor, quads, roundedIntSize(borderRadii.topLeft()), roundedIntSize(borderRadii.topRight()), roundedIntSize(borderRadii.bottomLeft()), roundedIntSize(borderRadii.bottomRight()))); |
| } |
| #else |
| UNUSED_PARAM(requestID); |
| UNUSED_PARAM(node); |
| #endif |
| } |
| |
| void WebPage::handleTwoFingerTapAtPoint(const WebCore::IntPoint& point, 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, WebCore::TwoFingerTap); |
| } |
| |
| void WebPage::potentialTapAtPosition(uint64_t requestID, const WebCore::FloatPoint& position) |
| { |
| m_potentialTapNode = m_page->mainFrame().nodeRespondingToClickEvents(position, m_potentialTapLocation); |
| sendTapHighlightForNodeIfNecessary(requestID, m_potentialTapNode.get()); |
| #if ENABLE(TOUCH_EVENTS) |
| if (m_potentialTapNode && !m_potentialTapNode->allowsDoubleTapGesture()) |
| send(Messages::WebPageProxy::DisableDoubleTapGesturesDuringTapIfNecessary(requestID)); |
| #endif |
| } |
| |
| void WebPage::commitPotentialTap(uint64_t lastLayerTreeTransactionId) |
| { |
| if (!m_potentialTapNode || (!m_potentialTapNode->renderer() && !is<HTMLAreaElement>(m_potentialTapNode.get()))) { |
| commitPotentialTapFailed(); |
| return; |
| } |
| |
| FloatPoint adjustedPoint; |
| Node* nodeRespondingToClick = m_page->mainFrame().nodeRespondingToClickEvents(m_potentialTapLocation, adjustedPoint); |
| 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); |
| } else |
| commitPotentialTapFailed(); |
| |
| m_potentialTapNode = nullptr; |
| m_potentialTapLocation = FloatPoint(); |
| } |
| |
| void WebPage::commitPotentialTapFailed() |
| { |
| send(Messages::WebPageProxy::CommitPotentialTapFailed()); |
| send(Messages::WebPageProxy::DidNotHandleTapAsClick(roundedIntPoint(m_potentialTapLocation))); |
| } |
| |
| void WebPage::cancelPotentialTap() |
| { |
| cancelPotentialTapInFrame(*m_mainFrame); |
| } |
| |
| void WebPage::cancelPotentialTapInFrame(WebFrame& frame) |
| { |
| if (m_potentialTapNode) { |
| Frame* potentialTapFrame = m_potentialTapNode->document().frame(); |
| if (potentialTapFrame && !potentialTapFrame->tree().isDescendantOf(frame.coreFrame())) |
| return; |
| } |
| |
| m_potentialTapNode = nullptr; |
| m_potentialTapLocation = FloatPoint(); |
| } |
| |
| 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, 0, WebCore::NoTap)); |
| mainframe.document()->updateStyleIfNeeded(); |
| } |
| |
| void WebPage::inspectorNodeSearchEndedAtPosition(const FloatPoint& position) |
| { |
| if (Node* node = m_page->mainFrame().deepestNodeAtLocation(position)) |
| node->inspect(); |
| } |
| |
| void WebPage::blurAssistedNode() |
| { |
| if (is<Element>(m_assistedNode.get())) |
| downcast<Element>(*m_assistedNode).blur(); |
| } |
| |
| void WebPage::setAssistedNodeValue(const String& value) |
| { |
| // FIXME: should also handle the case of HTMLSelectElement. |
| if (is<HTMLInputElement>(m_assistedNode.get())) |
| downcast<HTMLInputElement>(*m_assistedNode).setValue(value, DispatchInputAndChangeEvent); |
| } |
| |
| void WebPage::setAssistedNodeValueAsNumber(double value) |
| { |
| if (is<HTMLInputElement>(m_assistedNode.get())) |
| downcast<HTMLInputElement>(*m_assistedNode).setValueAsNumber(value, DispatchInputAndChangeEvent); |
| } |
| |
| void WebPage::setAssistedNodeSelectedIndex(uint32_t index, bool allowMultipleSelection) |
| { |
| if (is<HTMLSelectElement>(m_assistedNode.get())) |
| downcast<HTMLSelectElement>(*m_assistedNode).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 FloatQuad innerFrameQuad(const Frame& frame, const Node& assistedNode) |
| { |
| frame.document()->updateLayoutIgnorePendingStylesheets(); |
| RenderElement* renderer = nullptr; |
| if (assistedNode.hasTagName(HTMLNames::textareaTag) || assistedNode.hasTagName(HTMLNames::inputTag) || assistedNode.hasTagName(HTMLNames::selectTag)) |
| renderer = downcast<RenderElement>(assistedNode.renderer()); |
| else if (Element* rootEditableElement = assistedNode.rootEditableElement()) |
| renderer = rootEditableElement->renderer(); |
| |
| if (!renderer) |
| return FloatQuad(); |
| |
| auto& style = renderer->style(); |
| IntRect boundingBox = renderer->absoluteBoundingBoxRect(true /* use transforms*/); |
| |
| boundingBox.move(style.borderLeftWidth(), style.borderTopWidth()); |
| boundingBox.setWidth(boundingBox.width() - style.borderLeftWidth() - style.borderRightWidth()); |
| boundingBox.setHeight(boundingBox.height() - style.borderBottomWidth() - style.borderTopWidth()); |
| |
| return FloatQuad(boundingBox); |
| } |
| |
| static IntPoint constrainPoint(const IntPoint& point, const Frame& frame, const Node& assistedNode) |
| { |
| ASSERT(&assistedNode.document() == frame.document()); |
| const int DEFAULT_CONSTRAIN_INSET = 2; |
| IntRect innerFrame = innerFrameQuad(frame, assistedNode).enclosingBoundingBox(); |
| 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; |
| } |
| |
| static IntRect selectionBoxForRange(WebCore::Range* range) |
| { |
| if (!range) |
| return IntRect(); |
| |
| IntRect boundingRect; |
| Vector<SelectionRect> selectionRects; |
| range->collectSelectionRects(selectionRects); |
| unsigned size = selectionRects.size(); |
| |
| for (unsigned i = 0; i < size; ++i) { |
| const IntRect &coreRect = selectionRects[i].rect(); |
| if (!i) |
| boundingRect = coreRect; |
| else |
| boundingRect.unite(coreRect); |
| } |
| return boundingRect; |
| } |
| |
| static bool canShrinkToTextSelection(Node* node) |
| { |
| if (node && !is<Element>(*node)) |
| node = node->parentElement(); |
| |
| auto* renderer = (node) ? node->renderer() : nullptr; |
| return renderer && renderer->childrenInline() && (is<RenderBlock>(*renderer) && !downcast<RenderBlock>(*renderer).inlineElementContinuation()) && !renderer->isTable(); |
| } |
| |
| static bool canShrinkToTextSelection(Range& range) |
| { |
| if (range.startContainer().isTextNode() && range.endContainer().isTextNode()) |
| return true; |
| return canShrinkToTextSelection(range.commonAncestorContainer()); |
| } |
| |
| static bool hasCustomLineHeight(Node& node) |
| { |
| auto* renderer = node.renderer(); |
| return renderer && renderer->style().lineHeight().isSpecified(); |
| } |
| |
| RefPtr<Range> WebPage::rangeForWebSelectionAtPosition(const IntPoint& point, const VisiblePosition& position, SelectionFlags& flags) |
| { |
| HitTestResult result = m_page->mainFrame().eventHandler().hitTestResultAtPoint((point), HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::DisallowUserAgentShadowContent | HitTestRequest::AllowChildFrameContent); |
| |
| Node* currentNode = result.innerNode(); |
| if (!currentNode) |
| return nullptr; |
| RefPtr<Range> range; |
| FloatRect boundingRectInScrollViewCoordinates; |
| |
| if (!currentNode->isTextNode() && !canShrinkToTextSelection(currentNode) && hasCustomLineHeight(*currentNode)) { |
| auto* renderer = currentNode->renderer(); |
| if (is<RenderBlockFlow>(renderer)) { |
| auto* renderText = downcast<RenderBlockFlow>(*renderer).findClosestTextAtAbsolutePoint(point); |
| if (renderText && renderText->textNode()) |
| currentNode = renderText->textNode(); |
| } |
| } |
| |
| if (currentNode->isTextNode()) { |
| range = enclosingTextUnitOfGranularity(position, ParagraphGranularity, DirectionForward); |
| if (!range || range->collapsed()) |
| range = Range::create(currentNode->document(), position, position); |
| else { |
| m_blockRectForTextSelection = selectionBoxForRange(range.get()); |
| range = wordRangeFromPosition(position); |
| } |
| |
| return range; |
| } |
| |
| if (!currentNode->isElementNode()) |
| currentNode = currentNode->parentElement(); |
| |
| Node* bestChoice = currentNode; |
| while (currentNode) { |
| if (currentNode->renderer()) { |
| boundingRectInScrollViewCoordinates = currentNode->renderer()->absoluteBoundingBoxRect(true); |
| boundingRectInScrollViewCoordinates.scale(m_page->pageScaleFactor()); |
| if (boundingRectInScrollViewCoordinates.width() > m_blockSelectionDesiredSize.width() && boundingRectInScrollViewCoordinates.height() > m_blockSelectionDesiredSize.height()) |
| break; |
| bestChoice = currentNode; |
| } |
| currentNode = currentNode->parentElement(); |
| } |
| |
| if (!bestChoice) |
| return nullptr; |
| |
| RenderObject* renderer = bestChoice->renderer(); |
| if (!renderer || renderer->style().userSelect() == SELECT_NONE) |
| return nullptr; |
| |
| if (renderer->childrenInline() && (is<RenderBlock>(*renderer) && !downcast<RenderBlock>(*renderer).inlineElementContinuation()) && !renderer->isTable()) { |
| range = enclosingTextUnitOfGranularity(position, WordGranularity, DirectionBackward); |
| if (range && !range->collapsed()) |
| return range; |
| } |
| |
| // If all we could find is a block whose height is very close to the height |
| // of the visible area, don't use it. |
| const float adjustmentFactor = .97; |
| boundingRectInScrollViewCoordinates = renderer->absoluteBoundingBoxRect(true); |
| |
| if (boundingRectInScrollViewCoordinates.height() > m_page->mainFrame().view()->exposedContentRect().height() * adjustmentFactor) |
| return nullptr; |
| |
| flags = IsBlockSelection; |
| range = Range::create(bestChoice->document()); |
| range->selectNodeContents(*bestChoice); |
| return range->collapsed() ? nullptr : range; |
| } |
| |
| RefPtr<Range> WebPage::rangeForBlockAtPoint(const IntPoint& point) |
| { |
| HitTestResult result = m_page->mainFrame().eventHandler().hitTestResultAtPoint((point), HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::DisallowUserAgentShadowContent | HitTestRequest::IgnoreClipping); |
| |
| Node* currentNode = result.innerNode(); |
| RefPtr<Range> range; |
| |
| if (currentNode->isTextNode()) { |
| range = enclosingTextUnitOfGranularity(m_page->focusController().focusedOrMainFrame().visiblePositionForPoint(point), ParagraphGranularity, DirectionForward); |
| if (range && !range->collapsed()) |
| return range; |
| } |
| |
| if (!currentNode->isElementNode()) |
| currentNode = currentNode->parentElement(); |
| |
| if (!currentNode) |
| return nullptr; |
| |
| range = Range::create(currentNode->document()); |
| range->selectNodeContents(*currentNode); |
| return range; |
| } |
| |
| void WebPage::selectWithGesture(const IntPoint& point, uint32_t granularity, uint32_t gestureType, uint32_t gestureState, bool isInteractingWithAssistedNode, CallbackID callbackID) |
| { |
| auto& frame = m_page->focusController().focusedOrMainFrame(); |
| VisiblePosition position = visiblePositionInFocusedNodeForPoint(frame, point, isInteractingWithAssistedNode); |
| |
| 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 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(); |
| } |
| } 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 |
| range = wordRangeFromPosition(position); |
| 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; |
| |
| case GestureType::MakeWebSelection: |
| if (wkGestureState == GestureRecognizerState::Began) { |
| m_blockSelectionDesiredSize.setWidth(blockSelectionStartWidth); |
| m_blockSelectionDesiredSize.setHeight(blockSelectionStartHeight); |
| m_currentBlockSelection = nullptr; |
| } |
| range = rangeForWebSelectionAtPosition(point, position, flags); |
| if (wkGestureState == GestureRecognizerState::Ended && flags & IsBlockSelection) |
| m_currentBlockSelection = range; |
| break; |
| |
| default: |
| break; |
| } |
| if (range) |
| frame.selection().setSelectedRange(range.get(), position.affinity(), true, UserTriggered); |
| |
| send(Messages::WebPageProxy::GestureCallback(point, gestureType, gestureState, static_cast<uint32_t>(flags), callbackID)); |
| } |
| |
| static RefPtr<Range> rangeForPosition(Frame* frame, const VisiblePosition& position, bool baseIsStart) |
| { |
| RefPtr<Range> range; |
| VisiblePosition result = position; |
| |
| if (baseIsStart) { |
| VisiblePosition selectionStart = frame->selection().selection().visibleStart(); |
| bool wouldFlip = position <= selectionStart; |
| |
| if (wouldFlip) |
| result = selectionStart.next(); |
| |
| if (result.isNotNull()) |
| range = Range::create(*frame->document(), selectionStart, result); |
| } else { |
| VisiblePosition selectionEnd = frame->selection().selection().visibleEnd(); |
| bool wouldFlip = position >= selectionEnd; |
| |
| if (wouldFlip) |
| result = selectionEnd.previous(); |
| |
| if (result.isNotNull()) |
| range = Range::create(*frame->document(), result, 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, LineBoundary, sameDirection) |
| && !atBoundaryOfGranularity(extent, LineBoundary, 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); |
| } |
| |
| static const int maxHitTests = 10; |
| |
| static inline float distanceBetweenRectsForPosition(IntRect& first, IntRect& second, SelectionHandlePosition handlePosition) |
| { |
| switch (handlePosition) { |
| case SelectionHandlePosition::Top: |
| return abs(first.y() - second.y()); |
| case SelectionHandlePosition::Right: |
| return abs(first.maxX() - second.maxX()); |
| case SelectionHandlePosition::Bottom: |
| return abs(first.maxY() - second.maxY()); |
| case SelectionHandlePosition::Left: |
| return abs(first.x() - second.x()); |
| } |
| } |
| |
| static inline bool rectsEssentiallyTheSame(IntRect& first, IntRect& second, float allowablePercentDifference) |
| { |
| const float minMagnitudeRatio = 1.0 - allowablePercentDifference; |
| const float maxDisplacementRatio = allowablePercentDifference; |
| |
| float xOriginShiftRatio = abs(first.x() - second.x())/std::min(first.width(), second.width()); |
| float yOriginShiftRatio = abs(first.y() - second.y())/std::min(first.height(), second.height()); |
| |
| float widthRatio = std::min(first.width() / second.width(), second.width() / first.width()); |
| float heightRatio = std::min(first.height() / second.height(), second.height() / first.height()); |
| return ((widthRatio > minMagnitudeRatio && xOriginShiftRatio < maxDisplacementRatio) |
| && (heightRatio > minMagnitudeRatio && yOriginShiftRatio < maxDisplacementRatio)); |
| } |
| |
| static inline RefPtr<Range> unionDOMRanges(Range* rangeA, Range* rangeB) |
| { |
| if (!rangeB) |
| return rangeA; |
| if (!rangeA) |
| return rangeB; |
| |
| auto startToStartComparison = rangeA->compareBoundaryPoints(Range::START_TO_START, *rangeB); |
| if (startToStartComparison.hasException()) |
| return nullptr; |
| |
| auto endToEndComparison = rangeA->compareBoundaryPoints(Range::END_TO_END, *rangeB); |
| if (endToEndComparison.hasException()) |
| return nullptr; |
| |
| auto* start = startToStartComparison.releaseReturnValue() <= 0 ? rangeA : rangeB; |
| auto* end = endToEndComparison.releaseReturnValue() <= 0 ? rangeB : rangeA; |
| |
| return Range::create(rangeA->ownerDocument(), &start->startContainer(), start->startOffset(), &end->endContainer(), end->endOffset()); |
| } |
| |
| static inline IntPoint computeEdgeCenter(const IntRect& box, SelectionHandlePosition handlePosition) |
| { |
| switch (handlePosition) { |
| case SelectionHandlePosition::Top: |
| return IntPoint(box.x() + box.width() / 2, box.y()); |
| case SelectionHandlePosition::Right: |
| return IntPoint(box.maxX(), box.y() + box.height() / 2); |
| case SelectionHandlePosition::Bottom: |
| return IntPoint(box.x() + box.width() / 2, box.maxY()); |
| case SelectionHandlePosition::Left: |
| return IntPoint(box.x(), box.y() + box.height() / 2); |
| } |
| } |
| |
| Ref<Range> WebPage::expandedRangeFromHandle(Range& currentRange, SelectionHandlePosition handlePosition) |
| { |
| IntRect currentBox = selectionBoxForRange(¤tRange); |
| IntPoint edgeCenter = computeEdgeCenter(currentBox, handlePosition); |
| static const float maxDistance = 1000; |
| const float multiple = powf(maxDistance, 1.0/(maxHitTests - 1)); |
| float distance = 1; |
| |
| RefPtr<Range> bestRange; |
| IntRect bestRect; |
| |
| while (distance < maxDistance) { |
| if (bestRange) { |
| if (distanceBetweenRectsForPosition(bestRect, currentBox, handlePosition) < distance) { |
| // Break early, we're unlikely to do any better. |
| break; |
| } |
| } |
| |
| IntPoint testPoint = edgeCenter; |
| switch (handlePosition) { |
| case SelectionHandlePosition::Top: |
| testPoint.move(0, -distance); |
| break; |
| case SelectionHandlePosition::Right: |
| testPoint.move(distance, 0); |
| break; |
| case SelectionHandlePosition::Bottom: |
| testPoint.move(0, distance); |
| break; |
| case SelectionHandlePosition::Left: |
| testPoint.move(-distance, 0); |
| break; |
| } |
| |
| distance = ceilf(distance * multiple); |
| |
| RefPtr<Range> newRange; |
| RefPtr<Range> rangeAtPosition = rangeForBlockAtPoint(testPoint); |
| if (!rangeAtPosition || ¤tRange.ownerDocument() != &rangeAtPosition->ownerDocument()) |
| continue; |
| |
| if (rangeAtPosition->contains(currentRange)) |
| newRange = rangeAtPosition; |
| else if (currentRange.contains(*rangeAtPosition.get())) |
| newRange = ¤tRange; |
| else |
| newRange = unionDOMRanges(¤tRange, rangeAtPosition.get()); |
| |
| IntRect copyRect = selectionBoxForRange(newRange.get()); |
| |
| // Is it different and bigger than the current? |
| bool isBetterChoice = !(rectsEssentiallyTheSame(copyRect, currentBox, .05)); |
| if (isBetterChoice) { |
| switch (handlePosition) { |
| case SelectionHandlePosition::Top: |
| case SelectionHandlePosition::Bottom: |
| isBetterChoice = (copyRect.height() > currentBox.height()); |
| break; |
| case SelectionHandlePosition::Right: |
| case SelectionHandlePosition::Left: |
| isBetterChoice = (copyRect.width() > currentBox.width()); |
| break; |
| } |
| |
| } |
| |
| if (bestRange && isBetterChoice) { |
| // Furtherore, is it smaller than the best we've found so far? |
| switch (handlePosition) { |
| case SelectionHandlePosition::Top: |
| case SelectionHandlePosition::Bottom: |
| isBetterChoice = (copyRect.height() < bestRect.height()); |
| break; |
| case SelectionHandlePosition::Right: |
| case SelectionHandlePosition::Left: |
| isBetterChoice = (copyRect.width() < bestRect.width()); |
| break; |
| } |
| } |
| |
| if (isBetterChoice) { |
| bestRange = newRange; |
| bestRect = copyRect; |
| } |
| } |
| |
| if (bestRange) |
| return bestRange.releaseNonNull(); |
| |
| return currentRange; |
| } |
| |
| Ref<Range> WebPage::contractedRangeFromHandle(Range& currentRange, SelectionHandlePosition handlePosition, SelectionFlags& flags) |
| { |
| // Shrinking with a base and extent will always give better results. If we only have a single element, |
| // see if we can break that down to a base and extent. Shrinking base and extent is comparatively straightforward. |
| // Shrinking down to another element is unlikely to move just one edge, but we can try that as a fallback. |
| |
| IntRect currentBox = selectionBoxForRange(¤tRange); |
| IntPoint edgeCenter = computeEdgeCenter(currentBox, handlePosition); |
| flags = IsBlockSelection; |
| |
| float maxDistance; |
| |
| switch (handlePosition) { |
| case SelectionHandlePosition::Top: |
| case SelectionHandlePosition::Bottom: |
| maxDistance = currentBox.height(); |
| break; |
| case SelectionHandlePosition::Right: |
| case SelectionHandlePosition::Left: |
| maxDistance = currentBox.width(); |
| break; |
| } |
| |
| const float multiple = powf(maxDistance - 1, 1.0/(maxHitTests - 1)); |
| float distance = 1; |
| RefPtr<Range> bestRange; |
| IntRect bestRect; |
| |
| while (distance < maxDistance) { |
| if (bestRange) { |
| float shrankDistance; |
| switch (handlePosition) { |
| case SelectionHandlePosition::Top: |
| case SelectionHandlePosition::Bottom: |
| shrankDistance = abs(currentBox.height() - bestRect.height()); |
| break; |
| case SelectionHandlePosition::Right: |
| case SelectionHandlePosition::Left: |
| shrankDistance = abs(currentBox.width() - bestRect.width()); |
| break; |
| } |
| if (shrankDistance > distance) { |
| // Certainly not going to do any better than that. |
| break; |
| } |
| } |
| |
| IntPoint testPoint = edgeCenter; |
| switch (handlePosition) { |
| case SelectionHandlePosition::Top: |
| testPoint.move(0, distance); |
| break; |
| case SelectionHandlePosition::Right: |
| testPoint.move(-distance, 0); |
| break; |
| case SelectionHandlePosition::Bottom: |
| testPoint.move(0, -distance); |
| break; |
| case SelectionHandlePosition::Left: |
| testPoint.move(distance, 0); |
| break; |
| } |
| |
| distance *= multiple; |
| |
| RefPtr<Range> newRange = rangeForBlockAtPoint(testPoint); |
| if (!newRange || &newRange->ownerDocument() != ¤tRange.ownerDocument()) |
| continue; |
| |
| if (handlePosition == SelectionHandlePosition::Top || handlePosition == SelectionHandlePosition::Left) |
| newRange = Range::create(newRange->startContainer().document(), newRange->endPosition(), currentRange.endPosition()); |
| else |
| newRange = Range::create(newRange->startContainer().document(), currentRange.startPosition(), newRange->startPosition()); |
| |
| IntRect copyRect = selectionBoxForRange(newRange.get()); |
| if (copyRect.isEmpty()) { |
| // If the new range is an empty rectangle, we try the block at the current point |
| // and see if that has a rectangle that is a better choice. |
| newRange = rangeForBlockAtPoint(testPoint); |
| copyRect = selectionBoxForRange(newRange.get()); |
| } |
| bool isBetterChoice; |
| switch (handlePosition) { |
| case SelectionHandlePosition::Top: |
| case SelectionHandlePosition::Bottom: |
| isBetterChoice = (copyRect.height() < currentBox.height()); |
| if (copyRect.height() == currentBox.height()) |
| isBetterChoice = canShrinkToTextSelection(*newRange.get()); |
| break; |
| case SelectionHandlePosition::Left: |
| case SelectionHandlePosition::Right: |
| isBetterChoice = (copyRect.width() > bestRect.width()); |
| break; |
| } |
| |
| isBetterChoice = isBetterChoice && !areRangesEqual(newRange.get(), ¤tRange); |
| if (bestRange && isBetterChoice) { |
| switch (handlePosition) { |
| case SelectionHandlePosition::Top: |
| case SelectionHandlePosition::Bottom: |
| isBetterChoice = (copyRect.height() > bestRect.height()); |
| break; |
| case SelectionHandlePosition::Left: |
| case SelectionHandlePosition::Right: |
| isBetterChoice = (copyRect.width() > bestRect.width()); |
| break; |
| } |
| } |
| if (isBetterChoice) { |
| bestRange = newRange; |
| bestRect = copyRect; |
| } |
| |
| } |
| |
| if (!bestRange) |
| bestRange = ¤tRange; |
| |
| // If we can shrink down to text only, the only reason we wouldn't is that |
| // there are multiple sub-element blocks beneath us. If we didn't find |
| // multiple sub-element blocks, don't shrink to a sub-element block. |
| |
| if (canShrinkToTextSelection(*bestRange.get())) |
| flags = None; |
| |
| return bestRange.releaseNonNull(); |
| } |
| |
| void WebPage::computeExpandAndShrinkThresholdsForHandle(const IntPoint& point, SelectionHandlePosition handlePosition, float& growThreshold, float& shrinkThreshold) |
| { |
| Frame& frame = m_page->focusController().focusedOrMainFrame(); |
| RefPtr<Range> currentRange = m_currentBlockSelection ? m_currentBlockSelection.get() : frame.selection().selection().toNormalizedRange(); |
| |
| if (!currentRange) |
| return; |
| |
| Ref<Range> expandedRange = expandedRangeFromHandle(*currentRange, handlePosition); |
| SelectionFlags flags; |
| RefPtr<Range> contractedRange = contractedRangeFromHandle(*currentRange, handlePosition, flags); |
| |
| IntRect currentBounds = selectionBoxForRange(currentRange.get()); |
| IntRect expandedBounds = selectionBoxForRange(expandedRange.ptr()); |
| IntRect contractedBounds = selectionBoxForRange(contractedRange.get()); |
| |
| float current; |
| float expanded; |
| float contracted; |
| float maxThreshold; |
| float minThreshold; |
| |
| switch (handlePosition) { |
| case SelectionHandlePosition::Top: { |
| current = currentBounds.y(); |
| expanded = expandedBounds.y(); |
| contracted = contractedBounds.y(); |
| maxThreshold = FLT_MIN; |
| minThreshold = FLT_MAX; |
| break; |
| } |
| case SelectionHandlePosition::Right: { |
| current = currentBounds.maxX(); |
| expanded = expandedBounds.maxX(); |
| contracted = contractedBounds.maxX(); |
| maxThreshold = FLT_MAX; |
| minThreshold = FLT_MIN; |
| break; |
| } |
| case SelectionHandlePosition::Bottom: { |
| current = currentBounds.maxY(); |
| expanded = expandedBounds.maxY(); |
| contracted = contractedBounds.maxY(); |
| maxThreshold = FLT_MAX; |
| minThreshold = FLT_MIN; |
| break; |
| } |
| case SelectionHandlePosition::Left: { |
| current = currentBounds.x(); |
| expanded = expandedBounds.x(); |
| contracted = contractedBounds.x(); |
| maxThreshold = FLT_MIN; |
| minThreshold = FLT_MAX; |
| break; |
| } |
| } |
| |
| static const float fractionToGrow = 0.3; |
| |
| growThreshold = current + (expanded - current) * fractionToGrow; |
| shrinkThreshold = current + (contracted - current) * (1 - fractionToGrow); |
| if (areRangesEqual(expandedRange.ptr(), currentRange.get())) |
| growThreshold = maxThreshold; |
| |
| if (flags & IsBlockSelection && areRangesEqual(contractedRange.get(), currentRange.get())) |
| shrinkThreshold = minThreshold; |
| } |
| |
| static inline bool shouldExpand(SelectionHandlePosition handlePosition, const IntRect& rect, const IntPoint& point) |
| { |
| switch (handlePosition) { |
| case SelectionHandlePosition::Top: |
| return (point.y() < rect.y()); |
| case SelectionHandlePosition::Left: |
| return (point.x() < rect.x()); |
| case SelectionHandlePosition::Right: |
| return (point.x() > rect.maxX()); |
| case SelectionHandlePosition::Bottom: |
| return (point.y() > rect.maxY()); |
| } |
| } |
| |
| RefPtr<WebCore::Range> WebPage::changeBlockSelection(const IntPoint& point, SelectionHandlePosition handlePosition, float& growThreshold, float& shrinkThreshold, SelectionFlags& flags) |
| { |
| Frame& frame = m_page->focusController().focusedOrMainFrame(); |
| RefPtr<Range> currentRange = m_currentBlockSelection ? m_currentBlockSelection.get() : frame.selection().selection().toNormalizedRange(); |
| if (!currentRange) |
| return nullptr; |
| RefPtr<Range> newRange = shouldExpand(handlePosition, selectionBoxForRange(currentRange.get()), point) ? expandedRangeFromHandle(*currentRange, handlePosition) : contractedRangeFromHandle(*currentRange, handlePosition, flags); |
| |
| if (newRange) { |
| m_currentBlockSelection = newRange; |
| frame.selection().setSelectedRange(newRange.get(), VP_DEFAULT_AFFINITY, true, UserTriggered); |
| } |
| |
| computeExpandAndShrinkThresholdsForHandle(point, handlePosition, growThreshold, shrinkThreshold); |
| return newRange; |
| } |
| |
| void WebPage::updateBlockSelectionWithTouch(const IntPoint& point, uint32_t touch, uint32_t handlePosition) |
| { |
| Frame& frame = m_page->focusController().focusedOrMainFrame(); |
| IntPoint adjustedPoint = frame.view()->rootViewToContents(point); |
| |
| float growThreshold = 0; |
| float shrinkThreshold = 0; |
| SelectionFlags flags = IsBlockSelection; |
| |
| switch (static_cast<SelectionTouch>(touch)) { |
| case SelectionTouch::Started: |
| computeExpandAndShrinkThresholdsForHandle(adjustedPoint, static_cast<SelectionHandlePosition>(handlePosition), growThreshold, shrinkThreshold); |
| break; |
| case SelectionTouch::Ended: |
| break; |
| case SelectionTouch::Moved: |
| changeBlockSelection(adjustedPoint, static_cast<SelectionHandlePosition>(handlePosition), growThreshold, shrinkThreshold, flags); |
| break; |
| default: |
| return; |
| } |
| |
| send(Messages::WebPageProxy::DidUpdateBlockSelectionWithTouch(touch, static_cast<uint32_t>(flags), growThreshold, shrinkThreshold)); |
| } |
| |
| void WebPage::clearSelection() |
| { |
| m_currentBlockSelection = nullptr; |
| m_page->focusController().focusedOrMainFrame().selection().clear(); |
| } |
| |
| RefPtr<Range> WebPage::switchToBlockSelectionAtPoint(const IntPoint& point, SelectionHandlePosition handlePosition) |
| { |
| Frame& frame = m_page->focusController().focusedOrMainFrame(); |
| RefPtr<Range> newRange; |
| RefPtr<Range> currentRange = frame.selection().selection().toNormalizedRange(); |
| if (currentRange) { |
| Node* currentNode = ¤tRange->startContainer(); |
| if (currentNode->isTextNode()) { |
| newRange = enclosingTextUnitOfGranularity(currentRange->startPosition(), ParagraphGranularity, DirectionBackward); |
| if (newRange && newRange->collapsed()) |
| newRange = nullptr; |
| } |
| |
| if (!newRange && !currentNode->isElementNode()) |
| currentNode = currentNode->parentElement(); |
| |
| if (!newRange && currentNode) { |
| newRange = Range::create(currentNode->document()); |
| newRange->selectNodeContents(*currentNode); |
| } |
| } |
| return newRange; |
| } |
| |
| bool WebPage::shouldSwitchToBlockModeForHandle(const IntPoint& handlePoint, SelectionHandlePosition handlePosition) |
| { |
| if (!m_allowsBlockSelection) |
| return false; |
| |
| if (!m_blockRectForTextSelection.height()) |
| return false; |
| switch (handlePosition) { |
| case SelectionHandlePosition::Top: |
| return handlePoint.y() < m_blockRectForTextSelection.y(); |
| case SelectionHandlePosition::Right: |
| return handlePoint.x() > m_blockRectForTextSelection.maxX(); |
| case SelectionHandlePosition::Bottom: |
| return handlePoint.y() > m_blockRectForTextSelection.maxY(); |
| case SelectionHandlePosition::Left: |
| return handlePoint.x() < m_blockRectForTextSelection.x(); |
| } |
| } |
| |
| 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; |
| SelectionHandlePosition handlePosition = baseIsStart ? SelectionHandlePosition::Bottom : SelectionHandlePosition::Top; |
| |
| switch (static_cast<SelectionTouch>(touches)) { |
| 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 = rangeForPosition(&frame, position, 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: |
| if (shouldSwitchToBlockModeForHandle(pointInDocument, handlePosition)) { |
| range = switchToBlockSelectionAtPoint(pointInDocument, handlePosition); |
| flags = IsBlockSelection; |
| } else |
| range = rangeForPosition(&frame, position, baseIsStart); |
| break; |
| } |
| if (range && flags != IsBlockSelection) |
| frame.selection().setSelectedRange(range.get(), position.affinity(), true, UserTriggered); |
| |
| send(Messages::WebPageProxy::TouchesCallback(point, touches, flags, callbackID)); |
| if (range && flags == IsBlockSelection) { |
| // We just switched to block selection therefore we need to compute the thresholds. |
| m_currentBlockSelection = range; |
| frame.selection().setSelectedRange(range.get(), position.affinity(), true, UserTriggered); |
| |
| float growThreshold = 0; |
| float shrinkThreshold = 0; |
| computeExpandAndShrinkThresholdsForHandle(point, handlePosition, growThreshold, shrinkThreshold); |
| send(Messages::WebPageProxy::DidUpdateBlockSelectionWithTouch(static_cast<uint32_t>(SelectionTouch::Started), static_cast<uint32_t>(IsBlockSelection), growThreshold, shrinkThreshold)); |
| } |
| } |
| |
| 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(), true, 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(); |
| frame.selection().setSelectedRange(wordRangeFromPosition(position).get(), position.affinity(), true, UserTriggered); |
| } |
| |
| 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(), true, 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(), true, UserTriggered); |
| send(Messages::WebPageProxy::VoidCallback(callbackID)); |
| } |
| |
| static VisiblePosition visiblePositionForPositionWithOffset(const VisiblePosition& position, int32_t offset) |
| { |
| RefPtr<ContainerNode> root; |
| unsigned startIndex = indexForVisiblePosition(position, root); |
| return visiblePositionForIndex(startIndex + offset, root.get()); |
| } |
| |
| void WebPage::getRectsForGranularityWithSelectionOffset(uint32_t granularity, int32_t offset, CallbackID callbackID) |
| { |
| Frame& frame = m_page->focusController().focusedOrMainFrame(); |
| VisibleSelection selection = frame.selection().selection(); |
| 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)); |
| } |
| |
| 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, 0, 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 = frame.selection().selection(); |
| 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 isInteractingWithAssistedNode) |
| { |
| IntPoint adjustedPoint(frame.view()->rootViewToContents(point)); |
| IntPoint constrainedPoint = m_assistedNode && isInteractingWithAssistedNode ? constrainPoint(adjustedPoint, frame, *m_assistedNode) : adjustedPoint; |
| return frame.visiblePositionForPoint(constrainedPoint); |
| } |
| |
| void WebPage::selectPositionAtPoint(const WebCore::IntPoint& point, bool isInteractingWithAssistedNode, CallbackID callbackID) |
| { |
| auto& frame = m_page->focusController().focusedOrMainFrame(); |
| VisiblePosition position = visiblePositionInFocusedNodeForPoint(frame, point, isInteractingWithAssistedNode); |
| |
| if (position.isNotNull()) |
| frame.selection().setSelectedRange(Range::create(*frame.document(), position, position).ptr(), position.affinity(), true, UserTriggered); |
| send(Messages::WebPageProxy::VoidCallback(callbackID)); |
| } |
| |
| void WebPage::selectPositionAtBoundaryWithDirection(const WebCore::IntPoint& point, uint32_t granularity, uint32_t direction, bool isInteractingWithAssistedNode, CallbackID callbackID) |
| { |
| auto& frame = m_page->focusController().focusedOrMainFrame(); |
| VisiblePosition position = visiblePositionInFocusedNodeForPoint(frame, point, isInteractingWithAssistedNode); |
| |
| 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, true, 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, true, UserTriggered); |
| } |
| send(Messages::WebPageProxy::VoidCallback(callbackID)); |
| } |
| |
| RefPtr<Range> WebPage::rangeForGranularityAtPoint(Frame& frame, const WebCore::IntPoint& point, uint32_t granularity, bool isInteractingWithAssistedNode) |
| { |
| VisiblePosition position = visiblePositionInFocusedNodeForPoint(frame, point, isInteractingWithAssistedNode); |
| |
| RefPtr<Range> range; |
| switch (static_cast<WebCore::TextGranularity>(granularity)) { |
| case WordGranularity: |
| range = wordRangeFromPosition(position); |
| break; |
| case SentenceGranularity: |
| range = enclosingTextUnitOfGranularity(position, SentenceGranularity, DirectionForward); |
| break; |
| case ParagraphGranularity: |
| range = enclosingTextUnitOfGranularity(position, ParagraphGranularity, DirectionForward); |
| break; |
| case DocumentGranularity: |
| 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::selectTextWithGranularityAtPoint(const WebCore::IntPoint& point, uint32_t granularity, bool isInteractingWithAssistedNode, CallbackID callbackID) |
| { |
| auto& frame = m_page->focusController().focusedOrMainFrame(); |
| RefPtr<Range> range = rangeForGranularityAtPoint(frame, point, granularity, isInteractingWithAssistedNode); |
| if (!isInteractingWithAssistedNode) { |
| m_blockSelectionDesiredSize.setWidth(blockSelectionStartWidth); |
| m_blockSelectionDesiredSize.setHeight(blockSelectionStartHeight); |
| m_currentBlockSelection = nullptr; |
| auto* renderer = range ? range->startContainer().renderer() : nullptr; |
| if (renderer && renderer->style().preserveNewline()) |
| m_blockRectForTextSelection = renderer->absoluteBoundingBoxRect(true); |
| else { |
| auto paragraphRange = enclosingTextUnitOfGranularity(visiblePositionInFocusedNodeForPoint(frame, point, isInteractingWithAssistedNode), ParagraphGranularity, DirectionForward); |
| if (paragraphRange && !paragraphRange->collapsed()) |
| m_blockRectForTextSelection = selectionBoxForRange(paragraphRange.get()); |
| } |
| |
| if (rectIsTooBigForSelection(m_blockRectForTextSelection, frame)) |
| m_blockRectForTextSelection.setHeight(0); |
| } |
| |
| if (range) |
| frame.selection().setSelectedRange(range.get(), UPSTREAM, true, 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 isInteractingWithAssistedNode, CallbackID callbackID) |
| { |
| auto& frame = m_page->focusController().focusedOrMainFrame(); |
| VisiblePosition position = visiblePositionInFocusedNodeForPoint(frame, point, isInteractingWithAssistedNode); |
| RefPtr<Range> newRange = rangeForGranularityAtPoint(frame, point, granularity, isInteractingWithAssistedNode); |
| |
| 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, true, UserTriggered); |
| |
| send(Messages::WebPageProxy::UnsignedCallback(selectionStart == m_initialSelection->startPosition(), callbackID)); |
| } |
| |
| void WebPage::updateSelectionWithExtentPoint(const WebCore::IntPoint& point, bool isInteractingWithAssistedNode, CallbackID callbackID) |
| { |
| auto& frame = m_page->focusController().focusedOrMainFrame(); |
| VisiblePosition position = visiblePositionInFocusedNodeForPoint(frame, point, isInteractingWithAssistedNode); |
| |
| if (position.isNull()) { |
| send(Messages::WebPageProxy::UnsignedCallback(false, callbackID)); |
| return; |
| } |
| |
| RefPtr<Range> range; |
| VisiblePosition selectionStart; |
| VisiblePosition selectionEnd; |
| |
| 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; |
| } |
| } |
| |
| if (selectionStart.isNotNull() && selectionEnd.isNotNull()) |
| range = Range::create(*frame.document(), selectionStart, selectionEnd); |
| |
| if (range) |
| frame.selection().setSelectedRange(range.get(), UPSTREAM, true, 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, true); |
| 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())); |
| RefPtr<Range> range = Range::create(*frame.document(), position, frame.selection().selection().start()); |
| |
| if (plainTextReplacingNoBreakSpace(range.get()) != 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.get(), UPSTREAM, true); |
| frame.editor().insertText(newText, 0); |
| frame.editor().setIgnoreSelectionChanges(false); |
| } |
| |
| void WebPage::requestAutocorrectionData(const String& textForAutocorrection, CallbackID callbackID) |
| { |
| Frame& frame = m_page->focusController().focusedOrMainFrame(); |
| if (!frame.selection().isCaret()) { |
| send(Messages::WebPageProxy::AutocorrectionDataCallback(Vector<FloatRect>(), String(), 0, 0, callbackID)); |
| return; |
| } |
| |
| VisiblePosition position = frame.selection().selection().start(); |
| RefPtr<Range> range = wordRangeFromPosition(position); |
| if (!range) { |
| send(Messages::WebPageProxy::AutocorrectionDataCallback(Vector<FloatRect>(), String(), 0, 0, callbackID)); |
| return; |
| } |
| |
| String 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(); |
| |
| CGFloat fontSize = CTFontGetSize(font); |
| uint64_t fontTraits = CTFontGetSymbolicTraits(font); |
| RetainPtr<NSString> fontName = adoptNS((NSString *)CTFontCopyFamilyName(font)); |
| send(Messages::WebPageProxy::AutocorrectionDataCallback(rectsForText, fontName.get(), fontSize, fontTraits, callbackID)); |
| } |
| |
| void WebPage::applyAutocorrection(const String& correction, const String& originalText, CallbackID callbackID) |
| { |
| bool correctionApplied; |
| syncApplyAutocorrection(correction, originalText, correctionApplied); |
| send(Messages::WebPageProxy::StringCallback(correctionApplied ? correction : String(), callbackID)); |
| } |
| |
| void WebPage::executeEditCommandWithCallback(const String& commandName, CallbackID callbackID) |
| { |
| executeEditCommand(commandName, String()); |
| send(Messages::WebPageProxy::VoidCallback(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, bool& correctionApplied) |
| { |
| correctionApplied = false; |
| |
| Frame& frame = m_page->focusController().focusedOrMainFrame(); |
| if (!frame.selection().isCaretOrRange()) |
| return; |
| |
| RefPtr<Range> range; |
| String textForRange; |
| |
| if (frame.selection().isCaret()) { |
| VisiblePosition position = frame.selection().selection().start(); |
| range = wordRangeFromPosition(position); |
| textForRange = plainTextReplacingNoBreakSpace(range.get()); |
| if (textForRange != originalText) { |
| // 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()); |
| if (range) |
| textForRange = (range) ? plainTextReplacingNoBreakSpace(range.get()) : emptyString(); |
| 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 = (range) ? plainTextReplacingNoBreakSpace(range.get()) : emptyString(); |
| loopCount++; |
| } |
| } |
| } else { |
| // Range selection. |
| range = frame.selection().toNormalizedRange(); |
| if (!range) { |
| correctionApplied = false; |
| return; |
| } |
| textForRange = plainTextReplacingNoBreakSpace(range.get()); |
| } |
| |
| if (textForRange != originalText) { |
| correctionApplied = false; |
| return; |
| } |
| |
| frame.selection().setSelectedRange(range.get(), UPSTREAM, true); |
| if (correction.length()) |
| frame.editor().insertText(correction, 0, originalText.isEmpty() ? TextEventInputKeyboard : TextEventInputAutocompletion); |
| else |
| frame.editor().deleteWithDirection(DirectionBackward, CharacterGranularity, false, true); |
| correctionApplied = true; |
| } |
| |
| static void computeAutocorrectionContext(Frame& frame, String& contextBefore, String& markedText, String& selectedText, String& contextAfter, uint64_t& location, uint64_t& length) |
| { |
| RefPtr<Range> range; |
| VisiblePosition startPosition = frame.selection().selection().start(); |
| VisiblePosition endPosition = frame.selection().selection().end(); |
| location = NSNotFound; |
| length = 0; |
| 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()) { |
| location = markedTextBefore.length(); |
| 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 = ASCIILiteral("\n ") + 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()); |
| } |
| } |
| } |
| |
| void WebPage::requestAutocorrectionContext(CallbackID callbackID) |
| { |
| String contextBefore; |
| String contextAfter; |
| String selectedText; |
| String markedText; |
| uint64_t location; |
| uint64_t length; |
| |
| computeAutocorrectionContext(m_page->focusController().focusedOrMainFrame(), contextBefore, markedText, selectedText, contextAfter, location, length); |
| |
| send(Messages::WebPageProxy::AutocorrectionContextCallback(contextBefore, markedText, selectedText, contextAfter, location, length, callbackID)); |
| } |
| |
| void WebPage::getAutocorrectionContext(String& contextBefore, String& markedText, String& selectedText, String& contextAfter, uint64_t& location, uint64_t& length) |
| { |
| computeAutocorrectionContext(m_page->focusController().focusedOrMainFrame(), contextBefore, markedText, selectedText, contextAfter, location, length); |
| } |
| |
| 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& node) |
| { |
| if (is<HTMLSelectElement>(node)) |
| return true; |
| if (is<HTMLTextAreaElement>(node)) |
| return true; |
| if (is<HTMLInputElement>(node)) { |
| HTMLInputElement& inputElement = downcast<HTMLInputElement>(node); |
| // FIXME: This laundry list of types is not a good way to factor this. Need a suitable function on HTMLInputElement itself. |
| return inputElement.isTextField() || inputElement.isDateField() || inputElement.isDateTimeLocalField() || inputElement.isMonthField() || inputElement.isTimeField(); |
| } |
| return node.isContentEditable(); |
| } |
| |
| void WebPage::getPositionInformation(const InteractionInformationRequest& request, InteractionInformationAtPosition& info) |
| { |
| info.request = request; |
| |
| FloatPoint adjustedPoint; |
| Node* hitNode = m_page->mainFrame().nodeRespondingToClickEvents(request.point, adjustedPoint); |
| |
| info.nodeAtPositionIsAssistedNode = (hitNode == m_assistedNode); |
| if (m_assistedNode) { |
| const Frame& frame = m_page->focusController().focusedOrMainFrame(); |
| if (frame.editor().hasComposition()) { |
| const uint32_t kHitAreaWidth = 66; |
| const uint32_t kHitAreaHeight = 66; |
| FrameView& view = *frame.view(); |
| IntPoint adjustedPoint(view.rootViewToContents(request.point)); |
| IntPoint constrainedPoint = m_assistedNode ? constrainPoint(adjustedPoint, frame, *m_assistedNode) : adjustedPoint; |
| VisiblePosition position = frame.visiblePositionForPoint(constrainedPoint); |
| |
| RefPtr<Range> compositionRange = frame.editor().compositionRange(); |
| 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); |
| } |
| } |
| bool elementIsLinkOrImage = false; |
| if (hitNode) { |
| Element* element = is<Element>(*hitNode) ? downcast<Element>(hitNode) : nullptr; |
| if (element) { |
| info.isElement = true; |
| info.idAttribute = element->getIdAttribute(); |
| Element* linkElement = nullptr; |
| if (element->renderer() && element->renderer()->isRenderImage()) { |
| elementIsLinkOrImage = true; |
| linkElement = containingLinkElement(element); |
| } else if (element->isLink()) { |
| linkElement = element; |
| elementIsLinkOrImage = true; |
| } |
| |
| if (elementIsLinkOrImage) { |
| if (linkElement) { |
| info.isLink = true; |
| |
| if (request.includeSnapshot) { |
| // 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)) |
| info.image = &snapshot->bitmap(); |
| } |
| |
| if (request.includeLinkIndicator) { |
| RefPtr<Range> linkRange = rangeOfContents(*linkElement); |
| if (linkRange) { |
| float deviceScaleFactor = corePage()->deviceScaleFactor(); |
| const float marginInPoints = 4; |
| |
| RefPtr<TextIndicator> textIndicator = TextIndicator::createWithRange(*linkRange, TextIndicatorOptionTightlyFitContent | TextIndicatorOptionRespectTextColor | TextIndicatorOptionPaintBackgrounds | TextIndicatorOptionUseBoundingRectAndPaintAllContentForComplexRanges | |
| TextIndicatorOptionIncludeMarginIfRangeMatchesSelection, TextIndicatorPresentationTransition::None, FloatSize(marginInPoints * deviceScaleFactor, marginInPoints * deviceScaleFactor)); |
| |
| if (textIndicator) |
| info.linkIndicator = textIndicator->data(); |
| } |
| } |
| |
| #if ENABLE(DATA_DETECTION) |
| info.isDataDetectorLink = DataDetection::isDataDetectorLink(*element); |
| if (info.isDataDetectorLink) { |
| const int dataDetectionExtendedContextLength = 350; |
| info.dataDetectorIdentifier = DataDetection::dataDetectorIdentifier(*element); |
| info.dataDetectorResults = element->document().frame()->dataDetectionResults(); |
| if (DataDetection::requiresExtendedContext(*element)) { |
| RefPtr<Range> 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 |
| } |
| if (element->renderer() && element->renderer()->isRenderImage()) { |
| info.isImage = true; |
| auto& renderImage = downcast<RenderImage>(*(element->renderer())); |
| if (renderImage.cachedImage() && !renderImage.cachedImage()->errorOccurred()) { |
| if (Image* image = renderImage.cachedImage()->imageForRenderer(&renderImage)) { |
| if (image->width() > 1 && image->height() > 1) { |
| info.imageURL = element->document().completeURL(renderImage.cachedImage()->url()); |
| info.isAnimatedImage = image->isAnimated(); |
| |
| if (request.includeSnapshot) { |
| FloatSize screenSizeInPixels = screenSize(); |
| screenSizeInPixels.scale(corePage()->deviceScaleFactor()); |
| FloatSize scaledSize = largestRectWithAspectRatioInsideRect(image->size().width() / image->size().height(), FloatRect(0, 0, screenSizeInPixels.width(), screenSizeInPixels.height())).size(); |
| FloatSize bitmapSize = scaledSize.width() < image->size().width() ? scaledSize : image->size(); |
| // FIXME: Only select ExtendedColor on images known to need wide gamut |
| ShareableBitmap::Flags flags = ShareableBitmap::SupportsAlpha; |
| flags |= screenSupportsExtendedColor() ? ShareableBitmap::SupportsExtendedColor : 0; |
| if (RefPtr<ShareableBitmap> sharedBitmap = ShareableBitmap::createShareable(IntSize(bitmapSize), flags)) { |
| auto graphicsContext = sharedBitmap->createGraphicsContext(); |
| graphicsContext->drawImage(*image, FloatRect(0, 0, bitmapSize.width(), bitmapSize.height())); |
| info.image = sharedBitmap; |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| if (linkElement) |
| info.url = linkElement->document().completeURL(stripLeadingAndTrailingHTMLSpaces(linkElement->getAttribute(HTMLNames::hrefAttr))); |
| 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 (RenderElement* renderer = element->renderer()) { |
| 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); |
| } |
| } |
| } |
| } |
| |
| if (!elementIsLinkOrImage) { |
| HitTestResult result = m_page->mainFrame().eventHandler().hitTestResultAtPoint(request.point, HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::DisallowUserAgentShadowContent | HitTestRequest::AllowChildFrameContent); |
| hitNode = result.innerNode(); |
| // Hit test could return HTMLHtmlElement that has no renderer, if the body is smaller than the document. |
| if (hitNode && hitNode->renderer()) { |
| RenderObject* renderer = hitNode->renderer(); |
| m_page->focusController().setFocusedFrame(result.innerNodeFrame()); |
| info.bounds = renderer->absoluteBoundingBoxRect(true); |
| // We don't want to select blocks that are larger than 97% of the visible area of the document. |
| if (is<HTMLAttachmentElement>(*hitNode)) { |
| info.isAttachment = true; |
| const HTMLAttachmentElement& attachment = downcast<HTMLAttachmentElement>(*hitNode); |
| info.title = attachment.attachmentTitle(); |
| if (attachment.file()) |
| info.url = URL::fileURLWithFileSystemPath(downcast<HTMLAttachmentElement>(*hitNode).file()->path()); |
| } else { |
| info.isSelectable = renderer->style().userSelect() != SELECT_NONE; |
| if (info.isSelectable && !hitNode->isTextNode()) |
| info.isSelectable = !isAssistableElement(*downcast<Element>(hitNode)) && !rectIsTooBigForSelection(info.bounds, *result.innerNodeFrame()); |
| } |
| } |
| } |
| |
| #if ENABLE(DATA_INTERACTION) |
| info.hasSelectionAtPosition = m_page->hasSelectionAtPosition(adjustedPoint); |
| #endif |
| info.adjustedPointForNodeRespondingToClickEvents = adjustedPoint; |
| } |
| |
| void WebPage::requestPositionInformation(const InteractionInformationRequest& request) |
| { |
| InteractionInformationAtPosition info; |
| |
| getPositionInformation(request, info); |
| send(Messages::WebPageProxy::DidReceivePositionInformation(info)); |
| } |
| |
| void WebPage::startInteractionWithElementAtPosition(const WebCore::IntPoint& point) |
| { |
| FloatPoint adjustedPoint; |
| m_interactionNode = m_page->mainFrame().nodeRespondingToClickEvents(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; |
| } |
| |
| static inline bool hasAssistableElement(Node* startNode, Page& page, bool isForward) |
| { |
| return nextAssistableElement(startNode, page, isForward); |
| } |
| |
| void WebPage::focusNextAssistedNode(bool isForward, CallbackID callbackID) |
| { |
| Element* nextElement = nextAssistableElement(m_assistedNode.get(), *m_page, isForward); |
| m_userIsInteracting = true; |
| if (nextElement) |
| nextElement->focus(); |
| m_userIsInteracting = false; |
| send(Messages::WebPageProxy::VoidCallback(callbackID)); |
| } |
| |
| void WebPage::getAssistedNodeInformation(AssistedNodeInformation& information) |
| { |
| layoutIfNeeded(); |
| |
| // FIXME: information.selectionRect should be set to the actual selection rect, but when this is called at focus time |
| // we don't have a selection yet. Using the last interaction location is a reasonable approximation for now. |
| information.selectionRect = IntRect(m_lastInteractionLocation, IntSize(1, 1)); |
| |
| if (RenderObject* renderer = m_assistedNode->renderer()) { |
| Frame& elementFrame = m_page->focusController().focusedOrMainFrame(); |
| information.elementRect = elementFrame.view()->contentsToRootView(renderer->absoluteBoundingBoxRect()); |
| information.nodeFontSize = renderer->style().fontDescription().computedSize(); |
| |
| bool inFixed = false; |
| renderer->localToContainerPoint(FloatPoint(), nullptr, UseTransforms, &inFixed); |
| information.insideFixedPosition = inFixed; |
| information.isRTL = renderer->style().direction() == RTL; |
| |
| FrameView* frameView = elementFrame.view(); |
| if (inFixed && elementFrame.isMainFrame() && !frameView->frame().settings().visualViewportEnabled()) { |
| IntRect currentFixedPositionRect = frameView->customFixedPositionLayoutRect(); |
| frameView->setCustomFixedPositionLayoutRect(frameView->renderView()->documentRect()); |
| information.elementRect = frameView->contentsToRootView(renderer->absoluteBoundingBoxRect()); |
| frameView->setCustomFixedPositionLayoutRect(currentFixedPositionRect); |
| |
| if (!information.elementRect.contains(m_lastInteractionLocation)) |
| information.selectionRect.setLocation(information.elementRect.location()); |
| } else { |
| // Don't use the selection rect if interaction was outside the element rect. |
| if (!information.elementRect.contains(m_lastInteractionLocation)) |
| information.selectionRect = IntRect(); |
| } |
| } else |
| information.elementRect = IntRect(); |
| |
| information.minimumScaleFactor = minimumPageScaleFactor(); |
| information.maximumScaleFactor = maximumPageScaleFactor(); |
| information.maximumScaleFactorIgnoringAlwaysScalable = maximumPageScaleFactorIgnoringAlwaysScalable(); |
| information.allowsUserScaling = m_viewportConfiguration.allowsUserScaling(); |
| information.allowsUserScalingIgnoringAlwaysScalable = m_viewportConfiguration.allowsUserScalingIgnoringAlwaysScalable(); |
| information.hasNextNode = hasAssistableElement(m_assistedNode.get(), *m_page, true); |
| information.hasPreviousNode = hasAssistableElement(m_assistedNode.get(), *m_page, false); |
| |
| if (is<HTMLSelectElement>(*m_assistedNode)) { |
| HTMLSelectElement& element = downcast<HTMLSelectElement>(*m_assistedNode); |
| 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>(*m_assistedNode)) { |
| HTMLTextAreaElement& element = downcast<HTMLTextAreaElement>(*m_assistedNode); |
| 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); |
| } else if (is<HTMLInputElement>(*m_assistedNode)) { |
| HTMLInputElement& element = downcast<HTMLInputElement>(*m_assistedNode); |
| HTMLFormElement* form = element.form(); |
| if (form) |
| information.formAction = form->getURLAttribute(WebCore::HTMLNames::actionAttr); |
| information.autocapitalizeType = element.autocapitalizeType(); |
| information.isAutocorrect = element.shouldAutocorrect(); |
| 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 AtomicString& 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; |
| } |
| } |
| |
| information.isReadOnly = element.isReadOnly(); |
| information.value = element.value(); |
| information.valueAsNumber = element.valueAsNumber(); |
| information.title = element.title(); |
| information.autofillFieldName = WebCore::toAutofillFieldName(element.autofillData().fieldName); |
| } else if (m_assistedNode->hasEditableStyle()) { |
| information.elementType = InputType::ContentEditable; |
| if (is<HTMLElement>(*m_assistedNode)) { |
| auto& assistedElement = downcast<HTMLElement>(*m_assistedNode); |
| information.isAutocorrect = assistedElement.shouldAutocorrect(); |
| information.autocapitalizeType = assistedElement.autocapitalizeType(); |
| } else { |
| information.isAutocorrect = true; |
| information.autocapitalizeType = AutocapitalizeTypeDefault; |
| } |
| information.isReadOnly = false; |
| } |
| } |
| |
| void WebPage::setViewportConfigurationMinimumLayoutSize(const FloatSize& size) |
| { |
| if (m_viewportConfiguration.setMinimumLayoutSize(size)) |
| viewportConfigurationChanged(); |
| } |
| |
| 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(); |
| } |
| |
| // 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::resetTextAutosizing() |
| { |
| for (Frame* frame = &m_page->mainFrame(); frame; frame = frame->tree().traverseNext()) { |
| Document* document = frame->document(); |
| if (!document || !document->renderView()) |
| continue; |
| document->renderView()->resetTextAutosizing(); |
| } |
| } |
| |
| void WebPage::dynamicViewportSizeUpdate(const FloatSize& minimumLayoutSize, const WebCore::FloatSize& maximumUnobscuredSize, const FloatRect& targetExposedContentRect, const FloatRect& targetUnobscuredRect, const WebCore::FloatRect& targetUnobscuredRectInScrollViewCoordinates, const WebCore::FloatBoxExtent& targetUnobscuredSafeAreaInsets, double targetScale, int32_t deviceOrientation, uint64_t 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 - 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 (RenderView* mainFrameRenderView = frameView.renderView()) |
| mainFrameRenderView->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; |
| } |
| } |
| } |
| |
| m_viewportConfiguration.setMinimumLayoutSize(minimumLayoutSize); |
| IntSize newLayoutSize = m_viewportConfiguration.layoutSize(); |
| |
| if (setFixedLayoutSize(newLayoutSize)) |
| resetTextAutosizing(); |
| |
| 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() == minimumLayoutSize.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(); |
| |
| if (frameView.frame().settings().visualViewportEnabled()) { |
| LayoutRect documentRect = IntRect(frameView.scrollOrigin(), frameView.contentsSize()); |
| LayoutRect layoutViewportRect = FrameView::computeUpdatedLayoutViewportRect(frameView.layoutViewportRect(), documentRect, LayoutSize(newUnobscuredContentRect.size()), LayoutRect(newUnobscuredContentRect), frameView.baseLayoutViewportSize(), frameView.minStableLayoutViewportOrigin(), frameView.maxStableLayoutViewportOrigin(), FrameView::LayoutViewportConstraint::ConstrainedToDocumentRect); |
| frameView.setLayoutViewportOverrideRect(layoutViewportRect); |
| } else { |
| IntRect fixedPositionLayoutRect = enclosingIntRect(frameView.viewportConstrainedObjectsRect()); |
| frameView.setCustomFixedPositionLayoutRect(fixedPositionLayoutRect); |
| } |
| |
| frameView.setCustomSizeForResizeEvent(expandedIntSize(targetUnobscuredRectInScrollViewCoordinates.size())); |
| setDeviceOrientation(deviceOrientation); |
| frameView.setScrollOffset(roundedUnobscuredContentRectPosition); |
| |
| m_drawingArea->scheduleCompositingLayerFlush(); |
| |
| send(Messages::WebPageProxy::DynamicViewportUpdateChangedTarget(pageScaleFactor(), frameView.scrollPosition(), dynamicViewportSizeUpdateID)); |
| } |
| |
| void WebPage::synchronizeDynamicViewportUpdate(double& newTargetScale, FloatPoint& newScrollPosition, uint64_t& nextValidLayerTreeTransactionID) |
| { |
| newTargetScale = pageScaleFactor(); |
| newScrollPosition = m_page->mainFrame().view()->scrollPosition(); |
| nextValidLayerTreeTransactionID = downcast<RemoteLayerTreeDrawingArea>(*m_drawingArea).nextTransactionID(); |
| } |
| |
| void WebPage::resetViewportDefaultConfiguration(WebFrame* frame) |
| { |
| if (m_useTestingViewportConfiguration) { |
| m_viewportConfiguration.setDefaultConfiguration(ViewportConfiguration::testingParameters()); |
| return; |
| } |
| |
| if (!frame) { |
| m_viewportConfiguration.setDefaultConfiguration(ViewportConfiguration::webpageParameters()); |
| return; |
| } |
| |
| Document* 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(ViewportConfiguration::webpageParameters()); |
| } |
| |
| void WebPage::viewportConfigurationChanged() |
| { |
| if (setFixedLayoutSize(m_viewportConfiguration.layoutSize())) |
| resetTextAutosizing(); |
| |
| double initialScale = m_viewportConfiguration.initialScale(); |
| double scale; |
| if (m_userHasChangedPageScaleFactor) |
| scale = std::max(std::min(pageScaleFactor(), m_viewportConfiguration.maximumScale()), m_viewportConfiguration.minimumScale()); |
| else |
| scale = initialScale; |
| |
| m_page->setZoomedOutPageScaleFactor(m_viewportConfiguration.minimumScale()); |
| |
| updateViewportSizeForCSSViewportUnits(); |
| |
| FrameView& frameView = *mainFrameView(); |
| IntPoint scrollPosition = frameView.scrollPosition(); |
| if (!m_hasReceivedVisibleContentRectsAfterDidCommitLoad) { |
| FloatSize minimumLayoutSizeInScrollViewCoordinates = m_viewportConfiguration.minimumLayoutSize(); |
| 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. |
| 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 largestUnobscuredRect = m_maximumUnobscuredSize; |
| if (largestUnobscuredRect.isEmpty()) |
| largestUnobscuredRect = m_viewportConfiguration.minimumLayoutSize(); |
| |
| FrameView& frameView = *mainFrameView(); |
| largestUnobscuredRect.scale(1 / m_viewportConfiguration.initialScaleIgnoringContentSize()); |
| frameView.setViewportSizeForCSSViewportUnits(roundedIntSize(largestUnobscuredRect)); |
| } |
| |
| void WebPage::applicationWillResignActive() |
| { |
| [[NSNotificationCenter defaultCenter] postNotificationName:WebUIApplicationWillResignActiveNotification object:nil]; |
| } |
| |
| void WebPage::applicationDidEnterBackground(bool isSuspendedUnderLock) |
| { |
| [[NSNotificationCenter defaultCenter] postNotificationName:WebUIApplicationDidEnterBackgroundNotification object:nil userInfo:@{@"isSuspendedUnderLock": [NSNumber numberWithBool:isSuspendedUnderLock]}]; |
| |
| m_isSuspendedUnderLock = isSuspendedUnderLock; |
| setLayerTreeStateIsFrozen(true); |
| } |
| |
| void WebPage::applicationDidFinishSnapshottingAfterEnteringBackground() |
| { |
| markLayersVolatile(); |
| } |
| |
| void WebPage::applicationWillEnterForeground(bool isSuspendedUnderLock) |
| { |
| m_isSuspendedUnderLock = false; |
| cancelMarkLayersVolatile(); |
| setLayerTreeStateIsFrozen(false); |
| |
| [[NSNotificationCenter defaultCenter] postNotificationName:WebUIApplicationWillEnterForegroundNotification object:nil userInfo:@{@"isSuspendedUnderLock": @(isSuspendedUnderLock)}]; |
| } |
| |
| void WebPage::applicationDidBecomeActive() |
| { |
| [[NSNotificationCenter defaultCenter] postNotificationName:WebUIApplicationDidBecomeActiveNotification object:nil]; |
| } |
| |
| static inline void adjustVelocityDataForBoundedScale(double& horizontalVelocity, double& verticalVelocity, double& scaleChangeRate, double exposedRectScale, double minimumScale, double maximumScale) |
| { |
| if (scaleChangeRate) { |
| horizontalVelocity = 0; |
| verticalVelocity = 0; |
| } |
| |
| if (exposedRectScale >= maximumScale || exposedRectScale <= minimumScale) |
| scaleChangeRate = 0; |
| } |
| |
| static inline FloatRect adjustExposedRectForBoundedScale(const FloatRect& exposedRect, double exposedRectScale, double newScale) |
| { |
| if (exposedRectScale < newScale) |
| return exposedRect; |
| |
| return adjustExposedRectForNewScale(exposedRect, exposedRectScale, newScale); |
| } |
| |
| std::optional<float> WebPage::scaleFromUIProcess(const VisibleContentRectUpdateInfo& visibleContentRectUpdateInfo) const |
| { |
| auto transactionIDForLastScaleSentToUIProcess = downcast<RemoteLayerTreeDrawingArea>(*m_drawingArea).lastCommittedTransactionID(); |
| auto transactionIDForLastScaleFromUIProcess = visibleContentRectUpdateInfo.lastLayerTreeTransactionID(); |
| if (transactionIDForLastScaleSentToUIProcess != transactionIDForLastScaleFromUIProcess) |
| return std::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 std::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::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.value_or(m_page->pageScaleFactor()); |
| FloatRect exposedContentRect = visibleContentRectUpdateInfo.exposedContentRect(); |
| FloatRect adjustedExposedContentRect = adjustExposedRectForBoundedScale(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->setObscuredInsets(visibleContentRectUpdateInfo.obscuredInsets()); |
| m_page->setUnobscuredSafeAreaInsets(visibleContentRectUpdateInfo.unobscuredSafeAreaInsets()); |
| m_page->setEnclosedInScrollableAncestorView(visibleContentRectUpdateInfo.enclosedInScrollableAncestorView()); |
| |
| double horizontalVelocity = visibleContentRectUpdateInfo.horizontalVelocity(); |
| double verticalVelocity = visibleContentRectUpdateInfo.verticalVelocity(); |
| double scaleChangeRate = visibleContentRectUpdateInfo.scaleChangeRate(); |
| adjustVelocityDataForBoundedScale(horizontalVelocity, verticalVelocity, scaleChangeRate, visibleContentRectUpdateInfo.scale(), m_viewportConfiguration.minimumScale(), m_viewportConfiguration.maximumScale()); |
| |
| frameView.setScrollVelocity(horizontalVelocity, verticalVelocity, scaleChangeRate, visibleContentRectUpdateInfo.timestamp()); |
| |
| if (m_isInStableState) { |
| if (frameView.frame().settings().visualViewportEnabled()) { |
| frameView.setLayoutViewportOverrideRect(LayoutRect(visibleContentRectUpdateInfo.customFixedPositionRect())); |
| if (selectionIsInsideFixedPositionContainer(frame)) { |
| // Ensure that the next layer tree commit contains up-to-date caret/selection rects. |
| frameView.frame().selection().setCaretRectNeedsUpdate(); |
| sendPartialEditorStateAndSchedulePostLayoutUpdate(); |
| } |
| } else |
| frameView.setCustomFixedPositionLayoutRect(enclosingIntRect(visibleContentRectUpdateInfo.customFixedPositionRect())); |
| } |
| |
| 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.customFixedPositionRect(), false, viewportStability, layerAction); |
| } |
| } |
| |
| void WebPage::willStartUserTriggeredZooming() |
| { |
| m_page->diagnosticLoggingClient().logDiagnosticMessage(DiagnosticLoggingKeys::webViewKey(), DiagnosticLoggingKeys::userZoomActionKey(), ShouldSample::No); |
| m_userHasChangedPageScaleFactor = true; |
| } |
| |
| #if ENABLE(WEBGL) |
| WebCore::WebGLLoadPolicy WebPage::webGLPolicyForURL(WebFrame*, const String&) |
| { |
| return WebGLAllowCreation; |
| } |
| |
| WebCore::WebGLLoadPolicy WebPage::resolveWebGLPolicyForURL(WebFrame*, const String&) |
| { |
| return WebGLAllowCreation; |
| } |
| #endif |
| |
| #if ENABLE(IOS_TOUCH_EVENTS) |
| void WebPage::dispatchAsynchronousTouchEvents(const Vector<WebTouchEvent, 1>& queue) |
| { |
| bool ignored; |
| for (const WebTouchEvent& event : queue) |
| dispatchTouchEvent(event, ignored); |
| } |
| #endif |
| |
| void WebPage::computePagesForPrintingAndDrawToPDF(uint64_t frameID, const PrintInfo& printInfo, CallbackID callbackID, Ref<Messages::WebPage::ComputePagesForPrintingAndDrawToPDF::DelayedReply>&& reply) |
| { |
| if (printInfo.snapshotFirstPage) { |
| reply->send(1); |
| IntSize snapshotSize { FloatSize { printInfo.availablePaperWidth, printInfo.availablePaperHeight } }; |
| IntRect snapshotRect { {0, 0}, snapshotSize }; |
| auto pdfData = pdfSnapshotAtSize(snapshotRect, snapshotSize, 0); |
| send(Messages::WebPageProxy::DrawToPDFCallback(IPC::DataReference(CFDataGetBytePtr(pdfData.get()), CFDataGetLength(pdfData.get())), callbackID)); |
| return; |
| } |
| |
| Vector<WebCore::IntRect> pageRects; |
| double totalScaleFactor; |
| computePagesForPrintingImpl(frameID, printInfo, pageRects, totalScaleFactor); |
| |
| ASSERT(pageRects.size() >= 1); |
| std::size_t pageCount = pageRects.size(); |
| ASSERT(pageCount <= std::numeric_limits<uint32_t>::max()); |
| reply->send(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 |
| { |
| return String(); |
| } |
| |
| #if USE(QUICK_LOOK) |
| void WebPage::didReceivePasswordForQuickLookDocument(const String& password) |
| { |
| WebPreviewLoaderClient::didReceivePassword(password, m_pageID); |
| } |
| #endif |
| |
| } // namespace WebKit |
| |
| #endif // PLATFORM(IOS) |