2 * Copyright (C) 2008, 2011 Apple Inc. All Rights Reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
27 #include "ScrollbarThemeMac.h"
29 #include "ImageBuffer.h"
30 #include "LocalCurrentGraphicsContext.h"
31 #include "NSScrollerImpDetails.h"
32 #include "PlatformMouseEvent.h"
33 #include "ScrollAnimatorMac.h"
34 #include "ScrollView.h"
35 #include "WebCoreSystemInterface.h"
36 #include <Carbon/Carbon.h>
37 #include <wtf/HashMap.h>
38 #include <wtf/StdLibExtras.h>
39 #include <wtf/UnusedParam.h>
41 // FIXME: There are repainting problems due to Aqua scroll bar buttons' visual overflow.
44 using namespace WebCore;
48 #if USE(SCROLLBAR_PAINTER)
49 typedef HashMap<Scrollbar*, RetainPtr<ScrollbarPainter> > ScrollbarPainterMap;
51 typedef HashSet<Scrollbar*> ScrollbarPainterMap;
54 static ScrollbarPainterMap* scrollbarMap()
56 static ScrollbarPainterMap* map = new ScrollbarPainterMap;
62 @interface ScrollbarPrefsObserver : NSObject
66 + (void)registerAsObserver;
67 + (void)appearancePrefsChanged:(NSNotification*)theNotification;
68 + (void)behaviorPrefsChanged:(NSNotification*)theNotification;
72 @implementation ScrollbarPrefsObserver
74 + (void)appearancePrefsChanged:(NSNotification*)unusedNotification
76 UNUSED_PARAM(unusedNotification);
78 static_cast<ScrollbarThemeMac*>(ScrollbarTheme::nativeTheme())->preferencesChanged();
79 if (scrollbarMap()->isEmpty())
81 ScrollbarPainterMap::iterator end = scrollbarMap()->end();
82 for (ScrollbarPainterMap::iterator it = scrollbarMap()->begin(); it != end; ++it) {
83 #if USE(SCROLLBAR_PAINTER)
84 it->first->styleChanged();
85 it->first->invalidate();
87 (*it)->styleChanged();
93 + (void)behaviorPrefsChanged:(NSNotification*)unusedNotification
95 UNUSED_PARAM(unusedNotification);
97 static_cast<ScrollbarThemeMac*>(ScrollbarTheme::nativeTheme())->preferencesChanged();
100 + (void)registerAsObserver
102 [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(appearancePrefsChanged:) name:@"AppleAquaScrollBarVariantChanged" object:nil suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
103 [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(behaviorPrefsChanged:) name:@"AppleNoRedisplayAppearancePreferenceChanged" object:nil suspensionBehavior:NSNotificationSuspensionBehaviorCoalesce];
110 ScrollbarTheme* ScrollbarTheme::nativeTheme()
112 DEFINE_STATIC_LOCAL(ScrollbarThemeMac, theme, ());
116 // FIXME: Get these numbers from CoreUI.
117 static int cRealButtonLength[] = { 28, 21 };
118 static int cButtonHitInset[] = { 3, 2 };
119 // cRealButtonLength - cButtonInset
120 static int cButtonLength[] = { 14, 10 };
121 #if !USE(SCROLLBAR_PAINTER)
122 static int cScrollbarThickness[] = { 15, 11 };
123 static int cButtonInset[] = { 14, 11 };
124 static int cThumbMinLength[] = { 26, 20 };
127 static int cOuterButtonLength[] = { 16, 14 }; // The outer button in a double button pair is a bit bigger.
128 static int cOuterButtonOverlap = 2;
130 static float gInitialButtonDelay = 0.5f;
131 static float gAutoscrollButtonDelay = 0.05f;
132 static bool gJumpOnTrackClick = false;
134 #if USE(SCROLLBAR_PAINTER)
135 static ScrollbarButtonsPlacement gButtonPlacement = ScrollbarButtonsNone;
137 static ScrollbarButtonsPlacement gButtonPlacement = ScrollbarButtonsDoubleEnd;
140 static void updateArrowPlacement()
142 #if USE(SCROLLBAR_PAINTER)
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;
154 gButtonPlacement = ScrollbarButtonsDoubleEnd;
158 void ScrollbarThemeMac::registerScrollbar(Scrollbar* scrollbar)
160 #if USE(SCROLLBAR_PAINTER)
161 bool isHorizontal = scrollbar->orientation() == HorizontalScrollbar;
162 ScrollbarPainter scrollbarPainter = [NSClassFromString(@"NSScrollerImp") scrollerImpWithStyle:wkRecommendedScrollerStyle() controlSize:(NSControlSize)scrollbar->controlSize() horizontal:isHorizontal replacingScrollerImp:nil];
163 scrollbarMap()->add(scrollbar, scrollbarPainter);
164 updateEnabledState(scrollbar);
165 updateScrollbarOverlayStyle(scrollbar);
167 scrollbarMap()->add(scrollbar);
171 void ScrollbarThemeMac::unregisterScrollbar(Scrollbar* scrollbar)
173 scrollbarMap()->remove(scrollbar);
176 #if USE(SCROLLBAR_PAINTER)
177 void ScrollbarThemeMac::setNewPainterForScrollbar(Scrollbar* scrollbar, ScrollbarPainter newPainter)
179 scrollbarMap()->set(scrollbar, newPainter);
180 updateEnabledState(scrollbar);
181 updateScrollbarOverlayStyle(scrollbar);
184 ScrollbarPainter ScrollbarThemeMac::painterForScrollbar(Scrollbar* scrollbar)
186 return scrollbarMap()->get(scrollbar).get();
190 ScrollbarThemeMac::ScrollbarThemeMac()
192 static bool initialized;
195 [ScrollbarPrefsObserver registerAsObserver];
196 preferencesChanged();
200 ScrollbarThemeMac::~ScrollbarThemeMac()
204 void ScrollbarThemeMac::preferencesChanged()
206 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
207 [defaults synchronize];
208 updateArrowPlacement();
209 gInitialButtonDelay = [defaults floatForKey:@"NSScrollerButtonDelay"];
210 gAutoscrollButtonDelay = [defaults floatForKey:@"NSScrollerButtonPeriod"];
211 gJumpOnTrackClick = [defaults boolForKey:@"AppleScrollerPagingBehavior"];
214 int ScrollbarThemeMac::scrollbarThickness(ScrollbarControlSize controlSize)
216 #if USE(SCROLLBAR_PAINTER)
217 ScrollbarPainter scrollbarPainter = [NSClassFromString(@"NSScrollerImp") scrollerImpWithStyle:wkRecommendedScrollerStyle() controlSize:controlSize horizontal:NO replacingScrollerImp:nil];
218 return [scrollbarPainter trackBoxWidth];
220 return cScrollbarThickness[controlSize];
224 bool ScrollbarThemeMac::usesOverlayScrollbars() const
226 #if USE(SCROLLBAR_PAINTER)
227 return wkRecommendedScrollerStyle() == NSScrollerStyleOverlay;
233 void ScrollbarThemeMac::updateScrollbarOverlayStyle(Scrollbar* scrollbar)
235 #if USE(SCROLLBAR_PAINTER)
236 ScrollbarPainter painter = painterForScrollbar(scrollbar);
237 switch (scrollbar->scrollableArea()->scrollbarOverlayStyle()) {
238 case ScrollbarOverlayStyleDefault:
239 [painter setKnobStyle:NSScrollerKnobStyleDefault];
241 case ScrollbarOverlayStyleDark:
242 [painter setKnobStyle:NSScrollerKnobStyleDark];
244 case ScrollbarOverlayStyleLight:
245 [painter setKnobStyle:NSScrollerKnobStyleLight];
249 UNUSED_PARAM(scrollbar);
253 double ScrollbarThemeMac::initialAutoscrollTimerDelay()
255 return gInitialButtonDelay;
258 double ScrollbarThemeMac::autoscrollTimerDelay()
260 return gAutoscrollButtonDelay;
263 ScrollbarButtonsPlacement ScrollbarThemeMac::buttonsPlacement() const
265 return gButtonPlacement;
268 bool ScrollbarThemeMac::hasButtons(Scrollbar* scrollbar)
270 return scrollbar->enabled() && gButtonPlacement != ScrollbarButtonsNone
271 && (scrollbar->orientation() == HorizontalScrollbar
273 : scrollbar->height()) >= 2 * (cRealButtonLength[scrollbar->controlSize()] - cButtonHitInset[scrollbar->controlSize()]);
276 bool ScrollbarThemeMac::hasThumb(Scrollbar* scrollbar)
278 int minLengthForThumb;
279 #if USE(SCROLLBAR_PAINTER)
280 ScrollbarPainter painter = scrollbarMap()->get(scrollbar).get();
281 minLengthForThumb = [painter knobMinLength] + [painter trackOverlapEndInset] + [painter knobOverlapEndInset]
282 + 2 * ([painter trackEndInset] + [painter knobEndInset]);
284 minLengthForThumb = 2 * cButtonInset[scrollbar->controlSize()] + cThumbMinLength[scrollbar->controlSize()] + 1;
286 return scrollbar->enabled() && (scrollbar->orientation() == HorizontalScrollbar ?
288 scrollbar->height()) >= minLengthForThumb;
291 static IntRect buttonRepaintRect(const IntRect& buttonRect, ScrollbarOrientation orientation, ScrollbarControlSize controlSize, bool start)
293 ASSERT(gButtonPlacement != ScrollbarButtonsNone);
295 IntRect paintRect(buttonRect);
296 if (orientation == HorizontalScrollbar) {
297 paintRect.setWidth(cRealButtonLength[controlSize]);
299 paintRect.setX(buttonRect.x() - (cRealButtonLength[controlSize] - buttonRect.width()));
301 paintRect.setHeight(cRealButtonLength[controlSize]);
303 paintRect.setY(buttonRect.y() - (cRealButtonLength[controlSize] - buttonRect.height()));
309 IntRect ScrollbarThemeMac::backButtonRect(Scrollbar* scrollbar, ScrollbarPart part, bool painting)
313 if (part == BackButtonStartPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleEnd))
316 if (part == BackButtonEndPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleStart || buttonsPlacement() == ScrollbarButtonsSingle))
319 int thickness = scrollbarThickness(scrollbar->controlSize());
320 bool outerButton = part == BackButtonStartPart && (buttonsPlacement() == ScrollbarButtonsDoubleStart || buttonsPlacement() == ScrollbarButtonsDoubleBoth);
322 if (scrollbar->orientation() == HorizontalScrollbar)
323 result = IntRect(scrollbar->x(), scrollbar->y(), cOuterButtonLength[scrollbar->controlSize()] + (painting ? cOuterButtonOverlap : 0), thickness);
325 result = IntRect(scrollbar->x(), scrollbar->y(), thickness, cOuterButtonLength[scrollbar->controlSize()] + (painting ? cOuterButtonOverlap : 0));
329 // Our repaint rect is slightly larger, since we are a button that is adjacent to the track.
330 if (scrollbar->orientation() == HorizontalScrollbar) {
331 int start = part == BackButtonStartPart ? scrollbar->x() : scrollbar->x() + scrollbar->width() - cOuterButtonLength[scrollbar->controlSize()] - cButtonLength[scrollbar->controlSize()];
332 result = IntRect(start, scrollbar->y(), cButtonLength[scrollbar->controlSize()], thickness);
334 int start = part == BackButtonStartPart ? scrollbar->y() : scrollbar->y() + scrollbar->height() - cOuterButtonLength[scrollbar->controlSize()] - cButtonLength[scrollbar->controlSize()];
335 result = IntRect(scrollbar->x(), start, thickness, cButtonLength[scrollbar->controlSize()]);
339 return buttonRepaintRect(result, scrollbar->orientation(), scrollbar->controlSize(), part == BackButtonStartPart);
343 IntRect ScrollbarThemeMac::forwardButtonRect(Scrollbar* scrollbar, ScrollbarPart part, bool painting)
347 if (part == ForwardButtonEndPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleStart))
350 if (part == ForwardButtonStartPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleEnd || buttonsPlacement() == ScrollbarButtonsSingle))
353 int thickness = scrollbarThickness(scrollbar->controlSize());
354 int outerButtonLength = cOuterButtonLength[scrollbar->controlSize()];
355 int buttonLength = cButtonLength[scrollbar->controlSize()];
357 bool outerButton = part == ForwardButtonEndPart && (buttonsPlacement() == ScrollbarButtonsDoubleEnd || buttonsPlacement() == ScrollbarButtonsDoubleBoth);
359 if (scrollbar->orientation() == HorizontalScrollbar) {
360 result = IntRect(scrollbar->x() + scrollbar->width() - outerButtonLength, scrollbar->y(), outerButtonLength, thickness);
362 result.inflateX(cOuterButtonOverlap);
364 result = IntRect(scrollbar->x(), scrollbar->y() + scrollbar->height() - outerButtonLength, thickness, outerButtonLength);
366 result.inflateY(cOuterButtonOverlap);
371 if (scrollbar->orientation() == HorizontalScrollbar) {
372 int start = part == ForwardButtonEndPart ? scrollbar->x() + scrollbar->width() - buttonLength : scrollbar->x() + outerButtonLength;
373 result = IntRect(start, scrollbar->y(), buttonLength, thickness);
375 int start = part == ForwardButtonEndPart ? scrollbar->y() + scrollbar->height() - buttonLength : scrollbar->y() + outerButtonLength;
376 result = IntRect(scrollbar->x(), start, thickness, buttonLength);
379 return buttonRepaintRect(result, scrollbar->orientation(), scrollbar->controlSize(), part == ForwardButtonStartPart);
383 IntRect ScrollbarThemeMac::trackRect(Scrollbar* scrollbar, bool painting)
385 if (painting || !hasButtons(scrollbar))
386 return scrollbar->frameRect();
389 int thickness = scrollbarThickness(scrollbar->controlSize());
392 int outerButtonLength = cOuterButtonLength[scrollbar->controlSize()];
393 int buttonLength = cButtonLength[scrollbar->controlSize()];
394 int doubleButtonLength = outerButtonLength + buttonLength;
395 switch (buttonsPlacement()) {
396 case ScrollbarButtonsSingle:
397 startWidth = buttonLength;
398 endWidth = buttonLength;
400 case ScrollbarButtonsDoubleStart:
401 startWidth = doubleButtonLength;
403 case ScrollbarButtonsDoubleEnd:
404 endWidth = doubleButtonLength;
406 case ScrollbarButtonsDoubleBoth:
407 startWidth = doubleButtonLength;
408 endWidth = doubleButtonLength;
414 int totalWidth = startWidth + endWidth;
415 if (scrollbar->orientation() == HorizontalScrollbar)
416 return IntRect(scrollbar->x() + startWidth, scrollbar->y(), scrollbar->width() - totalWidth, thickness);
417 return IntRect(scrollbar->x(), scrollbar->y() + startWidth, thickness, scrollbar->height() - totalWidth);
420 int ScrollbarThemeMac::minimumThumbLength(Scrollbar* scrollbar)
422 #if USE(SCROLLBAR_PAINTER)
423 return [scrollbarMap()->get(scrollbar).get() knobMinLength];
425 return cThumbMinLength[scrollbar->controlSize()];
429 bool ScrollbarThemeMac::shouldCenterOnThumb(Scrollbar*, const PlatformMouseEvent& evt)
431 if (evt.button() != LeftButton)
433 if (gJumpOnTrackClick)
434 return !evt.altKey();
438 bool ScrollbarThemeMac::shouldDragDocumentInsteadOfThumb(Scrollbar*, const PlatformMouseEvent& event)
440 return event.altKey();
443 static int scrollbarPartToHIPressedState(ScrollbarPart part)
446 case BackButtonStartPart:
447 return kThemeTopOutsideArrowPressed;
448 case BackButtonEndPart:
449 return kThemeTopOutsideArrowPressed; // This does not make much sense. For some reason the outside constant is required.
450 case ForwardButtonStartPart:
451 return kThemeTopInsideArrowPressed;
452 case ForwardButtonEndPart:
453 return kThemeBottomOutsideArrowPressed;
455 return kThemeThumbPressed;
461 void ScrollbarThemeMac::updateEnabledState(Scrollbar* scrollbar)
463 #if USE(SCROLLBAR_PAINTER)
464 [scrollbarMap()->get(scrollbar).get() setEnabled:scrollbar->enabled()];
466 UNUSED_PARAM(scrollbar);
470 #if USE(SCROLLBAR_PAINTER)
471 static void scrollbarPainterPaint(ScrollbarPainter scrollbarPainter, bool enabled, double value, CGFloat proportion, CGRect frameRect)
473 [scrollbarPainter setEnabled:enabled];
474 [scrollbarPainter setBoundsSize: NSSizeFromCGSize(frameRect.size)];
475 [scrollbarPainter setDoubleValue:value];
476 [scrollbarPainter setKnobProportion:proportion];
478 // The scrollbar's frameRect includes a side inset for overlay scrollers, so we have to use the
479 // trackWidth for drawKnobSlotInRect
481 if ([scrollbarPainter isHorizontal])
482 trackRect = NSMakeRect(0, 0, frameRect.size.width, [scrollbarPainter trackWidth]);
484 trackRect = NSMakeRect(0, 0, [scrollbarPainter trackWidth], frameRect.size.height);
485 [scrollbarPainter drawKnobSlotInRect:trackRect highlight:NO];
487 // If the scrollbar is not enabled, then there is nothing to scroll to, and we shouldn't
490 [scrollbarPainter drawKnob];
494 bool ScrollbarThemeMac::paint(Scrollbar* scrollbar, GraphicsContext* context, const IntRect& damageRect)
496 #if USE(SCROLLBAR_PAINTER)
500 if (scrollbar->currentPos() < 0) {
501 // Scrolled past the top.
503 overhang = -scrollbar->currentPos();
504 } else if (scrollbar->visibleSize() + scrollbar->currentPos() > scrollbar->totalSize()) {
505 // Scrolled past the bottom.
507 overhang = scrollbar->currentPos() + scrollbar->visibleSize() - scrollbar->totalSize();
509 // Within the bounds of the scrollable area.
510 int maximum = scrollbar->maximum();
512 value = scrollbar->currentPos() / maximum;
517 ScrollAnimatorMac* scrollAnimator = static_cast<ScrollAnimatorMac*>(scrollbar->scrollableArea()->scrollAnimator());
518 scrollAnimator->setIsDrawingIntoLayer(context->isCALayerContext());
520 GraphicsContextStateSaver stateSaver(*context);
521 context->clip(damageRect);
522 context->translate(scrollbar->frameRect().x(), scrollbar->frameRect().y());
523 LocalCurrentGraphicsContext localContext(context);
524 scrollbarPainterPaint(scrollbarMap()->get(scrollbar).get(),
525 scrollbar->enabled(),
527 (static_cast<CGFloat>(scrollbar->visibleSize()) - overhang) / scrollbar->totalSize(),
528 scrollbar->frameRect());
530 scrollAnimator->setIsDrawingIntoLayer(false);
534 HIThemeTrackDrawInfo trackInfo;
535 trackInfo.version = 0;
536 trackInfo.kind = scrollbar->controlSize() == RegularScrollbar ? kThemeMediumScrollBar : kThemeSmallScrollBar;
537 trackInfo.bounds = scrollbar->frameRect();
539 float maximum = 0.0f;
540 float position = 0.0f;
541 if (scrollbar->currentPos() < 0) {
542 // Scrolled past the top.
543 maximum = (scrollbar->totalSize() - scrollbar->currentPos()) - scrollbar->visibleSize();
545 } else if (scrollbar->visibleSize() + scrollbar->currentPos() > scrollbar->totalSize()) {
546 // Scrolled past the bottom.
547 maximum = scrollbar->currentPos();
550 // Within the bounds of the scrollable area.
551 maximum = scrollbar->maximum();
552 position = scrollbar->currentPos();
556 trackInfo.max = static_cast<int>(maximum);
557 trackInfo.value = static_cast<int>(position);
559 trackInfo.trackInfo.scrollbar.viewsize = scrollbar->visibleSize();
560 trackInfo.attributes = 0;
561 if (scrollbar->orientation() == HorizontalScrollbar)
562 trackInfo.attributes |= kThemeTrackHorizontal;
564 if (!scrollbar->enabled())
565 trackInfo.enableState = kThemeTrackDisabled;
567 trackInfo.enableState = scrollbar->scrollableArea()->isActive() ? kThemeTrackActive : kThemeTrackInactive;
569 if (hasThumb(scrollbar))
570 trackInfo.attributes |= kThemeTrackShowThumb;
571 else if (!hasButtons(scrollbar))
572 trackInfo.enableState = kThemeTrackNothingToScroll;
573 trackInfo.trackInfo.scrollbar.pressState = scrollbarPartToHIPressedState(scrollbar->pressedPart());
575 // The Aqua scrollbar is buggy when rotated and scaled. We will just draw into a bitmap if we detect a scale or rotation.
576 const AffineTransform& currentCTM = context->getCTM();
577 bool canDrawDirectly = currentCTM.isIdentityOrTranslationOrFlipped();
579 HIThemeDrawTrack(&trackInfo, 0, context->platformContext(), kHIThemeOrientationNormal);
581 trackInfo.bounds = IntRect(IntPoint(), scrollbar->frameRect().size());
583 IntRect bufferRect(scrollbar->frameRect());
584 bufferRect.intersect(damageRect);
586 OwnPtr<ImageBuffer> imageBuffer = ImageBuffer::create(bufferRect.size());
590 imageBuffer->context()->translate(scrollbar->frameRect().x() - bufferRect.x(), scrollbar->frameRect().y() - bufferRect.y());
591 HIThemeDrawTrack(&trackInfo, 0, imageBuffer->context()->platformContext(), kHIThemeOrientationNormal);
592 context->drawImageBuffer(imageBuffer.get(), ColorSpaceDeviceRGB, bufferRect.location());