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