blob: a39d2e1056d7d5b83218f09684e5db08377f8277 [file] [log] [blame]
/*
* Copyright (C) 2021 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 "KeyboardScrollingAnimator.h"
#include "EventNames.h"
#include "PlatformKeyboardEvent.h"
#include "ScrollTypes.h"
#include "ScrollableArea.h"
#include "WritingMode.h"
#include <wtf/SortedArrayMap.h>
namespace WebCore {
enum class KeyboardScrollingKey : uint8_t {
LeftArrow,
RightArrow,
UpArrow,
DownArrow,
Space,
PageUp,
PageDown
};
KeyboardScrollingAnimator::KeyboardScrollingAnimator(ScrollAnimator& scrollAnimator, ScrollingEffectsController& scrollController)
: m_scrollAnimator(scrollAnimator)
, m_scrollController(scrollController)
{
}
RectEdges<bool> KeyboardScrollingAnimator::scrollableDirectionsFromPosition(FloatPoint position) const
{
auto minimumScrollPosition = m_scrollAnimator.scrollableArea().minimumScrollPosition();
auto maximumScrollPosition = m_scrollAnimator.scrollableArea().maximumScrollPosition();
RectEdges<bool> edges;
edges.setTop(position.y() > minimumScrollPosition.y());
edges.setBottom(position.y() < maximumScrollPosition.y());
edges.setLeft(position.x() > minimumScrollPosition.x());
edges.setRight(position.x() < maximumScrollPosition.x());
return edges;
}
static BoxSide boxSideForDirection(ScrollDirection direction)
{
switch (direction) {
case ScrollDirection::ScrollUp:
return BoxSide::Top;
case ScrollDirection::ScrollDown:
return BoxSide::Bottom;
case ScrollDirection::ScrollLeft:
return BoxSide::Left;
case ScrollDirection::ScrollRight:
return BoxSide::Right;
}
ASSERT_NOT_REACHED();
return BoxSide::Top;
}
static FloatSize perpendicularAbsoluteUnitVector(ScrollDirection direction)
{
switch (direction) {
case ScrollDirection::ScrollUp:
case ScrollDirection::ScrollDown:
return { 1, 0 };
case ScrollDirection::ScrollLeft:
case ScrollDirection::ScrollRight:
return { 0, 1 };
}
ASSERT_NOT_REACHED();
return { };
}
void KeyboardScrollingAnimator::updateKeyboardScrollPosition(MonotonicTime currentTime)
{
auto force = FloatSize { };
auto axesToApplySpring = FloatSize { 1, 1 };
KeyboardScrollParameters params = KeyboardScrollParameters::parameters();
if (m_currentKeyboardScroll) {
auto scrollableDirections = scrollableDirectionsFromPosition(m_scrollAnimator.currentPosition());
auto direction = m_currentKeyboardScroll->direction;
if (scrollableDirections.at(boxSideForDirection(direction))) {
// Apply the scrolling force. Only apply the spring in the perpendicular axis,
// otherwise it drags against the direction of motion.
axesToApplySpring = perpendicularAbsoluteUnitVector(direction);
force = m_currentKeyboardScroll->force;
} else {
// The scroll view cannot scroll in this direction, and is rubber-banding.
// Apply a constant and significant force; otherwise, the force for a
// single-line increment is not strong enough to rubber-band perceptibly.
force = unitVectorForScrollDirection(direction).scaled(params.rubberBandForce);
}
if (fabs(m_velocity.width()) >= fabs(m_currentKeyboardScroll->maximumVelocity.width()))
force.setWidth(0);
if (fabs(m_velocity.height()) >= fabs(m_currentKeyboardScroll->maximumVelocity.height()))
force.setHeight(0);
}
ScrollPosition idealPosition = m_scrollAnimator.scrollableArea().constrainedScrollPosition(IntPoint(m_currentKeyboardScroll ? m_scrollAnimator.currentPosition() : m_idealPosition));
FloatSize displacement = m_scrollAnimator.currentPosition() - idealPosition;
auto springForce = -displacement.scaled(params.springStiffness) - m_velocity.scaled(params.springDamping);
force += springForce * axesToApplySpring;
float frameDuration = (currentTime - m_timeAtLastFrame).value();
m_timeAtLastFrame = currentTime;
FloatSize acceleration = force.scaled(1. / params.springMass);
m_velocity += acceleration.scaled(frameDuration);
FloatPoint newPosition = m_scrollAnimator.currentPosition() + m_velocity.scaled(frameDuration);
m_scrollAnimator.scrollToPositionWithoutAnimation(newPosition);
if (!m_scrollTriggeringKeyIsPressed && m_velocity.diagonalLengthSquared() < 1) {
m_scrollController.didStopKeyboardScrolling();
m_velocity = { };
}
}
float KeyboardScrollingAnimator::scrollDistance(ScrollDirection direction, ScrollGranularity granularity) const
{
auto scrollbar = m_scrollAnimator.scrollableArea().scrollbarForDirection(direction);
if (scrollbar) {
switch (granularity) {
case ScrollGranularity::Line:
return scrollbar->lineStep();
case ScrollGranularity::Page:
return scrollbar->pageStep();
case ScrollGranularity::Document:
return scrollbar->totalSize();
case ScrollGranularity::Pixel:
return scrollbar->pixelStep();
}
}
return 0;
}
static std::optional<KeyboardScrollingKey> keyboardScrollingKeyFromEvent(const PlatformKeyboardEvent& event)
{
static constexpr std::pair<ComparableASCIILiteral, KeyboardScrollingKey> mappings[] = {
{ "Down", KeyboardScrollingKey::DownArrow },
{ "Left", KeyboardScrollingKey::LeftArrow },
{ "PageDown", KeyboardScrollingKey::PageDown },
{ "PageUp", KeyboardScrollingKey::PageUp },
{ "Right", KeyboardScrollingKey::RightArrow },
{ "Up", KeyboardScrollingKey::UpArrow },
};
static constexpr SortedArrayMap map { mappings };
auto identifier = event.keyIdentifier();
if (auto* result = map.tryGet(identifier))
return *result;
if (event.text().characterStartingAt(0) == ' ')
return KeyboardScrollingKey::Space;
return { };
}
std::optional<KeyboardScroll> KeyboardScrollingAnimator::keyboardScrollForKeyboardEvent(const PlatformKeyboardEvent& event) const
{
auto key = keyboardScrollingKeyFromEvent(event);
if (!key)
return { };
// FIXME (bug 227459): This logic does not account for writing-mode.
auto granularity = [&] {
switch (key.value()) {
case KeyboardScrollingKey::LeftArrow:
case KeyboardScrollingKey::RightArrow:
return event.altKey() ? ScrollGranularity::Page : ScrollGranularity::Line;
case KeyboardScrollingKey::UpArrow:
case KeyboardScrollingKey::DownArrow:
if (event.metaKey())
return ScrollGranularity::Document;
if (event.altKey())
return ScrollGranularity::Page;
return ScrollGranularity::Line;
case KeyboardScrollingKey::Space:
case KeyboardScrollingKey::PageUp:
case KeyboardScrollingKey::PageDown:
return ScrollGranularity::Page;
};
RELEASE_ASSERT_NOT_REACHED();
}();
auto direction = [&] {
switch (key.value()) {
case KeyboardScrollingKey::LeftArrow:
return ScrollDirection::ScrollLeft;
case KeyboardScrollingKey::RightArrow:
return ScrollDirection::ScrollRight;
case KeyboardScrollingKey::UpArrow:
case KeyboardScrollingKey::PageUp:
return ScrollDirection::ScrollUp;
case KeyboardScrollingKey::DownArrow:
case KeyboardScrollingKey::PageDown:
return ScrollDirection::ScrollDown;
case KeyboardScrollingKey::Space:
return event.shiftKey() ? ScrollDirection::ScrollUp : ScrollDirection::ScrollDown;
}
RELEASE_ASSERT_NOT_REACHED();
}();
float distance = scrollDistance(direction, granularity);
if (!distance)
return std::nullopt;
KeyboardScroll scroll;
scroll.offset = unitVectorForScrollDirection(direction).scaled(distance);
scroll.granularity = granularity;
scroll.direction = direction;
scroll.maximumVelocity = scroll.offset.scaled(KeyboardScrollParameters::parameters().maximumVelocityMultiplier);
scroll.force = scroll.maximumVelocity.scaled(KeyboardScrollParameters::parameters().springMass / KeyboardScrollParameters::parameters().timeToMaximumVelocity);
return scroll;
}
bool KeyboardScrollingAnimator::beginKeyboardScrollGesture(const PlatformKeyboardEvent& event)
{
auto scroll = keyboardScrollForKeyboardEvent(event);
if (!scroll)
return false;
m_currentKeyboardScroll = scroll;
// PlatformEvent::Char is a "keypress" event.
if (!(event.type() == PlatformEvent::RawKeyDown || event.type() == PlatformEvent::Char))
return false;
if (m_scrollTriggeringKeyIsPressed)
return false;
if (m_currentKeyboardScroll->granularity == ScrollGranularity::Document) {
m_velocity = { };
stopKeyboardScrollAnimation();
auto newPosition = IntPoint(m_scrollAnimator.currentPosition() + m_currentKeyboardScroll->offset);
m_scrollAnimator.scrollToPositionWithAnimation(newPosition);
return true;
}
m_timeAtLastFrame = MonotonicTime::now();
m_scrollTriggeringKeyIsPressed = true;
m_idealPositionForMinimumTravel = m_scrollAnimator.currentPosition() + m_currentKeyboardScroll->offset;
m_scrollController.willBeginKeyboardScrolling();
return true;
}
static ScrollPosition farthestPointInDirection(FloatPoint a, FloatPoint b, ScrollDirection direction)
{
switch (direction) {
case ScrollDirection::ScrollUp:
return ScrollPosition(a.x(), std::min(a.y(), b.y()));
case ScrollDirection::ScrollDown:
return ScrollPosition(a.x(), std::max(a.y(), b.y()));
case ScrollDirection::ScrollLeft:
return ScrollPosition(std::min(a.x(), b.x()), a.y());
case ScrollDirection::ScrollRight:
return ScrollPosition(std::max(a.x(), b.x()), a.y());
}
ASSERT_NOT_REACHED();
return { };
}
void KeyboardScrollingAnimator::stopKeyboardScrollAnimation()
{
if (!m_currentKeyboardScroll)
return;
auto params = KeyboardScrollParameters::parameters();
// Determine the settling position of the spring, conserving the system's current energy.
// Kinetic = elastic potential
// 1/2 * m * v^2 = 1/2 * k * x^2
// x = sqrt(v^2 * m / k)
auto displacementMagnitudeSquared = (m_velocity * m_velocity).scaled(params.springMass / params.springStiffness);
FloatSize displacement = {
std::copysign(sqrt(displacementMagnitudeSquared.width()), m_velocity.width()),
std::copysign(sqrt(displacementMagnitudeSquared.height()), m_velocity.height())
};
// If the spring would settle before the minimum travel distance
// for an instantaneous tap, move the settling position of the spring
// out to that point.
ScrollPosition farthestPoint = farthestPointInDirection(m_scrollAnimator.currentPosition() + displacement, m_idealPositionForMinimumTravel, m_currentKeyboardScroll->direction);
m_idealPosition = m_scrollAnimator.scrollableArea().constrainedScrollPosition(farthestPoint);
m_currentKeyboardScroll = std::nullopt;
}
void KeyboardScrollingAnimator::handleKeyUpEvent()
{
if (!m_scrollTriggeringKeyIsPressed)
return;
stopKeyboardScrollAnimation();
m_scrollTriggeringKeyIsPressed = false;
}
} // namespace WebCore