/* * Copyright (C) 2006-2020 Apple Inc. All rights reserved. * Copyright (C) 2019 Adobe. All rights reserved. * Copyright (C) 2020 Igalia S.L. * * Portions are Copyright (C) 1998 Netscape Communications Corporation. * * Other contributors: * Robert O'Callahan * David Baron * Christian Biesinger * Randall Jesup * Roland Mainz * Josh Soref * Boris Zbarsky * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Alternatively, the contents of this file may be used under the terms * of either the Mozilla Public License Version 1.1, found at * http://www.mozilla.org/MPL/ (the "MPL") or the GNU General Public * License Version 2.0, found at http://www.fsf.org/copyleft/gpl.html * (the "GPL"), in which case the provisions of the MPL or the GPL are * applicable instead of those above. If you wish to allow use of your * version of this file only under the terms of one of those two * licenses (the MPL or the GPL) and not to allow others to use your * version of this file under the LGPL, indicate your decision by * deletingthe provisions above and replace them with the notice and * other provisions required by the MPL or the GPL, as the case may be. * If you do not delete the provisions above, a recipient may use your * version of this file under any of the LGPL, the MPL or the GPL. */ #include "config.h" #include "RenderLayerScrollableArea.h" #include "Chrome.h" #include "DebugPageOverlays.h" #include "DeprecatedGlobalSettings.h" #include "Editor.h" #include "ElementRuleCollector.h" #include "EventHandler.h" #include "FocusController.h" #include "FrameSelection.h" #include "HitTestResult.h" #include "Logging.h" #include "RenderFlexibleBox.h" #include "RenderGeometryMap.h" #include "RenderLayerBacking.h" #include "RenderLayerCompositor.h" #include "RenderMarquee.h" #include "RenderScrollbar.h" #include "RenderScrollbarPart.h" #include "RenderView.h" #include "ScrollAnimator.h" #include "ScrollbarTheme.h" #include "ScrollingCoordinator.h" #include "ShadowRoot.h" #include namespace WebCore { RenderLayerScrollableArea::RenderLayerScrollableArea(RenderLayer& layer) : m_layer(layer) { } RenderLayerScrollableArea::~RenderLayerScrollableArea() { } void RenderLayerScrollableArea::clear() { auto& renderer = m_layer.renderer(); ASSERT(m_registeredScrollableArea == renderer.view().frameView().containsScrollableArea(this)); if (m_registeredScrollableArea) renderer.view().frameView().removeScrollableArea(this); #if ENABLE(IOS_TOUCH_EVENTS) unregisterAsTouchEventListenerForScrolling(); #endif if (Element* element = renderer.element()) element->setSavedLayerScrollPosition(m_scrollPosition); destroyScrollbar(HorizontalScrollbar); destroyScrollbar(VerticalScrollbar); if (auto* scrollingCoordinator = renderer.page().scrollingCoordinator()) scrollingCoordinator->willDestroyScrollableArea(*this); clearScrollCorner(); clearResizer(); } void RenderLayerScrollableArea::restoreScrollPosition() { auto* element = m_layer.renderer().element(); if (!element) return; if (m_layer.renderBox()) { // We save and restore only the scrollOffset as the other scroll values are recalculated. m_scrollPosition = element->savedLayerScrollPosition(); if (!m_scrollPosition.isZero()) scrollAnimator().setCurrentPosition(m_scrollPosition); } element->setSavedLayerScrollPosition(IntPoint()); } bool RenderLayerScrollableArea::shouldPlaceVerticalScrollbarOnLeft() const { return m_layer.renderer().shouldPlaceVerticalScrollbarOnLeft(); } #if ENABLE(IOS_TOUCH_EVENTS) bool RenderLayerScrollableArea::handleTouchEvent(const PlatformTouchEvent& touchEvent) { // If we have accelerated scrolling, let the scrolling be handled outside of WebKit. if (hasCompositedScrollableOverflow()) return false; return ScrollableArea::handleTouchEvent(touchEvent); } void RenderLayerScrollableArea::registerAsTouchEventListenerForScrolling() { auto& renderer = m_layer.renderer(); if (!renderer.element() || m_registeredAsTouchEventListenerForScrolling) return; renderer.document().addTouchEventHandler(*renderer.element()); m_registeredAsTouchEventListenerForScrolling = true; } void RenderLayerScrollableArea::unregisterAsTouchEventListenerForScrolling() { auto& renderer = m_layer.renderer(); if (!renderer.element() || !m_registeredAsTouchEventListenerForScrolling) return; renderer.document().removeTouchEventHandler(*renderer.element()); m_registeredAsTouchEventListenerForScrolling = false; } #endif // ENABLE(IOS_TOUCH_EVENTS) IntRect RenderLayerScrollableArea::scrollableAreaBoundingBox(bool* isInsideFixed) const { return m_layer.renderer().absoluteBoundingBoxRect(/* useTransforms */ true, isInsideFixed); } bool RenderLayerScrollableArea::isUserScrollInProgress() const { if (!scrollsOverflow()) return false; if (auto scrollingCoordinator = m_layer.page().scrollingCoordinator()) { if (scrollingCoordinator->isUserScrollInProgress(scrollingNodeID())) return true; } if (auto scrollAnimator = existingScrollAnimator()) return scrollAnimator->isUserScrollInProgress(); return false; } bool RenderLayerScrollableArea::isRubberBandInProgress() const { #if ENABLE(RUBBER_BANDING) if (!scrollsOverflow()) return false; if (auto scrollingCoordinator = m_layer.page().scrollingCoordinator()) { if (scrollingCoordinator->isRubberBandInProgress(scrollingNodeID())) return true; } if (auto scrollAnimator = existingScrollAnimator()) return scrollAnimator->isRubberBandInProgress(); #endif return false; } bool RenderLayerScrollableArea::forceUpdateScrollbarsOnMainThreadForPerformanceTesting() const { return m_layer.renderer().settings().scrollingPerformanceTestingEnabled(); } // FIXME: this is only valid after we've made layers. bool RenderLayerScrollableArea::usesAsyncScrolling() const { return m_layer.compositor().useCoordinatedScrollingForLayer(m_layer); } void RenderLayerScrollableArea::setPostLayoutScrollPosition(std::optional position) { m_postLayoutScrollPosition = position; } void RenderLayerScrollableArea::applyPostLayoutScrollPositionIfNeeded() { if (!m_postLayoutScrollPosition) return; scrollToOffset(scrollOffsetFromPosition(m_postLayoutScrollPosition.value())); m_postLayoutScrollPosition = std::nullopt; } void RenderLayerScrollableArea::scrollToXPosition(int x, const ScrollPositionChangeOptions& options) { ScrollPosition position(x, m_scrollPosition.y()); setScrollPosition(position, options); } void RenderLayerScrollableArea::scrollToYPosition(int y, const ScrollPositionChangeOptions& options) { ScrollPosition position(m_scrollPosition.x(), y); setScrollPosition(position, options); } void RenderLayerScrollableArea::setScrollPosition(const ScrollPosition& position, const ScrollPositionChangeOptions& options) { scrollToOffset(scrollOffsetFromPosition(position), options); } ScrollOffset RenderLayerScrollableArea::clampScrollOffset(const ScrollOffset& scrollOffset) const { return scrollOffset.constrainedBetween(IntPoint(), maximumScrollOffset()); } bool RenderLayerScrollableArea::requestScrollPositionUpdate(const ScrollPosition& position, ScrollType scrollType, ScrollClamping clamping) { #if ENABLE(ASYNC_SCROLLING) LOG_WITH_STREAM(Scrolling, stream << m_layer << " requestScrollPositionUpdate " << position); if (auto* scrollingCoordinator = m_layer.page().scrollingCoordinator()) return scrollingCoordinator->requestScrollPositionUpdate(*this, position, scrollType, clamping); #endif return false; } ScrollOffset RenderLayerScrollableArea::scrollToOffset(const ScrollOffset& scrollOffset, const ScrollPositionChangeOptions& options) { if (currentScrollBehaviorStatus() == ScrollBehaviorStatus::InNonNativeAnimation) scrollAnimator().cancelAnimations(); ScrollOffset clampedScrollOffset = options.clamping == ScrollClamping::Clamped ? clampScrollOffset(scrollOffset) : scrollOffset; if (clampedScrollOffset == this->scrollOffset()) return clampedScrollOffset; auto previousScrollType = currentScrollType(); setCurrentScrollType(options.type); ScrollOffset snappedOffset = ceiledIntPoint(scrollAnimator().adjustScrollOffsetForSnappingIfNeeded(clampedScrollOffset, options.snapPointSelectionMethod)); auto snappedPosition = scrollPositionFromOffset(snappedOffset); if (options.animated == AnimatedScroll::Yes) ScrollableArea::scrollToPositionWithAnimation(snappedPosition); else { if (!requestScrollPositionUpdate(snappedPosition, options.type, options.clamping)) scrollToPositionWithoutAnimation(snappedPosition, options.clamping); setScrollBehaviorStatus(ScrollBehaviorStatus::NotInAnimation); } setCurrentScrollType(previousScrollType); return snappedOffset; } void RenderLayerScrollableArea::scrollTo(const ScrollPosition& position) { RenderBox* box = m_layer.renderBox(); if (!box) return; LOG_WITH_STREAM(Scrolling, stream << "RenderLayerScrollableArea [" << scrollingNodeID() << "] scrollTo " << position << " from " << m_scrollPosition << " (is user scroll " << (currentScrollType() == ScrollType::User) << ")"); ScrollPosition newPosition = position; if (!box->isHTMLMarquee()) { // Ensure that the dimensions will be computed if they need to be (for overflow:hidden blocks). if (m_scrollDimensionsDirty) computeScrollDimensions(); #if PLATFORM(IOS_FAMILY) if (adjustForIOSCaretWhenScrolling()) { // FIXME: It's not clear what this code is trying to do. Behavior seems reasonable with it removed. int maxOffset = scrollWidth() - roundToInt(box->clientWidth()); ScrollOffset newOffset = scrollOffsetFromPosition(newPosition); int scrollXOffset = newOffset.x(); if (scrollXOffset > maxOffset - caretWidth) { scrollXOffset += caretWidth; if (scrollXOffset <= caretWidth) scrollXOffset = 0; } else if (scrollXOffset < m_scrollPosition.x() - caretWidth) scrollXOffset -= caretWidth; newOffset.setX(scrollXOffset); newPosition = scrollPositionFromOffset(newOffset); } #endif } if (m_scrollPosition == newPosition && currentScrollBehaviorStatus() == ScrollBehaviorStatus::NotInAnimation) { // FIXME: Nothing guarantees we get a scrollTo() with an unchanged position at the end of a user gesture. // The ScrollingCoordinator probably needs to message the main thread when a gesture ends. if (requiresScrollPositionReconciliation()) { m_layer.setNeedsCompositingGeometryUpdate(); updateCompositingLayersAfterScroll(); } return; } m_scrollPosition = newPosition; auto& renderer = m_layer.renderer(); RenderView& view = renderer.view(); // Update the positions of our child layers (if needed as only fixed layers should be impacted by a scroll). // We don't update compositing layers, because we need to do a deep update from the compositing ancestor. if (!view.frameView().layoutContext().isInRenderTreeLayout()) { // If we're in the middle of layout, we'll just update layers once layout has finished. updateLayerPositionsAfterOverflowScroll(); view.frameView().scheduleUpdateWidgetPositions(); if (!m_updatingMarqueePosition) { // Avoid updating compositing layers if, higher on the stack, we're already updating layer // positions. Updating layer positions requires a full walk of up-to-date RenderLayers, and // in this case we're still updating their positions; we'll update compositing layers later // when that completes. if (usesCompositedScrolling()) { m_layer.setNeedsCompositingGeometryUpdate(); // Scroll position can affect the location of a composited descendant (which may be a sibling in z-order), // so trigger a descendant walk from the stacking context. if (auto* paintParent = m_layer.stackingContext()) paintParent->setDescendantsNeedUpdateBackingAndHierarchyTraversal(); } updateCompositingLayersAfterScroll(); } // Update regions, scrolling may change the clip of a particular region. renderer.document().invalidateRenderingDependentRegions(); DebugPageOverlays::didLayout(renderer.frame()); } Frame& frame = renderer.frame(); RenderLayerModelObject* repaintContainer = renderer.containerForRepaint(); // The caret rect needs to be invalidated after scrolling frame.selection().setCaretRectNeedsUpdate(); LayoutRect rectForRepaint = layer().repaintRects() ? layer().repaintRects()->clippedOverflowRect : renderer.clippedOverflowRectForRepaint(repaintContainer); FloatQuad quadForFakeMouseMoveEvent = FloatQuad(rectForRepaint); if (repaintContainer) quadForFakeMouseMoveEvent = repaintContainer->localToAbsoluteQuad(quadForFakeMouseMoveEvent); frame.eventHandler().dispatchFakeMouseMoveEventSoonInQuad(quadForFakeMouseMoveEvent); bool requiresRepaint = true; if (usesCompositedScrolling()) { m_layer.setNeedsCompositingGeometryUpdate(); m_layer.setDescendantsNeedUpdateBackingAndHierarchyTraversal(); requiresRepaint = m_layer.backing()->needsRepaintOnCompositedScroll(); } // Just schedule a full repaint of our object. if (requiresRepaint) renderer.repaintUsingContainer(repaintContainer, rectForRepaint); // Schedule the scroll and scroll-related DOM events. if (Element* element = renderer.element()) element->document().addPendingScrollEventTarget(*element); if (scrollsOverflow()) view.frameView().didChangeScrollOffset(); view.frameView().viewportContentsChanged(); frame.editor().renderLayerDidScroll(m_layer); } void RenderLayerScrollableArea::updateCompositingLayersAfterScroll() { if (m_layer.compositor().hasContentCompositingLayers()) { // Our stacking container is guaranteed to contain all of our descendants that may need // repositioning, so update compositing layers from there. if (RenderLayer* compositingAncestor = m_layer.stackingContext()->enclosingCompositingLayer()) { if (usesCompositedScrolling()) m_layer.compositor().updateCompositingLayers(CompositingUpdateType::OnCompositedScroll, compositingAncestor); else { // FIXME: would be nice to only dirty layers whose positions were affected by scrolling. compositingAncestor->setDescendantsNeedUpdateBackingAndHierarchyTraversal(); m_layer.compositor().updateCompositingLayers(CompositingUpdateType::OnScroll, compositingAncestor); } } } } int RenderLayerScrollableArea::scrollWidth() const { ASSERT(m_layer.renderBox()); if (m_scrollDimensionsDirty) const_cast(this)->computeScrollDimensions(); // FIXME: This should use snappedIntSize() instead with absolute coordinates. return m_scrollWidth; } int RenderLayerScrollableArea::scrollHeight() const { ASSERT(m_layer.renderBox()); if (m_scrollDimensionsDirty) const_cast(this)->computeScrollDimensions(); // FIXME: This should use snappedIntSize() instead with absolute coordinates. return m_scrollHeight; } void RenderLayerScrollableArea::updateMarqueePosition() { if (!m_marquee) return; // FIXME: would like to use SetForScope<> but it doesn't work with bitfields. bool oldUpdatingMarqueePosition = m_updatingMarqueePosition; m_updatingMarqueePosition = true; m_marquee->updateMarqueePosition(); m_updatingMarqueePosition = oldUpdatingMarqueePosition; } void RenderLayerScrollableArea::createOrDestroyMarquee() { auto& renderer = m_layer.renderer(); if (renderer.isHTMLMarquee() && renderer.style().marqueeBehavior() != MarqueeBehavior::None && renderer.isBox()) { if (!m_marquee) m_marquee = makeUnique(&m_layer); m_marquee->updateMarqueeStyle(); } else if (m_marquee) m_marquee = nullptr; } bool RenderLayerScrollableArea::scrollsOverflow() const { auto& renderer = m_layer.renderer(); if (!is(renderer)) return false; return downcast(renderer).scrollsOverflow(); } bool RenderLayerScrollableArea::canUseCompositedScrolling() const { auto& renderer = m_layer.renderer(); bool isVisible = renderer.style().visibility() == Visibility::Visible; if (renderer.settings().asyncOverflowScrollingEnabled()) return isVisible && scrollsOverflow() && !m_layer.isInsideSVGForeignObject(); #if PLATFORM(IOS_FAMILY) && ENABLE(OVERFLOW_SCROLLING_TOUCH) return isVisible && scrollsOverflow() && (renderer.style().useTouchOverflowScrolling() || renderer.settings().alwaysUseAcceleratedOverflowScroll()); #else return false; #endif } void RenderLayerScrollableArea::setScrollOffset(const ScrollOffset& offset) { scrollTo(scrollPositionFromOffset(offset)); } ScrollingNodeID RenderLayerScrollableArea::scrollingNodeID() const { if (!m_layer.isComposited()) return 0; return m_layer.backing()->scrollingNodeIDForRole(ScrollCoordinationRole::Scrolling); } bool RenderLayerScrollableArea::handleWheelEventForScrolling(const PlatformWheelEvent& wheelEvent, std::optional gestureState) { if (!isScrollableOrRubberbandable()) return false; #if ENABLE(ASYNC_SCROLLING) if (usesAsyncScrolling() && scrollingNodeID()) { if (auto* scrollingCoordinator = m_layer.page().scrollingCoordinator()) return scrollingCoordinator->handleWheelEventForScrolling(wheelEvent, scrollingNodeID(), gestureState); } #endif return ScrollableArea::handleWheelEventForScrolling(wheelEvent, gestureState); } IntRect RenderLayerScrollableArea::visibleContentRectInternal(VisibleContentRectIncludesScrollbars scrollbarInclusion, VisibleContentRectBehavior) const { IntSize scrollbarSpace; if (showsOverflowControls() && scrollbarInclusion == IncludeScrollbars) scrollbarSpace = scrollbarIntrusion(); auto visibleSize = this->visibleSize(); return { scrollPosition(), { std::max(0, visibleSize.width() - scrollbarSpace.width()), std::max(0, visibleSize.height() - scrollbarSpace.height()) } }; } IntSize RenderLayerScrollableArea::overhangAmount() const { #if ENABLE(RUBBER_BANDING) auto& renderer = m_layer.renderer(); if (!renderer.settings().rubberBandingForSubScrollableRegionsEnabled()) return IntSize(); IntSize stretch; // FIXME: use maximumScrollOffset(), or just move this to ScrollableArea. ScrollOffset scrollOffset = scrollOffsetFromPosition(scrollPosition()); auto reachableSize = reachableTotalContentsSize(); if (scrollOffset.y() < 0) stretch.setHeight(scrollOffset.y()); else if (reachableSize.height() && scrollOffset.y() > reachableSize.height() - visibleHeight()) stretch.setHeight(scrollOffset.y() - (reachableSize.height() - visibleHeight())); if (scrollOffset.x() < 0) stretch.setWidth(scrollOffset.x()); else if (reachableSize.width() && scrollOffset.x() > reachableSize.width() - visibleWidth()) stretch.setWidth(scrollOffset.x() - (reachableSize.width() - visibleWidth())); return stretch; #else return IntSize(); #endif } IntRect RenderLayerScrollableArea::scrollCornerRect() const { return overflowControlsRects().scrollCorner; } bool RenderLayerScrollableArea::isScrollCornerVisible() const { ASSERT(m_layer.renderer().isBox()); return !scrollCornerRect().isEmpty(); } IntRect RenderLayerScrollableArea::convertFromScrollbarToContainingView(const Scrollbar& scrollbar, const IntRect& scrollbarRect) const { auto& renderer = m_layer.renderer(); IntRect rect = scrollbarRect; rect.move(scrollbarOffset(scrollbar)); return renderer.view().frameView().convertFromRendererToContainingView(&renderer, rect); } IntRect RenderLayerScrollableArea::convertFromContainingViewToScrollbar(const Scrollbar& scrollbar, const IntRect& parentRect) const { auto& renderer = m_layer.renderer(); IntRect rect = renderer.view().frameView().convertFromContainingViewToRenderer(&renderer, parentRect); rect.move(-scrollbarOffset(scrollbar)); return rect; } IntPoint RenderLayerScrollableArea::convertFromScrollbarToContainingView(const Scrollbar& scrollbar, const IntPoint& scrollbarPoint) const { auto& renderer = m_layer.renderer(); IntPoint point = scrollbarPoint; point.move(scrollbarOffset(scrollbar)); return renderer.view().frameView().convertFromRendererToContainingView(&renderer, point); } IntPoint RenderLayerScrollableArea::convertFromContainingViewToScrollbar(const Scrollbar& scrollbar, const IntPoint& parentPoint) const { auto& renderer = m_layer.renderer(); IntPoint point = renderer.view().frameView().convertFromContainingViewToRenderer(&renderer, parentPoint); point.move(-scrollbarOffset(scrollbar)); return point; } IntSize RenderLayerScrollableArea::visibleSize() const { return m_layer.visibleSize(); } IntSize RenderLayerScrollableArea::contentsSize() const { return IntSize(scrollWidth(), scrollHeight()); } IntSize RenderLayerScrollableArea::reachableTotalContentsSize() const { IntSize contentsSize = this->contentsSize(); if (!hasScrollableHorizontalOverflow()) contentsSize.setWidth(std::min(contentsSize.width(), visibleSize().width())); if (!hasScrollableVerticalOverflow()) contentsSize.setHeight(std::min(contentsSize.height(), visibleSize().height())); return contentsSize; } void RenderLayerScrollableArea::availableContentSizeChanged(AvailableSizeChangeReason reason) { ScrollableArea::availableContentSizeChanged(reason); auto& renderer = m_layer.renderer(); if (reason == AvailableSizeChangeReason::ScrollbarsChanged) { if (is(renderer)) downcast(renderer).setShouldForceRelayoutChildren(true); renderer.setNeedsLayout(); } } bool RenderLayerScrollableArea::shouldSuspendScrollAnimations() const { auto& renderer = m_layer.renderer(); return renderer.view().frameView().shouldSuspendScrollAnimations(); } #if PLATFORM(IOS_FAMILY) void RenderLayerScrollableArea::didStartScroll() { m_layer.page().chrome().client().didStartOverflowScroll(); } void RenderLayerScrollableArea::didEndScroll() { m_layer.page().chrome().client().didEndOverflowScroll(); } void RenderLayerScrollableArea::didUpdateScroll() { // Send this notification when we scroll, since this is how we keep selection updated. m_layer.page().chrome().client().didLayout(ChromeClient::Scroll); } #endif RenderLayer::OverflowControlRects RenderLayerScrollableArea::overflowControlsRects() const { auto& renderer = m_layer.renderer(); ASSERT(is(renderer)); auto& renderBox = downcast(renderer); // Scrollbars sit inside the border box. auto overflowControlsPositioningRect = snappedIntRect(renderBox.paddingBoxRectIncludingScrollbar()); auto horizontalScrollbarHeight = m_hBar ? m_hBar->height() : 0; auto verticalScrollbarWidth = m_vBar ? m_vBar->width() : 0; auto isNonOverlayScrollbar = [](const Scrollbar* scrollbar) { return scrollbar && !scrollbar->isOverlayScrollbar(); }; bool haveNonOverlayHorizontalScrollbar = isNonOverlayScrollbar(m_hBar.get()); bool haveNonOverlayVerticalScrollbar = isNonOverlayScrollbar(m_vBar.get()); bool placeVerticalScrollbarOnTheLeft = shouldPlaceVerticalScrollbarOnLeft(); bool haveResizer = renderer.style().resize() != Resize::None; bool scrollbarsAvoidCorner = (haveNonOverlayHorizontalScrollbar && haveNonOverlayVerticalScrollbar) || (haveResizer && (haveNonOverlayHorizontalScrollbar || haveNonOverlayVerticalScrollbar)); IntSize cornerSize; if (scrollbarsAvoidCorner) { // If only one scrollbar is present, the corner is square. cornerSize = IntSize { verticalScrollbarWidth ? verticalScrollbarWidth : horizontalScrollbarHeight, horizontalScrollbarHeight ? horizontalScrollbarHeight : verticalScrollbarWidth }; } RenderLayer::OverflowControlRects result; if (m_hBar) { auto barRect = overflowControlsPositioningRect; barRect.shiftYEdgeTo(barRect.maxY() - horizontalScrollbarHeight); if (scrollbarsAvoidCorner) { if (placeVerticalScrollbarOnTheLeft) barRect.shiftXEdgeTo(barRect.x() + cornerSize.width()); else barRect.contract(cornerSize.width(), 0); } result.horizontalScrollbar = barRect; } if (m_vBar) { auto barRect = overflowControlsPositioningRect; if (placeVerticalScrollbarOnTheLeft) barRect.setWidth(verticalScrollbarWidth); else barRect.shiftXEdgeTo(barRect.maxX() - verticalScrollbarWidth); if (scrollbarsAvoidCorner) barRect.contract(0, cornerSize.height()); result.verticalScrollbar = barRect; } auto cornerRect = [&](IntSize cornerSize) { if (placeVerticalScrollbarOnTheLeft) { auto bottomLeftCorner = overflowControlsPositioningRect.minXMaxYCorner(); return IntRect { { bottomLeftCorner.x(), bottomLeftCorner.y() - cornerSize.height(), }, cornerSize }; } return IntRect { overflowControlsPositioningRect.maxXMaxYCorner() - cornerSize, cornerSize }; }; if (scrollbarsAvoidCorner) result.scrollCorner = cornerRect(cornerSize); if (haveResizer) { if (scrollbarsAvoidCorner) result.resizer = result.scrollCorner; else { auto scrollbarThickness = ScrollbarTheme::theme().scrollbarThickness(); result.resizer = cornerRect({ scrollbarThickness, scrollbarThickness }); } } return result; } IntSize RenderLayerScrollableArea::scrollbarOffset(const Scrollbar& scrollbar) const { auto rects = overflowControlsRects(); if (&scrollbar == m_vBar.get()) return toIntSize(rects.verticalScrollbar.location()); if (&scrollbar == m_hBar.get()) return toIntSize(rects.horizontalScrollbar.location()); ASSERT_NOT_REACHED(); return { }; } void RenderLayerScrollableArea::invalidateScrollbarRect(Scrollbar& scrollbar, const IntRect& rect) { if (!showsOverflowControls()) return; if (&scrollbar == m_vBar.get()) { if (GraphicsLayer* layer = layerForVerticalScrollbar()) { layer->setNeedsDisplayInRect(rect); return; } } else { if (GraphicsLayer* layer = layerForHorizontalScrollbar()) { layer->setNeedsDisplayInRect(rect); return; } } auto scrollRect = rect; RenderBox* box = m_layer.renderBox(); ASSERT(box); // If we are not yet inserted into the tree, there is no need to repaint. if (!box->parent()) return; auto rects = overflowControlsRects(); if (&scrollbar == m_vBar.get()) scrollRect.moveBy(rects.verticalScrollbar.location()); else scrollRect.moveBy(rects.horizontalScrollbar.location()); LayoutRect repaintRect = scrollRect; box->flipForWritingMode(repaintRect); box->repaintRectangle(repaintRect); } void RenderLayerScrollableArea::invalidateScrollCornerRect(const IntRect& rect) { if (!showsOverflowControls()) return; if (GraphicsLayer* layer = layerForScrollCorner()) { layer->setNeedsDisplayInRect(rect); return; } if (m_scrollCorner) m_scrollCorner->repaintRectangle(rect); if (m_resizer) m_resizer->repaintRectangle(rect); } static bool scrollbarHiddenByStyle(Scrollbar* scrollbar) { return scrollbar && scrollbar->isHiddenByStyle(); } bool RenderLayerScrollableArea::horizontalScrollbarHiddenByStyle() const { return scrollbarHiddenByStyle(horizontalScrollbar()); } bool RenderLayerScrollableArea::verticalScrollbarHiddenByStyle() const { return scrollbarHiddenByStyle(verticalScrollbar()); } static inline RenderElement* rendererForScrollbar(RenderLayerModelObject& renderer) { if (Element* element = renderer.element()) { if (ShadowRoot* shadowRoot = element->containingShadowRoot()) { if (shadowRoot->mode() == ShadowRootMode::UserAgent) return shadowRoot->host()->renderer(); } } return &renderer; } Ref RenderLayerScrollableArea::createScrollbar(ScrollbarOrientation orientation) { auto& renderer = m_layer.renderer(); RefPtr widget; ASSERT(rendererForScrollbar(renderer)); auto& actualRenderer = *rendererForScrollbar(renderer); bool hasCustomScrollbarStyle = is(actualRenderer) && downcast(actualRenderer).style().hasPseudoStyle(PseudoId::Scrollbar); auto element = downcast(actualRenderer).element(); if (hasCustomScrollbarStyle && element) widget = RenderScrollbar::createCustomScrollbar(*this, orientation, element); else { widget = Scrollbar::createNativeScrollbar(*this, orientation, ScrollbarControlSize::Regular); didAddScrollbar(widget.get(), orientation); if (m_layer.page().isMonitoringWheelEvents()) scrollAnimator().setWheelEventTestMonitor(m_layer.page().wheelEventTestMonitor()); } renderer.view().frameView().addChild(*widget); return widget.releaseNonNull(); } void RenderLayerScrollableArea::destroyScrollbar(ScrollbarOrientation orientation) { RefPtr& scrollbar = orientation == HorizontalScrollbar ? m_hBar : m_vBar; if (!scrollbar) return; if (!scrollbar->isCustomScrollbar()) willRemoveScrollbar(scrollbar.get(), orientation); scrollbar->removeFromParent(); scrollbar = nullptr; } void RenderLayerScrollableArea::setHasHorizontalScrollbar(bool hasScrollbar) { if (hasScrollbar == hasHorizontalScrollbar()) return; if (hasScrollbar) { m_hBar = createScrollbar(HorizontalScrollbar); #if ENABLE(RUBBER_BANDING) auto& renderer = m_layer.renderer(); ScrollElasticity elasticity = scrollsOverflow() && renderer.settings().rubberBandingForSubScrollableRegionsEnabled() ? ScrollElasticityAutomatic : ScrollElasticityNone; ScrollableArea::setHorizontalScrollElasticity(elasticity); #endif } else { destroyScrollbar(HorizontalScrollbar); #if ENABLE(RUBBER_BANDING) ScrollableArea::setHorizontalScrollElasticity(ScrollElasticityNone); #endif } // Destroying or creating one bar can cause our scrollbar corner to come and go. We need to update the opposite scrollbar's style. if (m_hBar) m_hBar->styleChanged(); if (m_vBar) m_vBar->styleChanged(); } void RenderLayerScrollableArea::setHasVerticalScrollbar(bool hasScrollbar) { if (hasScrollbar == hasVerticalScrollbar()) return; if (hasScrollbar) { m_vBar = createScrollbar(VerticalScrollbar); #if ENABLE(RUBBER_BANDING) auto& renderer = m_layer.renderer(); ScrollElasticity elasticity = scrollsOverflow() && renderer.settings().rubberBandingForSubScrollableRegionsEnabled() ? ScrollElasticityAutomatic : ScrollElasticityNone; ScrollableArea::setVerticalScrollElasticity(elasticity); #endif } else { destroyScrollbar(VerticalScrollbar); #if ENABLE(RUBBER_BANDING) ScrollableArea::setVerticalScrollElasticity(ScrollElasticityNone); #endif } // Destroying or creating one bar can cause our scrollbar corner to come and go. We need to update the opposite scrollbar's style. if (m_hBar) m_hBar->styleChanged(); if (m_vBar) m_vBar->styleChanged(); } ScrollableArea* RenderLayerScrollableArea::enclosingScrollableArea() const { if (auto* scrollableLayer = m_layer.enclosingScrollableLayer(IncludeSelfOrNot::ExcludeSelf, CrossFrameBoundaries::No)) return scrollableLayer->scrollableArea(); auto& renderer = m_layer.renderer(); return &renderer.view().frameView(); } bool RenderLayerScrollableArea::isScrollableOrRubberbandable() { auto& renderer = m_layer.renderer(); return renderer.isScrollableOrRubberbandableBox(); } bool RenderLayerScrollableArea::hasScrollableOrRubberbandableAncestor() { for (auto* nextLayer = m_layer.enclosingContainingBlockLayer(CrossFrameBoundaries::Yes); nextLayer; nextLayer = nextLayer->enclosingContainingBlockLayer(CrossFrameBoundaries::Yes)) { if (nextLayer->renderer().isScrollableOrRubberbandableBox()) return true; } return false; } int RenderLayerScrollableArea::verticalScrollbarWidth(OverlayScrollbarSizeRelevancy relevancy) const { if (!m_vBar || !showsOverflowControls() || (m_vBar->isOverlayScrollbar() && (relevancy == IgnoreOverlayScrollbarSize || !m_vBar->shouldParticipateInHitTesting()))) return 0; return m_vBar->width(); } int RenderLayerScrollableArea::horizontalScrollbarHeight(OverlayScrollbarSizeRelevancy relevancy) const { if (!m_hBar || !showsOverflowControls() || (m_hBar->isOverlayScrollbar() && (relevancy == IgnoreOverlayScrollbarSize || !m_hBar->shouldParticipateInHitTesting()))) return 0; return m_hBar->height(); } bool RenderLayerScrollableArea::hasOverflowControls() const { return m_hBar || m_vBar || m_scrollCorner || m_layer.renderer().style().resize() != Resize::None; } void RenderLayerScrollableArea::positionOverflowControls(const IntSize& offsetFromRoot) { if (!m_hBar && !m_vBar && !m_layer.canResize()) return; if (!m_layer.renderBox()) return; auto rects = overflowControlsRects(); if (m_vBar) { rects.verticalScrollbar.move(offsetFromRoot); m_vBar->setFrameRect(rects.verticalScrollbar); } if (m_hBar) { rects.horizontalScrollbar.move(offsetFromRoot); m_hBar->setFrameRect(rects.horizontalScrollbar); } if (m_scrollCorner) m_scrollCorner->setFrameRect(rects.scrollCorner); if (m_resizer) m_resizer->setFrameRect(rects.resizer); } LayoutUnit RenderLayerScrollableArea::overflowTop() const { RenderBox* box = m_layer.renderBox(); LayoutRect overflowRect(box->layoutOverflowRect()); box->flipForWritingMode(overflowRect); return overflowRect.y(); } LayoutUnit RenderLayerScrollableArea::overflowBottom() const { RenderBox* box = m_layer.renderBox(); LayoutRect overflowRect(box->layoutOverflowRect()); box->flipForWritingMode(overflowRect); return overflowRect.maxY(); } LayoutUnit RenderLayerScrollableArea::overflowLeft() const { RenderBox* box = m_layer.renderBox(); LayoutRect overflowRect(box->layoutOverflowRect()); box->flipForWritingMode(overflowRect); return overflowRect.x(); } LayoutUnit RenderLayerScrollableArea::overflowRight() const { RenderBox* box = m_layer.renderBox(); LayoutRect overflowRect(box->layoutOverflowRect()); box->flipForWritingMode(overflowRect); return overflowRect.maxX(); } void RenderLayerScrollableArea::computeScrollDimensions() { m_scrollDimensionsDirty = false; m_scrollWidth = roundToInt(overflowRight() - overflowLeft()); m_scrollHeight = roundToInt(overflowBottom() - overflowTop()); computeScrollOrigin(); computeHasCompositedScrollableOverflow(); } void RenderLayerScrollableArea::computeScrollOrigin() { RenderBox* box = m_layer.renderBox(); ASSERT(box); int scrollableLeftOverflow = roundToInt(overflowLeft() - box->borderLeft()); if (shouldPlaceVerticalScrollbarOnLeft() /*|| box->style().writingMode() == WritingMode::RightToLeft*/) scrollableLeftOverflow -= verticalScrollbarWidth(); int scrollableTopOverflow = roundToInt(overflowTop() - box->borderTop()); setScrollOrigin(IntPoint(-scrollableLeftOverflow, -scrollableTopOverflow)); // Horizontal scrollbar offsets depend on the scroll origin when vertical // scrollbars are on the left. if (m_hBar) m_hBar->offsetDidChange(); } void RenderLayerScrollableArea::computeHasCompositedScrollableOverflow() { m_hasCompositedScrollableOverflow = canUseCompositedScrolling() && (hasScrollableHorizontalOverflow() || hasScrollableVerticalOverflow()); } bool RenderLayerScrollableArea::hasScrollableHorizontalOverflow() const { return hasHorizontalOverflow() && m_layer.renderBox()->scrollsOverflowX(); } bool RenderLayerScrollableArea::hasScrollableVerticalOverflow() const { return hasVerticalOverflow() && m_layer.renderBox()->scrollsOverflowY(); } bool RenderLayerScrollableArea::hasHorizontalOverflow() const { ASSERT(!m_scrollDimensionsDirty); return scrollWidth() > roundToInt(m_layer.renderBox()->clientWidth()); } bool RenderLayerScrollableArea::hasVerticalOverflow() const { ASSERT(!m_scrollDimensionsDirty); return scrollHeight() > roundToInt(m_layer.renderBox()->clientHeight()); } void RenderLayerScrollableArea::updateScrollbarPresenceAndState(std::optional hasHorizontalOverflow, std::optional hasVerticalOverflow) { auto* box = m_layer.renderBox(); ASSERT(box); enum class ScrollbarState { NoScrollbar, Enabled, Disabled }; auto scrollbarForAxis = [&](ScrollbarOrientation orientation) -> RefPtr& { return orientation == ScrollbarOrientation::HorizontalScrollbar ? m_hBar : m_vBar; }; auto stateForScrollbar = [&](ScrollbarOrientation orientation, std::optional hasOverflow, ScrollbarState nonScrollableState) { if (hasOverflow) return *hasOverflow ? ScrollbarState::Enabled : nonScrollableState; // If we don't have information about overflow (because we haven't done layout yet), just return the current state of the scrollbar. auto existingScrollbar = scrollbarForAxis(orientation); return (existingScrollbar && existingScrollbar->enabled()) ? ScrollbarState::Enabled : nonScrollableState; }; auto stateForScrollbarOnAxis = [&](ScrollbarOrientation orientation, std::optional hasOverflow) { if (box->hasAlwaysPresentScrollbar(orientation)) return stateForScrollbar(orientation, hasOverflow, ScrollbarState::Disabled); if (box->hasAutoScrollbar(orientation)) return stateForScrollbar(orientation, hasOverflow, ScrollbarState::NoScrollbar); return ScrollbarState::NoScrollbar; }; auto horizontalBarState = stateForScrollbarOnAxis(ScrollbarOrientation::HorizontalScrollbar, hasHorizontalOverflow); setHasHorizontalScrollbar(horizontalBarState != ScrollbarState::NoScrollbar); if (horizontalBarState != ScrollbarState::NoScrollbar) m_hBar->setEnabled(horizontalBarState == ScrollbarState::Enabled); auto verticalBarState = stateForScrollbarOnAxis(ScrollbarOrientation::VerticalScrollbar, hasVerticalOverflow); setHasVerticalScrollbar(verticalBarState != ScrollbarState::NoScrollbar); if (verticalBarState != ScrollbarState::NoScrollbar) m_vBar->setEnabled(verticalBarState == ScrollbarState::Enabled); } void RenderLayerScrollableArea::updateScrollbarsAfterStyleChange(const RenderStyle* oldStyle) { // Overflow is a box concept. RenderBox* box = m_layer.renderBox(); if (!box) return; // List box parts handle the scrollbars by themselves so we have nothing to do. if (box->style().appearance() == ListboxPart) return; bool hadVerticalScrollbar = hasVerticalScrollbar(); updateScrollbarPresenceAndState(); bool hasVerticalScrollbar = this->hasVerticalScrollbar(); if (hadVerticalScrollbar != hasVerticalScrollbar || (hasVerticalScrollbar && oldStyle && oldStyle->shouldPlaceVerticalScrollbarOnLeft() != box->style().shouldPlaceVerticalScrollbarOnLeft())) computeScrollOrigin(); if (!m_scrollDimensionsDirty) updateScrollableAreaSet(hasScrollableHorizontalOverflow() || hasScrollableVerticalOverflow()); } void RenderLayerScrollableArea::updateScrollbarsAfterLayout() { RenderBox* box = m_layer.renderBox(); ASSERT(box); // List box parts handle the scrollbars by themselves so we have nothing to do. if (box->style().appearance() == ListboxPart) return; bool hadHorizontalScrollbar = hasHorizontalScrollbar(); bool hadVerticalScrollbar = hasVerticalScrollbar(); updateScrollbarPresenceAndState(hasHorizontalOverflow(), hasVerticalOverflow()); // Scrollbars with auto behavior may need to lay out again if scrollbars got added or removed. bool autoHorizontalScrollBarChanged = box->hasAutoScrollbar(ScrollbarOrientation::HorizontalScrollbar) && (hadHorizontalScrollbar != hasHorizontalScrollbar()); bool autoVerticalScrollBarChanged = box->hasAutoScrollbar(ScrollbarOrientation::VerticalScrollbar) && (hadVerticalScrollbar != hasVerticalScrollbar()); if (autoHorizontalScrollBarChanged || autoVerticalScrollBarChanged) { if (autoVerticalScrollBarChanged && shouldPlaceVerticalScrollbarOnLeft()) computeScrollOrigin(); m_layer.updateSelfPaintingLayer(); auto& renderer = m_layer.renderer(); renderer.repaint(); if (renderer.style().overflowX() == Overflow::Auto || renderer.style().overflowY() == Overflow::Auto) { if (!m_inOverflowRelayout) { SetForScope inOverflowRelayoutScope(m_inOverflowRelayout, true); renderer.setNeedsLayout(MarkOnlyThis); if (is(renderer)) { auto& block = downcast(renderer); block.scrollbarsChanged(autoHorizontalScrollBarChanged, autoVerticalScrollBarChanged); block.layoutBlock(true); } else renderer.layout(); } } // FIXME: This does not belong here. RenderObject* parent = renderer.parent(); if (parent && parent->isFlexibleBox() && renderer.isBox()) downcast(parent)->clearCachedMainSizeForChild(*m_layer.renderBox()); } // Set up the range. if (m_hBar) m_hBar->setProportion(roundToInt(box->clientWidth()), m_scrollWidth); if (m_vBar) m_vBar->setProportion(roundToInt(box->clientHeight()), m_scrollHeight); updateScrollbarSteps(); updateScrollableAreaSet(hasScrollableHorizontalOverflow() || hasScrollableVerticalOverflow()); } void RenderLayerScrollableArea::updateScrollbarSteps() { RenderBox* box = m_layer.renderBox(); ASSERT(box); LayoutRect paddedLayerBounds(0_lu, 0_lu, box->clientWidth(), box->clientHeight()); paddedLayerBounds.contract(box->scrollPaddingForViewportRect(paddedLayerBounds)); // Set up the page step/line step. if (m_hBar) { int pageStep = Scrollbar::pageStep(roundToInt(paddedLayerBounds.width())); m_hBar->setSteps(Scrollbar::pixelsPerLineStep(), pageStep); } if (m_vBar) { int pageStep = Scrollbar::pageStep(roundToInt(paddedLayerBounds.height())); m_vBar->setSteps(Scrollbar::pixelsPerLineStep(), pageStep); } } // This is called from layout code (before updateLayerPositions). void RenderLayerScrollableArea::updateScrollInfoAfterLayout() { RenderBox* box = m_layer.renderBox(); if (!box) return; m_scrollDimensionsDirty = true; ScrollPosition originalScrollPosition = scrollPosition(); computeScrollDimensions(); m_layer.updateSelfPaintingLayer(); // FIXME: Ensure that offsets are also updated in case of programmatic style changes. // https://bugs.webkit.org/show_bug.cgi?id=135964 updateSnapOffsets(); if (!box->isHTMLMarquee() && !isRubberBandInProgress() && !isUserScrollInProgress()) { // Layout may cause us to be at an invalid scroll position. In this case we need // to pull our scroll offsets back to the max (or push them up to the min). ScrollOffset clampedScrollOffset = clampScrollOffset(scrollOffset()); #if PLATFORM(IOS_FAMILY) // FIXME: This looks wrong. The caret adjust mode should only be enabled on editing related entry points. // This code was added to fix an issue where the text insertion point would always be drawn on the right edge // of a text field whose content overflowed its bounds. See for more details. setAdjustForIOSCaretWhenScrolling(true); #endif if (clampedScrollOffset != scrollOffset()) scrollToOffset(clampedScrollOffset); #if PLATFORM(IOS_FAMILY) setAdjustForIOSCaretWhenScrolling(false); #endif } updateScrollbarsAfterLayout(); LOG_WITH_STREAM(Scrolling, stream << "RenderLayerScrollableArea [" << scrollingNodeID() << "] updateScrollInfoAfterLayout - new scroll width " << m_scrollWidth << " scroll height " << m_scrollHeight << " rubber banding " << isRubberBandInProgress() << " user scrolling " << isUserScrollInProgress() << " scroll position updated from " << originalScrollPosition << " to " << scrollPosition()); if (originalScrollPosition != scrollPosition()) scrollToPositionWithoutAnimation(IntPoint(scrollPosition())); if (m_layer.isComposited()) { m_layer.setNeedsCompositingGeometryUpdate(); m_layer.setNeedsCompositingConfigurationUpdate(); } if (canUseCompositedScrolling()) m_layer.setNeedsPostLayoutCompositingUpdate(); resnapAfterLayout(); } bool RenderLayerScrollableArea::overflowControlsIntersectRect(const IntRect& localRect) const { auto rects = overflowControlsRects(); if (rects.horizontalScrollbar.intersects(localRect)) return true; if (rects.verticalScrollbar.intersects(localRect)) return true; if (rects.scrollCorner.intersects(localRect)) return true; if (rects.resizer.intersects(localRect)) return true; return false; } bool RenderLayerScrollableArea::showsOverflowControls() const { #if PLATFORM(IOS_FAMILY) // On iOS, the scrollbars are made in the UI process. return !canUseCompositedScrolling(); #endif return true; } void RenderLayerScrollableArea::paintOverflowControls(GraphicsContext& context, const IntPoint& paintOffset, const IntRect& damageRect, bool paintingOverlayControls) { // Don't do anything if we have no overflow. auto& renderer = m_layer.renderer(); if (!renderer.hasNonVisibleOverflow()) return; if (!showsOverflowControls()) return; // Overlay scrollbars paint in a second pass through the layer tree so that they will paint // on top of everything else. If this is the normal painting pass, paintingOverlayControls // will be false, and we should just tell the root layer that there are overlay scrollbars // that need to be painted. That will cause the second pass through the layer tree to run, // and we'll paint the scrollbars then. In the meantime, cache tx and ty so that the // second pass doesn't need to re-enter the RenderTree to get it right. if (hasOverlayScrollbars() && !paintingOverlayControls) { m_cachedOverlayScrollbarOffset = paintOffset; // It's not necessary to do the second pass if the scrollbars paint into layers. if ((m_hBar && layerForHorizontalScrollbar()) || (m_vBar && layerForVerticalScrollbar())) return; IntRect localDamgeRect = damageRect; localDamgeRect.moveBy(-paintOffset); if (!overflowControlsIntersectRect(localDamgeRect)) return; RenderLayer* paintingRoot = m_layer.enclosingCompositingLayer(); if (!paintingRoot) paintingRoot = renderer.view().layer(); if (auto* scrollableArea = paintingRoot->scrollableArea()) scrollableArea->setContainsDirtyOverlayScrollbars(true); return; } // This check is required to avoid painting custom CSS scrollbars twice. if (paintingOverlayControls && !hasOverlayScrollbars()) return; IntPoint adjustedPaintOffset = paintOffset; if (paintingOverlayControls) adjustedPaintOffset = m_cachedOverlayScrollbarOffset; // Move the scrollbar widgets if necessary. We normally move and resize widgets during layout, but sometimes // widgets can move without layout occurring (most notably when you scroll a document that // contains fixed positioned elements). positionOverflowControls(toIntSize(adjustedPaintOffset)); // Now that we're sure the scrollbars are in the right place, paint them. if (m_hBar && !layerForHorizontalScrollbar()) m_hBar->paint(context, damageRect); if (m_vBar && !layerForVerticalScrollbar()) m_vBar->paint(context, damageRect); if (layerForScrollCorner()) return; // We fill our scroll corner with white if we have a scrollbar that doesn't run all the way up to the // edge of the box. paintScrollCorner(context, adjustedPaintOffset, damageRect); // Paint our resizer last, since it sits on top of the scroll corner. paintResizer(context, adjustedPaintOffset, damageRect); } void RenderLayerScrollableArea::paintScrollCorner(GraphicsContext& context, const IntPoint& paintOffset, const IntRect& damageRect) { IntRect absRect = scrollCornerRect(); absRect.moveBy(paintOffset); if (!absRect.intersects(damageRect)) return; if (context.invalidatingControlTints()) { updateScrollCornerStyle(); return; } if (m_scrollCorner) { m_scrollCorner->paintIntoRect(context, paintOffset, absRect); return; } // We don't want to paint a corner if we have overlay scrollbars, since we need // to see what is behind it. if (!hasOverlayScrollbars()) ScrollbarTheme::theme().paintScrollCorner(*this, context, absRect); } void RenderLayerScrollableArea::drawPlatformResizerImage(GraphicsContext& context, const LayoutRect& resizerCornerRect) { auto& renderer = m_layer.renderer(); RefPtr resizeCornerImage; FloatSize cornerResizerSize; if (renderer.document().deviceScaleFactor() >= 2) { static NeverDestroyed resizeCornerImageHiRes(&Image::loadPlatformResource("textAreaResizeCorner@2x").leakRef()); resizeCornerImage = resizeCornerImageHiRes; cornerResizerSize = resizeCornerImage->size(); cornerResizerSize.scale(0.5f); } else { static NeverDestroyed resizeCornerImageLoRes(&Image::loadPlatformResource("textAreaResizeCorner").leakRef()); resizeCornerImage = resizeCornerImageLoRes; cornerResizerSize = resizeCornerImage->size(); } if (shouldPlaceVerticalScrollbarOnLeft()) { context.save(); context.translate(resizerCornerRect.x() + cornerResizerSize.width(), resizerCornerRect.y() + resizerCornerRect.height() - cornerResizerSize.height()); context.scale(FloatSize(-1.0, 1.0)); if (resizeCornerImage) context.drawImage(*resizeCornerImage, FloatRect(FloatPoint(), cornerResizerSize)); context.restore(); return; } if (!resizeCornerImage) return; FloatRect imageRect = snapRectToDevicePixels(LayoutRect(resizerCornerRect.maxXMaxYCorner() - cornerResizerSize, cornerResizerSize), renderer.document().deviceScaleFactor()); context.drawImage(*resizeCornerImage, imageRect); } void RenderLayerScrollableArea::paintResizer(GraphicsContext& context, const LayoutPoint& paintOffset, const LayoutRect& damageRect) { auto& renderer = m_layer.renderer(); if (renderer.style().resize() == Resize::None) return; auto rects = overflowControlsRects(); LayoutRect resizerAbsRect = rects.resizer; resizerAbsRect.moveBy(paintOffset); if (!resizerAbsRect.intersects(damageRect)) return; if (context.invalidatingControlTints()) { updateResizerStyle(); return; } if (m_resizer) { m_resizer->paintIntoRect(context, paintOffset, resizerAbsRect); return; } drawPlatformResizerImage(context, resizerAbsRect); // Draw a frame around the resizer (1px grey line) if there are any scrollbars present. // Clipping will exclude the right and bottom edges of this frame. if (!hasOverlayScrollbars() && (m_vBar || m_hBar)) { GraphicsContextStateSaver stateSaver(context); context.clip(resizerAbsRect); LayoutRect largerCorner = resizerAbsRect; largerCorner.setSize(LayoutSize(largerCorner.width() + 1_lu, largerCorner.height() + 1_lu)); context.setStrokeColor(SRGBA { 217, 217, 217 }); context.setStrokeThickness(1.0f); context.setFillColor(Color::transparentBlack); context.drawRect(snappedIntRect(largerCorner)); } } bool RenderLayerScrollableArea::hitTestOverflowControls(HitTestResult& result, const IntPoint& localPoint) { if (!m_hBar && !m_vBar && !m_layer.canResize()) return false; auto rects = overflowControlsRects(); IntRect resizeControlRect; auto& renderer = m_layer.renderer(); if (renderer.style().resize() != Resize::None) { if (rects.resizer.contains(localPoint)) return true; } // FIXME: We should hit test the m_scrollCorner and pass it back through the result. if (m_vBar && m_vBar->shouldParticipateInHitTesting()) { if (rects.verticalScrollbar.contains(localPoint)) { result.setScrollbar(m_vBar.get()); return true; } } if (m_hBar && m_hBar->shouldParticipateInHitTesting()) { if (rects.horizontalScrollbar.contains(localPoint)) { result.setScrollbar(m_hBar.get()); return true; } } return false; } bool RenderLayerScrollableArea::scroll(ScrollDirection direction, ScrollGranularity granularity, float multiplier) { return ScrollableArea::scroll(direction, granularity, multiplier); } bool RenderLayerScrollableArea::isActive() const { return m_layer.page().focusController().isActive(); } IntPoint RenderLayerScrollableArea::lastKnownMousePositionInView() const { return m_layer.renderer().view().frameView().lastKnownMousePositionInView(); } bool RenderLayerScrollableArea::isHandlingWheelEvent() const { return m_layer.renderer().frame().eventHandler().isHandlingWheelEvent(); } bool RenderLayerScrollableArea::useDarkAppearance() const { return m_layer.renderer().useDarkAppearance(); } void RenderLayerScrollableArea::updateSnapOffsets() { // FIXME: Extend support beyond HTMLElements. if (!is(m_layer.enclosingElement()) || !m_layer.enclosingElement()->renderBox()) return; RenderBox* box = m_layer.enclosingElement()->renderBox(); updateSnapOffsetsForScrollableArea(*this, *box, box->style(), box->paddingBoxRect(), box->style().writingMode(), box->style().direction()); } bool RenderLayerScrollableArea::isScrollSnapInProgress() const { if (!scrollsOverflow()) return false; if (auto* scrollingCoordinator = m_layer.page().scrollingCoordinator()) { if (scrollingCoordinator->isScrollSnapInProgress(scrollingNodeID())) return true; } if (auto* scrollAnimator = existingScrollAnimator()) return scrollAnimator->isScrollSnapInProgress(); return false; } void RenderLayerScrollableArea::paintOverlayScrollbars(GraphicsContext& context, const LayoutRect& damageRect, OptionSet paintBehavior, RenderObject* subtreePaintRoot) { if (!m_containsDirtyOverlayScrollbars) return; RenderLayer::LayerPaintingInfo paintingInfo(&m_layer, enclosingIntRect(damageRect), paintBehavior, LayoutSize(), subtreePaintRoot); m_layer.paintLayer(context, paintingInfo, RenderLayer::PaintLayerFlag::PaintingOverlayScrollbars); m_containsDirtyOverlayScrollbars = false; } bool RenderLayerScrollableArea::hitTestResizerInFragments(const LayerFragments& layerFragments, const HitTestLocation& hitTestLocation, LayoutPoint& pointInFragment) const { if (layerFragments.isEmpty()) return false; auto& renderer = m_layer.renderer(); auto borderBoxRect = snappedIntRect(downcast(renderer).borderBoxRect()); auto rects = overflowControlsRects(); auto cornerRectInFragment = [&](const IntRect& fragmentBounds, const IntRect& resizerRect) { if (shouldPlaceVerticalScrollbarOnLeft()) { IntSize offsetFromBottomLeft = borderBoxRect.minXMaxYCorner() - resizerRect.minXMaxYCorner(); return IntRect { fragmentBounds.minXMaxYCorner() - offsetFromBottomLeft - IntSize { 0, resizerRect.height() }, resizerRect.size() }; } IntSize offsetFromBottomRight = borderBoxRect.maxXMaxYCorner() - resizerRect.maxXMaxYCorner(); return IntRect { fragmentBounds.maxXMaxYCorner() - offsetFromBottomRight - resizerRect.size(), resizerRect.size() }; }; for (int i = layerFragments.size() - 1; i >= 0; --i) { const LayerFragment& fragment = layerFragments.at(i); auto resizerRectInFragment = cornerRectInFragment(snappedIntRect(fragment.layerBounds), rects.resizer); if (fragment.backgroundRect.intersects(hitTestLocation) && resizerRectInFragment.contains(hitTestLocation.roundedPoint())) { pointInFragment = toLayoutPoint(hitTestLocation.point() - fragment.layerBounds.location()); return true; } } return false; } GraphicsLayer* RenderLayerScrollableArea::layerForHorizontalScrollbar() const { return m_layer.backing() ? m_layer.backing()->layerForHorizontalScrollbar() : nullptr; } GraphicsLayer* RenderLayerScrollableArea::layerForVerticalScrollbar() const { return m_layer.backing() ? m_layer.backing()->layerForVerticalScrollbar() : nullptr; } GraphicsLayer* RenderLayerScrollableArea::layerForScrollCorner() const { return m_layer.backing() ? m_layer.backing()->layerForScrollCorner() : nullptr; } bool RenderLayerScrollableArea::scrollingMayRevealBackground() const { return scrollsOverflow() || usesCompositedScrolling(); } void RenderLayerScrollableArea::updateScrollableAreaSet(bool hasOverflow) { auto& renderer = m_layer.renderer(); FrameView& frameView = renderer.view().frameView(); bool isVisibleToHitTest = renderer.visibleToHitTesting(); if (HTMLFrameOwnerElement* owner = frameView.frame().ownerElement()) isVisibleToHitTest &= owner->renderer() && owner->renderer()->visibleToHitTesting(); bool isScrollable = hasOverflow && isVisibleToHitTest; bool addedOrRemoved = false; ASSERT(m_registeredScrollableArea == frameView.containsScrollableArea(this)); if (isScrollable) { if (!m_registeredScrollableArea) { addedOrRemoved = frameView.addScrollableArea(this); m_registeredScrollableArea = true; } } else if (m_registeredScrollableArea) { addedOrRemoved = frameView.removeScrollableArea(this); m_registeredScrollableArea = false; } #if ENABLE(IOS_TOUCH_EVENTS) if (addedOrRemoved) { if (isScrollable && !canUseCompositedScrolling()) registerAsTouchEventListenerForScrolling(); else { // We only need the touch listener for unaccelerated overflow scrolling, so if we became // accelerated, remove ourselves as a touch event listener. unregisterAsTouchEventListenerForScrolling(); } } #else UNUSED_VARIABLE(addedOrRemoved); #endif } void RenderLayerScrollableArea::updateScrollCornerStyle() { auto& renderer = m_layer.renderer(); RenderElement* actualRenderer = rendererForScrollbar(renderer); auto corner = renderer.hasNonVisibleOverflow() ? actualRenderer->getUncachedPseudoStyle({ PseudoId::ScrollbarCorner }, &actualRenderer->style()) : nullptr; if (!corner) { clearScrollCorner(); return; } if (!m_scrollCorner) { m_scrollCorner = createRenderer(renderer.document(), WTFMove(*corner)); // FIXME: A renderer should be a child of its parent! m_scrollCorner->setParent(&renderer); m_scrollCorner->initializeStyle(); } else m_scrollCorner->setStyle(WTFMove(*corner)); } void RenderLayerScrollableArea::clearScrollCorner() { if (!m_scrollCorner) return; m_scrollCorner->setParent(nullptr); m_scrollCorner = nullptr; } void RenderLayerScrollableArea::updateResizerStyle() { auto& renderer = m_layer.renderer(); RenderElement* actualRenderer = rendererForScrollbar(renderer); auto resizer = renderer.hasNonVisibleOverflow() ? actualRenderer->getUncachedPseudoStyle({ PseudoId::Resizer }, &actualRenderer->style()) : nullptr; if (!resizer) { clearResizer(); return; } if (!m_resizer) { m_resizer = createRenderer(renderer.document(), WTFMove(*resizer)); // FIXME: A renderer should be a child of its parent! m_resizer->setParent(&renderer); m_resizer->initializeStyle(); } else m_resizer->setStyle(WTFMove(*resizer)); } void RenderLayerScrollableArea::clearResizer() { if (!m_resizer) return; m_resizer->setParent(nullptr); m_resizer = nullptr; } void RenderLayerScrollableArea::updateAllScrollbarRelatedStyle() { if (m_hBar) m_hBar->styleChanged(); if (m_vBar) m_vBar->styleChanged(); updateScrollCornerStyle(); updateResizerStyle(); } // FIXME: this is only valid after we've made layers. bool RenderLayerScrollableArea::usesCompositedScrolling() const { return m_layer.isComposited() && m_layer.backing()->hasScrollingLayer(); } static inline int adjustedScrollDelta(int beginningDelta) { // This implemention matches Firefox's. // http://mxr.mozilla.org/firefox/source/toolkit/content/widgets/browser.xml#856. const int speedReducer = 12; int adjustedDelta = beginningDelta / speedReducer; if (adjustedDelta > 1) adjustedDelta = static_cast(adjustedDelta * sqrt(static_cast(adjustedDelta))) - 1; else if (adjustedDelta < -1) adjustedDelta = static_cast(adjustedDelta * sqrt(static_cast(-adjustedDelta))) + 1; return adjustedDelta; } static inline IntSize adjustedScrollDelta(const IntSize& delta) { return IntSize(adjustedScrollDelta(delta.width()), adjustedScrollDelta(delta.height())); } void RenderLayerScrollableArea::panScrollFromPoint(const IntPoint& sourcePoint) { IntPoint lastKnownMousePosition = m_layer.renderer().frame().eventHandler().lastKnownMousePosition(); // We need to check if the last known mouse position is out of the window. When the mouse is out of the window, the position is incoherent static IntPoint previousMousePosition; if (lastKnownMousePosition.x() < 0 || lastKnownMousePosition.y() < 0) lastKnownMousePosition = previousMousePosition; else previousMousePosition = lastKnownMousePosition; IntSize delta = lastKnownMousePosition - sourcePoint; if (abs(delta.width()) <= ScrollView::noPanScrollRadius) // at the center we let the space for the icon delta.setWidth(0); if (abs(delta.height()) <= ScrollView::noPanScrollRadius) delta.setHeight(0); scrollByRecursively(adjustedScrollDelta(delta)); } std::optional RenderLayerScrollableArea::updateScrollPosition(const ScrollPositionChangeOptions& options, const LayoutRect& revealRect, const LayoutRect& localExposeRect) { ASSERT(m_layer.allowsCurrentScroll()); RenderBox* box = m_layer.renderBox(); ASSERT(box); ScrollOffset clampedScrollOffset = clampScrollOffset(scrollOffset() + toIntSize(roundedIntRect(revealRect).location())); if (clampedScrollOffset != scrollOffset() || currentScrollBehaviorStatus() != ScrollBehaviorStatus::NotInAnimation) { ScrollOffset oldScrollOffset = scrollOffset(); ScrollOffset realScrollOffset = scrollToOffset(clampedScrollOffset, options); IntSize scrollOffsetDifference = realScrollOffset - oldScrollOffset; auto localExposeRectScrolled = localExposeRect; localExposeRectScrolled.move(-scrollOffsetDifference); return LayoutRect(box->localToAbsoluteQuad(FloatQuad(FloatRect(localExposeRectScrolled)), UseTransforms).boundingBox()); } return std::nullopt; } void RenderLayerScrollableArea::scrollByRecursively(const IntSize& delta, ScrollableArea** scrolledArea) { if (delta.isZero()) return; auto& renderer = m_layer.renderer(); bool restrictedByLineClamp = false; if (renderer.parent()) restrictedByLineClamp = !renderer.parent()->style().lineClamp().isNone(); if (renderer.hasNonVisibleOverflow() && !restrictedByLineClamp) { ScrollOffset newScrollOffset = scrollOffset() + delta; scrollToOffset(newScrollOffset); if (scrolledArea) *scrolledArea = this; // If this layer can't do the scroll we ask the next layer up that can scroll to try IntSize remainingScrollOffset = newScrollOffset - scrollOffset(); if (!remainingScrollOffset.isZero() && renderer.parent()) { // FIXME: This skips scrollable frames. if (auto* enclosingScrollableLayer = m_layer.enclosingScrollableLayer(IncludeSelfOrNot::ExcludeSelf, CrossFrameBoundaries::Yes)) { if (auto* scrollableArea = enclosingScrollableLayer->scrollableArea()) scrollableArea->scrollByRecursively(remainingScrollOffset, scrolledArea); } renderer.frame().eventHandler().updateAutoscrollRenderer(); } } else { // If we are here, we were called on a renderer that can be programmatically scrolled, but doesn't // have an overflow clip. Which means that it is a document node that can be scrolled. renderer.view().frameView().scrollBy(delta); if (scrolledArea) *scrolledArea = &renderer.view().frameView(); // FIXME: If we didn't scroll the whole way, do we want to try looking at the frames ownerElement? // https://bugs.webkit.org/show_bug.cgi?id=28237 } } void RenderLayerScrollableArea::updateLayerPositionsAfterDocumentScroll() { ASSERT(&m_layer == m_layer.renderer().view().layer()); LOG(Scrolling, "RenderLayerScrollableArea::updateLayerPositionsAfterDocumentScroll"); RenderGeometryMap geometryMap(UseTransforms); m_layer.updateLayerPositionsAfterScroll(&geometryMap); } void RenderLayerScrollableArea::updateLayerPositionsAfterOverflowScroll() { RenderGeometryMap geometryMap(UseTransforms); if (&m_layer != m_layer.renderer().view().layer()) geometryMap.pushMappingsToAncestor(m_layer.parent(), nullptr); // FIXME: why is it OK to not check the ancestors of this layer in order to // initialize the HasSeenViewportConstrainedAncestor and HasSeenAncestorWithOverflowClip flags? m_layer.updateLayerPositionsAfterScroll(&geometryMap, RenderLayer::IsOverflowScroll); } bool RenderLayerScrollableArea::usesMockScrollAnimator() const { return DeprecatedGlobalSettings::usesMockScrollAnimator(); } void RenderLayerScrollableArea::logMockScrollAnimatorMessage(const String& message) const { m_layer.renderer().document().addConsoleMessage(MessageSource::Other, MessageLevel::Debug, "RenderLayer: " + message); } String RenderLayerScrollableArea::debugDescription() const { return m_layer.debugDescription(); } } // namespace WebCore