1/*
2 * Copyright (C) 2013 Google 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 are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31#include "config.h"
32#include "platform/scroll/ScrollbarThemeMacNonOverlayAPI.h"
33
34#include "platform/graphics/GraphicsContext.h"
35#include "platform/graphics/ImageBuffer.h"
36#include "platform/scroll/ScrollbarThemeClient.h"
37#include "public/platform/Platform.h"
38#include "public/platform/WebRect.h"
39#include "public/platform/WebThemeEngine.h"
40#include "skia/ext/skia_utils_mac.h"
41#include <Carbon/Carbon.h>
42
43namespace WebCore {
44
45// FIXME: Get these numbers from CoreUI.
46static int cRealButtonLength[] = { 28, 21 };
47static int cButtonHitInset[] = { 3, 2 };
48// cRealButtonLength - cButtonInset
49static int cButtonLength[] = { 14, 10 };
50static int cScrollbarThickness[] = { 15, 11 };
51static int cButtonInset[] = { 14, 11 };
52static int cThumbMinLength[] = { 26, 20 };
53
54static int cOuterButtonLength[] = { 16, 14 }; // The outer button in a double button pair is a bit bigger.
55static int cOuterButtonOverlap = 2;
56
57static ScrollbarButtonsPlacement gButtonPlacement = ScrollbarButtonsDoubleEnd;
58
59void ScrollbarThemeMacNonOverlayAPI::updateButtonPlacement()
60{
61    NSString *buttonPlacement = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleScrollBarVariant"];
62    if ([buttonPlacement isEqualToString:@"Single"])
63        gButtonPlacement = ScrollbarButtonsSingle;
64    else if ([buttonPlacement isEqualToString:@"DoubleMin"])
65        gButtonPlacement = ScrollbarButtonsDoubleStart;
66    else if ([buttonPlacement isEqualToString:@"DoubleBoth"])
67        gButtonPlacement = ScrollbarButtonsDoubleBoth;
68    else {
69        gButtonPlacement = ScrollbarButtonsDoubleEnd;
70    }
71}
72
73static blink::WebThemeEngine::State scrollbarStateToThemeState(ScrollbarThemeClient* scrollbar)
74{
75    if (!scrollbar->enabled())
76        return blink::WebThemeEngine::StateDisabled;
77    if (!scrollbar->isScrollableAreaActive())
78        return blink::WebThemeEngine::StateInactive;
79    if (scrollbar->pressedPart() == ThumbPart)
80        return blink::WebThemeEngine::StatePressed;
81
82    return blink::WebThemeEngine::StateActive;
83}
84
85// Override ScrollbarThemeMacCommon::paint() to add support for the following:
86//     - drawing using WebThemeEngine functions
87//     - drawing tickmarks
88//     - Skia specific changes
89bool ScrollbarThemeMacNonOverlayAPI::paint(ScrollbarThemeClient* scrollbar, GraphicsContext* context, const IntRect& damageRect)
90{
91    if (context->paintingDisabled())
92        return true;
93    // Get the tickmarks for the frameview.
94    Vector<IntRect> tickmarks;
95    scrollbar->getTickmarks(tickmarks);
96
97    HIThemeTrackDrawInfo trackInfo;
98    trackInfo.version = 0;
99    trackInfo.kind = scrollbar->controlSize() == RegularScrollbar ? kThemeMediumScrollBar : kThemeSmallScrollBar;
100    trackInfo.bounds = scrollbar->frameRect();
101    trackInfo.min = 0;
102    trackInfo.max = scrollbar->maximum();
103    trackInfo.value = scrollbar->currentPos();
104    trackInfo.trackInfo.scrollbar.viewsize = scrollbar->visibleSize();
105    trackInfo.attributes = 0;
106    if (scrollbar->orientation() == HorizontalScrollbar)
107        trackInfo.attributes |= kThemeTrackHorizontal;
108
109    if (!scrollbar->enabled())
110        trackInfo.enableState = kThemeTrackDisabled;
111    else
112        trackInfo.enableState = scrollbar->isScrollableAreaActive() ? kThemeTrackActive : kThemeTrackInactive;
113
114    if (!hasButtons(scrollbar))
115        trackInfo.enableState = kThemeTrackNothingToScroll;
116    trackInfo.trackInfo.scrollbar.pressState = scrollbarPartToHIPressedState(scrollbar->pressedPart());
117
118    SkCanvas* canvas = context->canvas();
119    CGAffineTransform currentCTM = gfx::SkMatrixToCGAffineTransform(canvas->getTotalMatrix());
120
121    // The Aqua scrollbar is buggy when rotated and scaled.  We will just draw into a bitmap if we detect a scale or rotation.
122    bool canDrawDirectly = currentCTM.a == 1.0f && currentCTM.b == 0.0f && currentCTM.c == 0.0f && (currentCTM.d == 1.0f || currentCTM.d == -1.0f);
123    GraphicsContext* drawingContext = context;
124    OwnPtr<ImageBuffer> imageBuffer;
125    if (!canDrawDirectly) {
126        trackInfo.bounds = IntRect(IntPoint(), scrollbar->frameRect().size());
127
128        IntRect bufferRect(scrollbar->frameRect());
129        bufferRect.intersect(damageRect);
130        bufferRect.move(-scrollbar->frameRect().x(), -scrollbar->frameRect().y());
131
132        imageBuffer = ImageBuffer::create(bufferRect.size());
133        if (!imageBuffer)
134            return true;
135
136        drawingContext = imageBuffer->context();
137    }
138
139    // Draw thumbless.
140    gfx::SkiaBitLocker bitLocker(drawingContext->canvas());
141    CGContextRef cgContext = bitLocker.cgContext();
142    HIThemeDrawTrack(&trackInfo, 0, cgContext, kHIThemeOrientationNormal);
143
144    IntRect tickmarkTrackRect = trackRect(scrollbar, false);
145    if (!canDrawDirectly) {
146        tickmarkTrackRect.setX(0);
147        tickmarkTrackRect.setY(0);
148    }
149    // The ends are rounded and the thumb doesn't go there.
150    tickmarkTrackRect.inflateY(-tickmarkTrackRect.width());
151    // Inset a bit.
152    tickmarkTrackRect.setX(tickmarkTrackRect.x() + 2);
153    tickmarkTrackRect.setWidth(tickmarkTrackRect.width() - 5);
154    paintGivenTickmarks(drawingContext, scrollbar, tickmarkTrackRect, tickmarks);
155
156    if (hasThumb(scrollbar)) {
157        blink::WebThemeEngine::ScrollbarInfo scrollbarInfo;
158        scrollbarInfo.orientation = scrollbar->orientation() == HorizontalScrollbar ? blink::WebThemeEngine::ScrollbarOrientationHorizontal : blink::WebThemeEngine::ScrollbarOrientationVertical;
159        scrollbarInfo.parent = scrollbar->isScrollViewScrollbar() ? blink::WebThemeEngine::ScrollbarParentScrollView : blink::WebThemeEngine::ScrollbarParentRenderLayer;
160        scrollbarInfo.maxValue = scrollbar->maximum();
161        scrollbarInfo.currentValue = scrollbar->currentPos();
162        scrollbarInfo.visibleSize = scrollbar->visibleSize();
163        scrollbarInfo.totalSize = scrollbar->totalSize();
164
165        blink::WebCanvas* webCanvas = drawingContext->canvas();
166        blink::Platform::current()->themeEngine()->paintScrollbarThumb(
167            webCanvas,
168            scrollbarStateToThemeState(scrollbar),
169            scrollbar->controlSize() == RegularScrollbar ? blink::WebThemeEngine::SizeRegular : blink::WebThemeEngine::SizeSmall,
170            blink::WebRect(scrollbar->frameRect()),
171            scrollbarInfo);
172    }
173
174    if (!canDrawDirectly) {
175        ASSERT(imageBuffer);
176        context->drawImageBuffer(imageBuffer.get(),
177            FloatRect(scrollbar->frameRect().location(), imageBuffer->size()));
178    }
179
180    return true;
181}
182
183int ScrollbarThemeMacNonOverlayAPI::scrollbarThickness(ScrollbarControlSize controlSize)
184{
185    return cScrollbarThickness[controlSize];
186}
187
188ScrollbarButtonsPlacement ScrollbarThemeMacNonOverlayAPI::buttonsPlacement() const
189{
190    return gButtonPlacement;
191}
192
193bool ScrollbarThemeMacNonOverlayAPI::hasButtons(ScrollbarThemeClient* scrollbar)
194{
195    return scrollbar->enabled() && buttonsPlacement() != ScrollbarButtonsNone
196             && (scrollbar->orientation() == HorizontalScrollbar
197             ? scrollbar->width()
198             : scrollbar->height()) >= 2 * (cRealButtonLength[scrollbar->controlSize()] - cButtonHitInset[scrollbar->controlSize()]);
199}
200
201bool ScrollbarThemeMacNonOverlayAPI::hasThumb(ScrollbarThemeClient* scrollbar)
202{
203    int minLengthForThumb = 2 * cButtonInset[scrollbar->controlSize()] + cThumbMinLength[scrollbar->controlSize()] + 1;
204    return scrollbar->enabled() && (scrollbar->orientation() == HorizontalScrollbar ?
205             scrollbar->width() :
206             scrollbar->height()) >= minLengthForThumb;
207}
208
209static IntRect buttonRepaintRect(const IntRect& buttonRect, ScrollbarOrientation orientation, ScrollbarControlSize controlSize, bool start)
210{
211    ASSERT(gButtonPlacement != ScrollbarButtonsNone);
212
213    IntRect paintRect(buttonRect);
214    if (orientation == HorizontalScrollbar) {
215        paintRect.setWidth(cRealButtonLength[controlSize]);
216        if (!start)
217            paintRect.setX(buttonRect.x() - (cRealButtonLength[controlSize] - buttonRect.width()));
218    } else {
219        paintRect.setHeight(cRealButtonLength[controlSize]);
220        if (!start)
221            paintRect.setY(buttonRect.y() - (cRealButtonLength[controlSize] - buttonRect.height()));
222    }
223
224    return paintRect;
225}
226
227IntRect ScrollbarThemeMacNonOverlayAPI::backButtonRect(ScrollbarThemeClient* scrollbar, ScrollbarPart part, bool painting)
228{
229    IntRect result;
230
231    if (part == BackButtonStartPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleEnd))
232        return result;
233
234    if (part == BackButtonEndPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleStart || buttonsPlacement() == ScrollbarButtonsSingle))
235        return result;
236
237    int thickness = scrollbarThickness(scrollbar->controlSize());
238    bool outerButton = part == BackButtonStartPart && (buttonsPlacement() == ScrollbarButtonsDoubleStart || buttonsPlacement() == ScrollbarButtonsDoubleBoth);
239    if (outerButton) {
240        if (scrollbar->orientation() == HorizontalScrollbar)
241            result = IntRect(scrollbar->x(), scrollbar->y(), cOuterButtonLength[scrollbar->controlSize()] + (painting ? cOuterButtonOverlap : 0), thickness);
242        else
243            result = IntRect(scrollbar->x(), scrollbar->y(), thickness, cOuterButtonLength[scrollbar->controlSize()] + (painting ? cOuterButtonOverlap : 0));
244        return result;
245    }
246
247    // Our repaint rect is slightly larger, since we are a button that is adjacent to the track.
248    if (scrollbar->orientation() == HorizontalScrollbar) {
249        int start = part == BackButtonStartPart ? scrollbar->x() : scrollbar->x() + scrollbar->width() - cOuterButtonLength[scrollbar->controlSize()] - cButtonLength[scrollbar->controlSize()];
250        result = IntRect(start, scrollbar->y(), cButtonLength[scrollbar->controlSize()], thickness);
251    } else {
252        int start = part == BackButtonStartPart ? scrollbar->y() : scrollbar->y() + scrollbar->height() - cOuterButtonLength[scrollbar->controlSize()] - cButtonLength[scrollbar->controlSize()];
253        result = IntRect(scrollbar->x(), start, thickness, cButtonLength[scrollbar->controlSize()]);
254    }
255
256    if (painting)
257        return buttonRepaintRect(result, scrollbar->orientation(), scrollbar->controlSize(), part == BackButtonStartPart);
258    return result;
259}
260
261IntRect ScrollbarThemeMacNonOverlayAPI::forwardButtonRect(ScrollbarThemeClient* scrollbar, ScrollbarPart part, bool painting)
262{
263    IntRect result;
264
265    if (part == ForwardButtonEndPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleStart))
266        return result;
267
268    if (part == ForwardButtonStartPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleEnd || buttonsPlacement() == ScrollbarButtonsSingle))
269        return result;
270
271    int thickness = scrollbarThickness(scrollbar->controlSize());
272    int outerButtonLength = cOuterButtonLength[scrollbar->controlSize()];
273    int buttonLength = cButtonLength[scrollbar->controlSize()];
274
275    bool outerButton = part == ForwardButtonEndPart && (buttonsPlacement() == ScrollbarButtonsDoubleEnd || buttonsPlacement() == ScrollbarButtonsDoubleBoth);
276    if (outerButton) {
277        if (scrollbar->orientation() == HorizontalScrollbar) {
278            result = IntRect(scrollbar->x() + scrollbar->width() - outerButtonLength, scrollbar->y(), outerButtonLength, thickness);
279            if (painting)
280                result.inflateX(cOuterButtonOverlap);
281        } else {
282            result = IntRect(scrollbar->x(), scrollbar->y() + scrollbar->height() - outerButtonLength, thickness, outerButtonLength);
283            if (painting)
284                result.inflateY(cOuterButtonOverlap);
285        }
286        return result;
287    }
288
289    if (scrollbar->orientation() == HorizontalScrollbar) {
290        int start = part == ForwardButtonEndPart ? scrollbar->x() + scrollbar->width() - buttonLength : scrollbar->x() + outerButtonLength;
291        result = IntRect(start, scrollbar->y(), buttonLength, thickness);
292    } else {
293        int start = part == ForwardButtonEndPart ? scrollbar->y() + scrollbar->height() - buttonLength : scrollbar->y() + outerButtonLength;
294        result = IntRect(scrollbar->x(), start, thickness, buttonLength);
295    }
296    if (painting)
297        return buttonRepaintRect(result, scrollbar->orientation(), scrollbar->controlSize(), part == ForwardButtonStartPart);
298    return result;
299}
300
301IntRect ScrollbarThemeMacNonOverlayAPI::trackRect(ScrollbarThemeClient* scrollbar, bool painting)
302{
303    if (painting || !hasButtons(scrollbar))
304        return scrollbar->frameRect();
305
306    IntRect result;
307    int thickness = scrollbarThickness(scrollbar->controlSize());
308    int startWidth = 0;
309    int endWidth = 0;
310    int outerButtonLength = cOuterButtonLength[scrollbar->controlSize()];
311    int buttonLength = cButtonLength[scrollbar->controlSize()];
312    int doubleButtonLength = outerButtonLength + buttonLength;
313    switch (buttonsPlacement()) {
314        case ScrollbarButtonsSingle:
315            startWidth = buttonLength;
316            endWidth = buttonLength;
317            break;
318        case ScrollbarButtonsDoubleStart:
319            startWidth = doubleButtonLength;
320            break;
321        case ScrollbarButtonsDoubleEnd:
322            endWidth = doubleButtonLength;
323            break;
324        case ScrollbarButtonsDoubleBoth:
325            startWidth = doubleButtonLength;
326            endWidth = doubleButtonLength;
327            break;
328        default:
329            break;
330    }
331
332    int totalWidth = startWidth + endWidth;
333    if (scrollbar->orientation() == HorizontalScrollbar)
334        return IntRect(scrollbar->x() + startWidth, scrollbar->y(), scrollbar->width() - totalWidth, thickness);
335    return IntRect(scrollbar->x(), scrollbar->y() + startWidth, thickness, scrollbar->height() - totalWidth);
336}
337
338int ScrollbarThemeMacNonOverlayAPI::minimumThumbLength(ScrollbarThemeClient* scrollbar)
339{
340    return cThumbMinLength[scrollbar->controlSize()];
341}
342
343}
344