1/*
2 * Copyright (C) 2008 Apple Inc. All Rights Reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "ScrollbarThemeComposite.h"
28
29#include "Chrome.h"
30#include "ChromeClient.h"
31#include "Frame.h"
32#include "FrameView.h"
33#include "GraphicsContext.h"
34#include "Page.h"
35#include "PlatformMouseEvent.h"
36#include "Scrollbar.h"
37#include "ScrollbarClient.h"
38#include "Settings.h"
39
40namespace WebCore {
41
42#if PLATFORM(WIN)
43static Page* pageForScrollView(ScrollView* view)
44{
45    if (!view)
46        return 0;
47    if (!view->isFrameView())
48        return 0;
49    FrameView* frameView = static_cast<FrameView*>(view);
50    if (!frameView->frame())
51        return 0;
52    return frameView->frame()->page();
53}
54#endif
55
56bool ScrollbarThemeComposite::paint(Scrollbar* scrollbar, GraphicsContext* graphicsContext, const IntRect& damageRect)
57{
58    // Create the ScrollbarControlPartMask based on the damageRect
59    ScrollbarControlPartMask scrollMask = NoPart;
60
61    IntRect backButtonStartPaintRect;
62    IntRect backButtonEndPaintRect;
63    IntRect forwardButtonStartPaintRect;
64    IntRect forwardButtonEndPaintRect;
65    if (hasButtons(scrollbar)) {
66        backButtonStartPaintRect = backButtonRect(scrollbar, BackButtonStartPart, true);
67        if (damageRect.intersects(backButtonStartPaintRect))
68            scrollMask |= BackButtonStartPart;
69        backButtonEndPaintRect = backButtonRect(scrollbar, BackButtonEndPart, true);
70        if (damageRect.intersects(backButtonEndPaintRect))
71            scrollMask |= BackButtonEndPart;
72        forwardButtonStartPaintRect = forwardButtonRect(scrollbar, ForwardButtonStartPart, true);
73        if (damageRect.intersects(forwardButtonStartPaintRect))
74            scrollMask |= ForwardButtonStartPart;
75        forwardButtonEndPaintRect = forwardButtonRect(scrollbar, ForwardButtonEndPart, true);
76        if (damageRect.intersects(forwardButtonEndPaintRect))
77            scrollMask |= ForwardButtonEndPart;
78    }
79
80    IntRect startTrackRect;
81    IntRect thumbRect;
82    IntRect endTrackRect;
83    IntRect trackPaintRect = trackRect(scrollbar, true);
84    if (damageRect.intersects(trackPaintRect))
85        scrollMask |= TrackBGPart;
86    bool thumbPresent = hasThumb(scrollbar);
87    if (thumbPresent) {
88        IntRect track = trackRect(scrollbar);
89        splitTrack(scrollbar, track, startTrackRect, thumbRect, endTrackRect);
90        if (damageRect.intersects(thumbRect))
91            scrollMask |= ThumbPart;
92        if (damageRect.intersects(startTrackRect))
93            scrollMask |= BackTrackPart;
94        if (damageRect.intersects(endTrackRect))
95            scrollMask |= ForwardTrackPart;
96    }
97
98#if PLATFORM(WIN)
99    // FIXME: This API makes the assumption that the custom scrollbar's metrics will match
100    // the theme's metrics.  This is not a valid assumption.  The ability for a client to paint
101    // custom scrollbars should be removed once scrollbars can be styled via CSS.
102    if (Page* page = pageForScrollView(scrollbar->parent())) {
103        if (page->settings()->shouldPaintCustomScrollbars()) {
104            float proportion = static_cast<float>(scrollbar->visibleSize()) / scrollbar->totalSize();
105            float value = scrollbar->currentPos() / static_cast<float>(scrollbar->maximum());
106            ScrollbarControlState s = 0;
107            if (scrollbar->client()->isActive())
108                s |= ActiveScrollbarState;
109            if (scrollbar->enabled())
110                s |= EnabledScrollbarState;
111            if (scrollbar->pressedPart() != NoPart)
112                s |= PressedScrollbarState;
113            if (page->chrome()->client()->paintCustomScrollbar(graphicsContext,
114                                                               scrollbar->frameRect(),
115                                                               scrollbar->controlSize(),
116                                                               s,
117                                                               scrollbar->pressedPart(),
118                                                               scrollbar->orientation() == VerticalScrollbar,
119                                                               value,
120                                                               proportion,
121                                                               scrollMask))
122                return true;
123        }
124    }
125#endif
126
127    // Paint the scrollbar background (only used by custom CSS scrollbars).
128    paintScrollbarBackground(graphicsContext, scrollbar);
129
130    // Paint the back and forward buttons.
131    if (scrollMask & BackButtonStartPart)
132        paintButton(graphicsContext, scrollbar, backButtonStartPaintRect, BackButtonStartPart);
133    if (scrollMask & BackButtonEndPart)
134        paintButton(graphicsContext, scrollbar, backButtonEndPaintRect, BackButtonEndPart);
135    if (scrollMask & ForwardButtonStartPart)
136        paintButton(graphicsContext, scrollbar, forwardButtonStartPaintRect, ForwardButtonStartPart);
137    if (scrollMask & ForwardButtonEndPart)
138        paintButton(graphicsContext, scrollbar, forwardButtonEndPaintRect, ForwardButtonEndPart);
139
140    if (scrollMask & TrackBGPart)
141        paintTrackBackground(graphicsContext, scrollbar, trackPaintRect);
142
143    if ((scrollMask & ForwardTrackPart) || (scrollMask & BackTrackPart)) {
144        // Paint the track pieces above and below the thumb.
145        if (scrollMask & BackTrackPart)
146            paintTrackPiece(graphicsContext, scrollbar, startTrackRect, BackTrackPart);
147        if (scrollMask & ForwardTrackPart)
148            paintTrackPiece(graphicsContext, scrollbar, endTrackRect, ForwardTrackPart);
149
150        paintTickmarks(graphicsContext, scrollbar, trackPaintRect);
151    }
152
153    // Paint the thumb.
154    if (scrollMask & ThumbPart)
155        paintThumb(graphicsContext, scrollbar, thumbRect);
156
157    return true;
158}
159
160ScrollbarPart ScrollbarThemeComposite::hitTest(Scrollbar* scrollbar, const PlatformMouseEvent& evt)
161{
162    ScrollbarPart result = NoPart;
163    if (!scrollbar->enabled())
164        return result;
165
166    IntPoint mousePosition = scrollbar->convertFromContainingWindow(evt.pos());
167    mousePosition.move(scrollbar->x(), scrollbar->y());
168
169    if (!scrollbar->frameRect().contains(mousePosition))
170        return NoPart;
171
172    result = ScrollbarBGPart;
173
174    IntRect track = trackRect(scrollbar);
175    if (track.contains(mousePosition)) {
176        IntRect beforeThumbRect;
177        IntRect thumbRect;
178        IntRect afterThumbRect;
179        splitTrack(scrollbar, track, beforeThumbRect, thumbRect, afterThumbRect);
180        if (thumbRect.contains(mousePosition))
181            result = ThumbPart;
182        else if (beforeThumbRect.contains(mousePosition))
183            result = BackTrackPart;
184        else if (afterThumbRect.contains(mousePosition))
185            result = ForwardTrackPart;
186        else
187            result = TrackBGPart;
188    } else if (backButtonRect(scrollbar, BackButtonStartPart).contains(mousePosition))
189        result = BackButtonStartPart;
190    else if (backButtonRect(scrollbar, BackButtonEndPart).contains(mousePosition))
191        result = BackButtonEndPart;
192    else if (forwardButtonRect(scrollbar, ForwardButtonStartPart).contains(mousePosition))
193        result = ForwardButtonStartPart;
194    else if (forwardButtonRect(scrollbar, ForwardButtonEndPart).contains(mousePosition))
195        result = ForwardButtonEndPart;
196    return result;
197}
198
199void ScrollbarThemeComposite::invalidatePart(Scrollbar* scrollbar, ScrollbarPart part)
200{
201    if (part == NoPart)
202        return;
203
204    IntRect result;
205    switch (part) {
206        case BackButtonStartPart:
207            result = backButtonRect(scrollbar, BackButtonStartPart, true);
208            break;
209        case BackButtonEndPart:
210            result = backButtonRect(scrollbar, BackButtonEndPart, true);
211            break;
212        case ForwardButtonStartPart:
213            result = forwardButtonRect(scrollbar, ForwardButtonStartPart, true);
214            break;
215        case ForwardButtonEndPart:
216            result = forwardButtonRect(scrollbar, ForwardButtonEndPart, true);
217            break;
218        case TrackBGPart:
219            result = trackRect(scrollbar, true);
220            break;
221        case ScrollbarBGPart:
222            result = scrollbar->frameRect();
223            break;
224        default: {
225            IntRect beforeThumbRect, thumbRect, afterThumbRect;
226            splitTrack(scrollbar, trackRect(scrollbar), beforeThumbRect, thumbRect, afterThumbRect);
227            if (part == BackTrackPart)
228                result = beforeThumbRect;
229            else if (part == ForwardTrackPart)
230                result = afterThumbRect;
231            else
232                result = thumbRect;
233        }
234    }
235    result.move(-scrollbar->x(), -scrollbar->y());
236    scrollbar->invalidateRect(result);
237}
238
239void ScrollbarThemeComposite::splitTrack(Scrollbar* scrollbar, const IntRect& unconstrainedTrackRect, IntRect& beforeThumbRect, IntRect& thumbRect, IntRect& afterThumbRect)
240{
241    // This function won't even get called unless we're big enough to have some combination of these three rects where at least
242    // one of them is non-empty.
243    IntRect trackRect = constrainTrackRectToTrackPieces(scrollbar, unconstrainedTrackRect);
244    int thickness = scrollbar->orientation() == HorizontalScrollbar ? scrollbar->height() : scrollbar->width();
245    int thumbPos = thumbPosition(scrollbar);
246    if (scrollbar->orientation() == HorizontalScrollbar) {
247        thumbRect = IntRect(trackRect.x() + thumbPos, trackRect.y() + (trackRect.height() - thickness) / 2, thumbLength(scrollbar), thickness);
248        beforeThumbRect = IntRect(trackRect.x(), trackRect.y(), thumbPos + thumbRect.width() / 2, trackRect.height());
249        afterThumbRect = IntRect(trackRect.x() + beforeThumbRect.width(), trackRect.y(), trackRect.right() - beforeThumbRect.right(), trackRect.height());
250    } else {
251        thumbRect = IntRect(trackRect.x() + (trackRect.width() - thickness) / 2, trackRect.y() + thumbPos, thickness, thumbLength(scrollbar));
252        beforeThumbRect = IntRect(trackRect.x(), trackRect.y(), trackRect.width(), thumbPos + thumbRect.height() / 2);
253        afterThumbRect = IntRect(trackRect.x(), trackRect.y() + beforeThumbRect.height(), trackRect.width(), trackRect.bottom() - beforeThumbRect.bottom());
254    }
255}
256
257int ScrollbarThemeComposite::thumbPosition(Scrollbar* scrollbar)
258{
259    if (scrollbar->enabled())
260        return scrollbar->currentPos() * (trackLength(scrollbar) - thumbLength(scrollbar)) / scrollbar->maximum();
261    return 0;
262}
263
264int ScrollbarThemeComposite::thumbLength(Scrollbar* scrollbar)
265{
266    if (!scrollbar->enabled())
267        return 0;
268
269    float proportion = (float)scrollbar->visibleSize() / scrollbar->totalSize();
270    int trackLen = trackLength(scrollbar);
271    int length = proportion * trackLen;
272    length = max(length, minimumThumbLength(scrollbar));
273    if (length > trackLen)
274        length = 0; // Once the thumb is below the track length, it just goes away (to make more room for the track).
275    return length;
276}
277
278int ScrollbarThemeComposite::minimumThumbLength(Scrollbar* scrollbar)
279{
280    return scrollbarThickness(scrollbar->controlSize());
281}
282
283int ScrollbarThemeComposite::trackPosition(Scrollbar* scrollbar)
284{
285    IntRect constrainedTrackRect = constrainTrackRectToTrackPieces(scrollbar, trackRect(scrollbar));
286    return (scrollbar->orientation() == HorizontalScrollbar) ? constrainedTrackRect.x() - scrollbar->x() : constrainedTrackRect.y() - scrollbar->y();
287}
288
289int ScrollbarThemeComposite::trackLength(Scrollbar* scrollbar)
290{
291    IntRect constrainedTrackRect = constrainTrackRectToTrackPieces(scrollbar, trackRect(scrollbar));
292    return (scrollbar->orientation() == HorizontalScrollbar) ? constrainedTrackRect.width() : constrainedTrackRect.height();
293}
294
295void ScrollbarThemeComposite::paintScrollCorner(ScrollView* view, GraphicsContext* context, const IntRect& cornerRect)
296{
297    FrameView* frameView = static_cast<FrameView*>(view);
298    Page* page = frameView->frame() ? frameView->frame()->page() : 0;
299    if (page && page->settings()->shouldPaintCustomScrollbars()) {
300        if (!page->chrome()->client()->paintCustomScrollCorner(context, cornerRect))
301            context->fillRect(cornerRect, Color::white, DeviceColorSpace);
302    }
303}
304
305}
306