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