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