Fix for <rdar://problem/8895140> Adopt WKScrollbar metrics
[WebKit-https.git] / Source / WebCore / platform / mac / ScrollbarThemeMac.mm
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 "ScrollbarThemeMac.h"
28
29 #include "ImageBuffer.h"
30 #include "LocalCurrentGraphicsContext.h"
31 #include "PlatformMouseEvent.h"
32 #include "ScrollView.h"
33 #include "WebCoreSystemInterface.h"
34 #include <Carbon/Carbon.h>
35 #include <wtf/HashMap.h>
36 #include <wtf/StdLibExtras.h>
37 #include <wtf/UnusedParam.h>
38
39 #if !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) && !defined(BUILDING_ON_SNOW_LEOPARD)
40 #define USE_WK_SCROLLBAR_PAINTER
41 #endif
42
43 // FIXME: There are repainting problems due to Aqua scroll bar buttons' visual overflow.
44
45 using namespace std;
46 using namespace WebCore;
47
48 namespace WebCore {
49
50 #if defined(USE_WK_SCROLLBAR_PAINTER)
51 typedef HashMap<Scrollbar*, RetainPtr<WKScrollbarPainterRef> > ScrollbarPainterMap;
52 #else
53 typedef HashSet<Scrollbar*> ScrollbarPainterMap;
54 #endif
55
56 static ScrollbarPainterMap* scrollbarMap()
57 {
58     static ScrollbarPainterMap* map = new ScrollbarPainterMap;
59     return map;
60 }
61     
62 }
63
64 @interface ScrollbarPrefsObserver : NSObject
65 {
66
67 }
68
69 + (void)registerAsObserver;
70 + (void)appearancePrefsChanged:(NSNotification*)theNotification;
71 + (void)behaviorPrefsChanged:(NSNotification*)theNotification;
72
73 @end
74
75 @implementation ScrollbarPrefsObserver
76
77 + (void)appearancePrefsChanged:(NSNotification*)unusedNotification
78 {
79     UNUSED_PARAM(unusedNotification);
80
81     static_cast<ScrollbarThemeMac*>(ScrollbarTheme::nativeTheme())->preferencesChanged();
82     if (scrollbarMap()->isEmpty())
83         return;
84     ScrollbarPainterMap::iterator end = scrollbarMap()->end();
85     for (ScrollbarPainterMap::iterator it = scrollbarMap()->begin(); it != end; ++it) {
86 #if defined(USE_WK_SCROLLBAR_PAINTER)
87         it->first->styleChanged();
88         it->first->invalidate();
89 #else
90         (*it)->styleChanged();
91         (*it)->invalidate();
92 #endif
93     }
94 }
95
96 + (void)behaviorPrefsChanged:(NSNotification*)unusedNotification
97 {
98     UNUSED_PARAM(unusedNotification);
99
100     static_cast<ScrollbarThemeMac*>(ScrollbarTheme::nativeTheme())->preferencesChanged();
101 }
102
103 + (void)registerAsObserver
104 {
105     [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(appearancePrefsChanged:) name:@"AppleAquaScrollBarVariantChanged" object:nil suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
106     [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(behaviorPrefsChanged:) name:@"AppleNoRedisplayAppearancePreferenceChanged" object:nil suspensionBehavior:NSNotificationSuspensionBehaviorCoalesce];
107 }
108
109 @end
110
111 namespace WebCore {
112
113 ScrollbarTheme* ScrollbarTheme::nativeTheme()
114 {
115     DEFINE_STATIC_LOCAL(ScrollbarThemeMac, theme, ());
116     return &theme;
117 }
118
119 // FIXME: Get these numbers from CoreUI.
120 static int cRealButtonLength[] = { 28, 21 };
121 static int cButtonHitInset[] = { 3, 2 };
122 // cRealButtonLength - cButtonInset
123 static int cButtonLength[] = { 14, 10 };
124 #if !defined(USE_WK_SCROLLBAR_PAINTER)
125 static int cScrollbarThickness[] = { 15, 11 };
126 static int cButtonInset[] = { 14, 11 };
127 static int cThumbMinLength[] = { 26, 20 };
128 #endif
129
130 static int cOuterButtonLength[] = { 16, 14 }; // The outer button in a double button pair is a bit bigger.
131 static int cOuterButtonOverlap = 2;
132
133 static float gInitialButtonDelay = 0.5f;
134 static float gAutoscrollButtonDelay = 0.05f;
135 static bool gJumpOnTrackClick = false;
136
137 #if defined(USE_WK_SCROLLBAR_PAINTER)
138 static ScrollbarButtonsPlacement gButtonPlacement = ScrollbarButtonsNone;
139 #else
140 static ScrollbarButtonsPlacement gButtonPlacement = ScrollbarButtonsDoubleEnd;
141 #endif
142
143 static void updateArrowPlacement()
144 {
145     NSString *buttonPlacement = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleScrollBarVariant"];
146     if ([buttonPlacement isEqualToString:@"Single"])
147         gButtonPlacement = ScrollbarButtonsSingle;
148     else if ([buttonPlacement isEqualToString:@"DoubleMin"])
149         gButtonPlacement = ScrollbarButtonsDoubleStart;
150     else if ([buttonPlacement isEqualToString:@"DoubleBoth"])
151         gButtonPlacement = ScrollbarButtonsDoubleBoth;
152     else {
153 #if defined(USE_WK_SCROLLBAR_PAINTER)
154         gButtonPlacement = ScrollbarButtonsNone;
155 #else
156         gButtonPlacement = ScrollbarButtonsDoubleEnd;
157 #endif
158     }
159 }
160
161 void ScrollbarThemeMac::registerScrollbar(Scrollbar* scrollbar)
162 {
163 #if defined(USE_WK_SCROLLBAR_PAINTER)
164     WKScrollbarPainterRef scrollbarPainter = wkMakeScrollbarPainter(scrollbar->controlSize(),
165         scrollbar->orientation() == HorizontalScrollbar);
166     scrollbarMap()->add(scrollbar, scrollbarPainter);
167 #else
168     scrollbarMap()->add(scrollbar);
169 #endif
170 }
171
172 void ScrollbarThemeMac::unregisterScrollbar(Scrollbar* scrollbar)
173 {
174     scrollbarMap()->remove(scrollbar);
175 }
176
177 ScrollbarThemeMac::ScrollbarThemeMac()
178 {
179     static bool initialized;
180     if (!initialized) {
181         initialized = true;
182         [ScrollbarPrefsObserver registerAsObserver];
183         preferencesChanged();
184     }
185 }
186
187 ScrollbarThemeMac::~ScrollbarThemeMac()
188 {
189 }
190
191 void ScrollbarThemeMac::preferencesChanged()
192 {
193     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
194     [defaults synchronize];
195     updateArrowPlacement();
196     gInitialButtonDelay = [defaults floatForKey:@"NSScrollerButtonDelay"];
197     gAutoscrollButtonDelay = [defaults floatForKey:@"NSScrollerButtonPeriod"];
198     gJumpOnTrackClick = [defaults boolForKey:@"AppleScrollerPagingBehavior"];
199 }
200
201 int ScrollbarThemeMac::scrollbarThickness(ScrollbarControlSize controlSize)
202 {
203 #if defined(USE_WK_SCROLLBAR_PAINTER)
204     return wkScrollbarThickness(controlSize);
205 #else
206     return cScrollbarThickness[controlSize];
207 #endif
208 }
209
210 bool ScrollbarThemeMac::usesOverlayScrollbars() const
211 {
212     // FIXME: This should be enabled when <rdar://problem/8492788> is resolved.
213     return false;
214 }
215
216 double ScrollbarThemeMac::initialAutoscrollTimerDelay()
217 {
218     return gInitialButtonDelay;
219 }
220
221 double ScrollbarThemeMac::autoscrollTimerDelay()
222 {
223     return gAutoscrollButtonDelay;
224 }
225     
226 ScrollbarButtonsPlacement ScrollbarThemeMac::buttonsPlacement() const
227 {
228     return gButtonPlacement;
229 }
230
231 bool ScrollbarThemeMac::hasButtons(Scrollbar* scrollbar)
232 {
233     return scrollbar->enabled() && gButtonPlacement != ScrollbarButtonsNone
234              && (scrollbar->orientation() == HorizontalScrollbar
235              ? scrollbar->width()
236              : scrollbar->height()) >= 2 * (cRealButtonLength[scrollbar->controlSize()] - cButtonHitInset[scrollbar->controlSize()]);
237 }
238
239 bool ScrollbarThemeMac::hasThumb(Scrollbar* scrollbar)
240 {
241     int minLengthForThumb;
242 #if defined(USE_WK_SCROLLBAR_PAINTER)
243     minLengthForThumb = wkScrollbarMinimumTotalLengthNeededForThumb(scrollbarMap()->get(scrollbar).get());
244 #else
245     minLengthForThumb = 2 * cButtonInset[scrollbar->controlSize()] + cThumbMinLength[scrollbar->controlSize()] + 1;
246 #endif
247     return scrollbar->enabled() && (scrollbar->orientation() == HorizontalScrollbar ? 
248              scrollbar->width() : 
249              scrollbar->height()) >= minLengthForThumb;
250 }
251
252 static IntRect buttonRepaintRect(const IntRect& buttonRect, ScrollbarOrientation orientation, ScrollbarControlSize controlSize, bool start)
253 {
254     ASSERT(gButtonPlacement != ScrollbarButtonsNone);
255
256     IntRect paintRect(buttonRect);
257     if (orientation == HorizontalScrollbar) {
258         paintRect.setWidth(cRealButtonLength[controlSize]);
259         if (!start)
260             paintRect.setX(buttonRect.x() - (cRealButtonLength[controlSize] - buttonRect.width()));
261     } else {
262         paintRect.setHeight(cRealButtonLength[controlSize]);
263         if (!start)
264             paintRect.setY(buttonRect.y() - (cRealButtonLength[controlSize] - buttonRect.height()));
265     }
266
267     return paintRect;
268 }
269
270 IntRect ScrollbarThemeMac::backButtonRect(Scrollbar* scrollbar, ScrollbarPart part, bool painting)
271 {
272     IntRect result;
273     
274     if (part == BackButtonStartPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleEnd))
275         return result;
276     
277     if (part == BackButtonEndPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleStart || buttonsPlacement() == ScrollbarButtonsSingle))
278         return result;
279         
280     int thickness = scrollbarThickness(scrollbar->controlSize());
281     bool outerButton = part == BackButtonStartPart && (buttonsPlacement() == ScrollbarButtonsDoubleStart || buttonsPlacement() == ScrollbarButtonsDoubleBoth);
282     if (outerButton) {
283         if (scrollbar->orientation() == HorizontalScrollbar)
284             result = IntRect(scrollbar->x(), scrollbar->y(), cOuterButtonLength[scrollbar->controlSize()] + painting ? cOuterButtonOverlap : 0, thickness);
285         else
286             result = IntRect(scrollbar->x(), scrollbar->y(), thickness, cOuterButtonLength[scrollbar->controlSize()] + painting ? cOuterButtonOverlap : 0);
287         return result;
288     }
289     
290     // Our repaint rect is slightly larger, since we are a button that is adjacent to the track.
291     if (scrollbar->orientation() == HorizontalScrollbar) {
292         int start = part == BackButtonStartPart ? scrollbar->x() : scrollbar->x() + scrollbar->width() - cOuterButtonLength[scrollbar->controlSize()] - cButtonLength[scrollbar->controlSize()];
293         result = IntRect(start, scrollbar->y(), cButtonLength[scrollbar->controlSize()], thickness);
294     } else {
295         int start = part == BackButtonStartPart ? scrollbar->y() : scrollbar->y() + scrollbar->height() - cOuterButtonLength[scrollbar->controlSize()] - cButtonLength[scrollbar->controlSize()];
296         result = IntRect(scrollbar->x(), start, thickness, cButtonLength[scrollbar->controlSize()]);
297     }
298     
299     if (painting)
300         return buttonRepaintRect(result, scrollbar->orientation(), scrollbar->controlSize(), part == BackButtonStartPart);
301     return result;
302 }
303
304 IntRect ScrollbarThemeMac::forwardButtonRect(Scrollbar* scrollbar, ScrollbarPart part, bool painting)
305 {
306     IntRect result;
307     
308     if (part == ForwardButtonEndPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleStart))
309         return result;
310     
311     if (part == ForwardButtonStartPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleEnd || buttonsPlacement() == ScrollbarButtonsSingle))
312         return result;
313         
314     int thickness = scrollbarThickness(scrollbar->controlSize());
315     int outerButtonLength = cOuterButtonLength[scrollbar->controlSize()];
316     int buttonLength = cButtonLength[scrollbar->controlSize()];
317     
318     bool outerButton = part == ForwardButtonEndPart && (buttonsPlacement() == ScrollbarButtonsDoubleEnd || buttonsPlacement() == ScrollbarButtonsDoubleBoth);
319     if (outerButton) {
320         if (scrollbar->orientation() == HorizontalScrollbar) {
321             result = IntRect(scrollbar->x() + scrollbar->width() - outerButtonLength, scrollbar->y(), outerButtonLength, thickness);
322             if (painting)
323                 result.inflateX(cOuterButtonOverlap);
324         } else {
325             result = IntRect(scrollbar->x(), scrollbar->y() + scrollbar->height() - outerButtonLength, thickness, outerButtonLength);
326             if (painting)
327                 result.inflateY(cOuterButtonOverlap);
328         }
329         return result;
330     }
331     
332     if (scrollbar->orientation() == HorizontalScrollbar) {
333         int start = part == ForwardButtonEndPart ? scrollbar->x() + scrollbar->width() - buttonLength : scrollbar->x() + outerButtonLength;
334         result = IntRect(start, scrollbar->y(), buttonLength, thickness);
335     } else {
336         int start = part == ForwardButtonEndPart ? scrollbar->y() + scrollbar->height() - buttonLength : scrollbar->y() + outerButtonLength;
337         result = IntRect(scrollbar->x(), start, thickness, buttonLength);
338     }
339     if (painting)
340         return buttonRepaintRect(result, scrollbar->orientation(), scrollbar->controlSize(), part == ForwardButtonStartPart);
341     return result;
342 }
343
344 IntRect ScrollbarThemeMac::trackRect(Scrollbar* scrollbar, bool painting)
345 {
346     if (painting || !hasButtons(scrollbar))
347         return scrollbar->frameRect();
348     
349     IntRect result;
350     int thickness = scrollbarThickness(scrollbar->controlSize());
351     int startWidth = 0;
352     int endWidth = 0;
353     int outerButtonLength = cOuterButtonLength[scrollbar->controlSize()];
354     int buttonLength = cButtonLength[scrollbar->controlSize()];
355     int doubleButtonLength = outerButtonLength + buttonLength;
356     switch (buttonsPlacement()) {
357         case ScrollbarButtonsSingle:
358             startWidth = buttonLength;
359             endWidth = buttonLength;
360             break;
361         case ScrollbarButtonsDoubleStart:
362             startWidth = doubleButtonLength;
363             break;
364         case ScrollbarButtonsDoubleEnd:
365             endWidth = doubleButtonLength;
366             break;
367         case ScrollbarButtonsDoubleBoth:
368             startWidth = doubleButtonLength;
369             endWidth = doubleButtonLength;
370             break;
371         default:
372             break;
373     }
374     
375     int totalWidth = startWidth + endWidth;
376     if (scrollbar->orientation() == HorizontalScrollbar)
377         return IntRect(scrollbar->x() + startWidth, scrollbar->y(), scrollbar->width() - totalWidth, thickness);
378     return IntRect(scrollbar->x(), scrollbar->y() + startWidth, thickness, scrollbar->height() - totalWidth);
379 }
380
381 int ScrollbarThemeMac::minimumThumbLength(Scrollbar* scrollbar)
382 {
383 #if defined(USE_WK_SCROLLBAR_PAINTER)
384     return wkScrollbarMinimumThumbLength(scrollbarMap()->get(scrollbar).get());
385 #else
386     return cThumbMinLength[scrollbar->controlSize()];
387 #endif
388 }
389
390 bool ScrollbarThemeMac::shouldCenterOnThumb(Scrollbar*, const PlatformMouseEvent& evt)
391 {
392     if (evt.button() != LeftButton)
393         return false;
394     if (gJumpOnTrackClick)
395         return !evt.altKey();
396     return evt.altKey();
397 }
398
399 static int scrollbarPartToHIPressedState(ScrollbarPart part)
400 {
401     switch (part) {
402         case BackButtonStartPart:
403             return kThemeTopOutsideArrowPressed;
404         case BackButtonEndPart:
405             return kThemeTopOutsideArrowPressed; // This does not make much sense.  For some reason the outside constant is required.
406         case ForwardButtonStartPart:
407             return kThemeTopInsideArrowPressed;
408         case ForwardButtonEndPart:
409             return kThemeBottomOutsideArrowPressed;
410         case ThumbPart:
411             return kThemeThumbPressed;
412         default:
413             return 0;
414     }
415 }
416
417 bool ScrollbarThemeMac::paint(Scrollbar* scrollbar, GraphicsContext* context, const IntRect& damageRect)
418 {
419 #if defined(USE_WK_SCROLLBAR_PAINTER)
420     context->save();
421     context->clip(damageRect);
422     context->translate(scrollbar->frameRect().x(), scrollbar->frameRect().y());
423     LocalCurrentGraphicsContext localContext(context);
424     wkScrollbarPainterPaint(scrollbarMap()->get(scrollbar).get(),
425                             scrollbar->enabled(),
426                             scrollbar->currentPos() / scrollbar->maximum(),
427                             static_cast<CGFloat>(scrollbar->visibleSize()) / scrollbar->totalSize(),
428                             scrollbar->frameRect());
429     context->restore();
430     return true;
431 #endif
432
433     HIThemeTrackDrawInfo trackInfo;
434     trackInfo.version = 0;
435     trackInfo.kind = scrollbar->controlSize() == RegularScrollbar ? kThemeMediumScrollBar : kThemeSmallScrollBar;
436     trackInfo.bounds = scrollbar->frameRect();
437     trackInfo.min = 0;
438     trackInfo.max = scrollbar->maximum();
439     trackInfo.value = scrollbar->currentPos();
440     trackInfo.trackInfo.scrollbar.viewsize = scrollbar->visibleSize();
441     trackInfo.attributes = 0;
442     if (scrollbar->orientation() == HorizontalScrollbar)
443         trackInfo.attributes |= kThemeTrackHorizontal;
444
445     if (!scrollbar->enabled())
446         trackInfo.enableState = kThemeTrackDisabled;
447     else
448         trackInfo.enableState = scrollbar->scrollableArea()->isActive() ? kThemeTrackActive : kThemeTrackInactive;
449
450     if (hasThumb(scrollbar))
451         trackInfo.attributes |= kThemeTrackShowThumb;
452     else if (!hasButtons(scrollbar))
453         trackInfo.enableState = kThemeTrackNothingToScroll;
454     trackInfo.trackInfo.scrollbar.pressState = scrollbarPartToHIPressedState(scrollbar->pressedPart());
455     
456     // The Aqua scrollbar is buggy when rotated and scaled.  We will just draw into a bitmap if we detect a scale or rotation.
457     const AffineTransform& currentCTM = context->getCTM();
458     bool canDrawDirectly = currentCTM.isIdentityOrTranslationOrFlipped();
459     if (canDrawDirectly)
460         HIThemeDrawTrack(&trackInfo, 0, context->platformContext(), kHIThemeOrientationNormal);
461     else {
462         trackInfo.bounds = IntRect(IntPoint(), scrollbar->frameRect().size());
463         
464         IntRect bufferRect(scrollbar->frameRect());
465         bufferRect.intersect(damageRect);
466         bufferRect.move(-scrollbar->frameRect().x(), -scrollbar->frameRect().y());
467         
468         OwnPtr<ImageBuffer> imageBuffer = ImageBuffer::create(bufferRect.size());
469         if (!imageBuffer)
470             return true;
471         
472         HIThemeDrawTrack(&trackInfo, 0, imageBuffer->context()->platformContext(), kHIThemeOrientationNormal);
473         context->drawImageBuffer(imageBuffer.get(), ColorSpaceDeviceRGB, scrollbar->frameRect().location());
474     }
475
476     return true;
477 }
478
479 }
480