blob: e8ff7d5bc2dd6c0e48b07fcaa0734e4d46e66e67 [file] [log] [blame]
/*
* Copyright (C) 2011, 2012 Nokia Corporation and/or its subsidiary(-ies)
* Copyright (C) 2011 Benjamin Poulain <benjamin@webkit.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this program; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
*/
#include "config.h"
#include "PageViewportController.h"
#include "AcceleratedDrawingAreaProxy.h"
#include "CoordinatedLayerTreeHostProxy.h"
#include "PageViewportControllerClient.h"
#include "WebPageProxy.h"
#include <WebCore/FloatRect.h>
#include <WebCore/FloatSize.h>
#include <wtf/MathExtras.h>
using namespace WebCore;
namespace WebKit {
PageViewportController::PageViewportController(WebKit::WebPageProxy* proxy, PageViewportControllerClient& client)
: m_webPageProxy(proxy)
, m_client(client)
, m_allowsUserScaling(false)
, m_minimumScaleToFit(1)
, m_initiallyFitToViewport(true)
, m_hadUserInteraction(false)
, m_pageScaleFactor(1)
, m_pendingPositionChange(false)
, m_pendingScaleChange(false)
, m_layerTreeStateIsFrozen(false)
{
// Initializing Viewport Raw Attributes to avoid random negative or infinity scale factors
// if there is a race condition between the first layout and setting the viewport attributes for the first time.
m_rawAttributes.minimumScale = 1;
m_rawAttributes.maximumScale = 1;
m_rawAttributes.userScalable = m_allowsUserScaling;
// The initial scale might be implicit and set to -1, in this case we have to infer it
// using the viewport size and the final layout size.
// To be able to assert for valid scale we initialize it to -1.
m_rawAttributes.initialScale = -1;
}
float PageViewportController::innerBoundedViewportScale(float viewportScale) const
{
return clampTo(viewportScale, m_minimumScaleToFit, m_rawAttributes.maximumScale);
}
float PageViewportController::outerBoundedViewportScale(float viewportScale) const
{
if (m_allowsUserScaling) {
// Bounded by [0.1, 10.0] like the viewport meta code in WebCore.
float hardMin = std::max<float>(0.1, 0.5 * m_minimumScaleToFit);
float hardMax = std::min<float>(10, 2 * m_rawAttributes.maximumScale);
return clampTo(viewportScale, hardMin, hardMax);
}
return innerBoundedViewportScale(viewportScale);
}
float PageViewportController::deviceScaleFactor() const
{
return m_webPageProxy->deviceScaleFactor();
}
static inline bool isIntegral(float value)
{
return static_cast<int>(value) == value;
}
FloatPoint PageViewportController::pixelAlignedFloatPoint(const FloatPoint& framePosition)
{
#if PLATFORM(EFL)
float effectiveScale = m_pageScaleFactor * deviceScaleFactor();
if (!isIntegral(effectiveScale)) {
// To avoid blurryness, modify the position so that it maps into a discrete device position.
FloatPoint scaledPos(framePosition);
// Scale by the effective scale factor to compute the screen-relative position.
scaledPos.scale(effectiveScale, effectiveScale);
// Round to integer boundaries.
FloatPoint alignedPos = roundedIntPoint(scaledPos);
// Convert back to CSS coordinates.
alignedPos.scale(1 / effectiveScale, 1 / effectiveScale);
return alignedPos;
}
#endif
return framePosition;
}
FloatPoint PageViewportController::boundContentsPositionAtScale(const WebCore::FloatPoint& framePosition, float scale)
{
// We need to floor the viewport here as to allow aligning the content in device units. If not,
// it might not be possible to scroll the last pixel and that affects fixed position elements.
FloatRect bounds;
bounds.setWidth(std::max(0.f, m_contentsSize.width() - floorf(m_viewportSize.width() / scale)));
bounds.setHeight(std::max(0.f, m_contentsSize.height() - floorf(m_viewportSize.height() / scale)));
FloatPoint position;
position.setX(clampTo(framePosition.x(), bounds.x(), bounds.width()));
position.setY(clampTo(framePosition.y(), bounds.y(), bounds.height()));
return position;
}
FloatPoint PageViewportController::boundContentsPosition(const WebCore::FloatPoint& framePosition)
{
return boundContentsPositionAtScale(framePosition, m_pageScaleFactor);
}
void PageViewportController::didCommitLoad()
{
// Do not count the previous committed page contents as covered.
m_lastFrameCoveredRect = FloatRect();
// Do not continue to use the content size of the previous page.
m_contentsSize = IntSize();
m_contentsPosition = FloatPoint();
m_layerTreeStateIsFrozen = true;
m_initiallyFitToViewport = true;
// Reset the position to the top, page/history scroll requests may override this before we re-enable rendering.
applyPositionAfterRenderingContents(FloatPoint());
}
void PageViewportController::didChangeContentsSize(const IntSize& newSize)
{
m_contentsSize = newSize;
bool minimumScaleUpdated = updateMinimumScaleToFit(false);
if (m_initiallyFitToViewport) {
// Restrict scale factors to m_minimumScaleToFit.
ASSERT(m_minimumScaleToFit > 0);
m_rawAttributes.initialScale = m_minimumScaleToFit;
WebCore::restrictScaleFactorToInitialScaleIfNotUserScalable(m_rawAttributes);
}
if (minimumScaleUpdated)
m_client.didChangeViewportAttributes();
// We might have pending position change which is now possible.
syncVisibleContents();
}
void PageViewportController::didRenderFrame(const IntSize& contentsSize, const IntRect& coveredRect)
{
if (m_clientContentsSize != contentsSize) {
m_clientContentsSize = contentsSize;
// Only update the viewport's contents dimensions along with its render if the
// size actually changed since animations on the page trigger DidRenderFrame
// messages without causing dimension changes.
m_client.didChangeContentsSize(contentsSize);
}
m_lastFrameCoveredRect = coveredRect;
// Apply any scale or scroll position we locked to be set on the viewport
// only when there is something to display there. The scale goes first to
// avoid offsetting our deferred position by scaling at the viewport center.
// All position and scale changes resulting from a web process event should
// go through here to be applied on the viewport to avoid showing incomplete
// tiles to the user during a few milliseconds.
if (m_pendingScaleChange) {
m_pendingScaleChange = false;
m_client.setPageScaleFactor(m_pageScaleFactor);
// The scale changed, we have to re-pixel align.
m_pendingPositionChange = true;
FloatPoint currentDiscretePos = roundedIntPoint(m_contentsPosition);
FloatPoint pixelAlignedPos = pixelAlignedFloatPoint(currentDiscretePos);
m_contentsPosition = boundContentsPosition(pixelAlignedPos);
m_webPageProxy->scalePage(m_pageScaleFactor, roundedIntPoint(m_contentsPosition));
}
// There might be rendered frames not covering our requested position yet, wait for it.
FloatRect endVisibleContentRect(m_contentsPosition, visibleContentsSize());
if (m_pendingPositionChange && endVisibleContentRect.intersects(coveredRect)) {
m_client.setViewportPosition(m_contentsPosition);
m_pendingPositionChange = false;
}
m_layerTreeStateIsFrozen = false;
}
void PageViewportController::pageTransitionViewportReady()
{
if (!m_rawAttributes.layoutSize.isEmpty() && m_initiallyFitToViewport) {
m_hadUserInteraction = false;
float initialScale = m_initiallyFitToViewport ? m_minimumScaleToFit : m_rawAttributes.initialScale;
applyScaleAfterRenderingContents(innerBoundedViewportScale(initialScale));
}
#if USE(COORDINATED_GRAPHICS_MULTIPROCESS)
// At this point we should already have received the first viewport arguments and the requested scroll
// position for the newly loaded page and sent our reactions to the web process. It's now safe to tell
// the web process to start rendering the new page contents and possibly re-use the current tiles.
// This assumes that all messages have been handled in order and that nothing has been pushed back on the event loop.
m_webPageProxy->commitPageTransitionViewport();
#endif
}
void PageViewportController::pageDidRequestScroll(const IntPoint& cssPosition)
{
// Ignore the request if suspended. Can only happen due to delay in event delivery.
if (m_webPageProxy->areActiveDOMObjectsAndAnimationsSuspended())
return;
FloatPoint boundPosition = boundContentsPosition(FloatPoint(cssPosition));
FloatPoint alignedPosition = pixelAlignedFloatPoint(boundPosition);
FloatRect endVisibleContentRect(alignedPosition, visibleContentsSize());
if (m_lastFrameCoveredRect.intersects(endVisibleContentRect))
m_client.setViewportPosition(alignedPosition);
else {
// Keep the unbound position in case the contents size is changed later on.
FloatPoint position = pixelAlignedFloatPoint(FloatPoint(cssPosition));
applyPositionAfterRenderingContents(position);
}
}
void PageViewportController::didChangeViewportSize(const FloatSize& newSize)
{
if (newSize.isEmpty())
return;
m_viewportSize = newSize;
}
void PageViewportController::didChangeContentsVisibility(const FloatPoint& position, float scale, const FloatPoint& trajectoryVector)
{
if (!m_pendingPositionChange)
m_contentsPosition = position;
if (!m_pendingScaleChange)
applyScaleAfterRenderingContents(scale);
syncVisibleContents(trajectoryVector);
}
bool PageViewportController::syncVisibleContents(const FloatPoint& trajectoryVector)
{
auto* drawingArea = static_cast<AcceleratedDrawingAreaProxy*>(m_webPageProxy->drawingArea());
if (!drawingArea || m_viewportSize.isEmpty() || m_contentsSize.isEmpty())
return false;
FloatRect visibleContentsRect(boundContentsPosition(m_contentsPosition), visibleContentsSize());
visibleContentsRect.intersect(FloatRect(FloatPoint::zero(), m_contentsSize));
drawingArea->coordinatedLayerTreeHostProxy().setVisibleContentsRect(visibleContentsRect, trajectoryVector);
if (!m_layerTreeStateIsFrozen)
m_client.didChangeVisibleContents();
return true;
}
void PageViewportController::didChangeViewportAttributes(const WebCore::ViewportAttributes& newAttributes)
{
if (!m_initiallyFitToViewport)
return;
if (newAttributes.layoutSize.isEmpty())
return;
m_rawAttributes = newAttributes;
m_allowsUserScaling = !!m_rawAttributes.userScalable;
m_initiallyFitToViewport = (m_rawAttributes.initialScale < 0);
if (!m_initiallyFitToViewport)
WebCore::restrictScaleFactorToInitialScaleIfNotUserScalable(m_rawAttributes);
updateMinimumScaleToFit(true);
// As the viewport attributes are calculated when loading pages, after load, or after
// viewport resize, it is important that we inform the client of the new scale and
// position, so that the content can be positioned correctly and pixel aligned.
m_pendingPositionChange = true;
m_pendingScaleChange = true;
m_client.didChangeViewportAttributes();
}
FloatSize PageViewportController::visibleContentsSize() const
{
return FloatSize(m_viewportSize.width() / m_pageScaleFactor, m_viewportSize.height() / m_pageScaleFactor);
}
void PageViewportController::applyScaleAfterRenderingContents(float scale)
{
if (m_pageScaleFactor == scale)
return;
float oldPageScaleFactor = m_pageScaleFactor;
m_pageScaleFactor = scale;
m_pendingScaleChange = true;
if (!syncVisibleContents()) {
m_pageScaleFactor = oldPageScaleFactor;
m_webPageProxy->scalePage(m_pageScaleFactor, roundedIntPoint(m_contentsPosition));
}
}
void PageViewportController::applyPositionAfterRenderingContents(const FloatPoint& pos)
{
if (m_contentsPosition == pos)
return;
m_contentsPosition = pos;
m_pendingPositionChange = true;
syncVisibleContents();
}
bool PageViewportController::updateMinimumScaleToFit(bool userInitiatedUpdate)
{
if (m_viewportSize.isEmpty() || m_contentsSize.isEmpty() || !m_initiallyFitToViewport || m_hadUserInteraction)
return false;
// FIXME: Why this arbitrary precision? We likely want to omit the third argument so that
// std::numeric_limits<float>::epsilon() is used instead, similarly to Mac / iOS.
bool currentlyScaledToFit = WTF::areEssentiallyEqual(m_pageScaleFactor, m_minimumScaleToFit, 0.0001f);
float minimumScale = WebCore::computeMinimumScaleFactorForContentContained(m_rawAttributes, WebCore::roundedIntSize(m_viewportSize), WebCore::roundedIntSize(m_contentsSize));
if (minimumScale <= 0)
return false;
if (!WTF::areEssentiallyEqual(minimumScale, m_minimumScaleToFit, 0.0001f)) {
m_minimumScaleToFit = minimumScale;
if (!m_webPageProxy->areActiveDOMObjectsAndAnimationsSuspended()) {
if (!m_hadUserInteraction || (userInitiatedUpdate && currentlyScaledToFit))
applyScaleAfterRenderingContents(m_minimumScaleToFit);
else {
// Ensure the effective scale stays within bounds.
float boundedScale = innerBoundedViewportScale(m_pageScaleFactor);
if (!WTF::areEssentiallyEqual(boundedScale, m_pageScaleFactor, 0.0001f))
applyScaleAfterRenderingContents(boundedScale);
}
}
return true;
}
return false;
}
} // namespace WebKit