[macOS] Color wells should appear pressed when presenting a color picker
[WebKit-https.git] / Source / WebCore / platform / mac / ThemeMac.mm
1 /*
2  * Copyright (C) 2008-2017 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 #import "config.h"
27 #import "ThemeMac.h"
28
29 #if PLATFORM(MAC)
30
31 #import "AXObjectCache.h"
32 #import "ControlStates.h"
33 #import "GraphicsContext.h"
34 #import "ImageBuffer.h"
35 #import "LengthSize.h"
36 #import "LocalCurrentGraphicsContext.h"
37 #import "LocalDefaultSystemAppearance.h"
38 #import "ScrollView.h"
39 #import <Carbon/Carbon.h>
40 #import <pal/spi/cocoa/NSButtonCellSPI.h>
41 #import <pal/spi/mac/NSGraphicsSPI.h>
42 #import <wtf/BlockObjCExceptions.h>
43 #import <wtf/NeverDestroyed.h>
44 #import <wtf/StdLibExtras.h>
45
46 static NSRect focusRingClipRect;
47 static BOOL themeWindowHasKeyAppearance;
48
49 @interface WebCoreThemeWindow : NSWindow
50 @end
51
52 @interface WebCoreThemeView : NSControl
53 @end
54
55 @implementation WebCoreThemeWindow
56
57 - (BOOL)hasKeyAppearance
58 {
59     return themeWindowHasKeyAppearance;
60 }
61
62 - (BOOL)isKeyWindow
63 {
64     return themeWindowHasKeyAppearance;
65 }
66
67 @end
68
69 @implementation WebCoreThemeView
70
71 - (NSWindow *)window
72 {
73     // Using defer:YES prevents us from wasting any window server resources for this window, since we're not actually
74     // going to draw into it. The other arguments match what you get when calling -[NSWindow init].
75     static WebCoreThemeWindow *window = [[WebCoreThemeWindow alloc] initWithContentRect:NSMakeRect(100, 100, 100, 100)
76         styleMask:NSWindowStyleMaskTitled backing:NSBackingStoreBuffered defer:YES];
77     return window;
78 }
79
80 - (BOOL)isFlipped
81 {
82     return YES;
83 }
84
85 - (NSText *)currentEditor
86 {
87     return nil;
88 }
89
90 - (BOOL)_automaticFocusRingDisabled
91 {
92     return YES;
93 }
94
95 - (NSRect)_focusRingVisibleRect
96 {
97     if (NSIsEmptyRect(focusRingClipRect))
98         return [self visibleRect];
99     return focusRingClipRect;
100 }
101
102 - (NSView *)_focusRingClipAncestor
103 {
104     return self;
105 }
106
107 - (void)addSubview:(NSView *)subview
108 {
109     // By doing nothing in this method we forbid controls from adding subviews.
110     // This tells AppKit to not use layer-backed animation for control rendering.
111     UNUSED_PARAM(subview);
112 }
113
114 @end
115
116 // FIXME: Default buttons really should be more like push buttons and not like buttons.
117
118 namespace WebCore {
119
120 enum {
121     topMargin,
122     rightMargin,
123     bottomMargin,
124     leftMargin
125 };
126
127 Theme& Theme::singleton()
128 {
129     static NeverDestroyed<ThemeMac> themeMac;
130     return themeMac;
131 }
132
133 // Helper functions used by a bunch of different control parts.
134
135 static NSControlSize controlSizeForFont(const FontCascade& font)
136 {
137     int fontSize = font.pixelSize();
138     if (fontSize >= 16)
139         return NSControlSizeRegular;
140     if (fontSize >= 11)
141         return NSControlSizeSmall;
142     return NSControlSizeMini;
143 }
144
145 static LengthSize sizeFromNSControlSize(NSControlSize nsControlSize, const LengthSize& zoomedSize, float zoomFactor, const std::array<IntSize, 3>& sizes)
146 {
147     IntSize controlSize = sizes[nsControlSize];
148     if (zoomFactor != 1.0f)
149         controlSize = IntSize(controlSize.width() * zoomFactor, controlSize.height() * zoomFactor);
150     LengthSize result = zoomedSize;
151     if (zoomedSize.width.isIntrinsicOrAuto() && controlSize.width() > 0)
152         result.width = { controlSize.width(), Fixed };
153     if (zoomedSize.height.isIntrinsicOrAuto() && controlSize.height() > 0)
154         result.height = { controlSize.height(), Fixed };
155     return result;
156 }
157
158 static LengthSize sizeFromFont(const FontCascade& font, const LengthSize& zoomedSize, float zoomFactor, const std::array<IntSize, 3>& sizes)
159 {
160     return sizeFromNSControlSize(controlSizeForFont(font), zoomedSize, zoomFactor, sizes);
161 }
162
163 static ControlSize controlSizeFromPixelSize(const std::array<IntSize, 3>& sizes, const IntSize& minZoomedSize, float zoomFactor)
164 {
165     if (minZoomedSize.width() >= static_cast<int>(sizes[NSControlSizeRegular].width() * zoomFactor)
166         && minZoomedSize.height() >= static_cast<int>(sizes[NSControlSizeRegular].height() * zoomFactor))
167         return NSControlSizeRegular;
168     if (minZoomedSize.width() >= static_cast<int>(sizes[NSControlSizeSmall].width() * zoomFactor)
169         && minZoomedSize.height() >= static_cast<int>(sizes[NSControlSizeSmall].height() * zoomFactor))
170         return NSControlSizeSmall;
171     return NSControlSizeMini;
172 }
173
174 static void setControlSize(NSCell* cell, const std::array<IntSize, 3>& sizes, const IntSize& minZoomedSize, float zoomFactor)
175 {
176     ControlSize size = controlSizeFromPixelSize(sizes, minZoomedSize, zoomFactor);
177     if (size != [cell controlSize]) // Only update if we have to, since AppKit does work even if the size is the same.
178         [cell setControlSize:(NSControlSize)size];
179 }
180
181 static void updateStates(NSCell* cell, const ControlStates& controlStates, bool useAnimation = false)
182 {
183     // The animated state cause this thread to start and stop repeatedly on CoreAnimation synchronize calls.
184     // This short burts of activity in between are not long enough for VoiceOver to retrieve accessibility attributes and makes the process appear unresponsive.
185     if (AXObjectCache::accessibilityEnhancedUserInterfaceEnabled())
186         useAnimation = false;
187     
188     ControlStates::States states = controlStates.states();
189
190     // Hover state is not supported by Aqua.
191     
192     // Pressed state
193     bool oldPressed = [cell isHighlighted];
194     bool pressed = states & ControlStates::PressedState;
195     if (pressed != oldPressed) {
196         [(NSButtonCell*)cell _setHighlighted:pressed animated:useAnimation];
197     }
198     
199     // Enabled state
200     bool oldEnabled = [cell isEnabled];
201     bool enabled = states & ControlStates::EnabledState;
202     if (enabled != oldEnabled)
203         [cell setEnabled:enabled];
204
205     // Checked and Indeterminate
206     bool oldIndeterminate = [cell state] == NSControlStateValueMixed;
207     bool indeterminate = (states & ControlStates::IndeterminateState);
208     bool checked = states & ControlStates::CheckedState;
209     bool oldChecked = [cell state] == NSControlStateValueOn;
210     if (oldIndeterminate != indeterminate || checked != oldChecked) {
211         NSControlStateValue newState = indeterminate ? NSControlStateValueMixed : (checked ? NSControlStateValueOn : NSControlStateValueOff);
212         [(NSButtonCell*)cell _setState:newState animated:useAnimation];
213     }
214
215     // Presenting state
216     if (states & ControlStates::PresentingState)
217         [(NSButtonCell*)cell _setHighlighted:YES animated:NO];
218
219     // Window inactive state does not need to be checked explicitly, since we paint parented to 
220     // a view in a window whose key state can be detected.
221 }
222
223 static ThemeDrawState convertControlStatesToThemeDrawState(ThemeButtonKind kind, const ControlStates& controlStates)
224 {
225     ControlStates::States states = controlStates.states();
226
227     if (!(states & ControlStates::EnabledState))
228         return kThemeStateUnavailableInactive;
229
230     // Do not process PressedState if !EnabledState.
231     if (states & ControlStates::PressedState) {
232         if (kind == kThemeIncDecButton || kind == kThemeIncDecButtonSmall || kind == kThemeIncDecButtonMini)
233             return states & ControlStates::SpinUpState ? kThemeStatePressedUp : kThemeStatePressedDown;
234         return kThemeStatePressed;
235     }
236     return kThemeStateActive;
237 }
238
239 static FloatRect inflateRect(const FloatRect& zoomedRect, const IntSize& zoomedSize, const int* margins, float zoomFactor)
240 {
241     // Only do the inflation if the available width/height are too small.
242     // Otherwise try to fit the glow/check space into the available box's width/height.
243     int widthDelta = zoomedRect.width() - (zoomedSize.width() + margins[leftMargin] * zoomFactor + margins[rightMargin] * zoomFactor);
244     int heightDelta = zoomedRect.height() - (zoomedSize.height() + margins[topMargin] * zoomFactor + margins[bottomMargin] * zoomFactor);
245     FloatRect result(zoomedRect);
246     if (widthDelta < 0) {
247         result.setX(result.x() - margins[leftMargin] * zoomFactor);
248         result.setWidth(result.width() - widthDelta);
249     }
250     if (heightDelta < 0) {
251         result.setY(result.y() - margins[topMargin] * zoomFactor);
252         result.setHeight(result.height() - heightDelta);
253     }
254     return result;
255 }
256
257 // Checkboxes and radio buttons
258
259 static const std::array<IntSize, 3>& checkboxSizes()
260 {
261     static const std::array<IntSize, 3> sizes = { { IntSize(14, 14), IntSize(12, 12), IntSize(10, 10) } };
262     return sizes;
263 }
264
265 static const int* checkboxMargins(NSControlSize controlSize)
266 {
267     static const int margins[3][4] =
268     {
269         // top right bottom left
270         { 2, 2, 2, 2 },
271         { 2, 1, 2, 1 },
272         { 0, 0, 1, 0 },
273     };
274     return margins[controlSize];
275 }
276
277 static LengthSize checkboxSize(const FontCascade& font, const LengthSize& zoomedSize, float zoomFactor)
278 {
279     // If the width and height are both specified, then we have nothing to do.
280     if (!zoomedSize.width.isIntrinsicOrAuto() && !zoomedSize.height.isIntrinsicOrAuto())
281         return zoomedSize;
282
283     // Use the font size to determine the intrinsic width of the control.
284     return sizeFromFont(font, zoomedSize, zoomFactor, checkboxSizes());
285 }
286
287 // Radio Buttons
288
289 static const std::array<IntSize, 3>& radioSizes()
290 {
291     static const std::array<IntSize, 3> sizes = { { IntSize(16, 16), IntSize(12, 12), IntSize(10, 10) } };
292     return sizes;
293 }
294
295 static const int* radioMargins(NSControlSize controlSize)
296 {
297     static const int margins[3][4] =
298     {
299         // top right bottom left
300         { 1, 0, 1, 2 },
301         { 1, 1, 2, 1 },
302         { 0, 0, 1, 1 },
303     };
304     return margins[controlSize];
305 }
306
307 static LengthSize radioSize(const FontCascade& font, const LengthSize& zoomedSize, float zoomFactor)
308 {
309     // If the width and height are both specified, then we have nothing to do.
310     if (!zoomedSize.width.isIntrinsicOrAuto() && !zoomedSize.height.isIntrinsicOrAuto())
311         return zoomedSize;
312
313     // Use the font size to determine the intrinsic width of the control.
314     return sizeFromFont(font, zoomedSize, zoomFactor, radioSizes());
315 }
316     
317 static void configureToggleButton(NSCell* cell, ControlPart buttonType, const ControlStates& states, const IntSize& zoomedSize, float zoomFactor, bool isStateChange)
318 {
319     // Set the control size based off the rectangle we're painting into.
320     setControlSize(cell, buttonType == CheckboxPart ? checkboxSizes() : radioSizes(), zoomedSize, zoomFactor);
321
322     // Update the various states we respond to.
323     updateStates(cell, states, isStateChange);
324 }
325     
326 static RetainPtr<NSButtonCell> createToggleButtonCell(ControlPart buttonType)
327 {
328     RetainPtr<NSButtonCell> toggleButtonCell = adoptNS([[NSButtonCell alloc] init]);
329     
330     if (buttonType == CheckboxPart) {
331         [toggleButtonCell setButtonType:NSButtonTypeSwitch];
332         [toggleButtonCell setAllowsMixedState:YES];
333     } else {
334         ASSERT(buttonType == RadioPart);
335         [toggleButtonCell setButtonType:NSButtonTypeRadio];
336     }
337     
338     [toggleButtonCell setTitle:nil];
339     [toggleButtonCell setFocusRingType:NSFocusRingTypeExterior];
340     return toggleButtonCell;
341 }
342     
343 static NSButtonCell *sharedRadioCell(const ControlStates& states, const IntSize& zoomedSize, float zoomFactor)
344 {
345     static NSButtonCell *radioCell = createToggleButtonCell(RadioPart).leakRef();
346
347     configureToggleButton(radioCell, RadioPart, states, zoomedSize, zoomFactor, false);
348     return radioCell;
349 }
350     
351 static NSButtonCell *sharedCheckboxCell(const ControlStates& states, const IntSize& zoomedSize, float zoomFactor)
352 {
353     static NSButtonCell *checkboxCell = createToggleButtonCell(CheckboxPart).leakRef();
354
355     configureToggleButton(checkboxCell, CheckboxPart, states, zoomedSize, zoomFactor, false);
356     return checkboxCell;
357 }
358
359 static bool drawCellFocusRingWithFrameAtTime(NSCell *cell, NSRect cellFrame, NSView *controlView, NSTimeInterval timeOffset)
360 {
361 #pragma clang diagnostic push
362 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
363     CGContextRef cgContext = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
364 #pragma clang diagnostic pop
365     CGContextSaveGState(cgContext);
366
367     CGFocusRingStyle focusRingStyle;
368     bool needsRepaint = NSInitializeCGFocusRingStyleForTime(NSFocusRingOnly, &focusRingStyle, timeOffset);
369
370     // We want to respect the CGContext clipping and also not overpaint any
371     // existing focus ring. The way to do this is set accumulate to
372     // -1. According to CoreGraphics, the reasoning for this behavior has been
373     // lost in time.
374     focusRingStyle.accumulate = -1;
375
376     // FIXME: This color should be shared with RenderThemeMac. For now just use the same NSColor color.
377     auto style = adoptCF(CGStyleCreateFocusRingWithColor(&focusRingStyle, [NSColor keyboardFocusIndicatorColor].CGColor));
378     CGContextSetStyle(cgContext, style.get());
379
380     CGContextBeginTransparencyLayerWithRect(cgContext, NSRectToCGRect(cellFrame), nullptr);
381     [cell drawFocusRingMaskWithFrame:cellFrame inView:controlView];
382     CGContextEndTransparencyLayer(cgContext);
383     CGContextRestoreGState(cgContext);
384
385     return needsRepaint;
386 }
387
388 static bool drawCellFocusRing(NSCell *cell, NSRect cellFrame, NSView *controlView)
389 {
390     drawCellFocusRingWithFrameAtTime(cell, cellFrame, controlView, std::numeric_limits<double>::max());
391     return false;
392 }
393
394 static void paintToggleButton(ControlPart buttonType, ControlStates& controlStates, GraphicsContext& context, const FloatRect& zoomedRect, float zoomFactor, ScrollView* scrollView, float deviceScaleFactor, float pageScaleFactor)
395 {
396     BEGIN_BLOCK_OBJC_EXCEPTIONS
397
398     RetainPtr<NSButtonCell> toggleButtonCell = static_cast<NSButtonCell *>(controlStates.platformControl());
399     IntSize zoomedRectSize = IntSize(zoomedRect.size());
400
401     if (controlStates.isDirty()) {
402         if (!toggleButtonCell)
403             toggleButtonCell = createToggleButtonCell(buttonType);
404         configureToggleButton(toggleButtonCell.get(), buttonType, controlStates, zoomedRectSize, zoomFactor, true);
405     } else {
406         if (!toggleButtonCell) {
407             if (buttonType == CheckboxPart)
408                 toggleButtonCell = sharedCheckboxCell(controlStates, zoomedRectSize, zoomFactor);
409             else {
410                 ASSERT(buttonType == RadioPart);
411                 toggleButtonCell = sharedRadioCell(controlStates, zoomedRectSize, zoomFactor);
412             }
413         }
414         configureToggleButton(toggleButtonCell.get(), buttonType, controlStates, zoomedRectSize, zoomFactor, false);
415     }
416     controlStates.setDirty(false);
417
418     GraphicsContextStateSaver stateSaver(context);
419
420     NSControlSize controlSize = [toggleButtonCell controlSize];
421     IntSize zoomedSize = buttonType == CheckboxPart ? checkboxSizes()[controlSize] : radioSizes()[controlSize];
422     zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
423     zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
424     const int* controlMargins = buttonType == CheckboxPart ? checkboxMargins(controlSize) : radioMargins(controlSize);
425     FloatRect inflatedRect = inflateRect(zoomedRect, zoomedSize, controlMargins, zoomFactor);
426
427     if (zoomFactor != 1.0f) {
428         inflatedRect.setWidth(inflatedRect.width() / zoomFactor);
429         inflatedRect.setHeight(inflatedRect.height() / zoomFactor);
430         context.translate(inflatedRect.location());
431         context.scale(zoomFactor);
432         context.translate(-inflatedRect.location());
433     }
434     LocalCurrentGraphicsContext localContext(context);
435
436     NSView *view = ThemeMac::ensuredView(scrollView, controlStates, true /* useUnparentedView */);
437
438     bool needsRepaint = false;
439     bool useImageBuffer = pageScaleFactor != 1.0f || zoomFactor != 1.0f;
440     bool isCellFocused = controlStates.states() & ControlStates::FocusState;
441
442     if ([toggleButtonCell _stateAnimationRunning]) {
443         context.translate(inflatedRect.location());
444         context.scale(FloatSize(1, -1));
445         context.translate(0, -inflatedRect.height());
446
447         [toggleButtonCell _renderCurrentAnimationFrameInContext:context.platformContext() atLocation:NSMakePoint(0, 0)];
448         if (![toggleButtonCell _stateAnimationRunning] && isCellFocused)
449             needsRepaint = ThemeMac::drawCellOrFocusRingWithViewIntoContext(toggleButtonCell.get(), context, inflatedRect, view, false, true, useImageBuffer, deviceScaleFactor);
450     } else
451         needsRepaint = ThemeMac::drawCellOrFocusRingWithViewIntoContext(toggleButtonCell.get(), context, inflatedRect, view, true, isCellFocused, useImageBuffer, deviceScaleFactor);
452
453     [toggleButtonCell setControlView:nil];
454
455     needsRepaint |= [toggleButtonCell _stateAnimationRunning];
456     controlStates.setNeedsRepaint(needsRepaint);
457     if (needsRepaint)
458         controlStates.setPlatformControl(toggleButtonCell.get());
459
460     END_BLOCK_OBJC_EXCEPTIONS
461 }
462
463 // Buttons
464
465 // Buttons really only constrain height. They respect width.
466 static const std::array<IntSize, 3>& buttonSizes()
467 {
468     static const std::array<IntSize, 3> sizes = { { IntSize(0, 21), IntSize(0, 18), IntSize(0, 15) } };
469     return sizes;
470 }
471
472 static const int* buttonMargins(NSControlSize controlSize)
473 {
474     static const int margins[3][4] =
475     {
476         { 4, 6, 7, 6 },
477         { 4, 5, 6, 5 },
478         { 0, 1, 1, 1 },
479     };
480     return margins[controlSize];
481 }
482
483 enum ButtonCellType { NormalButtonCell, DefaultButtonCell };
484
485 static NSButtonCell *leakButtonCell(ButtonCellType type)
486 {
487     NSButtonCell *cell = [[NSButtonCell alloc] init];
488     [cell setTitle:nil];
489     [cell setButtonType:NSButtonTypeMomentaryPushIn];
490     if (type == DefaultButtonCell)
491         [cell setKeyEquivalent:@"\r"];
492     return cell;
493 }
494
495 static void setUpButtonCell(NSButtonCell *cell, ControlPart part, const ControlStates& states, const IntSize& zoomedSize, float zoomFactor)
496 {
497     // Set the control size based off the rectangle we're painting into.
498     const std::array<IntSize, 3>& sizes = buttonSizes();
499     if (part == SquareButtonPart
500 #if ENABLE(INPUT_TYPE_COLOR)
501         || part == ColorWellPart
502 #endif
503         || zoomedSize.height() > buttonSizes()[NSControlSizeRegular].height() * zoomFactor) {
504         // Use the square button
505         if ([cell bezelStyle] != NSBezelStyleShadowlessSquare)
506             [cell setBezelStyle:NSBezelStyleShadowlessSquare];
507     } else if ([cell bezelStyle] != NSBezelStyleRounded)
508         [cell setBezelStyle:NSBezelStyleRounded];
509
510     setControlSize(cell, sizes, zoomedSize, zoomFactor);
511
512     // Update the various states we respond to.
513     updateStates(cell, states);
514 }
515
516 static NSButtonCell *button(ControlPart part, const ControlStates& controlStates, const IntSize& zoomedSize, float zoomFactor)
517 {
518     ControlStates::States states = controlStates.states();
519     NSButtonCell *cell;
520     if (states & ControlStates::DefaultState) {
521         static NSButtonCell *defaultCell = leakButtonCell(DefaultButtonCell);
522         cell = defaultCell;
523     } else {
524         static NSButtonCell *normalCell = leakButtonCell(NormalButtonCell);
525         cell = normalCell;
526     }
527     setUpButtonCell(cell, part, controlStates, zoomedSize, zoomFactor);
528     return cell;
529 }
530     
531 static void paintButton(ControlPart part, ControlStates& controlStates, GraphicsContext& context, const FloatRect& zoomedRect, float zoomFactor, ScrollView* scrollView, float deviceScaleFactor, float pageScaleFactor)
532 {
533     BEGIN_BLOCK_OBJC_EXCEPTIONS
534     
535     // Determine the width and height needed for the control and prepare the cell for painting.
536     ControlStates::States states = controlStates.states();
537     NSButtonCell *buttonCell = button(part, controlStates, IntSize(zoomedRect.size()), zoomFactor);
538     GraphicsContextStateSaver stateSaver(context);
539
540     NSControlSize controlSize = [buttonCell controlSize];
541     IntSize zoomedSize = buttonSizes()[controlSize];
542     zoomedSize.setWidth(zoomedRect.width()); // Buttons don't ever constrain width, so the zoomed width can just be honored.
543     zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
544     FloatRect inflatedRect = zoomedRect;
545     if ([buttonCell bezelStyle] == NSBezelStyleRounded) {
546         // Center the button within the available space.
547         if (inflatedRect.height() > zoomedSize.height()) {
548             inflatedRect.setY(inflatedRect.y() + (inflatedRect.height() - zoomedSize.height()) / 2);
549             inflatedRect.setHeight(zoomedSize.height());
550         }
551
552         // Now inflate it to account for the shadow.
553         inflatedRect = inflateRect(inflatedRect, zoomedSize, buttonMargins(controlSize), zoomFactor);
554
555         if (zoomFactor != 1.0f) {
556             inflatedRect.setWidth(inflatedRect.width() / zoomFactor);
557             inflatedRect.setHeight(inflatedRect.height() / zoomFactor);
558             context.translate(inflatedRect.location());
559             context.scale(zoomFactor);
560             context.translate(-inflatedRect.location());
561         }
562     }
563     
564     LocalCurrentGraphicsContext localContext(context);
565     
566     NSView *view = ThemeMac::ensuredView(scrollView, controlStates);
567     NSWindow *window = [view window];
568     NSButtonCell *previousDefaultButtonCell = [window defaultButtonCell];
569
570     bool useImageBuffer = pageScaleFactor != 1.0f || zoomFactor != 1.0f;
571     bool needsRepaint = ThemeMac::drawCellOrFocusRingWithViewIntoContext(buttonCell, context, inflatedRect, view, true, states & ControlStates::FocusState, useImageBuffer, deviceScaleFactor);
572     if (states & ControlStates::DefaultState)
573         [window setDefaultButtonCell:buttonCell];
574     else if ([previousDefaultButtonCell isEqual:buttonCell])
575         [window setDefaultButtonCell:nil];
576     
577     controlStates.setNeedsRepaint(needsRepaint);
578
579     [buttonCell setControlView:nil];
580
581     if (![previousDefaultButtonCell isEqual:buttonCell])
582         [window setDefaultButtonCell:previousDefaultButtonCell];
583
584     END_BLOCK_OBJC_EXCEPTIONS
585 }
586
587 // Stepper
588
589 static const std::array<IntSize, 3>& stepperSizes()
590 {
591     static const std::array<IntSize, 3> sizes = { { IntSize(19, 27), IntSize(15, 22), IntSize(13, 15) } };
592     return sizes;
593 }
594
595 // We don't use controlSizeForFont() for steppers because the stepper height
596 // should be equal to or less than the corresponding text field height,
597 static NSControlSize stepperControlSizeForFont(const FontCascade& font)
598 {
599     int fontSize = font.pixelSize();
600     if (fontSize >= 18)
601         return NSControlSizeRegular;
602     if (fontSize >= 13)
603         return NSControlSizeSmall;
604     return NSControlSizeMini;
605 }
606
607 static void paintStepper(ControlStates& states, GraphicsContext& context, const FloatRect& zoomedRect, float zoomFactor, ScrollView*)
608 {
609     // We don't use NSStepperCell because there are no ways to draw an
610     // NSStepperCell with the up button highlighted.
611
612     HIThemeButtonDrawInfo drawInfo;
613     drawInfo.version = 0;
614     drawInfo.state = convertControlStatesToThemeDrawState(kThemeIncDecButton, states);
615     drawInfo.adornment = kThemeAdornmentDefault;
616     ControlSize controlSize = controlSizeFromPixelSize(stepperSizes(), IntSize(zoomedRect.size()), zoomFactor);
617     if (controlSize == NSControlSizeSmall)
618         drawInfo.kind = kThemeIncDecButtonSmall;
619     else if (controlSize == NSControlSizeMini)
620         drawInfo.kind = kThemeIncDecButtonMini;
621     else
622         drawInfo.kind = kThemeIncDecButton;
623
624     IntRect rect(zoomedRect);
625     GraphicsContextStateSaver stateSaver(context);
626     if (zoomFactor != 1.0f) {
627         rect.setWidth(rect.width() / zoomFactor);
628         rect.setHeight(rect.height() / zoomFactor);
629         context.translate(rect.location());
630         context.scale(zoomFactor);
631         context.translate(-rect.location());
632     }
633     CGRect bounds(rect);
634     CGRect backgroundBounds;
635     HIThemeGetButtonBackgroundBounds(&bounds, &drawInfo, &backgroundBounds);
636     // Center the stepper rectangle in the specified area.
637     backgroundBounds.origin.x = bounds.origin.x + (bounds.size.width - backgroundBounds.size.width) / 2;
638     if (backgroundBounds.size.height < bounds.size.height) {
639         int heightDiff = clampToInteger(bounds.size.height - backgroundBounds.size.height);
640         backgroundBounds.origin.y = bounds.origin.y + (heightDiff / 2) + 1;
641     }
642
643     LocalCurrentGraphicsContext localContext(context);
644     HIThemeDrawButton(&backgroundBounds, &drawInfo, localContext.cgContext(), kHIThemeOrientationNormal, 0);
645 }
646
647 // This will ensure that we always return a valid NSView, even if ScrollView doesn't have an associated document NSView.
648 // If the ScrollView doesn't have an NSView, we will return a fake NSView set up in the way AppKit expects.
649 NSView *ThemeMac::ensuredView(ScrollView* scrollView, const ControlStates& controlStates, bool useUnparentedView)
650 {
651     if (!useUnparentedView) {
652         if (NSView *documentView = scrollView->documentView())
653             return documentView;
654     }
655
656     // Use a fake view.
657     static WebCoreThemeView *themeView = [[WebCoreThemeView alloc] init];
658     [themeView setFrameSize:NSSizeFromCGSize(scrollView->totalContentsSize())];
659     [themeView setAppearance:[NSAppearance currentAppearance]];
660
661     themeWindowHasKeyAppearance = !(controlStates.states() & ControlStates::WindowInactiveState);
662
663     return themeView;
664 }
665
666 void ThemeMac::setFocusRingClipRect(const FloatRect& rect)
667 {
668     focusRingClipRect = rect;
669 }
670
671 const float buttonFocusRectOutlineWidth = 3.0f;
672
673 static inline bool drawCellOrFocusRingIntoRectWithView(NSCell *cell, NSRect rect, NSView *view, bool drawButtonCell, bool drawFocusRing)
674 {
675     if (drawButtonCell) {
676         if ([cell isKindOfClass:[NSSliderCell class]]) {
677             // For slider cells, draw only the knob.
678             [(NSSliderCell *)cell drawKnob:rect];
679         } else
680             [cell drawWithFrame:rect inView:view];
681     }
682     if (drawFocusRing)
683         return drawCellFocusRing(cell, rect, view);
684
685     return false;
686 }
687
688 bool ThemeMac::drawCellOrFocusRingWithViewIntoContext(NSCell *cell, GraphicsContext& context, const FloatRect& rect, NSView *view, bool drawButtonCell, bool drawFocusRing, bool useImageBuffer, float deviceScaleFactor)
689 {
690     ASSERT(drawButtonCell || drawFocusRing);
691     bool needsRepaint = false;
692     if (useImageBuffer) {
693         NSRect imageBufferDrawRect = NSRect(FloatRect(buttonFocusRectOutlineWidth, buttonFocusRectOutlineWidth, rect.width(), rect.height()));
694         auto imageBuffer = ImageBuffer::createCompatibleBuffer(rect.size() + 2 * FloatSize(buttonFocusRectOutlineWidth, buttonFocusRectOutlineWidth), deviceScaleFactor, ColorSpaceSRGB, context);
695         if (!imageBuffer)
696             return needsRepaint;
697         {
698             LocalCurrentGraphicsContext localContext(imageBuffer->context());
699             needsRepaint = drawCellOrFocusRingIntoRectWithView(cell, imageBufferDrawRect, view, drawButtonCell, drawFocusRing);
700         }
701         context.drawConsumingImageBuffer(WTFMove(imageBuffer), rect.location() - FloatSize(buttonFocusRectOutlineWidth, buttonFocusRectOutlineWidth));
702         return needsRepaint;
703     }
704     if (drawButtonCell)
705         needsRepaint = drawCellOrFocusRingIntoRectWithView(cell, NSRect(rect), view, drawButtonCell, drawFocusRing);
706     
707     return needsRepaint;
708 }
709
710 // Color Well
711
712 #if ENABLE(INPUT_TYPE_COLOR)
713 static void paintColorWell(ControlStates& controlStates, GraphicsContext& context, const FloatRect& zoomedRect, float zoomFactor, ScrollView* scrollView, float deviceScaleFactor, float pageScaleFactor)
714 {
715     BEGIN_BLOCK_OBJC_EXCEPTIONS
716
717     // Determine the width and height needed for the control and prepare the cell for painting.
718     ControlStates::States states = controlStates.states();
719     NSButtonCell *buttonCell = button(ColorWellPart, controlStates, IntSize(zoomedRect.size()), zoomFactor);
720     GraphicsContextStateSaver stateSaver(context);
721
722     NSControlSize controlSize = [buttonCell controlSize];
723     IntSize zoomedSize = buttonSizes()[controlSize];
724     zoomedSize.setWidth(zoomedRect.width()); // Buttons don't ever constrain width, so the zoomed width can just be honored.
725     zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
726     FloatRect inflatedRect = zoomedRect;
727
728     LocalCurrentGraphicsContext localContext(context);
729
730     NSView *view = ThemeMac::ensuredView(scrollView, controlStates);
731     NSWindow *window = [view window];
732     NSButtonCell *previousDefaultButtonCell = [window defaultButtonCell];
733
734     bool useImageBuffer = pageScaleFactor != 1.0f || zoomFactor != 1.0f;
735     bool needsRepaint = ThemeMac::drawCellOrFocusRingWithViewIntoContext(buttonCell, context, inflatedRect, view, true, states & ControlStates::FocusState, useImageBuffer, deviceScaleFactor);
736     if ([previousDefaultButtonCell isEqual:buttonCell])
737         [window setDefaultButtonCell:nil];
738
739     controlStates.setNeedsRepaint(needsRepaint);
740
741     [buttonCell setControlView:nil];
742
743     if (![previousDefaultButtonCell isEqual:buttonCell])
744         [window setDefaultButtonCell:previousDefaultButtonCell];
745
746     END_BLOCK_OBJC_EXCEPTIONS
747 }
748 #endif
749
750 // Theme overrides
751
752 int ThemeMac::baselinePositionAdjustment(ControlPart part) const
753 {
754     if (part == CheckboxPart || part == RadioPart)
755         return -2;
756     return Theme::baselinePositionAdjustment(part);
757 }
758
759 std::optional<FontCascadeDescription> ThemeMac::controlFont(ControlPart part, const FontCascade& font, float zoomFactor) const
760 {
761     switch (part) {
762     case PushButtonPart: {
763         FontCascadeDescription fontDescription;
764         fontDescription.setIsAbsoluteSize(true);
765
766         NSFont* nsFont = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:controlSizeForFont(font)]];
767         fontDescription.setOneFamily(AtomicString("-apple-system", AtomicString::ConstructFromLiteral));
768         fontDescription.setComputedSize([nsFont pointSize] * zoomFactor);
769         fontDescription.setSpecifiedSize([nsFont pointSize] * zoomFactor);
770         return fontDescription;
771     }
772     default:
773         return std::nullopt;
774     }
775 }
776
777 LengthSize ThemeMac::controlSize(ControlPart part, const FontCascade& font, const LengthSize& zoomedSize, float zoomFactor) const
778 {
779     switch (part) {
780     case CheckboxPart:
781         return checkboxSize(font, zoomedSize, zoomFactor);
782     case RadioPart:
783         return radioSize(font, zoomedSize, zoomFactor);
784     case PushButtonPart:
785         // Height is reset to auto so that specified heights can be ignored.
786         return sizeFromFont(font, { zoomedSize.width, { } }, zoomFactor, buttonSizes());
787     case InnerSpinButtonPart:
788         if (!zoomedSize.width.isIntrinsicOrAuto() && !zoomedSize.height.isIntrinsicOrAuto())
789             return zoomedSize;
790         return sizeFromNSControlSize(stepperControlSizeForFont(font), zoomedSize, zoomFactor, stepperSizes());
791     default:
792         return zoomedSize;
793     }
794 }
795
796 LengthSize ThemeMac::minimumControlSize(ControlPart part, const FontCascade& font, float zoomFactor) const
797 {
798     switch (part) {
799     case SquareButtonPart:
800 #if ENABLE(INPUT_TYPE_COLOR)
801     case ColorWellPart:
802 #endif
803     case DefaultButtonPart:
804     case ButtonPart:
805         return { { 0, Fixed }, { static_cast<int>(15 * zoomFactor), Fixed } };
806     case InnerSpinButtonPart: {
807         auto& base = stepperSizes()[NSControlSizeMini];
808         return { { static_cast<int>(base.width() * zoomFactor), Fixed },
809             { static_cast<int>(base.height() * zoomFactor), Fixed } };
810     }
811     default:
812         return Theme::minimumControlSize(part, font, zoomFactor);
813     }
814 }
815
816 LengthBox ThemeMac::controlBorder(ControlPart part, const FontCascade& font, const LengthBox& zoomedBox, float zoomFactor) const
817 {
818     switch (part) {
819     case SquareButtonPart:
820 #if ENABLE(INPUT_TYPE_COLOR)
821     case ColorWellPart:
822 #endif
823     case DefaultButtonPart:
824     case ButtonPart:
825         return LengthBox(0, zoomedBox.right().value(), 0, zoomedBox.left().value());
826     default:
827         return Theme::controlBorder(part, font, zoomedBox, zoomFactor);
828     }
829 }
830
831 LengthBox ThemeMac::controlPadding(ControlPart part, const FontCascade& font, const LengthBox& zoomedBox, float zoomFactor) const
832 {
833     switch (part) {
834     case PushButtonPart: {
835         // Just use 8px. AppKit wants to use 11px for mini buttons, but that padding is just too large
836         // for real-world Web sites (creating a huge necessary minimum width for buttons whose space is
837         // by definition constrained, since we select mini only for small cramped environments).
838         // This also guarantees the HTML <button> will match our rendering by default, since we're using
839         // a consistent padding.
840         int padding = 8 * zoomFactor;
841         return LengthBox(2, padding, 3, padding);
842     }
843     default:
844         return Theme::controlPadding(part, font, zoomedBox, zoomFactor);
845     }
846 }
847
848 void ThemeMac::inflateControlPaintRect(ControlPart part, const ControlStates& states, FloatRect& zoomedRect, float zoomFactor) const
849 {
850     BEGIN_BLOCK_OBJC_EXCEPTIONS
851     IntSize zoomRectSize = IntSize(zoomedRect.size());
852     switch (part) {
853         case CheckboxPart: {
854             // We inflate the rect as needed to account for padding included in the cell to accommodate the checkbox
855             // shadow" and the check. We don't consider this part of the bounds of the control in WebKit.
856             NSCell *cell = sharedCheckboxCell(states, zoomRectSize, zoomFactor);
857             NSControlSize controlSize = [cell controlSize];
858             IntSize zoomedSize = checkboxSizes()[controlSize];
859             zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
860             zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
861             zoomedRect = inflateRect(zoomedRect, zoomedSize, checkboxMargins(controlSize), zoomFactor);
862             break;
863         }
864         case RadioPart: {
865             // We inflate the rect as needed to account for padding included in the cell to accommodate the radio button
866             // shadow". We don't consider this part of the bounds of the control in WebKit.
867             NSCell *cell = sharedRadioCell(states, zoomRectSize, zoomFactor);
868             NSControlSize controlSize = [cell controlSize];
869             IntSize zoomedSize = radioSizes()[controlSize];
870             zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
871             zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
872             zoomedRect = inflateRect(zoomedRect, zoomedSize, radioMargins(controlSize), zoomFactor);
873             break;
874         }
875         case PushButtonPart:
876         case DefaultButtonPart:
877         case ButtonPart: {
878             NSButtonCell *cell = button(part, states, zoomRectSize, zoomFactor);
879             NSControlSize controlSize = [cell controlSize];
880
881             // We inflate the rect as needed to account for the Aqua button's shadow.
882             if ([cell bezelStyle] == NSBezelStyleRounded) {
883                 IntSize zoomedSize = buttonSizes()[controlSize];
884                 zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
885                 zoomedSize.setWidth(zoomedRect.width()); // Buttons don't ever constrain width, so the zoomed width can just be honored.
886                 zoomedRect = inflateRect(zoomedRect, zoomedSize, buttonMargins(controlSize), zoomFactor);
887             }
888             break;
889         }
890         case InnerSpinButtonPart: {
891             static const int stepperMargin[4] = { 0, 0, 0, 0 };
892             ControlSize controlSize = controlSizeFromPixelSize(stepperSizes(), zoomRectSize, zoomFactor);
893             IntSize zoomedSize = stepperSizes()[controlSize];
894             zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
895             zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
896             zoomedRect = inflateRect(zoomedRect, zoomedSize, stepperMargin, zoomFactor);
897             break;
898         }
899         default:
900             break;
901     }
902     END_BLOCK_OBJC_EXCEPTIONS
903 }
904
905 void ThemeMac::paint(ControlPart part, ControlStates& states, GraphicsContext& context, const FloatRect& zoomedRect, float zoomFactor, ScrollView* scrollView, float deviceScaleFactor, float pageScaleFactor, bool useSystemAppearance, bool useDarkAppearance)
906 {
907     LocalDefaultSystemAppearance localAppearance(useSystemAppearance, useDarkAppearance);
908
909     switch (part) {
910         case CheckboxPart:
911             paintToggleButton(part, states, context, zoomedRect, zoomFactor, scrollView, deviceScaleFactor, pageScaleFactor);
912             break;
913         case RadioPart:
914             paintToggleButton(part, states, context, zoomedRect, zoomFactor, scrollView, deviceScaleFactor, pageScaleFactor);
915             break;
916         case PushButtonPart:
917         case DefaultButtonPart:
918         case ButtonPart:
919         case SquareButtonPart:
920             paintButton(part, states, context, zoomedRect, zoomFactor, scrollView, deviceScaleFactor, pageScaleFactor);
921             break;
922 #if ENABLE(INPUT_TYPE_COLOR)
923         case ColorWellPart:
924             paintColorWell(states, context, zoomedRect, zoomFactor, scrollView, deviceScaleFactor, pageScaleFactor);
925             break;
926 #endif
927         case InnerSpinButtonPart:
928             paintStepper(states, context, zoomedRect, zoomFactor, scrollView);
929             break;
930         default:
931             break;
932     }
933 }
934
935 bool ThemeMac::userPrefersReducedMotion() const
936 {
937     return [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldReduceMotion];
938 }
939
940 }
941
942 #endif // PLATFORM(MAC)