1/* 2 * Copyright (c) 2010, Google Inc. All rights reserved. 3 * Copyright (C) 2008, 2011, 2014 Apple Inc. All Rights Reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are 7 * met: 8 * 9 * * Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * * Redistributions in binary form must reproduce the above 12 * copyright notice, this list of conditions and the following disclaimer 13 * in the documentation and/or other materials provided with the 14 * distribution. 15 * * Neither the name of Google Inc. nor the names of its 16 * contributors may be used to endorse or promote products derived from 17 * this software without specific prior written permission. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32#include "config.h" 33#include "ScrollableArea.h" 34 35#include "GraphicsContext.h" 36#include "GraphicsLayer.h" 37#include "FloatPoint.h" 38#include "LayoutRect.h" 39#include "PlatformWheelEvent.h" 40#include "ScrollAnimator.h" 41#include "ScrollbarTheme.h" 42#include <wtf/PassOwnPtr.h> 43 44namespace WebCore { 45 46struct SameSizeAsScrollableArea { 47 virtual ~SameSizeAsScrollableArea(); 48 void* pointer; 49 IntPoint origin; 50 unsigned bitfields : 16; 51}; 52 53COMPILE_ASSERT(sizeof(ScrollableArea) == sizeof(SameSizeAsScrollableArea), ScrollableArea_should_stay_small); 54 55ScrollableArea::ScrollableArea() 56 : m_constrainsScrollingToContentEdge(true) 57 , m_inLiveResize(false) 58 , m_verticalScrollElasticity(ScrollElasticityNone) 59 , m_horizontalScrollElasticity(ScrollElasticityNone) 60 , m_scrollbarOverlayStyle(ScrollbarOverlayStyleDefault) 61 , m_scrollOriginChanged(false) 62 , m_scrolledProgrammatically(false) 63{ 64} 65 66ScrollableArea::~ScrollableArea() 67{ 68} 69 70ScrollAnimator* ScrollableArea::scrollAnimator() const 71{ 72 if (!m_scrollAnimator) 73 m_scrollAnimator = ScrollAnimator::create(const_cast<ScrollableArea*>(this)); 74 75 return m_scrollAnimator.get(); 76} 77 78void ScrollableArea::setScrollOrigin(const IntPoint& origin) 79{ 80 if (m_scrollOrigin != origin) { 81 m_scrollOrigin = origin; 82 m_scrollOriginChanged = true; 83 } 84} 85 86bool ScrollableArea::scroll(ScrollDirection direction, ScrollGranularity granularity, float multiplier) 87{ 88 ScrollbarOrientation orientation; 89 Scrollbar* scrollbar; 90 if (direction == ScrollUp || direction == ScrollDown) { 91 orientation = VerticalScrollbar; 92 scrollbar = verticalScrollbar(); 93 } else { 94 orientation = HorizontalScrollbar; 95 scrollbar = horizontalScrollbar(); 96 } 97 98 if (!scrollbar) 99 return false; 100 101 float step = 0; 102 switch (granularity) { 103 case ScrollByLine: 104 step = scrollbar->lineStep(); 105 break; 106 case ScrollByPage: 107 step = scrollbar->pageStep(); 108 break; 109 case ScrollByDocument: 110 step = scrollbar->totalSize(); 111 break; 112 case ScrollByPixel: 113 case ScrollByPrecisePixel: 114 step = scrollbar->pixelStep(); 115 break; 116 } 117 118 if (direction == ScrollUp || direction == ScrollLeft) 119 multiplier = -multiplier; 120 121 return scrollAnimator()->scroll(orientation, granularity, step, multiplier); 122} 123 124void ScrollableArea::scrollToOffsetWithoutAnimation(const FloatPoint& offset) 125{ 126 scrollAnimator()->scrollToOffsetWithoutAnimation(offset); 127} 128 129void ScrollableArea::scrollToOffsetWithoutAnimation(ScrollbarOrientation orientation, float offset) 130{ 131 if (orientation == HorizontalScrollbar) 132 scrollToOffsetWithoutAnimation(FloatPoint(offset, scrollAnimator()->currentPosition().y())); 133 else 134 scrollToOffsetWithoutAnimation(FloatPoint(scrollAnimator()->currentPosition().x(), offset)); 135} 136 137void ScrollableArea::notifyScrollPositionChanged(const IntPoint& position) 138{ 139 scrollPositionChanged(position); 140 scrollAnimator()->setCurrentPosition(position); 141} 142 143void ScrollableArea::scrollPositionChanged(const IntPoint& position) 144{ 145 IntPoint oldPosition = scrollPosition(); 146 // Tell the derived class to scroll its contents. 147 setScrollOffset(position); 148 149 Scrollbar* verticalScrollbar = this->verticalScrollbar(); 150 151 // Tell the scrollbars to update their thumb postions. 152 if (Scrollbar* horizontalScrollbar = this->horizontalScrollbar()) { 153 horizontalScrollbar->offsetDidChange(); 154 if (horizontalScrollbar->isOverlayScrollbar() && !hasLayerForHorizontalScrollbar()) { 155 if (!verticalScrollbar) 156 horizontalScrollbar->invalidate(); 157 else { 158 // If there is both a horizontalScrollbar and a verticalScrollbar, 159 // then we must also invalidate the corner between them. 160 IntRect boundsAndCorner = horizontalScrollbar->boundsRect(); 161 boundsAndCorner.setWidth(boundsAndCorner.width() + verticalScrollbar->width()); 162 horizontalScrollbar->invalidateRect(boundsAndCorner); 163 } 164 } 165 } 166 if (verticalScrollbar) { 167 verticalScrollbar->offsetDidChange(); 168 if (verticalScrollbar->isOverlayScrollbar() && !hasLayerForVerticalScrollbar()) 169 verticalScrollbar->invalidate(); 170 } 171 172 if (scrollPosition() != oldPosition) 173 scrollAnimator()->notifyContentAreaScrolled(scrollPosition() - oldPosition); 174} 175 176bool ScrollableArea::handleWheelEvent(const PlatformWheelEvent& wheelEvent) 177{ 178 return scrollAnimator()->handleWheelEvent(wheelEvent); 179} 180 181#if ENABLE(TOUCH_EVENTS) 182bool ScrollableArea::handleTouchEvent(const PlatformTouchEvent& touchEvent) 183{ 184 return scrollAnimator()->handleTouchEvent(touchEvent); 185} 186#endif 187 188// NOTE: Only called from Internals for testing. 189void ScrollableArea::setScrollOffsetFromInternals(const IntPoint& offset) 190{ 191 setScrollOffsetFromAnimation(offset); 192} 193 194void ScrollableArea::setScrollOffsetFromAnimation(const IntPoint& offset) 195{ 196 if (requestScrollPositionUpdate(offset)) 197 return; 198 199 scrollPositionChanged(offset); 200} 201 202void ScrollableArea::willStartLiveResize() 203{ 204 if (m_inLiveResize) 205 return; 206 m_inLiveResize = true; 207 if (ScrollAnimator* scrollAnimator = existingScrollAnimator()) 208 scrollAnimator->willStartLiveResize(); 209} 210 211void ScrollableArea::willEndLiveResize() 212{ 213 if (!m_inLiveResize) 214 return; 215 m_inLiveResize = false; 216 if (ScrollAnimator* scrollAnimator = existingScrollAnimator()) 217 scrollAnimator->willEndLiveResize(); 218} 219 220void ScrollableArea::contentAreaWillPaint() const 221{ 222 if (ScrollAnimator* scrollAnimator = existingScrollAnimator()) 223 scrollAnimator->contentAreaWillPaint(); 224} 225 226void ScrollableArea::mouseEnteredContentArea() const 227{ 228 if (ScrollAnimator* scrollAnimator = existingScrollAnimator()) 229 scrollAnimator->mouseEnteredContentArea(); 230} 231 232void ScrollableArea::mouseExitedContentArea() const 233{ 234 if (ScrollAnimator* scrollAnimator = existingScrollAnimator()) 235 scrollAnimator->mouseEnteredContentArea(); 236} 237 238void ScrollableArea::mouseMovedInContentArea() const 239{ 240 if (ScrollAnimator* scrollAnimator = existingScrollAnimator()) 241 scrollAnimator->mouseMovedInContentArea(); 242} 243 244void ScrollableArea::mouseEnteredScrollbar(Scrollbar* scrollbar) const 245{ 246 scrollAnimator()->mouseEnteredScrollbar(scrollbar); 247} 248 249void ScrollableArea::mouseExitedScrollbar(Scrollbar* scrollbar) const 250{ 251 scrollAnimator()->mouseExitedScrollbar(scrollbar); 252} 253 254void ScrollableArea::contentAreaDidShow() const 255{ 256 if (ScrollAnimator* scrollAnimator = existingScrollAnimator()) 257 scrollAnimator->contentAreaDidShow(); 258} 259 260void ScrollableArea::contentAreaDidHide() const 261{ 262 if (ScrollAnimator* scrollAnimator = existingScrollAnimator()) 263 scrollAnimator->contentAreaDidHide(); 264} 265 266void ScrollableArea::lockOverlayScrollbarStateToHidden(bool shouldLockState) const 267{ 268 if (ScrollAnimator* scrollAnimator = existingScrollAnimator()) 269 scrollAnimator->lockOverlayScrollbarStateToHidden(shouldLockState); 270} 271 272bool ScrollableArea::scrollbarsCanBeActive() const 273{ 274 if (ScrollAnimator* scrollAnimator = existingScrollAnimator()) 275 return scrollAnimator->scrollbarsCanBeActive(); 276 return true; 277} 278 279void ScrollableArea::didAddScrollbar(Scrollbar* scrollbar, ScrollbarOrientation orientation) 280{ 281 if (orientation == VerticalScrollbar) 282 scrollAnimator()->didAddVerticalScrollbar(scrollbar); 283 else 284 scrollAnimator()->didAddHorizontalScrollbar(scrollbar); 285 286 // <rdar://problem/9797253> AppKit resets the scrollbar's style when you attach a scrollbar 287 setScrollbarOverlayStyle(scrollbarOverlayStyle()); 288} 289 290void ScrollableArea::willRemoveScrollbar(Scrollbar* scrollbar, ScrollbarOrientation orientation) 291{ 292 if (orientation == VerticalScrollbar) 293 scrollAnimator()->willRemoveVerticalScrollbar(scrollbar); 294 else 295 scrollAnimator()->willRemoveHorizontalScrollbar(scrollbar); 296} 297 298void ScrollableArea::contentsResized() 299{ 300 if (ScrollAnimator* scrollAnimator = existingScrollAnimator()) 301 scrollAnimator->contentsResized(); 302} 303 304bool ScrollableArea::hasOverlayScrollbars() const 305{ 306 return (verticalScrollbar() && verticalScrollbar()->isOverlayScrollbar()) 307 || (horizontalScrollbar() && horizontalScrollbar()->isOverlayScrollbar()); 308} 309 310void ScrollableArea::setScrollbarOverlayStyle(ScrollbarOverlayStyle overlayStyle) 311{ 312 m_scrollbarOverlayStyle = overlayStyle; 313 314 if (horizontalScrollbar()) { 315 ScrollbarTheme::theme()->updateScrollbarOverlayStyle(horizontalScrollbar()); 316 horizontalScrollbar()->invalidate(); 317 } 318 319 if (verticalScrollbar()) { 320 ScrollbarTheme::theme()->updateScrollbarOverlayStyle(verticalScrollbar()); 321 verticalScrollbar()->invalidate(); 322 } 323} 324 325void ScrollableArea::invalidateScrollbar(Scrollbar* scrollbar, const IntRect& rect) 326{ 327 if (scrollbar == horizontalScrollbar()) { 328 if (GraphicsLayer* graphicsLayer = layerForHorizontalScrollbar()) { 329 graphicsLayer->setNeedsDisplay(); 330 graphicsLayer->setContentsNeedsDisplay(); 331 return; 332 } 333 } else if (scrollbar == verticalScrollbar()) { 334 if (GraphicsLayer* graphicsLayer = layerForVerticalScrollbar()) { 335 graphicsLayer->setNeedsDisplay(); 336 graphicsLayer->setContentsNeedsDisplay(); 337 return; 338 } 339 } 340 341 invalidateScrollbarRect(scrollbar, rect); 342} 343 344void ScrollableArea::invalidateScrollCorner(const IntRect& rect) 345{ 346 if (GraphicsLayer* graphicsLayer = layerForScrollCorner()) { 347 graphicsLayer->setNeedsDisplay(); 348 return; 349 } 350 351 invalidateScrollCornerRect(rect); 352} 353 354void ScrollableArea::verticalScrollbarLayerDidChange() 355{ 356 scrollAnimator()->verticalScrollbarLayerDidChange(); 357} 358 359void ScrollableArea::horizontalScrollbarLayerDidChange() 360{ 361 scrollAnimator()->horizontalScrollbarLayerDidChange(); 362} 363 364bool ScrollableArea::hasLayerForHorizontalScrollbar() const 365{ 366 return layerForHorizontalScrollbar(); 367} 368 369bool ScrollableArea::hasLayerForVerticalScrollbar() const 370{ 371 return layerForVerticalScrollbar(); 372} 373 374bool ScrollableArea::hasLayerForScrollCorner() const 375{ 376 return layerForScrollCorner(); 377} 378 379void ScrollableArea::serviceScrollAnimations() 380{ 381 if (ScrollAnimator* scrollAnimator = existingScrollAnimator()) 382 scrollAnimator->serviceScrollAnimations(); 383} 384 385#if PLATFORM(IOS) 386bool ScrollableArea::isPinnedInBothDirections(const IntSize& scrollDelta) const 387{ 388 return isPinnedHorizontallyInDirection(scrollDelta.width()) && isPinnedVerticallyInDirection(scrollDelta.height()); 389} 390 391bool ScrollableArea::isPinnedHorizontallyInDirection(int horizontalScrollDelta) const 392{ 393 if (horizontalScrollDelta < 0 && isHorizontalScrollerPinnedToMinimumPosition()) 394 return true; 395 if (horizontalScrollDelta > 0 && isHorizontalScrollerPinnedToMaximumPosition()) 396 return true; 397 return false; 398} 399 400bool ScrollableArea::isPinnedVerticallyInDirection(int verticalScrollDelta) const 401{ 402 if (verticalScrollDelta < 0 && isVerticalScrollerPinnedToMinimumPosition()) 403 return true; 404 if (verticalScrollDelta > 0 && isVerticalScrollerPinnedToMaximumPosition()) 405 return true; 406 return false; 407} 408#endif // PLATFORM(IOS) 409 410IntPoint ScrollableArea::scrollPosition() const 411{ 412 int x = horizontalScrollbar() ? horizontalScrollbar()->value() : 0; 413 int y = verticalScrollbar() ? verticalScrollbar()->value() : 0; 414 return IntPoint(x, y); 415} 416 417IntPoint ScrollableArea::minimumScrollPosition() const 418{ 419 return IntPoint(); 420} 421 422IntPoint ScrollableArea::maximumScrollPosition() const 423{ 424 return IntPoint(totalContentsSize().width() - visibleWidth(), totalContentsSize().height() - visibleHeight()); 425} 426 427bool ScrollableArea::scrolledToTop() const 428{ 429 return scrollPosition().y() <= minimumScrollPosition().y(); 430} 431 432bool ScrollableArea::scrolledToBottom() const 433{ 434 return scrollPosition().y() >= maximumScrollPosition().y(); 435} 436 437bool ScrollableArea::scrolledToLeft() const 438{ 439 return scrollPosition().x() <= minimumScrollPosition().x(); 440} 441 442bool ScrollableArea::scrolledToRight() const 443{ 444 return scrollPosition().x() >= maximumScrollPosition().x(); 445} 446 447IntSize ScrollableArea::totalContentsSize() const 448{ 449 IntSize totalContentsSize = contentsSize(); 450 totalContentsSize.setHeight(totalContentsSize.height() + headerHeight() + footerHeight()); 451 return totalContentsSize; 452} 453 454IntRect ScrollableArea::visibleContentRect(VisibleContentRectBehavior visibleContentRectBehavior) const 455{ 456 return visibleContentRectInternal(ExcludeScrollbars, visibleContentRectBehavior); 457} 458 459IntRect ScrollableArea::visibleContentRectIncludingScrollbars(VisibleContentRectBehavior visibleContentRectBehavior) const 460{ 461 return visibleContentRectInternal(IncludeScrollbars, visibleContentRectBehavior); 462} 463 464IntRect ScrollableArea::visibleContentRectInternal(VisibleContentRectIncludesScrollbars scrollbarInclusion, VisibleContentRectBehavior) const 465{ 466 int verticalScrollbarWidth = 0; 467 int horizontalScrollbarHeight = 0; 468 469 if (scrollbarInclusion == IncludeScrollbars) { 470 if (Scrollbar* verticalBar = verticalScrollbar()) 471 verticalScrollbarWidth = !verticalBar->isOverlayScrollbar() ? verticalBar->width() : 0; 472 if (Scrollbar* horizontalBar = horizontalScrollbar()) 473 horizontalScrollbarHeight = !horizontalBar->isOverlayScrollbar() ? horizontalBar->height() : 0; 474 } 475 476 return IntRect(scrollPosition().x(), 477 scrollPosition().y(), 478 std::max(0, visibleWidth() + verticalScrollbarWidth), 479 std::max(0, visibleHeight() + horizontalScrollbarHeight)); 480} 481 482LayoutPoint ScrollableArea::constrainScrollPositionForOverhang(const LayoutRect& visibleContentRect, const LayoutSize& totalContentsSize, const LayoutPoint& scrollPosition, const LayoutPoint& scrollOrigin, int headerHeight, int footerHeight) 483{ 484 // The viewport rect that we're scrolling shouldn't be larger than our document. 485 LayoutSize idealScrollRectSize(std::min(visibleContentRect.width(), totalContentsSize.width()), std::min(visibleContentRect.height(), totalContentsSize.height())); 486 487 LayoutRect scrollRect(scrollPosition + scrollOrigin - LayoutSize(0, headerHeight), idealScrollRectSize); 488 LayoutRect documentRect(LayoutPoint(), LayoutSize(totalContentsSize.width(), totalContentsSize.height() - headerHeight - footerHeight)); 489 490 // Use intersection to constrain our ideal scroll rect by the document rect. 491 scrollRect.intersect(documentRect); 492 493 if (scrollRect.size() != idealScrollRectSize) { 494 // If the rect was clipped, restore its size, effectively pushing it "down" from the top left. 495 scrollRect.setSize(idealScrollRectSize); 496 497 // If we still clip, push our rect "up" from the bottom right. 498 scrollRect.intersect(documentRect); 499 if (scrollRect.width() < idealScrollRectSize.width()) 500 scrollRect.move(-(idealScrollRectSize.width() - scrollRect.width()), 0); 501 if (scrollRect.height() < idealScrollRectSize.height()) 502 scrollRect.move(0, -(idealScrollRectSize.height() - scrollRect.height())); 503 } 504 505 return scrollRect.location() - toLayoutSize(scrollOrigin); 506} 507 508LayoutPoint ScrollableArea::constrainScrollPositionForOverhang(const LayoutPoint& scrollPosition) 509{ 510 return constrainScrollPositionForOverhang(visibleContentRect(), totalContentsSize(), scrollPosition, scrollOrigin(), headerHeight(), footerHeight()); 511} 512 513void ScrollableArea::computeScrollbarValueAndOverhang(float currentPosition, float totalSize, float visibleSize, float& doubleValue, float& overhangAmount) 514{ 515 doubleValue = 0; 516 overhangAmount = 0; 517 float maximum = totalSize - visibleSize; 518 519 if (currentPosition < 0) { 520 // Scrolled past the top. 521 doubleValue = 0; 522 overhangAmount = -currentPosition; 523 } else if (visibleSize + currentPosition > totalSize) { 524 // Scrolled past the bottom. 525 doubleValue = 1; 526 overhangAmount = currentPosition + visibleSize - totalSize; 527 } else { 528 // Within the bounds of the scrollable area. 529 if (maximum > 0) 530 doubleValue = currentPosition / maximum; 531 else 532 doubleValue = 0; 533 } 534} 535 536} // namespace WebCore 537