1/*
2 * Copyright (c) 2010, Google Inc. All rights reserved.
3 * Copyright (C) 2008, 2011 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 "PlatformWheelEvent.h"
39#include "ScrollAnimator.h"
40#include "ScrollbarTheme.h"
41#include <wtf/PassOwnPtr.h>
42
43namespace WebCore {
44
45struct SameSizeAsScrollableArea {
46    virtual ~SameSizeAsScrollableArea();
47    void* pointer;
48    unsigned bitfields : 16;
49    IntPoint origin;
50};
51
52COMPILE_ASSERT(sizeof(ScrollableArea) == sizeof(SameSizeAsScrollableArea), ScrollableArea_should_stay_small);
53
54ScrollableArea::ScrollableArea()
55    : m_constrainsScrollingToContentEdge(true)
56    , m_inLiveResize(false)
57    , m_verticalScrollElasticity(ScrollElasticityNone)
58    , m_horizontalScrollElasticity(ScrollElasticityNone)
59    , m_scrollbarOverlayStyle(ScrollbarOverlayStyleDefault)
60    , m_scrollOriginChanged(false)
61{
62}
63
64ScrollableArea::~ScrollableArea()
65{
66}
67
68ScrollAnimator* ScrollableArea::scrollAnimator() const
69{
70    if (!m_scrollAnimator)
71        m_scrollAnimator = ScrollAnimator::create(const_cast<ScrollableArea*>(this));
72
73    return m_scrollAnimator.get();
74}
75
76void ScrollableArea::setScrollOrigin(const IntPoint& origin)
77{
78    if (m_scrollOrigin != origin) {
79        m_scrollOrigin = origin;
80        m_scrollOriginChanged = true;
81    }
82}
83
84bool ScrollableArea::scroll(ScrollDirection direction, ScrollGranularity granularity, float multiplier)
85{
86    ScrollbarOrientation orientation;
87    Scrollbar* scrollbar;
88    if (direction == ScrollUp || direction == ScrollDown) {
89        orientation = VerticalScrollbar;
90        scrollbar = verticalScrollbar();
91    } else {
92        orientation = HorizontalScrollbar;
93        scrollbar = horizontalScrollbar();
94    }
95
96    if (!scrollbar)
97        return false;
98
99    float step = 0;
100    switch (granularity) {
101    case ScrollByLine:
102        step = scrollbar->lineStep();
103        break;
104    case ScrollByPage:
105        step = scrollbar->pageStep();
106        break;
107    case ScrollByDocument:
108        step = scrollbar->totalSize();
109        break;
110    case ScrollByPixel:
111    case ScrollByPrecisePixel:
112        step = scrollbar->pixelStep();
113        break;
114    }
115
116    if (direction == ScrollUp || direction == ScrollLeft)
117        multiplier = -multiplier;
118
119    return scrollAnimator()->scroll(orientation, granularity, step, multiplier);
120}
121
122void ScrollableArea::scrollToOffsetWithoutAnimation(const FloatPoint& offset)
123{
124    scrollAnimator()->scrollToOffsetWithoutAnimation(offset);
125}
126
127void ScrollableArea::scrollToOffsetWithoutAnimation(ScrollbarOrientation orientation, float offset)
128{
129    if (orientation == HorizontalScrollbar)
130        scrollToOffsetWithoutAnimation(FloatPoint(offset, scrollAnimator()->currentPosition().y()));
131    else
132        scrollToOffsetWithoutAnimation(FloatPoint(scrollAnimator()->currentPosition().x(), offset));
133}
134
135void ScrollableArea::notifyScrollPositionChanged(const IntPoint& position)
136{
137    scrollPositionChanged(position);
138    scrollAnimator()->setCurrentPosition(position);
139}
140
141void ScrollableArea::scrollPositionChanged(const IntPoint& position)
142{
143    IntPoint oldPosition = scrollPosition();
144    // Tell the derived class to scroll its contents.
145    setScrollOffset(position);
146
147    Scrollbar* verticalScrollbar = this->verticalScrollbar();
148
149    // Tell the scrollbars to update their thumb postions.
150    if (Scrollbar* horizontalScrollbar = this->horizontalScrollbar()) {
151        horizontalScrollbar->offsetDidChange();
152        if (horizontalScrollbar->isOverlayScrollbar() && !hasLayerForHorizontalScrollbar()) {
153            if (!verticalScrollbar)
154                horizontalScrollbar->invalidate();
155            else {
156                // If there is both a horizontalScrollbar and a verticalScrollbar,
157                // then we must also invalidate the corner between them.
158                IntRect boundsAndCorner = horizontalScrollbar->boundsRect();
159                boundsAndCorner.setWidth(boundsAndCorner.width() + verticalScrollbar->width());
160                horizontalScrollbar->invalidateRect(boundsAndCorner);
161            }
162        }
163    }
164    if (verticalScrollbar) {
165        verticalScrollbar->offsetDidChange();
166        if (verticalScrollbar->isOverlayScrollbar() && !hasLayerForVerticalScrollbar())
167            verticalScrollbar->invalidate();
168    }
169
170    if (scrollPosition() != oldPosition)
171        scrollAnimator()->notifyContentAreaScrolled(scrollPosition() - oldPosition);
172}
173
174bool ScrollableArea::handleWheelEvent(const PlatformWheelEvent& wheelEvent)
175{
176    return scrollAnimator()->handleWheelEvent(wheelEvent);
177}
178
179// NOTE: Only called from Internals for testing.
180void ScrollableArea::setScrollOffsetFromInternals(const IntPoint& offset)
181{
182    setScrollOffsetFromAnimation(offset);
183}
184
185void ScrollableArea::setScrollOffsetFromAnimation(const IntPoint& offset)
186{
187    if (requestScrollPositionUpdate(offset))
188        return;
189
190    scrollPositionChanged(offset);
191}
192
193void ScrollableArea::willStartLiveResize()
194{
195    if (m_inLiveResize)
196        return;
197    m_inLiveResize = true;
198    if (ScrollAnimator* scrollAnimator = existingScrollAnimator())
199        scrollAnimator->willStartLiveResize();
200}
201
202void ScrollableArea::willEndLiveResize()
203{
204    if (!m_inLiveResize)
205        return;
206    m_inLiveResize = false;
207    if (ScrollAnimator* scrollAnimator = existingScrollAnimator())
208        scrollAnimator->willEndLiveResize();
209}
210
211void ScrollableArea::contentAreaWillPaint() const
212{
213    if (ScrollAnimator* scrollAnimator = existingScrollAnimator())
214        scrollAnimator->contentAreaWillPaint();
215}
216
217void ScrollableArea::mouseEnteredContentArea() const
218{
219    if (ScrollAnimator* scrollAnimator = existingScrollAnimator())
220        scrollAnimator->mouseEnteredContentArea();
221}
222
223void ScrollableArea::mouseExitedContentArea() const
224{
225    if (ScrollAnimator* scrollAnimator = existingScrollAnimator())
226        scrollAnimator->mouseEnteredContentArea();
227}
228
229void ScrollableArea::mouseMovedInContentArea() const
230{
231    if (ScrollAnimator* scrollAnimator = existingScrollAnimator())
232        scrollAnimator->mouseMovedInContentArea();
233}
234
235void ScrollableArea::mouseEnteredScrollbar(Scrollbar* scrollbar) const
236{
237    scrollAnimator()->mouseEnteredScrollbar(scrollbar);
238}
239
240void ScrollableArea::mouseExitedScrollbar(Scrollbar* scrollbar) const
241{
242    scrollAnimator()->mouseExitedScrollbar(scrollbar);
243}
244
245void ScrollableArea::contentAreaDidShow() const
246{
247    if (ScrollAnimator* scrollAnimator = existingScrollAnimator())
248        scrollAnimator->contentAreaDidShow();
249}
250
251void ScrollableArea::contentAreaDidHide() const
252{
253    if (ScrollAnimator* scrollAnimator = existingScrollAnimator())
254        scrollAnimator->contentAreaDidHide();
255}
256
257void ScrollableArea::finishCurrentScrollAnimations() const
258{
259    if (ScrollAnimator* scrollAnimator = existingScrollAnimator())
260        scrollAnimator->finishCurrentScrollAnimations();
261}
262
263void ScrollableArea::didAddScrollbar(Scrollbar* scrollbar, ScrollbarOrientation orientation)
264{
265    if (orientation == VerticalScrollbar)
266        scrollAnimator()->didAddVerticalScrollbar(scrollbar);
267    else
268        scrollAnimator()->didAddHorizontalScrollbar(scrollbar);
269
270    // <rdar://problem/9797253> AppKit resets the scrollbar's style when you attach a scrollbar
271    setScrollbarOverlayStyle(scrollbarOverlayStyle());
272}
273
274void ScrollableArea::willRemoveScrollbar(Scrollbar* scrollbar, ScrollbarOrientation orientation)
275{
276    if (orientation == VerticalScrollbar)
277        scrollAnimator()->willRemoveVerticalScrollbar(scrollbar);
278    else
279        scrollAnimator()->willRemoveHorizontalScrollbar(scrollbar);
280}
281
282void ScrollableArea::contentsResized()
283{
284    if (ScrollAnimator* scrollAnimator = existingScrollAnimator())
285        scrollAnimator->contentsResized();
286}
287
288bool ScrollableArea::hasOverlayScrollbars() const
289{
290    return (verticalScrollbar() && verticalScrollbar()->isOverlayScrollbar())
291        || (horizontalScrollbar() && horizontalScrollbar()->isOverlayScrollbar());
292}
293
294void ScrollableArea::setScrollbarOverlayStyle(ScrollbarOverlayStyle overlayStyle)
295{
296    m_scrollbarOverlayStyle = overlayStyle;
297
298    if (horizontalScrollbar()) {
299        ScrollbarTheme::theme()->updateScrollbarOverlayStyle(horizontalScrollbar());
300        horizontalScrollbar()->invalidate();
301    }
302
303    if (verticalScrollbar()) {
304        ScrollbarTheme::theme()->updateScrollbarOverlayStyle(verticalScrollbar());
305        verticalScrollbar()->invalidate();
306    }
307}
308
309void ScrollableArea::invalidateScrollbar(Scrollbar* scrollbar, const IntRect& rect)
310{
311#if USE(ACCELERATED_COMPOSITING)
312    if (scrollbar == horizontalScrollbar()) {
313        if (GraphicsLayer* graphicsLayer = layerForHorizontalScrollbar()) {
314            graphicsLayer->setNeedsDisplay();
315            graphicsLayer->setContentsNeedsDisplay();
316            return;
317        }
318    } else if (scrollbar == verticalScrollbar()) {
319        if (GraphicsLayer* graphicsLayer = layerForVerticalScrollbar()) {
320            graphicsLayer->setNeedsDisplay();
321            graphicsLayer->setContentsNeedsDisplay();
322            return;
323        }
324    }
325#endif
326    invalidateScrollbarRect(scrollbar, rect);
327}
328
329void ScrollableArea::invalidateScrollCorner(const IntRect& rect)
330{
331#if USE(ACCELERATED_COMPOSITING)
332    if (GraphicsLayer* graphicsLayer = layerForScrollCorner()) {
333        graphicsLayer->setNeedsDisplay();
334        return;
335    }
336#endif
337    invalidateScrollCornerRect(rect);
338}
339
340bool ScrollableArea::hasLayerForHorizontalScrollbar() const
341{
342#if USE(ACCELERATED_COMPOSITING)
343    return layerForHorizontalScrollbar();
344#else
345    return false;
346#endif
347}
348
349bool ScrollableArea::hasLayerForVerticalScrollbar() const
350{
351#if USE(ACCELERATED_COMPOSITING)
352    return layerForVerticalScrollbar();
353#else
354    return false;
355#endif
356}
357
358bool ScrollableArea::hasLayerForScrollCorner() const
359{
360#if USE(ACCELERATED_COMPOSITING)
361    return layerForScrollCorner();
362#else
363    return false;
364#endif
365}
366
367void ScrollableArea::serviceScrollAnimations()
368{
369    if (ScrollAnimator* scrollAnimator = existingScrollAnimator())
370        scrollAnimator->serviceScrollAnimations();
371}
372
373IntPoint ScrollableArea::scrollPosition() const
374{
375    int x = horizontalScrollbar() ? horizontalScrollbar()->value() : 0;
376    int y = verticalScrollbar() ? verticalScrollbar()->value() : 0;
377    return IntPoint(x, y);
378}
379
380IntPoint ScrollableArea::minimumScrollPosition() const
381{
382    return IntPoint();
383}
384
385IntPoint ScrollableArea::maximumScrollPosition() const
386{
387    return IntPoint(totalContentsSize().width() - visibleWidth(), totalContentsSize().height() - visibleHeight());
388}
389
390IntSize ScrollableArea::totalContentsSize() const
391{
392    IntSize totalContentsSize = contentsSize();
393    totalContentsSize.setHeight(totalContentsSize.height() + headerHeight() + footerHeight());
394    return totalContentsSize;
395}
396
397IntRect ScrollableArea::visibleContentRect(VisibleContentRectIncludesScrollbars scrollbarInclusion) const
398{
399    int verticalScrollbarWidth = 0;
400    int horizontalScrollbarHeight = 0;
401
402    if (scrollbarInclusion == IncludeScrollbars) {
403        if (Scrollbar* verticalBar = verticalScrollbar())
404            verticalScrollbarWidth = !verticalBar->isOverlayScrollbar() ? verticalBar->width() : 0;
405        if (Scrollbar* horizontalBar = horizontalScrollbar())
406            horizontalScrollbarHeight = !horizontalBar->isOverlayScrollbar() ? horizontalBar->height() : 0;
407    }
408
409    return IntRect(scrollPosition().x(),
410                   scrollPosition().y(),
411                   std::max(0, visibleWidth() + verticalScrollbarWidth),
412                   std::max(0, visibleHeight() + horizontalScrollbarHeight));
413}
414
415IntPoint ScrollableArea::constrainScrollPositionForOverhang(const IntRect& visibleContentRect, const IntSize& totalContentsSize, const IntPoint& scrollPosition, const IntPoint& scrollOrigin, int headerHeight, int footerHeight)
416{
417    // The viewport rect that we're scrolling shouldn't be larger than our document.
418    IntSize idealScrollRectSize(std::min(visibleContentRect.width(), totalContentsSize.width()), std::min(visibleContentRect.height(), totalContentsSize.height()));
419
420    IntRect scrollRect(scrollPosition + scrollOrigin - IntSize(0, headerHeight), idealScrollRectSize);
421    IntRect documentRect(IntPoint(), IntSize(totalContentsSize.width(), totalContentsSize.height() - headerHeight - footerHeight));
422
423    // Use intersection to constrain our ideal scroll rect by the document rect.
424    scrollRect.intersect(documentRect);
425
426    if (scrollRect.size() != idealScrollRectSize) {
427        // If the rect was clipped, restore its size, effectively pushing it "down" from the top left.
428        scrollRect.setSize(idealScrollRectSize);
429
430        // If we still clip, push our rect "up" from the bottom right.
431        scrollRect.intersect(documentRect);
432        if (scrollRect.width() < idealScrollRectSize.width())
433            scrollRect.move(-(idealScrollRectSize.width() - scrollRect.width()), 0);
434        if (scrollRect.height() < idealScrollRectSize.height())
435            scrollRect.move(0, -(idealScrollRectSize.height() - scrollRect.height()));
436    }
437
438    return scrollRect.location() - toIntSize(scrollOrigin);
439}
440
441IntPoint ScrollableArea::constrainScrollPositionForOverhang(const IntPoint& scrollPosition)
442{
443    return constrainScrollPositionForOverhang(visibleContentRect(), totalContentsSize(), scrollPosition, scrollOrigin(), headerHeight(), footerHeight());
444}
445
446} // namespace WebCore
447