273 lines
10 KiB
C++
273 lines
10 KiB
C++
/*
|
|
* Copyright (C) 2020 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 = makeWeakPtr(latchedElement.get());
|
|
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.findMatching([&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)
|