1/*
2 * Copyright (C) 2011, 2012 Nokia Corporation and/or its subsidiary(-ies)
3 * Copyright (C) 2011 Benjamin Poulain <benjamin@webkit.org>
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Library General Public
7 * License as published by the Free Software Foundation; either
8 * version 2 of the License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 * Library General Public License for more details.
14 *
15 * You should have received a copy of the GNU Library General Public License
16 * along with this program; see the file COPYING.LIB.  If not, write to
17 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 * Boston, MA 02110-1301, USA.
19 *
20 */
21
22#include "config.h"
23#include "PageViewportController.h"
24
25#include "CoordinatedDrawingAreaProxy.h"
26#include "PageViewportControllerClient.h"
27#include "WebPageProxy.h"
28#include <WebCore/FloatRect.h>
29#include <WebCore/FloatSize.h>
30#include <wtf/MathExtras.h>
31
32using namespace WebCore;
33
34namespace WebKit {
35
36bool fuzzyCompare(float a, float b, float epsilon)
37{
38    return std::abs(a - b) < epsilon;
39}
40
41PageViewportController::PageViewportController(WebKit::WebPageProxy* proxy, PageViewportControllerClient* client)
42    : m_webPageProxy(proxy)
43    , m_client(client)
44    , m_allowsUserScaling(false)
45    , m_minimumScaleToFit(1)
46    , m_initiallyFitToViewport(true)
47    , m_hadUserInteraction(false)
48    , m_pageScaleFactor(1)
49    , m_pendingPositionChange(false)
50    , m_pendingScaleChange(false)
51{
52    // Initializing Viewport Raw Attributes to avoid random negative or infinity scale factors
53    // if there is a race condition between the first layout and setting the viewport attributes for the first time.
54    m_rawAttributes.minimumScale = 1;
55    m_rawAttributes.maximumScale = 1;
56    m_rawAttributes.userScalable = m_allowsUserScaling;
57
58    // The initial scale might be implicit and set to -1, in this case we have to infer it
59    // using the viewport size and the final layout size.
60    // To be able to assert for valid scale we initialize it to -1.
61    m_rawAttributes.initialScale = -1;
62
63    ASSERT(m_client);
64    m_client->setController(this);
65}
66
67float PageViewportController::innerBoundedViewportScale(float viewportScale) const
68{
69    return clampTo(viewportScale, m_minimumScaleToFit, m_rawAttributes.maximumScale);
70}
71
72float PageViewportController::outerBoundedViewportScale(float viewportScale) const
73{
74    if (m_allowsUserScaling) {
75        // Bounded by [0.1, 10.0] like the viewport meta code in WebCore.
76        float hardMin = std::max<float>(0.1, 0.5 * m_minimumScaleToFit);
77        float hardMax = std::min<float>(10, 2 * m_rawAttributes.maximumScale);
78        return clampTo(viewportScale, hardMin, hardMax);
79    }
80    return innerBoundedViewportScale(viewportScale);
81}
82
83float PageViewportController::deviceScaleFactor() const
84{
85    return m_webPageProxy->deviceScaleFactor();
86}
87
88static inline bool isIntegral(float value)
89{
90    return static_cast<int>(value) == value;
91}
92
93FloatPoint PageViewportController::pixelAlignedFloatPoint(const FloatPoint& framePosition)
94{
95#if PLATFORM(EFL)
96    float effectiveScale = m_pageScaleFactor * deviceScaleFactor();
97    if (!isIntegral(effectiveScale)) {
98        // To avoid blurryness, modify the position so that it maps into a discrete device position.
99        FloatPoint scaledPos(framePosition);
100
101        // Scale by the effective scale factor to compute the screen-relative position.
102        scaledPos.scale(effectiveScale, effectiveScale);
103
104        // Round to integer boundaries.
105        FloatPoint alignedPos = roundedIntPoint(scaledPos);
106
107        // Convert back to CSS coordinates.
108        alignedPos.scale(1 / effectiveScale, 1 / effectiveScale);
109
110        return alignedPos;
111    }
112#endif
113
114    return framePosition;
115}
116
117FloatPoint PageViewportController::boundContentsPositionAtScale(const WebCore::FloatPoint& framePosition, float scale)
118{
119    // We need to floor the viewport here as to allow aligning the content in device units. If not,
120    // it might not be possible to scroll the last pixel and that affects fixed position elements.
121    FloatRect bounds;
122    bounds.setWidth(std::max(0.f, m_contentsSize.width() - floorf(m_viewportSize.width() / scale)));
123    bounds.setHeight(std::max(0.f, m_contentsSize.height() - floorf(m_viewportSize.height() / scale)));
124
125    FloatPoint position;
126    position.setX(clampTo(framePosition.x(), bounds.x(), bounds.width()));
127    position.setY(clampTo(framePosition.y(), bounds.y(), bounds.height()));
128
129    return position;
130}
131
132FloatPoint PageViewportController::boundContentsPosition(const WebCore::FloatPoint& framePosition)
133{
134    return boundContentsPositionAtScale(framePosition, m_pageScaleFactor);
135}
136
137void PageViewportController::didCommitLoad()
138{
139    // Do not count the previous committed page contents as covered.
140    m_lastFrameCoveredRect = FloatRect();
141
142    // Do not continue to use the content size of the previous page.
143    m_contentsSize = IntSize();
144
145    // Reset the position to the top, page/history scroll requests may override this before we re-enable rendering.
146    applyPositionAfterRenderingContents(FloatPoint());
147}
148
149void PageViewportController::didChangeContentsSize(const IntSize& newSize)
150{
151    m_contentsSize = newSize;
152
153    bool minimumScaleUpdated = updateMinimumScaleToFit(false);
154
155    if (m_initiallyFitToViewport) {
156        // Restrict scale factors to m_minimumScaleToFit.
157        ASSERT(m_minimumScaleToFit > 0);
158        m_rawAttributes.initialScale = m_minimumScaleToFit;
159        WebCore::restrictScaleFactorToInitialScaleIfNotUserScalable(m_rawAttributes);
160    }
161
162    if (minimumScaleUpdated)
163        m_client->didChangeViewportAttributes();
164
165    // We might have pending position change which is now possible.
166    syncVisibleContents();
167}
168
169void PageViewportController::didRenderFrame(const IntSize& contentsSize, const IntRect& coveredRect)
170{
171    if (m_clientContentsSize != contentsSize) {
172        m_clientContentsSize = contentsSize;
173        // Only update the viewport's contents dimensions along with its render if the
174        // size actually changed since animations on the page trigger DidRenderFrame
175        // messages without causing dimension changes.
176        m_client->didChangeContentsSize(contentsSize);
177    }
178
179    m_lastFrameCoveredRect = coveredRect;
180
181    // Apply any scale or scroll position we locked to be set on the viewport
182    // only when there is something to display there. The scale goes first to
183    // avoid offsetting our deferred position by scaling at the viewport center.
184    // All position and scale changes resulting from a web process event should
185    // go through here to be applied on the viewport to avoid showing incomplete
186    // tiles to the user during a few milliseconds.
187
188    if (m_pendingScaleChange) {
189        m_pendingScaleChange = false;
190        m_client->setPageScaleFactor(m_pageScaleFactor);
191
192        // The scale changed, we have to re-pixel align.
193        m_pendingPositionChange = true;
194        FloatPoint currentDiscretePos = roundedIntPoint(m_contentsPosition);
195        FloatPoint pixelAlignedPos = pixelAlignedFloatPoint(currentDiscretePos);
196        m_contentsPosition = boundContentsPosition(pixelAlignedPos);
197
198        m_webPageProxy->scalePage(m_pageScaleFactor, roundedIntPoint(m_contentsPosition));
199    }
200
201    // There might be rendered frames not covering our requested position yet, wait for it.
202    FloatRect endVisibleContentRect(m_contentsPosition, visibleContentsSize());
203    if (m_pendingPositionChange && endVisibleContentRect.intersects(coveredRect)) {
204        m_client->setViewportPosition(m_contentsPosition);
205        m_pendingPositionChange = false;
206    }
207}
208
209void PageViewportController::pageTransitionViewportReady()
210{
211    if (!m_rawAttributes.layoutSize.isEmpty()) {
212        m_hadUserInteraction = false;
213        float initialScale = m_initiallyFitToViewport ? m_minimumScaleToFit : m_rawAttributes.initialScale;
214        applyScaleAfterRenderingContents(innerBoundedViewportScale(initialScale));
215    }
216
217    // At this point we should already have received the first viewport arguments and the requested scroll
218    // position for the newly loaded page and sent our reactions to the web process. It's now safe to tell
219    // the web process to start rendering the new page contents and possibly re-use the current tiles.
220    // This assumes that all messages have been handled in order and that nothing has been pushed back on the event loop.
221    m_webPageProxy->commitPageTransitionViewport();
222}
223
224void PageViewportController::pageDidRequestScroll(const IntPoint& cssPosition)
225{
226    // Ignore the request if suspended. Can only happen due to delay in event delivery.
227    if (m_webPageProxy->areActiveDOMObjectsAndAnimationsSuspended())
228        return;
229
230    FloatPoint boundPosition = boundContentsPosition(FloatPoint(cssPosition));
231    FloatPoint alignedPosition = pixelAlignedFloatPoint(boundPosition);
232    FloatRect endVisibleContentRect(alignedPosition, visibleContentsSize());
233
234    if (m_lastFrameCoveredRect.intersects(endVisibleContentRect))
235        m_client->setViewportPosition(alignedPosition);
236    else {
237        // Keep the unbound position in case the contents size is changed later on.
238        FloatPoint position = pixelAlignedFloatPoint(FloatPoint(cssPosition));
239        applyPositionAfterRenderingContents(position);
240    }
241}
242
243void PageViewportController::didChangeViewportSize(const FloatSize& newSize)
244{
245    if (newSize.isEmpty())
246        return;
247
248    m_viewportSize = newSize;
249}
250
251void PageViewportController::didChangeContentsVisibility(const FloatPoint& position, float scale, const FloatPoint& trajectoryVector)
252{
253    if (!m_pendingPositionChange)
254        m_contentsPosition = position;
255    if (!m_pendingScaleChange)
256        applyScaleAfterRenderingContents(scale);
257
258    syncVisibleContents(trajectoryVector);
259}
260
261void PageViewportController::syncVisibleContents(const FloatPoint& trajectoryVector)
262{
263    CoordinatedDrawingAreaProxy* drawingArea = static_cast<CoordinatedDrawingAreaProxy*>(m_webPageProxy->drawingArea());
264    if (!drawingArea || m_viewportSize.isEmpty() || m_contentsSize.isEmpty())
265        return;
266
267    FloatRect visibleContentsRect(boundContentsPosition(m_contentsPosition), visibleContentsSize());
268    visibleContentsRect.intersect(FloatRect(FloatPoint::zero(), m_contentsSize));
269    drawingArea->setVisibleContentsRect(visibleContentsRect, trajectoryVector);
270
271    m_client->didChangeVisibleContents();
272}
273
274void PageViewportController::didChangeViewportAttributes(const WebCore::ViewportAttributes& newAttributes)
275{
276    if (newAttributes.layoutSize.isEmpty())
277        return;
278
279    m_rawAttributes = newAttributes;
280    m_allowsUserScaling = !!m_rawAttributes.userScalable;
281    m_initiallyFitToViewport = (m_rawAttributes.initialScale < 0);
282
283    if (!m_initiallyFitToViewport)
284        WebCore::restrictScaleFactorToInitialScaleIfNotUserScalable(m_rawAttributes);
285
286    updateMinimumScaleToFit(true);
287
288    // As the viewport attributes are calculated when loading pages, after load, or after
289    // viewport resize, it is important that we inform the client of the new scale and
290    // position, so that the content can be positioned correctly and pixel aligned.
291    m_pendingPositionChange = true;
292    m_pendingScaleChange = true;
293
294    m_client->didChangeViewportAttributes();
295}
296
297FloatSize PageViewportController::visibleContentsSize() const
298{
299    return FloatSize(m_viewportSize.width() / m_pageScaleFactor, m_viewportSize.height() / m_pageScaleFactor);
300}
301
302void PageViewportController::applyScaleAfterRenderingContents(float scale)
303{
304    if (m_pageScaleFactor == scale)
305        return;
306
307    m_pageScaleFactor = scale;
308    m_pendingScaleChange = true;
309    syncVisibleContents();
310}
311
312void PageViewportController::applyPositionAfterRenderingContents(const FloatPoint& pos)
313{
314    if (m_contentsPosition == pos)
315        return;
316
317    m_contentsPosition = pos;
318    m_pendingPositionChange = true;
319    syncVisibleContents();
320}
321
322bool PageViewportController::updateMinimumScaleToFit(bool userInitiatedUpdate)
323{
324    if (m_viewportSize.isEmpty() || m_contentsSize.isEmpty())
325        return false;
326
327    bool currentlyScaledToFit = fuzzyCompare(m_pageScaleFactor, m_minimumScaleToFit, 0.0001);
328
329    float minimumScale = WebCore::computeMinimumScaleFactorForContentContained(m_rawAttributes, WebCore::roundedIntSize(m_viewportSize), WebCore::roundedIntSize(m_contentsSize));
330
331    if (minimumScale <= 0)
332        return false;
333
334    if (!fuzzyCompare(minimumScale, m_minimumScaleToFit, 0.0001)) {
335        m_minimumScaleToFit = minimumScale;
336
337        if (!m_webPageProxy->areActiveDOMObjectsAndAnimationsSuspended()) {
338            if (!m_hadUserInteraction || (userInitiatedUpdate && currentlyScaledToFit))
339                applyScaleAfterRenderingContents(m_minimumScaleToFit);
340            else {
341                // Ensure the effective scale stays within bounds.
342                float boundedScale = innerBoundedViewportScale(m_pageScaleFactor);
343                if (!fuzzyCompare(boundedScale, m_pageScaleFactor, 0.0001))
344                    applyScaleAfterRenderingContents(boundedScale);
345            }
346        }
347
348        return true;
349    }
350    return false;
351}
352
353} // namespace WebKit
354