| /* |
| * Copyright (C) 2020-2022 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. |
| */ |
| |
| #include "config.h" |
| #include "ScrollLatchingController.h" |
| |
| #if ENABLE(WHEEL_EVENT_LATCHING) |
| |
| #include "Element.h" |
| #include "Frame.h" |
| #include "FrameView.h" |
| #include "Logging.h" |
| #include "PlatformWheelEvent.h" |
| #include "ScrollableArea.h" |
| #include <wtf/text/TextStream.h> |
| |
| namespace WebCore { |
| |
| // See also ScrollTreeLatchingController.cpp |
| static const Seconds resetLatchedStateTimeout { 100_ms }; |
| |
| ScrollLatchingController::ScrollLatchingController() |
| : m_clearLatchingStateTimer(*this, &ScrollLatchingController::clearTimerFired) |
| { |
| } |
| |
| ScrollLatchingController::~ScrollLatchingController() = default; |
| |
| void ScrollLatchingController::clear() |
| { |
| LOG_WITH_STREAM(ScrollLatching, stream << "ScrollLatchingController::clear()"); |
| m_cumulativeEventDelta = { }; |
| m_frameStateStack.clear(); |
| } |
| |
| // FIXME: This logic is different from ScrollingTreeLatchingController, which simply lets the latching state elapse after 100ms. |
| void ScrollLatchingController::clearOrScheduleClearIfNeeded(const PlatformWheelEvent& wheelEvent) |
| { |
| if (wheelEvent.shouldResetLatching() || wheelEvent.isNonGestureEvent()) { |
| clear(); |
| LOG_WITH_STREAM(ScrollLatching, stream << "ScrollLatchingController::clearOrScheduleClearingLatchedStateIfNeeded() - event" << wheelEvent << ", resetting latching"); |
| return; |
| } |
| |
| if (m_clearLatchingStateTimer.isActive()) { |
| // If another wheel event scrolling starts, stop the timer manually, and reset the latched state immediately. |
| if (wheelEvent.isGestureStart()) { |
| LOG_WITH_STREAM(ScrollLatching, stream << "ScrollLatchingController::clearOrScheduleClearingLatchedStateIfNeeded() - event" << wheelEvent << ", timer pending, another scroll starting"); |
| clear(); |
| m_clearLatchingStateTimer.stop(); |
| } else if (wheelEvent.isTransitioningToMomentumScroll()) { |
| // Wheel events machinery is transitioning to momentum scrolling, so no need to reset latched state. Stop the timer. |
| m_clearLatchingStateTimer.stop(); |
| } |
| return; |
| } |
| |
| if (wheelEvent.isEndOfNonMomentumScroll()) { |
| LOG_WITH_STREAM(ScrollLatching, stream << "ScrollLatchingController::clearOrScheduleClearingLatchedStateIfNeeded() - event" << wheelEvent << ", scheduling clear timer"); |
| m_clearLatchingStateTimer.startOneShot(resetLatchedStateTimeout); |
| } |
| } |
| |
| void ScrollLatchingController::clearTimerFired() |
| { |
| LOG_WITH_STREAM(ScrollLatching, stream << "ScrollLatchingController::clearTimerFired() - clearing state"); |
| clear(); |
| } |
| |
| void ScrollLatchingController::receivedWheelEvent(const PlatformWheelEvent& wheelEvent) |
| { |
| clearOrScheduleClearIfNeeded(wheelEvent); |
| |
| if (wheelEvent.isGestureStart() || wheelEvent.isNonGestureEvent()) |
| m_cumulativeEventDelta = wheelEvent.delta(); |
| else |
| m_cumulativeEventDelta += wheelEvent.delta(); |
| } |
| |
| bool ScrollLatchingController::latchingAllowsScrollingInFrame(const Frame& frame, WeakPtr<ScrollableArea>& latchedScroller) const |
| { |
| if (m_frameStateStack.isEmpty()) |
| return true; |
| |
| if (auto* frameState = stateForFrame(frame)) { |
| latchedScroller = frameState->scrollableArea; |
| return !!frameState->scrollableArea; |
| } |
| return false; |
| } |
| |
| void ScrollLatchingController::updateAndFetchLatchingStateForFrame(Frame& frame, const PlatformWheelEvent& wheelEvent, RefPtr<Element>& latchedElement, WeakPtr<ScrollableArea>& scrollableArea, bool& isOverWidget) |
| { |
| if (wheelEvent.isGestureStart()) { |
| // We can have existing state here because state is cleared on a timer. |
| if (!hasStateForFrame(frame)) { |
| FrameState state; |
| state.frame = &frame; |
| state.wheelEventElement = latchedElement; |
| if (shouldLatchToScrollableArea(frame, scrollableArea.get(), m_cumulativeEventDelta)) |
| state.scrollableArea = scrollableArea; |
| state.isOverWidget = isOverWidget; |
| |
| LOG_WITH_STREAM(ScrollLatching, stream << "ScrollLatchingController::updateAndFetchLatchingStateForFrame() - pushing state for Frame " << &frame << " element " << state.wheelEventElement.get() << " scrollableArea " << state.scrollableArea); |
| m_frameStateStack.append(WTFMove(state)); |
| return; |
| } |
| } |
| |
| if (wheelEvent.isGestureContinuation()) { |
| auto* state = stateForFrame(frame); |
| if (!state) |
| return; |
| |
| // We may not have latched at gesture start because of small deltas. Re-evaluate latching based on accumulated delta. |
| if (!state->scrollableArea && shouldLatchToScrollableArea(frame, scrollableArea.get(), m_cumulativeEventDelta)) |
| state->scrollableArea = scrollableArea; |
| } |
| |
| if (!wheelEvent.useLatchedEventElement()) |
| return; |
| |
| for (const auto& state : m_frameStateStack) { |
| if (state.frame == &frame) { |
| latchedElement = state.wheelEventElement.get(); |
| scrollableArea = state.scrollableArea; |
| isOverWidget = state.isOverWidget; |
| |
| LOG_WITH_STREAM(ScrollLatching, stream << "ScrollLatchingController::updateAndFetchLatchingStateForFrame() - using state for Frame " << &frame << " element " << latchedElement.get() << " scrollableArea " << scrollableArea); |
| } |
| } |
| } |
| |
| void ScrollLatchingController::removeLatchingStateForTarget(const Element& element) |
| { |
| if (m_frameStateStack.isEmpty()) |
| return; |
| |
| auto findResult = m_frameStateStack.findIf([&element] (const auto& state) { |
| auto* wheelElement = state.wheelEventElement.get(); |
| return wheelElement && element.isEqualNode(wheelElement); |
| }); |
| |
| // If this element is found in the latching stack, just clear the whole stack. We can't just remove one entry, |
| // since the stack has to match the frame hierarchy. |
| if (findResult != notFound) |
| m_frameStateStack.clear(); |
| } |
| |
| void ScrollLatchingController::removeLatchingStateForFrame(const Frame& frame) |
| { |
| if (m_frameStateStack.isEmpty()) |
| return; |
| |
| // If the frame was in the latching stack, just clear state. |
| if (auto* frameState = stateForFrame(frame)) |
| clear(); |
| } |
| |
| static bool deltaIsPredominantlyVertical(FloatSize delta) |
| { |
| return std::abs(delta.height()) > std::abs(delta.width()); |
| } |
| |
| bool ScrollLatchingController::shouldLatchToScrollableArea(const Frame& frame, ScrollableArea* scrollableArea, FloatSize scrollDelta) const |
| { |
| if (!scrollableArea) |
| return false; |
| |
| // We always allow the main frame to receive wheel events to permit rubber-banding. |
| if (frame.isMainFrame() && scrollableArea == frame.view()) |
| return true; |
| |
| if (!scrollableArea->canHaveScrollbars()) |
| return false; |
| |
| if (scrollDelta.isZero()) |
| return false; |
| |
| if (!deltaIsPredominantlyVertical(scrollDelta) && scrollDelta.width()) { |
| if (!scrollableArea->horizontalScrollbar()) |
| return false; |
| |
| if (scrollDelta.width() < 0) |
| return !scrollableArea->scrolledToRight(); |
| |
| return !scrollableArea->scrolledToLeft(); |
| } |
| |
| if (!scrollableArea->verticalScrollbar()) |
| return false; |
| |
| if (scrollDelta.height() < 0) |
| return !scrollableArea->scrolledToBottom(); |
| |
| return !scrollableArea->scrolledToTop(); |
| } |
| |
| bool ScrollLatchingController::hasStateForFrame(const Frame& frame) const |
| { |
| for (const auto& state : m_frameStateStack) { |
| if (state.frame == &frame) |
| return true; |
| } |
| return false; |
| } |
| |
| ScrollLatchingController::FrameState* ScrollLatchingController::stateForFrame(const Frame& frame) |
| { |
| for (auto& state : m_frameStateStack) { |
| if (state.frame == &frame) |
| return &state; |
| } |
| return nullptr; |
| } |
| |
| const ScrollLatchingController::FrameState* ScrollLatchingController::stateForFrame(const Frame& frame) const |
| { |
| for (const auto& state : m_frameStateStack) { |
| if (state.frame == &frame) |
| return &state; |
| } |
| return nullptr; |
| } |
| |
| void ScrollLatchingController::dump(WTF::TextStream& ts) const |
| { |
| TextStream multilineStream; |
| multilineStream.setIndent(ts.indent() + 2); |
| |
| for (const auto& state : m_frameStateStack) { |
| TextStream::GroupScope groupScope(multilineStream); |
| multilineStream.dumpProperty("frame", ValueOrNull(state.frame)); |
| multilineStream.dumpProperty("element", ValueOrNull(state.wheelEventElement.get())); |
| multilineStream.dumpProperty("scrollable area", ValueOrNull(state.scrollableArea.get())); |
| multilineStream.dumpProperty("is over widget", state.isOverWidget); |
| } |
| |
| ts << "ScrollLatchingController state " << multilineStream.release(); |
| } |
| |
| TextStream& operator<<(TextStream& ts, const ScrollLatchingController& controller) |
| { |
| controller.dump(ts); |
| return ts; |
| } |
| |
| } // namespace WebCore |
| |
| #endif // ENABLE(WHEEL_EVENT_LATCHING) |