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