1/*
2 * Copyright (C) 2005-2014 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "ViewportConfiguration.h"
28
29#include <WebCore/TextStream.h>
30#include <wtf/Assertions.h>
31#include <wtf/MathExtras.h>
32#include <wtf/text/CString.h>
33
34#if PLATFORM(IOS)
35#include "WebCoreSystemInterface.h"
36#endif
37
38namespace WebCore {
39
40#if !ASSERT_DISABLED
41static bool constraintsAreAllRelative(const ViewportConfiguration::Parameters& configuration)
42{
43    return !configuration.widthIsSet && !configuration.heightIsSet && !configuration.initialScaleIsSet;
44}
45#endif
46
47ViewportConfiguration::ViewportConfiguration()
48    : m_minimumLayoutSize(1024, 768)
49    , m_minimumLayoutSizeForMinimalUI(m_minimumLayoutSize)
50    , m_usesMinimalUI(false)
51    , m_pageDidFinishDocumentLoad(false)
52{
53    // Setup a reasonable default configuration to avoid computing infinite scale/sizes.
54    // Those are the original iPhone configuration.
55    m_defaultConfiguration = ViewportConfiguration::webpageParameters();
56    updateConfiguration();
57}
58
59void ViewportConfiguration::setDefaultConfiguration(const ViewportConfiguration::Parameters& defaultConfiguration)
60{
61    ASSERT(!constraintsAreAllRelative(m_configuration));
62    ASSERT(!m_defaultConfiguration.initialScaleIsSet || defaultConfiguration.initialScale > 0);
63    ASSERT(defaultConfiguration.minimumScale > 0);
64    ASSERT(defaultConfiguration.maximumScale >= defaultConfiguration.minimumScale);
65
66    m_defaultConfiguration = defaultConfiguration;
67    updateConfiguration();
68}
69
70void ViewportConfiguration::setContentsSize(const IntSize& contentSize)
71{
72    if (m_contentSize == contentSize)
73        return;
74
75    m_contentSize = contentSize;
76    updateConfiguration();
77}
78
79void ViewportConfiguration::setMinimumLayoutSize(const FloatSize& minimumLayoutSize)
80{
81    if (m_minimumLayoutSize == minimumLayoutSize)
82        return;
83
84    m_minimumLayoutSize = minimumLayoutSize;
85}
86
87void ViewportConfiguration::setMinimumLayoutSizeForMinimalUI(const FloatSize& minimumLayoutSizeForMinimalUI)
88{
89    if (m_minimumLayoutSizeForMinimalUI == minimumLayoutSizeForMinimalUI)
90        return;
91
92    m_minimumLayoutSizeForMinimalUI = minimumLayoutSizeForMinimalUI;
93}
94
95const FloatSize& ViewportConfiguration::activeMinimumLayoutSizeInScrollViewCoordinates() const
96{
97    if (m_usesMinimalUI)
98        return m_minimumLayoutSizeForMinimalUI;
99    return m_minimumLayoutSize;
100}
101
102void ViewportConfiguration::setViewportArguments(const ViewportArguments& viewportArguments)
103{
104    if (m_viewportArguments == viewportArguments)
105        return;
106
107    m_viewportArguments = viewportArguments;
108    updateConfiguration();
109}
110
111void ViewportConfiguration::resetMinimalUI()
112{
113    m_usesMinimalUI = false;
114    m_pageDidFinishDocumentLoad = false;
115}
116
117void ViewportConfiguration::didFinishDocumentLoad()
118{
119    m_pageDidFinishDocumentLoad = true;
120}
121
122IntSize ViewportConfiguration::layoutSize() const
123{
124    return IntSize(layoutWidth(), layoutHeight());
125}
126
127double ViewportConfiguration::initialScale() const
128{
129    ASSERT(!constraintsAreAllRelative(m_configuration));
130
131    // If the document has specified its own initial scale, use it regardless.
132    // This is guaranteed to be sanity checked already, so no need for MIN/MAX.
133    if (m_configuration.initialScaleIsSet)
134        return m_configuration.initialScale;
135
136    // If not, it is up to us to determine the initial scale.
137    // We want a scale small enough to fit the document width-wise.
138    const FloatSize& minimumLayoutSize = activeMinimumLayoutSizeInScrollViewCoordinates();
139    double width = m_contentSize.width() > 0 ? m_contentSize.width() : layoutWidth();
140    double initialScale = 0;
141    if (width > 0)
142        initialScale = minimumLayoutSize.width() / width;
143
144    // Prevent the intial scale from shrinking to a height smaller than our view's minimum height.
145    double height = m_contentSize.height() > 0 ? m_contentSize.height() : layoutHeight();
146    if (height > 0 && height * initialScale < minimumLayoutSize.height())
147        initialScale = minimumLayoutSize.height() / height;
148    return std::min(std::max(initialScale, m_configuration.minimumScale), m_configuration.maximumScale);
149}
150
151double ViewportConfiguration::minimumScale() const
152{
153    // If we scale to fit, then this is our minimum scale as well.
154    if (!m_configuration.initialScaleIsSet)
155        return initialScale();
156
157    // If not, we still need to sanity check our value.
158    double minimumScale = m_configuration.minimumScale;
159
160    const FloatSize& minimumLayoutSize = activeMinimumLayoutSizeInScrollViewCoordinates();
161    double contentWidth = m_contentSize.width();
162    if (contentWidth > 0 && contentWidth * minimumScale < minimumLayoutSize.width())
163        minimumScale = minimumLayoutSize.width() / contentWidth;
164
165    double contentHeight = m_contentSize.height();
166    if (contentHeight > 0 && contentHeight * minimumScale < minimumLayoutSize.height())
167        minimumScale = minimumLayoutSize.height() / contentHeight;
168
169    minimumScale = std::min(std::max(minimumScale, m_configuration.minimumScale), m_configuration.maximumScale);
170
171    return minimumScale;
172}
173
174ViewportConfiguration::Parameters ViewportConfiguration::webpageParameters()
175{
176    Parameters parameters;
177    parameters.width = 980;
178    parameters.widthIsSet = true;
179    parameters.allowsUserScaling = true;
180    parameters.minimumScale = 0.25;
181    parameters.maximumScale = 5;
182    return parameters;
183}
184
185ViewportConfiguration::Parameters ViewportConfiguration::textDocumentParameters()
186{
187    Parameters parameters;
188
189#if PLATFORM(IOS)
190    parameters.width = static_cast<int>(wkGetScreenSize().width);
191#else
192    // FIXME: this needs to be unified with ViewportArguments on all ports.
193    parameters.width = 320;
194#endif
195
196    parameters.widthIsSet = true;
197    parameters.allowsUserScaling = true;
198    parameters.minimumScale = 0.25;
199    parameters.maximumScale = 5;
200    return parameters;
201}
202
203ViewportConfiguration::Parameters ViewportConfiguration::imageDocumentParameters()
204{
205    Parameters parameters;
206    parameters.width = 980;
207    parameters.widthIsSet = true;
208    parameters.allowsUserScaling = true;
209    parameters.minimumScale = 0.01;
210    parameters.maximumScale = 5;
211    return parameters;
212}
213
214ViewportConfiguration::Parameters ViewportConfiguration::xhtmlMobileParameters()
215{
216    Parameters parameters = webpageParameters();
217    parameters.width = 320;
218    return parameters;
219}
220
221ViewportConfiguration::Parameters ViewportConfiguration::testingParameters()
222{
223    Parameters parameters;
224    parameters.initialScale = 1;
225    parameters.initialScaleIsSet = true;
226    parameters.minimumScale = 1;
227    parameters.maximumScale = 5;
228    return parameters;
229}
230
231static inline bool viewportArgumentValueIsValid(float value)
232{
233    return value > 0;
234}
235
236template<typename ValueType, typename ViewportArgumentsType>
237static inline void applyViewportArgument(ValueType& value, ViewportArgumentsType viewportArgumentValue, ValueType minimum, ValueType maximum)
238{
239    if (viewportArgumentValueIsValid(viewportArgumentValue))
240        value = std::min(maximum, std::max(minimum, static_cast<ValueType>(viewportArgumentValue)));
241}
242
243template<typename ValueType, typename ViewportArgumentsType>
244static inline void applyViewportArgument(ValueType& value, bool& valueIsSet, ViewportArgumentsType viewportArgumentValue, ValueType minimum, ValueType maximum)
245{
246    if (viewportArgumentValueIsValid(viewportArgumentValue)) {
247        value = std::min(maximum, std::max(minimum, static_cast<ValueType>(viewportArgumentValue)));
248        valueIsSet = true;
249    } else
250        valueIsSet = false;
251}
252
253static inline bool viewportArgumentUserZoomIsSet(float value)
254{
255    return !value || value == 1;
256}
257
258void ViewportConfiguration::updateConfiguration()
259{
260    m_configuration = m_defaultConfiguration;
261
262    const double minimumViewportArgumentsScaleFactor = 0.1;
263    const double maximumViewportArgumentsScaleFactor = 10.0;
264
265    bool viewportArgumentsOverridesInitialScale;
266    bool viewportArgumentsOverridesWidth;
267    bool viewportArgumentsOverridesHeight;
268
269    applyViewportArgument(m_configuration.minimumScale, m_viewportArguments.minZoom, minimumViewportArgumentsScaleFactor, maximumViewportArgumentsScaleFactor);
270    applyViewportArgument(m_configuration.maximumScale, m_viewportArguments.maxZoom, m_configuration.minimumScale, maximumViewportArgumentsScaleFactor);
271    applyViewportArgument(m_configuration.initialScale, viewportArgumentsOverridesInitialScale, m_viewportArguments.zoom, m_configuration.minimumScale, m_configuration.maximumScale);
272
273    double minimumViewportArgumentsDimension = 10;
274    double maximumViewportArgumentsDimension = 10000;
275    applyViewportArgument(m_configuration.width, viewportArgumentsOverridesWidth, m_viewportArguments.width, minimumViewportArgumentsDimension, maximumViewportArgumentsDimension);
276    applyViewportArgument(m_configuration.height, viewportArgumentsOverridesHeight, m_viewportArguments.height, minimumViewportArgumentsDimension, maximumViewportArgumentsDimension);
277
278    if (viewportArgumentsOverridesInitialScale || viewportArgumentsOverridesWidth || viewportArgumentsOverridesHeight) {
279        m_configuration.initialScaleIsSet = viewportArgumentsOverridesInitialScale;
280        m_configuration.widthIsSet = viewportArgumentsOverridesWidth;
281        m_configuration.heightIsSet = viewportArgumentsOverridesHeight;
282    }
283
284    if (viewportArgumentUserZoomIsSet(m_viewportArguments.userZoom))
285        m_configuration.allowsUserScaling = m_viewportArguments.userZoom != 0.;
286
287#if PLATFORM(IOS)
288    if (!m_pageDidFinishDocumentLoad)
289        m_usesMinimalUI = m_usesMinimalUI || m_viewportArguments.minimalUI;
290#endif
291}
292
293int ViewportConfiguration::layoutWidth() const
294{
295    ASSERT(!constraintsAreAllRelative(m_configuration));
296
297    const FloatSize& minimumLayoutSize = activeMinimumLayoutSizeInScrollViewCoordinates();
298    if (m_configuration.widthIsSet) {
299        // If we scale to fit, then accept the viewport width with sanity checking.
300        if (!m_configuration.initialScaleIsSet) {
301            double maximumScale = this->maximumScale();
302            double maximumContentWidthInViewportCoordinate = maximumScale * m_configuration.width;
303            if (maximumContentWidthInViewportCoordinate < minimumLayoutSize.width()) {
304                // The content zoomed to maxScale does not fit the the view. Return the minimum width
305                // satisfying the constraint maximumScale.
306                return std::round(minimumLayoutSize.width() / maximumScale);
307            }
308            return std::round(m_configuration.width);
309        }
310
311        // If not, make sure the viewport width and initial scale can co-exist.
312        double initialContentWidthInViewportCoordinate = m_configuration.width * m_configuration.initialScale;
313        if (initialContentWidthInViewportCoordinate < minimumLayoutSize.width()) {
314            // The specified width does not fit in viewport. Return the minimum width that satisfy the initialScale constraint.
315            return std::round(minimumLayoutSize.width() / m_configuration.initialScale);
316        }
317        return std::round(m_configuration.width);
318    }
319
320    // If the page has a real scale, then just return the minimum size over the initial scale.
321    if (m_configuration.initialScaleIsSet && !m_configuration.heightIsSet)
322        return std::round(minimumLayoutSize.width() / m_configuration.initialScale);
323
324    if (minimumLayoutSize.height() > 0)
325        return std::round(minimumLayoutSize.width() * layoutHeight() / minimumLayoutSize.height());
326    return minimumLayoutSize.width();
327}
328
329int ViewportConfiguration::layoutHeight() const
330{
331    ASSERT(!constraintsAreAllRelative(m_configuration));
332
333    const FloatSize& minimumLayoutSize = activeMinimumLayoutSizeInScrollViewCoordinates();
334    if (m_configuration.heightIsSet) {
335        // If we scale to fit, then accept the viewport height with sanity checking.
336        if (!m_configuration.initialScaleIsSet) {
337            double maximumScale = this->maximumScale();
338            double maximumContentHeightInViewportCoordinate = maximumScale * m_configuration.height;
339            if (maximumContentHeightInViewportCoordinate < minimumLayoutSize.height()) {
340                // The content zoomed to maxScale does not fit the the view. Return the minimum height that
341                // satisfy the constraint maximumScale.
342                return std::round(minimumLayoutSize.height() / maximumScale);
343            }
344            return std::round(m_configuration.height);
345        }
346
347        // If not, make sure the viewport width and initial scale can co-exist.
348        double initialContentHeightInViewportCoordinate = m_configuration.height * m_configuration.initialScale;
349        if (initialContentHeightInViewportCoordinate < minimumLayoutSize.height()) {
350            // The specified width does not fit in viewport. Return the minimum height that satisfy the initialScale constraint.
351            return std::round(minimumLayoutSize.height() / m_configuration.initialScale);
352        }
353        return std::round(m_configuration.height);
354    }
355
356    // If the page has a real scale, then just return the minimum size over the initial scale.
357    if (m_configuration.initialScaleIsSet && !m_configuration.widthIsSet)
358        return std::round(minimumLayoutSize.height() / m_configuration.initialScale);
359
360    if (minimumLayoutSize.width() > 0)
361        return std::round(minimumLayoutSize.height() * layoutWidth() / minimumLayoutSize.width());
362    return minimumLayoutSize.height();
363}
364
365#ifndef NDEBUG
366class ViewportConfigurationTextStream : public TextStream {
367public:
368    ViewportConfigurationTextStream()
369        : m_indent(0)
370    {
371    }
372
373    using TextStream::operator<<;
374
375    ViewportConfigurationTextStream& operator<<(const ViewportConfiguration::Parameters&);
376    ViewportConfigurationTextStream& operator<<(const ViewportArguments&);
377
378    void increaseIndent() { ++m_indent; }
379    void decreaseIndent() { --m_indent; ASSERT(m_indent >= 0); }
380
381    void writeIndent();
382
383private:
384    int m_indent;
385};
386
387template <typename T>
388static void dumpProperty(ViewportConfigurationTextStream& ts, String name, T value)
389{
390    ts << "\n";
391    ts.increaseIndent();
392    ts.writeIndent();
393    ts << "(" << name << " ";
394    ts << value << ")";
395    ts.decreaseIndent();
396}
397
398void ViewportConfigurationTextStream::writeIndent()
399{
400    for (int i = 0; i < m_indent; ++i)
401        *this << "  ";
402}
403
404ViewportConfigurationTextStream& ViewportConfigurationTextStream::operator<<(const ViewportConfiguration::Parameters& parameters)
405{
406    ViewportConfigurationTextStream& ts = *this;
407
408    ts.increaseIndent();
409    ts << "\n";
410    ts.writeIndent();
411    ts << "(width " << parameters.width << ", set: " << (parameters.widthIsSet ? "true" : "false") << ")";
412
413    ts << "\n";
414    ts.writeIndent();
415    ts << "(height " << parameters.height << ", set: " << (parameters.heightIsSet ? "true" : "false") << ")";
416
417    ts << "\n";
418    ts.writeIndent();
419    ts << "(initialScale " << parameters.width << ", set: " << (parameters.initialScaleIsSet ? "true" : "false") << ")";
420    ts.decreaseIndent();
421
422    dumpProperty(ts, "minimumScale", parameters.minimumScale);
423    dumpProperty(ts, "maximumScale", parameters.maximumScale);
424    dumpProperty(ts, "allowsUserScaling", parameters.allowsUserScaling);
425
426    return ts;
427}
428
429ViewportConfigurationTextStream& ViewportConfigurationTextStream::operator<<(const ViewportArguments& viewportArguments)
430{
431    ViewportConfigurationTextStream& ts = *this;
432
433    ts.increaseIndent();
434
435    ts << "\n";
436    ts.writeIndent();
437    ts << "(width " << viewportArguments.width << ", minWidth " << viewportArguments.minWidth << ", maxWidth " << viewportArguments.maxWidth << ")";
438
439    ts << "\n";
440    ts.writeIndent();
441    ts << "(height " << viewportArguments.height << ", minHeight " << viewportArguments.minHeight << ", maxHeight " << viewportArguments.maxHeight << ")";
442
443    ts << "\n";
444    ts.writeIndent();
445    ts << "(zoom " << viewportArguments.zoom << ", minZoom " << viewportArguments.minZoom << ", maxZoom " << viewportArguments.maxZoom << ")";
446    ts.decreaseIndent();
447
448#if PLATFORM(IOS)
449    dumpProperty(ts, "minimalUI", viewportArguments.minimalUI);
450#endif
451    return ts;
452}
453
454CString ViewportConfiguration::description() const
455{
456    ViewportConfigurationTextStream ts;
457
458    ts << "(viewport-configuration " << (void*)this;
459    ts << "\n";
460    ts.increaseIndent();
461    ts.writeIndent();
462    ts << "(viewport arguments";
463    ts << m_viewportArguments;
464    ts << ")";
465    ts.decreaseIndent();
466
467    ts << "\n";
468    ts.increaseIndent();
469    ts.writeIndent();
470    ts << "(configuration";
471    ts << m_configuration;
472    ts << ")";
473    ts.decreaseIndent();
474
475    ts << "\n";
476    ts.increaseIndent();
477    ts.writeIndent();
478    ts << "(default configuration";
479    ts << m_defaultConfiguration;
480    ts << ")";
481    ts.decreaseIndent();
482
483    dumpProperty(ts, "contentSize", m_contentSize);
484    dumpProperty(ts, "minimumLayoutSize", m_minimumLayoutSize);
485    dumpProperty(ts, "minimumLayoutSizeForMinimalUI", m_minimumLayoutSizeForMinimalUI);
486    ts << "(uses minimal UI " << m_usesMinimalUI << ")";
487
488    ts << "\n";
489    ts.increaseIndent();
490    ts.writeIndent();
491    ts << "(computed initial scale " << initialScale() << ")\n";
492    ts.writeIndent();
493    ts << "(computed minimum scale " << minimumScale() << ")\n";
494    ts.writeIndent();
495    ts << "(computed layout size " << layoutSize() << ")";
496    ts.decreaseIndent();
497
498    ts << ")\n";
499
500    return ts.release().utf8();
501}
502
503void ViewportConfiguration::dump() const
504{
505    fprintf(stderr, "%s", description().data());
506}
507
508#endif
509
510} // namespace WebCore
511